Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/application/finalization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/application/intent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/application/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
28 changes: 20 additions & 8 deletions pkg/arkade/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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
Expand Down
133 changes: 133 additions & 0 deletions pkg/arkade/script_test.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
134 changes: 134 additions & 0 deletions pkg/arkade/testdata/read_arkade_script.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
19 changes: 2 additions & 17 deletions test/tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion test/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading