Skip to content

Commit 57af7f5

Browse files
authored
Support every type of tapscript closure (#30)
* support every type of tapscript closure * unit tests: ReadArkadeScript
1 parent 585ee23 commit 57af7f5

8 files changed

Lines changed: 293 additions & 29 deletions

File tree

internal/application/finalization.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func getSignedInputs(ptx psbt.Packet, signerPublicKey *btcec.PublicKey) (map[wir
151151
continue // not signed: skip
152152
}
153153

154-
script, err := arkade.ReadArkadeScript(&ptx, inputIndex, signerPublicKey, entry)
154+
script, err := arkade.ReadArkadeScript(&ptx, signerPublicKey, entry)
155155
if err != nil {
156156
return nil, fmt.Errorf("failed to read arkade script: %w", err)
157157
}

internal/application/intent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func (s *service) SubmitIntent(ctx context.Context, intent Intent) (*psbt.Packet
4747
continue
4848
}
4949

50-
script, err := arkade.ReadArkadeScript(ptx, inputIndex, signerPublicKey, entry)
50+
script, err := arkade.ReadArkadeScript(ptx, signerPublicKey, entry)
5151
if err != nil {
5252
// skip if the input is not a valid arkade script
5353
continue

internal/application/tx.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func (s *service) SubmitTx(ctx context.Context, tx OffchainTx) (*OffchainTx, err
4646
var nSigned = 0
4747
for _, entry := range packet {
4848
inputIndex := int(entry.Vin)
49-
script, err := arkade.ReadArkadeScript(arkPtx, inputIndex, signerPublicKey, entry)
49+
script, err := arkade.ReadArkadeScript(arkPtx, signerPublicKey, entry)
5050
if err != nil {
5151
// there may be input/entry pairs attributed to a different signer
5252
if errors.Is(err, arkade.ErrTweakedArkadePubKeyNotFound) && len(arkPtx.Inputs) > 1 {

pkg/arkade/script.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ func WithDebugCallback(callback func(*StepInfo, *Engine) error) ExecuteOption {
3434
}
3535
}
3636

37-
func ReadArkadeScript(ptx *psbt.Packet, inputIndex int, signerPublicKey *btcec.PublicKey, entry IntrospectorEntry) (*ArkadeScript, error) {
37+
func ReadArkadeScript(ptx *psbt.Packet, signerPublicKey *btcec.PublicKey, entry IntrospectorEntry) (*ArkadeScript, error) {
38+
inputIndex := int(entry.Vin)
3839
if len(ptx.Inputs) <= inputIndex {
3940
return nil, fmt.Errorf("input index out of range")
4041
}
@@ -53,18 +54,29 @@ func ReadArkadeScript(ptx *psbt.Packet, inputIndex int, signerPublicKey *btcec.P
5354
expectedPublicKey := ComputeArkadeScriptPublicKey(signerPublicKey, scriptHash)
5455
expectedPublicKeyXonly := schnorr.SerializePubKey(expectedPublicKey)
5556

56-
// TODO: allow any type of closure (condition, cltv ...)
57-
var tapscript scriptlib.MultisigClosure
58-
valid, err := tapscript.Decode(spendingTapscript.Script)
57+
closure, err := scriptlib.DecodeClosure(spendingTapscript.Script)
5958
if err != nil {
60-
return nil, fmt.Errorf("unexpected error while decoding tapscript: %w", err)
59+
return nil, fmt.Errorf("failed to decode tapscript: %w", err)
6160
}
62-
if !valid {
63-
return nil, fmt.Errorf("spendingtapscript is not a MultisigClosure")
61+
62+
var pubkeys []*btcec.PublicKey
63+
switch c := closure.(type) {
64+
case *scriptlib.MultisigClosure:
65+
pubkeys = c.PubKeys
66+
case *scriptlib.CSVMultisigClosure:
67+
pubkeys = c.PubKeys
68+
case *scriptlib.CLTVMultisigClosure:
69+
pubkeys = c.PubKeys
70+
case *scriptlib.ConditionMultisigClosure:
71+
pubkeys = c.PubKeys
72+
case *scriptlib.ConditionCSVMultisigClosure:
73+
pubkeys = c.PubKeys
74+
default:
75+
return nil, fmt.Errorf("unsupported closure type: %T", closure)
6476
}
6577

6678
found := false
67-
for _, pubkey := range tapscript.PubKeys {
79+
for _, pubkey := range pubkeys {
6880
xonly := schnorr.SerializePubKey(pubkey)
6981
if bytes.Equal(xonly, expectedPublicKeyXonly) {
7082
found = true

pkg/arkade/script_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package arkade
2+
3+
import (
4+
"encoding/hex"
5+
"encoding/json"
6+
"os"
7+
"strings"
8+
"testing"
9+
10+
"github.com/btcsuite/btcd/btcec/v2"
11+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
12+
"github.com/btcsuite/btcd/btcutil/psbt"
13+
"github.com/btcsuite/btcd/txscript"
14+
"github.com/btcsuite/btcd/wire"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
19+
func TestReadArkadeScript(t *testing.T) {
20+
fix := readScriptFixtures(t)
21+
22+
t.Run("valid", func(t *testing.T) {
23+
for _, f := range fix.Valid {
24+
t.Run(f.Name, func(t *testing.T) {
25+
ptx := decodePSBT(t, f.Psbt)
26+
signerPubKey := decodeXOnlyPubKey(t, f.SignerPublicKey)
27+
entry := decodeEntry(t, f.Entry)
28+
29+
result, err := ReadArkadeScript(ptx, signerPubKey, entry)
30+
require.NoError(t, err)
31+
require.NotNil(t, result)
32+
33+
require.Equal(t, entry.Script, result.script)
34+
require.Equal(t, ArkadeScriptHash(entry.Script), result.hash)
35+
require.Equal(t, len(entry.Witness), len(result.witness))
36+
for i := range entry.Witness {
37+
require.Equal(t, entry.Witness[i], result.witness[i])
38+
}
39+
40+
expectedPubKey := ComputeArkadeScriptPublicKey(signerPubKey, result.hash)
41+
require.True(t, expectedPubKey.IsEqual(result.pubkey))
42+
43+
tapscript := ptx.Inputs[entry.Vin].TaprootLeafScript[0].Script
44+
require.Equal(t, txscript.NewBaseTapLeaf(tapscript), result.tapLeaf)
45+
})
46+
}
47+
})
48+
49+
t.Run("invalid", func(t *testing.T) {
50+
for _, f := range fix.Invalid {
51+
t.Run(f.Name, func(t *testing.T) {
52+
ptx := decodePSBT(t, f.Psbt)
53+
signerPubKey := decodeXOnlyPubKey(t, f.SignerPublicKey)
54+
entry := decodeEntry(t, f.Entry)
55+
56+
_, err := ReadArkadeScript(ptx, signerPubKey, entry)
57+
require.Error(t, err)
58+
require.Contains(t, err.Error(), f.ErrorContains)
59+
})
60+
}
61+
})
62+
}
63+
64+
type scriptFixtureEntry struct {
65+
Vin int `json:"vin"`
66+
Script string `json:"script"`
67+
Witness []string `json:"witness"`
68+
}
69+
70+
type validScriptFixture struct {
71+
Name string `json:"name"`
72+
SignerPublicKey string `json:"signerPublicKey"`
73+
Psbt string `json:"psbt"`
74+
Entry scriptFixtureEntry `json:"entry"`
75+
}
76+
77+
type invalidScriptFixture struct {
78+
Name string `json:"name"`
79+
SignerPublicKey string `json:"signerPublicKey"`
80+
Psbt string `json:"psbt"`
81+
Entry scriptFixtureEntry `json:"entry"`
82+
ErrorContains string `json:"errorContains"`
83+
}
84+
85+
type scriptFixtures struct {
86+
Valid []validScriptFixture `json:"valid"`
87+
Invalid []invalidScriptFixture `json:"invalid"`
88+
}
89+
90+
91+
func readScriptFixtures(t *testing.T) scriptFixtures {
92+
t.Helper()
93+
data, err := os.ReadFile("testdata/read_arkade_script.json")
94+
require.NoError(t, err)
95+
96+
var fix scriptFixtures
97+
require.NoError(t, json.Unmarshal(data, &fix))
98+
return fix
99+
}
100+
101+
func decodePSBT(t *testing.T, b64 string) *psbt.Packet {
102+
t.Helper()
103+
ptx, err := psbt.NewFromRawBytes(strings.NewReader(b64), true)
104+
require.NoError(t, err)
105+
return ptx
106+
}
107+
108+
func decodeXOnlyPubKey(t *testing.T, hexStr string) *btcec.PublicKey {
109+
t.Helper()
110+
data, err := hex.DecodeString(hexStr)
111+
require.NoError(t, err)
112+
pubKey, err := schnorr.ParsePubKey(data)
113+
require.NoError(t, err)
114+
return pubKey
115+
}
116+
117+
func decodeEntry(t *testing.T, raw scriptFixtureEntry) IntrospectorEntry {
118+
t.Helper()
119+
script, err := hex.DecodeString(raw.Script)
120+
require.NoError(t, err)
121+
122+
witness := make(wire.TxWitness, len(raw.Witness))
123+
for i, w := range raw.Witness {
124+
witness[i], err = hex.DecodeString(w)
125+
require.NoError(t, err)
126+
}
127+
128+
return IntrospectorEntry{
129+
Vin: uint16(raw.Vin),
130+
Script: script,
131+
Witness: witness,
132+
}
133+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
{
2+
"valid": [
3+
{
4+
"name": "multisig closure (checksig)",
5+
"signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
6+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARSBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZq0g0jHBebSFpXbjA9Dk50Bdv1PRbqKWwJAw9xEERODAz2uswAAA",
7+
"entry": {
8+
"vin": 0,
9+
"script": "51",
10+
"witness": []
11+
}
12+
},
13+
{
14+
"name": "multisig closure (checksigadd)",
15+
"signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
16+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARyBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZqwg0jHBebSFpXbjA9Dk50Bdv1PRbqKWwJAw9xEERODAz2u6UpzAAAA=",
17+
"entry": {
18+
"vin": 0,
19+
"script": "51",
20+
"witness": []
21+
}
22+
},
23+
{
24+
"name": "csv multisig closure",
25+
"signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
26+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrASFqydSBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZq0g0jHBebSFpXbjA9Dk50Bdv1PRbqKWwJAw9xEERODAz2uswAAA",
27+
"entry": {
28+
"vin": 0,
29+
"script": "51",
30+
"witness": []
31+
}
32+
},
33+
{
34+
"name": "cltv multisig closure",
35+
"signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
36+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrASQFksXUgTUts0TYQMsqb0q652QCqTUXZ6tgKyUIzdMRRpyVNB2atINIxwXm0haV24wPQ5OdAXb9T0W6ilsCQMPcRBETgwM9rrMAAAA==",
37+
"entry": {
38+
"vin": 0,
39+
"script": "51",
40+
"witness": []
41+
}
42+
},
43+
{
44+
"name": "condition multisig closure",
45+
"signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
46+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAR1FpIE1LbNE2EDLKm9KuudkAqk1F2erYCslCM3TEUaclTQdmrSDSMcF5tIWlduMD0OTnQF2/U9FuopbAkDD3EQRE4MDPa6zAAAA=",
47+
"entry": {
48+
"vin": 0,
49+
"script": "51",
50+
"witness": []
51+
}
52+
},
53+
{
54+
"name": "condition csv multisig closure",
55+
"signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
56+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrASlFpWrJ1IE1LbNE2EDLKm9KuudkAqk1F2erYCslCM3TEUaclTQdmrSDSMcF5tIWlduMD0OTnQF2/U9FuopbAkDD3EQRE4MDPa6zAAAA=",
57+
"entry": {
58+
"vin": 0,
59+
"script": "51",
60+
"witness": []
61+
}
62+
},
63+
{
64+
"name": "with witness data",
65+
"signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
66+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARSDSMcF5tIWlduMD0OTnQF2/U9FuopbAkDD3EQRE4MDPa60gTUts0TYQMsqb0q652QCqTUXZ6tgKyUIzdMRRpyVNB2aswAAA",
67+
"entry": {
68+
"vin": 0,
69+
"script": "51",
70+
"witness": [
71+
"deadbeef",
72+
"cafebabe"
73+
]
74+
}
75+
}
76+
],
77+
"invalid": [
78+
{
79+
"name": "input index out of range",
80+
"signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
81+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARSBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZq0g0jHBebSFpXbjA9Dk50Bdv1PRbqKWwJAw9xEERODAz2uswAAA",
82+
"entry": {
83+
"vin": 2,
84+
"script": "51",
85+
"witness": []
86+
},
87+
"errorContains": "input index out of range"
88+
},
89+
{
90+
"name": "no taproot leaf script",
91+
"signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
92+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
93+
"entry": {
94+
"vin": 0,
95+
"script": "51",
96+
"witness": []
97+
},
98+
"errorContains": "TaprootLeafScript"
99+
},
100+
{
101+
"name": "invalid tapscript",
102+
"signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
103+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrABd6tvu/AAAA=",
104+
"entry": {
105+
"vin": 0,
106+
"script": "51",
107+
"witness": []
108+
},
109+
"errorContains": "failed to decode tapscript"
110+
},
111+
{
112+
"name": "tweaked pubkey not found (wrong signer)",
113+
"signerPublicKey": "462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b",
114+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARSBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZq0g0jHBebSFpXbjA9Dk50Bdv1PRbqKWwJAw9xEERODAz2uswAAA",
115+
"entry": {
116+
"vin": 0,
117+
"script": "51",
118+
"witness": []
119+
},
120+
"errorContains": "tweaked arkade script public key not found"
121+
},
122+
{
123+
"name": "tweaked pubkey not found (wrong arkade script)",
124+
"signerPublicKey": "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f",
125+
"psbt": "cHNidP8BAFICAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AegDAAAAAAAAFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIVwFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrARSBNS2zRNhAyypvSrrnZAKpNRdnq2ArJQjN0xFGnJU0HZq0gUx/mBoE0UD0nIxMyJ8hnrI+myDxTfppEw8W9vcsf4zeswAAA",
126+
"entry": {
127+
"vin": 0,
128+
"script": "51",
129+
"witness": []
130+
},
131+
"errorContains": "tweaked arkade script public key not found"
132+
}
133+
]
134+
}

test/tx_test.go

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -934,24 +934,9 @@ func TestIntrospectorRejectsInvalidArkadeScript(t *testing.T) {
934934
ptx.Inputs[0].TaprootLeafScript = nil
935935
},
936936
},
937-
{
938-
name: "non-multisig tapscript",
939-
contains: "spendingtapscript is not a MultisigClosure",
940-
entry: arkade.IntrospectorEntry{
941-
Vin: 0,
942-
Script: arkadeScript,
943-
},
944-
mutateTx: func(t *testing.T, ptx *psbt.Packet) {
945-
t.Helper()
946-
require.NotEmpty(t, ptx.Inputs)
947-
require.NotEmpty(t, ptx.Inputs[0].TaprootLeafScript)
948-
require.NotNil(t, ptx.Inputs[0].TaprootLeafScript[0])
949-
ptx.Inputs[0].TaprootLeafScript[0].Script = []byte{txscript.OP_TRUE}
950-
},
951-
},
952937
{
953938
name: "malformed tapscript decode",
954-
contains: "unexpected error while decoding tapscript",
939+
contains: "failed to decode tapscript",
955940
entry: arkade.IntrospectorEntry{
956941
Vin: 0,
957942
Script: arkadeScript,
@@ -1013,7 +998,7 @@ func TestIntrospectorRejectsInvalidArkadeScript(t *testing.T) {
1013998
require.Len(t, packet, 1)
1014999

10151000
entry := packet[0]
1016-
_, err = arkade.ReadArkadeScript(invalidTx, int(entry.Vin), introspectorPublicKey, entry)
1001+
_, err = arkade.ReadArkadeScript(invalidTx, introspectorPublicKey, entry)
10171002
require.Error(t, err)
10181003
require.Contains(t, err.Error(), tc.contains)
10191004
}

test/utils_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,7 @@ func debugExecuteArkadeScripts(t *testing.T, ptx *psbt.Packet, signerPublicKey *
728728

729729
for _, entry := range packet {
730730
inputIndex := int(entry.Vin)
731-
script, err := arkade.ReadArkadeScript(ptx, inputIndex, signerPublicKey, entry)
731+
script, err := arkade.ReadArkadeScript(ptx, signerPublicKey, entry)
732732
if err != nil {
733733
return fmt.Errorf("failed to read arkade script at input %d: %w", inputIndex, err)
734734
}

0 commit comments

Comments
 (0)