diff --git a/internal/application/finalization.go b/internal/application/finalization.go index a3a99a2..71be6fb 100644 --- a/internal/application/finalization.go +++ b/internal/application/finalization.go @@ -151,7 +151,7 @@ func getSignedInputs(ptx psbt.Packet, signerPublicKey *btcec.PublicKey) (map[wir continue // not signed: skip } - script, err := arkade.ReadArkadeScript(&ptx, inputIndex, signerPublicKey, entry) + script, err := arkade.ReadArkadeScript(&ptx, signerPublicKey, entry) if err != nil { return nil, fmt.Errorf("failed to read arkade script: %w", err) } diff --git a/internal/application/intent.go b/internal/application/intent.go index e586c45..c8ab5f8 100644 --- a/internal/application/intent.go +++ b/internal/application/intent.go @@ -47,7 +47,7 @@ func (s *service) SubmitIntent(ctx context.Context, intent Intent) (*psbt.Packet continue } - script, err := arkade.ReadArkadeScript(ptx, inputIndex, signerPublicKey, entry) + script, err := arkade.ReadArkadeScript(ptx, signerPublicKey, entry) if err != nil { // skip if the input is not a valid arkade script continue diff --git a/internal/application/tx.go b/internal/application/tx.go index 1012dd8..7c42f03 100644 --- a/internal/application/tx.go +++ b/internal/application/tx.go @@ -46,7 +46,7 @@ func (s *service) SubmitTx(ctx context.Context, tx OffchainTx) (*OffchainTx, err var nSigned = 0 for _, entry := range packet { inputIndex := int(entry.Vin) - script, err := arkade.ReadArkadeScript(arkPtx, inputIndex, signerPublicKey, entry) + script, err := arkade.ReadArkadeScript(arkPtx, signerPublicKey, entry) if err != nil { // there may be input/entry pairs attributed to a different signer if errors.Is(err, arkade.ErrTweakedArkadePubKeyNotFound) && len(arkPtx.Inputs) > 1 { diff --git a/pkg/arkade/script.go b/pkg/arkade/script.go index 7cc034e..408fde7 100644 --- a/pkg/arkade/script.go +++ b/pkg/arkade/script.go @@ -34,7 +34,8 @@ func WithDebugCallback(callback func(*StepInfo, *Engine) error) ExecuteOption { } } -func ReadArkadeScript(ptx *psbt.Packet, inputIndex int, signerPublicKey *btcec.PublicKey, entry IntrospectorEntry) (*ArkadeScript, error) { +func ReadArkadeScript(ptx *psbt.Packet, signerPublicKey *btcec.PublicKey, entry IntrospectorEntry) (*ArkadeScript, error) { + inputIndex := int(entry.Vin) if len(ptx.Inputs) <= inputIndex { return nil, fmt.Errorf("input index out of range") } @@ -53,18 +54,29 @@ func ReadArkadeScript(ptx *psbt.Packet, inputIndex int, signerPublicKey *btcec.P expectedPublicKey := ComputeArkadeScriptPublicKey(signerPublicKey, scriptHash) expectedPublicKeyXonly := schnorr.SerializePubKey(expectedPublicKey) - // TODO: allow any type of closure (condition, cltv ...) - var tapscript scriptlib.MultisigClosure - valid, err := tapscript.Decode(spendingTapscript.Script) + closure, err := scriptlib.DecodeClosure(spendingTapscript.Script) if err != nil { - return nil, fmt.Errorf("unexpected error while decoding tapscript: %w", err) + return nil, fmt.Errorf("failed to decode tapscript: %w", err) } - if !valid { - return nil, fmt.Errorf("spendingtapscript is not a MultisigClosure") + + var pubkeys []*btcec.PublicKey + switch c := closure.(type) { + case *scriptlib.MultisigClosure: + pubkeys = c.PubKeys + case *scriptlib.CSVMultisigClosure: + pubkeys = c.PubKeys + case *scriptlib.CLTVMultisigClosure: + pubkeys = c.PubKeys + case *scriptlib.ConditionMultisigClosure: + pubkeys = c.PubKeys + case *scriptlib.ConditionCSVMultisigClosure: + pubkeys = c.PubKeys + default: + return nil, fmt.Errorf("unsupported closure type: %T", closure) } found := false - for _, pubkey := range tapscript.PubKeys { + for _, pubkey := range pubkeys { xonly := schnorr.SerializePubKey(pubkey) if bytes.Equal(xonly, expectedPublicKeyXonly) { found = true diff --git a/pkg/arkade/script_test.go b/pkg/arkade/script_test.go new file mode 100644 index 0000000..88c001b --- /dev/null +++ b/pkg/arkade/script_test.go @@ -0,0 +1,133 @@ +package arkade + +import ( + "encoding/hex" + "encoding/json" + "os" + "strings" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/stretchr/testify/require" +) + + +func TestReadArkadeScript(t *testing.T) { + fix := readScriptFixtures(t) + + t.Run("valid", func(t *testing.T) { + for _, f := range fix.Valid { + t.Run(f.Name, func(t *testing.T) { + ptx := decodePSBT(t, f.Psbt) + signerPubKey := decodeXOnlyPubKey(t, f.SignerPublicKey) + entry := decodeEntry(t, f.Entry) + + result, err := ReadArkadeScript(ptx, signerPubKey, entry) + require.NoError(t, err) + require.NotNil(t, result) + + require.Equal(t, entry.Script, result.script) + require.Equal(t, ArkadeScriptHash(entry.Script), result.hash) + require.Equal(t, len(entry.Witness), len(result.witness)) + for i := range entry.Witness { + require.Equal(t, entry.Witness[i], result.witness[i]) + } + + expectedPubKey := ComputeArkadeScriptPublicKey(signerPubKey, result.hash) + require.True(t, expectedPubKey.IsEqual(result.pubkey)) + + tapscript := ptx.Inputs[entry.Vin].TaprootLeafScript[0].Script + require.Equal(t, txscript.NewBaseTapLeaf(tapscript), result.tapLeaf) + }) + } + }) + + t.Run("invalid", func(t *testing.T) { + for _, f := range fix.Invalid { + t.Run(f.Name, func(t *testing.T) { + ptx := decodePSBT(t, f.Psbt) + signerPubKey := decodeXOnlyPubKey(t, f.SignerPublicKey) + entry := decodeEntry(t, f.Entry) + + _, err := ReadArkadeScript(ptx, signerPubKey, entry) + require.Error(t, err) + require.Contains(t, err.Error(), f.ErrorContains) + }) + } + }) +} + +type scriptFixtureEntry struct { + Vin int `json:"vin"` + Script string `json:"script"` + Witness []string `json:"witness"` +} + +type validScriptFixture struct { + Name string `json:"name"` + SignerPublicKey string `json:"signerPublicKey"` + Psbt string `json:"psbt"` + Entry scriptFixtureEntry `json:"entry"` +} + +type invalidScriptFixture struct { + Name string `json:"name"` + SignerPublicKey string `json:"signerPublicKey"` + Psbt string `json:"psbt"` + Entry scriptFixtureEntry `json:"entry"` + ErrorContains string `json:"errorContains"` +} + +type scriptFixtures struct { + Valid []validScriptFixture `json:"valid"` + Invalid []invalidScriptFixture `json:"invalid"` +} + + +func readScriptFixtures(t *testing.T) scriptFixtures { + t.Helper() + data, err := os.ReadFile("testdata/read_arkade_script.json") + require.NoError(t, err) + + var fix scriptFixtures + require.NoError(t, json.Unmarshal(data, &fix)) + return fix +} + +func decodePSBT(t *testing.T, b64 string) *psbt.Packet { + t.Helper() + ptx, err := psbt.NewFromRawBytes(strings.NewReader(b64), true) + require.NoError(t, err) + return ptx +} + +func decodeXOnlyPubKey(t *testing.T, hexStr string) *btcec.PublicKey { + t.Helper() + data, err := hex.DecodeString(hexStr) + require.NoError(t, err) + pubKey, err := schnorr.ParsePubKey(data) + require.NoError(t, err) + return pubKey +} + +func decodeEntry(t *testing.T, raw scriptFixtureEntry) IntrospectorEntry { + t.Helper() + script, err := hex.DecodeString(raw.Script) + require.NoError(t, err) + + witness := make(wire.TxWitness, len(raw.Witness)) + for i, w := range raw.Witness { + witness[i], err = hex.DecodeString(w) + require.NoError(t, err) + } + + return IntrospectorEntry{ + Vin: uint16(raw.Vin), + Script: script, + Witness: witness, + } +} diff --git a/pkg/arkade/testdata/read_arkade_script.json b/pkg/arkade/testdata/read_arkade_script.json new file mode 100644 index 0000000..7d742e9 --- /dev/null +++ b/pkg/arkade/testdata/read_arkade_script.json @@ -0,0 +1,134 @@ +{ + "valid": [ + { + "name": "multisig closure (checksig)", + "signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARSBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZq0g0jHBebSFpXbjA9Dk50Bdv1PRbqKWwJAw9xEERODAz2uswAAA", + "entry": { + "vin": 0, + "script": "51", + "witness": [] + } + }, + { + "name": "multisig closure (checksigadd)", + "signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARyBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZqwg0jHBebSFpXbjA9Dk50Bdv1PRbqKWwJAw9xEERODAz2u6UpzAAAA=", + "entry": { + "vin": 0, + "script": "51", + "witness": [] + } + }, + { + "name": "csv multisig closure", + "signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrASFqydSBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZq0g0jHBebSFpXbjA9Dk50Bdv1PRbqKWwJAw9xEERODAz2uswAAA", + "entry": { + "vin": 0, + "script": "51", + "witness": [] + } + }, + { + "name": "cltv multisig closure", + "signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrASQFksXUgTUts0TYQMsqb0q652QCqTUXZ6tgKyUIzdMRRpyVNB2atINIxwXm0haV24wPQ5OdAXb9T0W6ilsCQMPcRBETgwM9rrMAAAA==", + "entry": { + "vin": 0, + "script": "51", + "witness": [] + } + }, + { + "name": "condition multisig closure", + "signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAR1FpIE1LbNE2EDLKm9KuudkAqk1F2erYCslCM3TEUaclTQdmrSDSMcF5tIWlduMD0OTnQF2/U9FuopbAkDD3EQRE4MDPa6zAAAA=", + "entry": { + "vin": 0, + "script": "51", + "witness": [] + } + }, + { + "name": "condition csv multisig closure", + "signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrASlFpWrJ1IE1LbNE2EDLKm9KuudkAqk1F2erYCslCM3TEUaclTQdmrSDSMcF5tIWlduMD0OTnQF2/U9FuopbAkDD3EQRE4MDPa6zAAAA=", + "entry": { + "vin": 0, + "script": "51", + "witness": [] + } + }, + { + "name": "with witness data", + "signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARSDSMcF5tIWlduMD0OTnQF2/U9FuopbAkDD3EQRE4MDPa60gTUts0TYQMsqb0q652QCqTUXZ6tgKyUIzdMRRpyVNB2aswAAA", + "entry": { + "vin": 0, + "script": "51", + "witness": [ + "deadbeef", + "cafebabe" + ] + } + } + ], + "invalid": [ + { + "name": "input index out of range", + "signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARSBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZq0g0jHBebSFpXbjA9Dk50Bdv1PRbqKWwJAw9xEERODAz2uswAAA", + "entry": { + "vin": 2, + "script": "51", + "witness": [] + }, + "errorContains": "input index out of range" + }, + { + "name": "no taproot leaf script", + "signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "entry": { + "vin": 0, + "script": "51", + "witness": [] + }, + "errorContains": "TaprootLeafScript" + }, + { + "name": "invalid tapscript", + "signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrABd6tvu/AAAA=", + "entry": { + "vin": 0, + "script": "51", + "witness": [] + }, + "errorContains": "failed to decode tapscript" + }, + { + "name": "tweaked pubkey not found (wrong signer)", + "signerPublicKey": "462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARSBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZq0g0jHBebSFpXbjA9Dk50Bdv1PRbqKWwJAw9xEERODAz2uswAAA", + "entry": { + "vin": 0, + "script": "51", + "witness": [] + }, + "errorContains": "tweaked arkade script public key not found" + }, + { + "name": "tweaked pubkey not found (wrong arkade script)", + "signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARSBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZq0gUx/mBoE0UD0nIxMyJ8hnrI+myDxTfppEw8W9vcsf4zeswAAA", + "entry": { + "vin": 0, + "script": "51", + "witness": [] + }, + "errorContains": "tweaked arkade script public key not found" + } + ] +} \ No newline at end of file diff --git a/test/tx_test.go b/test/tx_test.go index 9216aa3..e71a04c 100644 --- a/test/tx_test.go +++ b/test/tx_test.go @@ -934,24 +934,9 @@ func TestIntrospectorRejectsInvalidArkadeScript(t *testing.T) { ptx.Inputs[0].TaprootLeafScript = nil }, }, - { - name: "non-multisig tapscript", - contains: "spendingtapscript is not a MultisigClosure", - entry: arkade.IntrospectorEntry{ - Vin: 0, - Script: arkadeScript, - }, - mutateTx: func(t *testing.T, ptx *psbt.Packet) { - t.Helper() - require.NotEmpty(t, ptx.Inputs) - require.NotEmpty(t, ptx.Inputs[0].TaprootLeafScript) - require.NotNil(t, ptx.Inputs[0].TaprootLeafScript[0]) - ptx.Inputs[0].TaprootLeafScript[0].Script = []byte{txscript.OP_TRUE} - }, - }, { name: "malformed tapscript decode", - contains: "unexpected error while decoding tapscript", + contains: "failed to decode tapscript", entry: arkade.IntrospectorEntry{ Vin: 0, Script: arkadeScript, @@ -1013,7 +998,7 @@ func TestIntrospectorRejectsInvalidArkadeScript(t *testing.T) { require.Len(t, packet, 1) entry := packet[0] - _, err = arkade.ReadArkadeScript(invalidTx, int(entry.Vin), introspectorPublicKey, entry) + _, err = arkade.ReadArkadeScript(invalidTx, introspectorPublicKey, entry) require.Error(t, err) require.Contains(t, err.Error(), tc.contains) } diff --git a/test/utils_test.go b/test/utils_test.go index 2a2ee75..32b7e52 100644 --- a/test/utils_test.go +++ b/test/utils_test.go @@ -728,7 +728,7 @@ func debugExecuteArkadeScripts(t *testing.T, ptx *psbt.Packet, signerPublicKey * for _, entry := range packet { inputIndex := int(entry.Vin) - script, err := arkade.ReadArkadeScript(ptx, inputIndex, signerPublicKey, entry) + script, err := arkade.ReadArkadeScript(ptx, signerPublicKey, entry) if err != nil { return fmt.Errorf("failed to read arkade script at input %d: %w", inputIndex, err) }