From 3554be58a75230b64b288c6b64c26875e7f071fd Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sun, 12 Nov 2023 16:35:00 -0600 Subject: [PATCH] txscript: add unit tests and test vectors for miniscript This commit adds test vectors taken from https://github.com/rust-bitcoin/rust-miniscript/blob/a0648b3a4d63abbe53f621308614f97f04a04096/src/miniscript/ms_tests.rs to the miniscript implementation. Co-authored-by: Oliver Gugger --- txscript/miniscript/miniscript_test.go | 479 ++ txscript/miniscript/testdata/README.md | 3 + .../testdata/conflict_from_alloy.txt | 1773 +++++ txscript/miniscript/testdata/invalid.txt | 5574 +++++++++++++ .../testdata/invalid_from_alloy.txt | 5574 +++++++++++++ .../testdata/malleable_from_alloy.txt | 7024 +++++++++++++++++ txscript/miniscript/testdata/redeem.json | 297 + .../testdata/valid_8f1e8_from_alloy.txt | 3492 ++++++++ .../miniscript/testdata/valid_from_alloy.txt | 5896 ++++++++++++++ 9 files changed, 30112 insertions(+) create mode 100644 txscript/miniscript/miniscript_test.go create mode 100644 txscript/miniscript/testdata/README.md create mode 100644 txscript/miniscript/testdata/conflict_from_alloy.txt create mode 100644 txscript/miniscript/testdata/invalid.txt create mode 100644 txscript/miniscript/testdata/invalid_from_alloy.txt create mode 100644 txscript/miniscript/testdata/malleable_from_alloy.txt create mode 100644 txscript/miniscript/testdata/redeem.json create mode 100644 txscript/miniscript/testdata/valid_8f1e8_from_alloy.txt create mode 100644 txscript/miniscript/testdata/valid_from_alloy.txt diff --git a/txscript/miniscript/miniscript_test.go b/txscript/miniscript/miniscript_test.go new file mode 100644 index 0000000000..ff407dfb51 --- /dev/null +++ b/txscript/miniscript/miniscript_test.go @@ -0,0 +1,479 @@ +package miniscript + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/stretchr/testify/require" +) + +// TestSplitString tests the splitString function. +func TestSplitString(t *testing.T) { + separators := func(c rune) bool { + return c == '(' || c == ')' || c == ',' + } + + testCases := []struct { + str string + expected []string + }{ + { + str: "", + expected: []string{}, + }, + { + str: "0", + expected: []string{"0"}, + }, + { + str: "0)(1(", + expected: []string{"0", ")", "(", "1", "("}, + }, + { + str: "or_b(pk(key_1),s:pk(key_2))", + expected: []string{ + "or_b", "(", "pk", "(", "key_1", ")", ",", + "s:pk", "(", "key_2", ")", ")", + }, + }, + } + + for _, tc := range testCases { + require.Equal(t, tc.expected, splitString(tc.str, separators)) + } +} + +// checkMiniscript makes sure the passed miniscript is top level, has the +// expected type and script length. +func checkMiniscript(miniscript, expectedType string) error { + node, err := Parse(miniscript) + if err := node.IsValidTopLevel(); err != nil { + return err + } + sortString := func(s string) string { + r := []rune(s) + sort.Slice(r, func(i, j int) bool { + return r[i] < r[j] + }) + return string(r) + } + if sortString(expectedType) != sortString(node.formattedType()) { + return fmt.Errorf("expected type %s, got %s", + sortString(expectedType), + sortString(node.formattedType())) + } + + err = node.ApplyVars(func(identifier string) ([]byte, error) { + if len(identifier) == 64 { + return nil, nil + } + + // Return an arbitrary unique 33 bytes. + return append( + chainhash.HashB([]byte(identifier)), 0, + ), nil + }) + if err != nil { + return err + } + + script, err := node.Script() + if err != nil { + return err + } + + if len(script) != node.scriptLen { + return fmt.Errorf("expected script length %d but got %d for "+ + "script %s", node.scriptLen, len(script), + node.DrawTree()) + } + + return nil +} + +// TestVectors asserts all test vectors in the test data text files pass. +func TestVectors(t *testing.T) { + t.Parallel() + + testCases := []struct { + fileName string + valid bool + }{ + { + // Invalid expressions (failed type check). + fileName: "testdata/invalid_from_alloy.txt", + valid: false, + }, + { + // Valid miniscript expressions including the expected + // type. + fileName: "testdata/valid_8f1e8_from_alloy.txt", + valid: true, + }, + { + // Valid miniscript expressions including the expected + // type. + fileName: "testdata/valid_from_alloy.txt", + valid: true, + }, + { + // Valid expressions but do not contain the `m` type + // property, i.e. the script is guaranteed to have a + // non-malleable satisfaction. + fileName: "testdata/malleable_from_alloy.txt", + valid: true, + }, + { + // miniscripts with time lock mixing in `after` (same + // expression contains both time-based and block-based + // time locks). This unit test is not testing this + // currently, see + // https://github.com/rust-bitcoin/rust-miniscript/issues/514. + fileName: "testdata/conflict_from_alloy.txt", + valid: true, + }, + } + + for _, tc := range testCases { + content, err := os.ReadFile(tc.fileName) + require.NoError(t, err) + + lines := strings.Split(string(content), "\n") + for i, line := range lines { + if line == "" { + continue + } + + if !tc.valid { + _, err := Parse(line) + require.Errorf( + t, err, "failure on line %d: %s", i, + line, + ) + + continue + } + + parts := strings.Split(line, " ") + require.Lenf( + t, parts, 2, "malformed test on line %d: %s", i, + line, + ) + + miniscript, expectedType := parts[0], parts[1] + require.NoError( + t, checkMiniscript(miniscript, expectedType), + "failure on line %d: %s", i, line, + ) + } + } +} + +type testSignFn func(pubKey []byte, hash []byte) (signature []byte, + available bool) + +func testRedeem(t *testing.T, miniscript string, + lookupVar func(identifier string) ([]byte, error), sequence uint32, + sign testSignFn, preimage PreimageFunc) error { + + // We construct a p2wsh() UTXO, which we will spend with a + // satisfaction generated from the miniscript. + node, err := Parse(miniscript) + if err != nil { + return err + } + err = node.IsSane() + if err != nil { + return err + } + err = node.ApplyVars(lookupVar) + if err != nil { + return err + } + t.Logf("Tree for miniscript %v: %v", miniscript, node.DrawTree()) + + t.Logf("Script: %v", scriptStr(node, false)) + + // Create the script. + witnessScript, err := node.Script() + if err != nil { + return err + } + + // Create the p2wsh(