mirror of
https://github.com/btcsuite/btcd.git
synced 2026-08-13 12:32:51 +02:00
In this commit, we add a new function, `ScriptTemplate` to make the process of making custom Bitcoin scripts a bit less verbose. ScriptTemplate processes a script template with parameters and returns the corresponding script bytes. This functions allows Bitcoin scripts to be created using a DSL-like syntax, based on Go's templating system. An example of a simple p2pkh template would be: `OP_DUP OP_HASH160 0x14e8948c7afa71b6e6fad621256474b5959e0305 OP_EQUALVERIFY OP_CHECKSIG` Strings that have the `0x` prefix are assumed to byte strings to be pushed ontop of the stack. Integers can be passed as normal. If a value can't be parsed as an integer, then it's assume that it's a byte slice without the 0x prefix. Normal go template operations can be used as well. The params argument houses paramters to pass into the script, for example a local variable storing a computed public key.
344 lines
7.5 KiB
Go
344 lines
7.5 KiB
Go
package txscript
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestScriptTemplateLooksLikeInt tests the looksLikeInt function.
|
|
func TestScriptTemplateLooksLikeInt(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected bool
|
|
}{
|
|
{"123", true},
|
|
{"-123", true},
|
|
{"+123", true},
|
|
{"0", true},
|
|
{"+0", true},
|
|
{"-0", true},
|
|
{"abc", false},
|
|
{"12a", false},
|
|
{"", false},
|
|
{"+", false},
|
|
{"-", false},
|
|
{"++123", false},
|
|
{"--123", false},
|
|
{"+-123", false},
|
|
{"1.23", false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
result := looksLikeInt(test.input)
|
|
require.Equal(
|
|
t, test.expected, result,
|
|
"looksLikeInt(%q) = %v, want %v",
|
|
test.input, result, test.expected,
|
|
)
|
|
}
|
|
}
|
|
|
|
// TestScriptTemplate tests the ScriptTemplate function.
|
|
func TestScriptTemplate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
template string
|
|
params map[string]interface{}
|
|
customFunc map[string]interface{}
|
|
expected string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "simple P2PKH",
|
|
template: "OP_DUP OP_HASH160 " +
|
|
"0x14e8948c7afa71b6e6fad621256474b5959e0305 " +
|
|
"OP_EQUALVERIFY OP_CHECKSIG",
|
|
params: nil,
|
|
expected: "OP_DUP OP_HASH160 " +
|
|
"14e8948c7afa71b6e6fad621256474b5959e0305 " +
|
|
"OP_EQUALVERIFY OP_CHECKSIG",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "with positive integer",
|
|
template: "123 OP_ADD",
|
|
params: nil,
|
|
expected: "7b OP_ADD",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "with negative integer",
|
|
template: "-42 OP_ADD",
|
|
params: nil,
|
|
expected: "aa OP_ADD",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "with zero bytes for OP_CHECKSIG",
|
|
template: "0x0000000000000000000000000000000000000000000000000000000000000000 OP_CHECKSIG",
|
|
params: nil,
|
|
expected: "0000000000000000000000000000000000000000000000000000000000000000 OP_CHECKSIG",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "with hex template function for zero bytes",
|
|
template: "{{ hex .ZeroSig }} OP_CHECKSIG",
|
|
params: map[string]interface{}{
|
|
"ZeroSig": make([]byte, 32),
|
|
},
|
|
expected: "0000000000000000000000000000000000000000000000000000000000000000 OP_CHECKSIG",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "with hex data without 0x prefix",
|
|
template: "abcdef OP_ADD",
|
|
params: nil,
|
|
expected: "abcdef OP_ADD",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "with template parameter",
|
|
template: "OP_DUP OP_HASH160 {{ hex .Pubkey }} " +
|
|
"OP_EQUALVERIFY OP_CHECKSIG",
|
|
params: map[string]interface{}{
|
|
"Pubkey": []byte{
|
|
0x14, 0xe8, 0x94, 0x8c, 0x7a, 0xfa,
|
|
0x71, 0xb6, 0xe6, 0xfa, 0xd6, 0x21,
|
|
0x25, 0x64, 0x74, 0xb5, 0x95, 0x9e,
|
|
0x03, 0x05,
|
|
},
|
|
},
|
|
expected: "OP_DUP OP_HASH160 " +
|
|
"14e8948c7afa71b6e6fad621256474b5959e0305 " +
|
|
"OP_EQUALVERIFY OP_CHECKSIG",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "with range iteration",
|
|
template: `
|
|
{{ range $i := range_iter 1 4 }}
|
|
OP_DUP OP_HASH160
|
|
{{ if eq $i 1 }}
|
|
0x01
|
|
{{ else if eq $i 2 }}
|
|
0x02
|
|
{{ else }}
|
|
0x03
|
|
{{ end }}
|
|
OP_EQUALVERIFY {{ end }}
|
|
OP_CHECKSIG`,
|
|
params: nil,
|
|
expected: "OP_DUP OP_HASH160 1 OP_EQUALVERIFY " +
|
|
"OP_DUP OP_HASH160 2 OP_EQUALVERIFY " +
|
|
"OP_DUP OP_HASH160 3 OP_EQUALVERIFY " +
|
|
"OP_CHECKSIG",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "with custom function",
|
|
template: "{{ add 10 5 }} OP_DROP",
|
|
params: nil,
|
|
customFunc: map[string]interface{}{
|
|
"add": func(a, b int) int {
|
|
return a + b
|
|
},
|
|
},
|
|
expected: "15 OP_DROP",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid opcode",
|
|
template: "OP_UNKNOWN",
|
|
params: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid hex",
|
|
template: "0xZZ",
|
|
params: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid integer",
|
|
template: "9999999999999999999999999999",
|
|
params: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid token",
|
|
template: "not_hex_or_op",
|
|
params: nil,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var opts []ScriptTemplateOption
|
|
if test.params != nil {
|
|
opts = append(
|
|
opts,
|
|
WithScriptTemplateParams(test.params),
|
|
)
|
|
}
|
|
|
|
// Add custom functions if specified.
|
|
for name, fn := range test.customFunc {
|
|
opts = append(
|
|
opts, WithCustomTemplateFunc(name, fn),
|
|
)
|
|
}
|
|
|
|
script, err := ScriptTemplate(test.template, opts...)
|
|
|
|
if test.wantErr {
|
|
require.Error(
|
|
t, err,
|
|
"ScriptTemplate(%q) expected error, got nil",
|
|
test.template,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
require.NoError(
|
|
t, err,
|
|
"ScriptTemplate(%q) unexpected error",
|
|
test.template,
|
|
)
|
|
|
|
// Disassemble the script and compare the string
|
|
// representation.
|
|
disasm, err := DisasmString(script)
|
|
require.NoError(t, err, "Failed to disassemble script")
|
|
|
|
require.Equal(
|
|
t, test.expected, disasm,
|
|
"ScriptTemplate(%q):\ngot: %s\nwant: %s",
|
|
test.template, disasm, test.expected,
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestScriptTemplateOptions tests the ScriptTemplate option functions.
|
|
func TestScriptTemplateOptions(t *testing.T) {
|
|
t.Run("WithScriptTemplateParams", func(t *testing.T) {
|
|
template := "{{ .Value }} OP_DROP"
|
|
params := map[string]interface{}{
|
|
"Value": 42,
|
|
}
|
|
|
|
script, err := ScriptTemplate(
|
|
template, WithScriptTemplateParams(params),
|
|
)
|
|
require.NoError(t, err, "unexpected error")
|
|
|
|
disasm, err := DisasmString(script)
|
|
require.NoError(t, err, "Failed to disassemble script")
|
|
|
|
expected := "2a OP_DROP"
|
|
require.Equal(
|
|
t, expected, disasm,
|
|
"ScriptTemplate(%q):\ngot: %s\nwant: %s",
|
|
template, disasm, expected,
|
|
)
|
|
})
|
|
|
|
t.Run("WithCustomTemplateFunc", func(t *testing.T) {
|
|
template := "{{ multiply 6 7 }} OP_DROP"
|
|
script, err := ScriptTemplate(
|
|
template,
|
|
WithCustomTemplateFunc("multiply", func(a, b int) int {
|
|
return a * b
|
|
}),
|
|
)
|
|
require.NoError(t, err, "Unexpected error")
|
|
|
|
disasm, err := DisasmString(script)
|
|
require.NoError(t, err, "Failed to disassemble script")
|
|
|
|
expected := "2a OP_DROP"
|
|
require.Equal(
|
|
t, expected, disasm,
|
|
"ScriptTemplate(%q):\ngot: %s\nwant: %s",
|
|
template, disasm, expected,
|
|
)
|
|
})
|
|
}
|
|
|
|
// TestScriptTemplateHelperFunctions tests the helper functions used in
|
|
// templates.
|
|
func TestScriptTemplateHelperFunctions(t *testing.T) {
|
|
t.Run("rangeIter", func(t *testing.T) {
|
|
result := rangeIter(2, 5)
|
|
expected := []int{2, 3, 4}
|
|
|
|
require.Equal(
|
|
t, expected, result,
|
|
"rangeIter(2, 5) returned unexpected result",
|
|
)
|
|
})
|
|
|
|
t.Run("hexEncode", func(t *testing.T) {
|
|
input := []byte{0x12, 0x34, 0x56}
|
|
result := hexEncode(input)
|
|
expected := "0x123456"
|
|
|
|
require.Equal(
|
|
t, expected, result,
|
|
"hexEncode(%v) = %q, want %q",
|
|
input, result, expected,
|
|
)
|
|
})
|
|
|
|
t.Run("hexStr", func(t *testing.T) {
|
|
input := []byte{0x12, 0x34, 0x56}
|
|
result := hexStr(input)
|
|
expected := "123456"
|
|
|
|
require.Equal(
|
|
t, expected, result,
|
|
"hexStr(%v) = %q, want %q",
|
|
input, result, expected,
|
|
)
|
|
})
|
|
|
|
t.Run("hexDecode", func(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected []byte
|
|
wantErr bool
|
|
}{
|
|
{"123456", []byte{0x12, 0x34, 0x56}, false},
|
|
{"0x123456", []byte{0x12, 0x34, 0x56}, false},
|
|
{"zz", nil, true},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
result, err := hexDecode(test.input)
|
|
|
|
if test.wantErr {
|
|
require.Error(
|
|
t, err,
|
|
"hexDecode(%q) expected error, got nil",
|
|
test.input,
|
|
)
|
|
continue
|
|
}
|
|
|
|
require.NoError(
|
|
t, err,
|
|
"hexDecode(%q) unexpected error",
|
|
test.input,
|
|
)
|
|
|
|
require.Equal(
|
|
t, test.expected, result,
|
|
"hexDecode(%q) = %v, want %v",
|
|
test.input, result, test.expected,
|
|
)
|
|
}
|
|
})
|
|
}
|