Skip to content

Commit

Permalink
Adding more L1 wallet validation in go-sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonCWang committed Nov 20, 2024
1 parent 80c3198 commit 3c82e1a
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 31 deletions.
25 changes: 25 additions & 0 deletions keyscripts/gen_xpub.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,28 @@ func GenHardenedXPub(masterSeedHex string, derivationPath []uint32, bitcoinNetwo
}
return xpub.String(), nil
}

func DeriveChildPubKeyFromExistingXPub(xpubStr string, remainingPath []uint32) ([]byte, error) {
extKey, err := hdkeychain.NewKeyFromString(xpubStr)
if err != nil {
return nil, fmt.Errorf("failed to parse xpub: %v", err)
}

key := extKey
for _, index := range remainingPath {
if index >= 0x80000000 {
return nil, fmt.Errorf("cannot do hardened derivation from xpub")
}
key, err = key.Derive(index)
if err != nil {
return nil, err
}
}

ecPubKey, err := key.ECPubKey()
if err != nil {
return nil, fmt.Errorf("failed to get EC pubkey: %v", err)
}

return ecPubKey.SerializeCompressed(), nil
}
3 changes: 1 addition & 2 deletions remotesigning/remote_signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@ func GraphQLResponseForRemoteSigningWebhook(
webhook webhooks.WebhookEvent,
seedBytes []byte,
) (SigningResponse, error) {
if !validator.ShouldSign(webhook) {
if !validator.ShouldSign(webhook, seedBytes) {
return nil, errors.New("declined to sign messages")
}

if webhook.EventType != objects.WebhookEventTypeRemoteSigning {
return nil, errors.New("webhook event is not for remote signing")
}
Expand Down
17 changes: 9 additions & 8 deletions remotesigning/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,14 +480,15 @@ func (r *ReleaseCounterpartyPerCommitmentSecretRequest) Type() objects.RemoteSig
// DerivationPath is the bip32 derivation path to get the key from the master key `k`.
// Then apply MulTweak * k + AddTweak to get the final signing key.
type SigningJob struct {
Id string `json:"id"`
DerivationPath string `json:"derivation_path"`
Message string `json:"message"`
AddTweak *string `json:"add_tweak"`
MulTweak *string `json:"mul_tweak"`
Script *string `json:"script"`
Transaction *string `json:"transaction"`
Amount *int64 `json:"amount"`
Id string `json:"id"`
DerivationPath string `json:"derivation_path"`
Message string `json:"message"`
AddTweak *string `json:"add_tweak"`
MulTweak *string `json:"mul_tweak"`
Script *string `json:"script"`
Transaction *string `json:"transaction"`
Amount *int64 `json:"amount"`
L1DerivationPath string `json:"l1_derivation_path"`
}

func (j *SigningJob) MulTweakBytes() ([]byte, error) {
Expand Down
166 changes: 166 additions & 0 deletions remotesigning/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"log"
"regexp"
"strconv"
"strings"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/bech32"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
utils "github.com/lightsparkdev/go-sdk/keyscripts"
)

func GetPaymentHashFromScript(scriptHex string) (*string, error) {
Expand Down Expand Up @@ -89,3 +97,161 @@ func CalculateWitnessHashPSBT(transaction string) (*string, error) {
result := hex.EncodeToString(hash)
return &result, nil
}

func validateScript(signing *SigningJob, seedBytes []byte) bool {
hardenedPath, _, err := SplitDerivationPath(signing.DerivationPath)
if err != nil {
log.Printf("Failed to parse derivation path (Previous Transaction): %v", err)
return false
}
_, remainingPathChangeAddress, err := SplitDerivationPath(signing.L1DerivationPath)
if err != nil {
log.Printf("Failed to parse derivation path (Next Transaction): %v", err)
return false
}

// Step 1: Derive Tx Script from Master Seed
masterSeedHex := hex.EncodeToString(seedBytes)
xpub, err := utils.GenHardenedXPub(masterSeedHex, hardenedPath, "mainnet")
if err != nil {
log.Printf("Failed to generate base xpub: %v", err)
return false
}
childPubkey, err := utils.DeriveChildPubKeyFromExistingXPub(xpub, remainingPathChangeAddress)
if err != nil {
log.Printf("Failed to derive pubkey: %v", err)
return false
}
generated_script, err := GenerateScriptFromChildPubkey(childPubkey)
if err != nil {
log.Printf("Failed to generate script from seed: %v", err)
return false
}
log.Printf("✓ Successfully generated script from seed! %x", generated_script)

// Step 2: Obtain Tx Script from Change Address (Directly from Transaction)
changeAddress, err := GetChangeAddressFromTransaction([]byte(*signing.Transaction))
if err != nil {
log.Printf("Failed to get change address from transaction: %v", err)
return false
}
expected_script, err := GenerateScriptFromChangeAddress(changeAddress)
if err != nil {
log.Printf("Failed to generate script from change address: %v", err)
return false
}
log.Printf("✓ Successfully generated script from change address! %x", expected_script)

// Step 3: Compare the two scripts
if !bytes.Equal(generated_script, expected_script) {
log.Printf("Tx scripts do not match! Refusing to sign...")
return false
}

return true
}

func SplitDerivationPath(path string) (hardenedPath []uint32, remainingPath []uint32, err error) {
if !strings.HasPrefix(path, "m/") {
return nil, nil, fmt.Errorf("invalid derivation path: must start with 'm/'")
}
components := strings.Split(path[2:], "/")
hardenedPath = make([]uint32, 0)
remainingPath = make([]uint32, 0)

for _, component := range components {
isHardened := strings.HasSuffix(component, "'")
if isHardened {
component = component[:len(component)-1]
}

num, err := strconv.ParseUint(component, 10, 32)
if err != nil {
return nil, nil, fmt.Errorf("invalid path: %s", component)
}

if isHardened {
hardenedPath = append(hardenedPath, uint32(num)+0x80000000)
} else {
remainingPath = append(remainingPath, uint32(num))
}
}

return hardenedPath, remainingPath, nil
}

func GenerateScriptFromChildPubkey(child_pubkey []byte) ([]byte, error) {
pkHash := btcutil.Hash160(child_pubkey)
// Create P2WPKH script: OP_0 <20-byte-key-hash>
return txscript.NewScriptBuilder().
AddOp(txscript.OP_0).
AddData(pkHash).
Script()
}

func GetChangeAddressFromTransaction(txBytes []byte) (string, error) {
// Convert hex string to bytes
txHex := string(txBytes)
rawTxBytes, err := hex.DecodeString(txHex)
if err != nil {
return "", fmt.Errorf("failed to decode transaction hex: %w", err)
}

// Deserialize the transaction
var tx wire.MsgTx
if err := tx.Deserialize(bytes.NewReader(rawTxBytes)); err != nil {
return "", fmt.Errorf("failed to deserialize transaction: %w", err)
}

// Change output is always the second output (index 1)
if len(tx.TxOut) < 2 {
return "", fmt.Errorf("no change output found in transaction")
}
changeOutput := tx.TxOut[1]
// The script format is: 0014<20-byte-pubkey-hash>
if len(changeOutput.PkScript) != 22 {
return "", fmt.Errorf("invalid script length: %d", len(changeOutput.PkScript))
}
pubKeyHash := changeOutput.PkScript[2:]

// Create a P2WPKH address from the pubkey hash
addr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, &chaincfg.RegressionNetParams)
if err != nil {
return "", fmt.Errorf("failed to create address: %w", err)
}
return addr.EncodeAddress(), nil
}

func GetWitnessProgramFromAddress(address string) ([]byte, error) {
// Decode the bech32 address
_, data, err := bech32.Decode(address)
if err != nil {
return nil, fmt.Errorf("invalid bech32 address!: %w", err)
}

// Convert from 5-bit to 8-bit encoding
witnessProgram, err := bech32.ConvertBits(data[1:], 5, 8, false)
if err != nil {
return nil, fmt.Errorf("failed to convert bits!: %w", err)
}

return witnessProgram, nil
}

func GenerateScriptFromChangeAddress(changeAddress string) ([]byte, error) {
// Get witness program from the change address
witnessProgram, err := GetWitnessProgramFromAddress(changeAddress)
if err != nil {
return nil, fmt.Errorf("failed to get witness program!: %w", err)
}
// Create expected script
expectedScript, err := txscript.NewScriptBuilder().
AddOp(txscript.OP_0).
AddData(witnessProgram).
Script()
if err != nil {
return nil, fmt.Errorf("failed to create script!: %w", err)
}

return expectedScript, nil
}
81 changes: 60 additions & 21 deletions remotesigning/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,90 @@
package remotesigning

import (
"log"
"strings"

"github.com/lightsparkdev/go-sdk/webhooks"
)

// Validator an interface which decides whether to sign or reject a remote signing webhook event.
type Validator interface {
ShouldSign(webhookEvent webhooks.WebhookEvent) bool
ShouldSign(webhook webhooks.WebhookEvent, seedBytes []byte) bool
}

type PositiveValidator struct{}

func (v PositiveValidator) ShouldSign(webhooks.WebhookEvent) bool {
func (v PositiveValidator) ShouldSign(webhook webhooks.WebhookEvent, seedBytes []byte) bool {
return true
}

type HashValidator struct{}

func (v HashValidator) ShouldSign(webhookEvent webhooks.WebhookEvent) bool {
func (v HashValidator) ShouldSign(webhookEvent webhooks.WebhookEvent, seedBytes []byte) bool {
request, err := ParseDeriveAndSignRequest(webhookEvent)
if err != nil {
// Only validate DeriveAndSignRequest events
return true
}

for _, signing := range request.SigningJobs {
// PaymentOutput or DelayedPaymentOutput
if strings.HasSuffix(signing.DerivationPath, "/2") || strings.HasSuffix(signing.DerivationPath, "/3") {
msg, err := CalculateWitnessHashPSBT(*signing.Transaction)
if err != nil {
return false
}
if strings.Compare(*msg, signing.Message) != 0 {
return false
}
} else {
msg, err := CalculateWitnessHash(*signing.Amount, *signing.Script, *signing.Transaction)
if err != nil {
return false
}
if strings.Compare(*msg, signing.Message) != 0 {
return false
}
if !validateSigningJob(&signing, seedBytes) {
return false
}
}

return true
}

func validateSigningJob(signing *SigningJob, seedBytes []byte) bool {
// Check if this is an L1 transaction
if strings.HasPrefix(signing.DerivationPath, "m/84") {
return validateL1Transaction(signing, seedBytes)
}

return validateLightningTransaction(signing)
}

func validateLightningTransaction(signing *SigningJob) bool {
// PaymentOutput or DelayedPaymentOutput
if strings.HasSuffix(signing.DerivationPath, "/2") || strings.HasSuffix(signing.DerivationPath, "/3") {
msg, err := CalculateWitnessHashPSBT(*signing.Transaction)
if err != nil {
log.Printf("Failed to calculate PSBT witness hash: %v", err)
return false
}
return strings.Compare(*msg, signing.Message) == 0
}

msg, err := CalculateWitnessHash(*signing.Amount, *signing.Script, *signing.Transaction)
if err != nil {
log.Printf("Failed to calculate witness hash: %v", err)
return false
}
return strings.Compare(*msg, signing.Message) == 0
}

func validateL1Transaction(signing *SigningJob, seedBytes []byte) bool {
log.Println("Starting L1 transaction validation...")
// 1. Address Validation
log.Println("Starting address validation...")
isValid := validateScript(signing, seedBytes)

if !isValid {
return false
}
log.Printf("✓ Address validated from script and matches derivation path!")

// 2. Witness Hash Validation
log.Println("Starting witness hash validation...")
msg, err := CalculateWitnessHash(*signing.Amount, *signing.Script, *signing.Transaction)
if err != nil {
return false
}
if strings.Compare(*msg, signing.Message) != 0 {
return false
}
log.Printf("✓ Witness hash validated!")

log.Println("✓ L1 transaction validation completed successfully")
return true
}

0 comments on commit 3c82e1a

Please sign in to comment.