diff --git a/internal/staging/stdscript/error.go b/internal/staging/stdscript/error.go index 70221bd8..2aaf1ba2 100644 --- a/internal/staging/stdscript/error.go +++ b/internal/staging/stdscript/error.go @@ -20,6 +20,11 @@ const ( // ErrPubKeyType is returned when a script contains invalid public keys. ErrPubKeyType = ErrorKind("ErrPubKeyType") + + // ErrTooMuchNullData is returned when attempting to generate a + // provably-pruneable script with data that exceeds the maximum allowed + // length. + ErrTooMuchNullData = ErrorKind("ErrTooMuchNullData") ) // Error satisfies the error interface and prints human-readable errors. diff --git a/internal/staging/stdscript/error_test.go b/internal/staging/stdscript/error_test.go index 0d61245b..20353fae 100644 --- a/internal/staging/stdscript/error_test.go +++ b/internal/staging/stdscript/error_test.go @@ -19,6 +19,7 @@ func TestErrorKindStringer(t *testing.T) { {ErrUnsupportedScriptVersion, "ErrUnsupportedScriptVersion"}, {ErrTooManyRequiredSigs, "ErrTooManyRequiredSigs"}, {ErrPubKeyType, "ErrPubKeyType"}, + {ErrTooMuchNullData, "ErrTooMuchNullData"}, } for i, test := range tests { diff --git a/internal/staging/stdscript/scriptv0.go b/internal/staging/stdscript/scriptv0.go index 721d364f..936275d0 100644 --- a/internal/staging/stdscript/scriptv0.go +++ b/internal/staging/stdscript/scriptv0.go @@ -722,6 +722,21 @@ func MultiSigScriptV0(threshold int, pubKeys ...[]byte) ([]byte, error) { return builder.Script() } +// ProvablyPruneableScriptV0 returns a valid version 0 provably-pruneable script +// which consists of an OP_RETURN followed by the passed data. An Error with +// kind ErrTooMuchNullData will be returned if the length of the passed data +// exceeds MaxDataCarrierSizeV0. +func ProvablyPruneableScriptV0(data []byte) ([]byte, error) { + if len(data) > MaxDataCarrierSizeV0 { + str := fmt.Sprintf("data size %d is larger than max allowed size %d", + len(data), MaxDataCarrierSizeV0) + return nil, makeError(ErrTooMuchNullData, str) + } + + builder := txscript.NewScriptBuilder() + return builder.AddOp(txscript.OP_RETURN).AddData(data).Script() +} + // AtomicSwapDataPushesV0 houses the data pushes found in hash-based atomic swap // contracts using version 0 scripts. type AtomicSwapDataPushesV0 struct { diff --git a/internal/staging/stdscript/scriptv0_test.go b/internal/staging/stdscript/scriptv0_test.go index 966a5f79..bc11c28c 100644 --- a/internal/staging/stdscript/scriptv0_test.go +++ b/internal/staging/stdscript/scriptv0_test.go @@ -32,7 +32,7 @@ func hexToBytes(s string) []byte { // test global versus inside a specific test function scope since it spans // multiple tests and benchmarks. var scriptV0Tests = func() []scriptTest { - // Convience function that combines fmt.Sprintf with mustParseShortForm + // Convenience function that combines fmt.Sprintf with mustParseShortForm // to create more compact tests. p := func(format string, a ...interface{}) []byte { const scriptVersion = 0 @@ -1235,6 +1235,85 @@ func TestExtractTreasuryGenScriptHashV0(t *testing.T) { } } +// TestProvablyPruneableScriptV0 ensures generating a version 0 +// provably-pruneable nulldata script works as intended. +func TestProvablyPruneableScriptV0(t *testing.T) { + // Convenience function that closes over the script version and invokes + // mustParseShortForm to create more compact tests. + const scriptVersion = 0 + p := func(format string, a ...interface{}) []byte { + return mustParseShortForm(scriptVersion, fmt.Sprintf(format, a...)) + } + + tests := []struct { + name string + data []byte + expected []byte + err error + typ ScriptType + }{{ + name: "small int", + data: hexToBytes("01"), + expected: p("RETURN 1"), + err: nil, + typ: STNullData, + }, { + name: "max small int", + data: hexToBytes("10"), + expected: p("RETURN 16"), + err: nil, + typ: STNullData, + }, { + name: "data of size before OP_PUSHDATA1 is needed", + data: bytes.Repeat(hexToBytes("00"), 75), + expected: p("RETURN DATA_75 0x00{75}"), + err: nil, + typ: STNullData, + }, { + name: "one less than max allowed size", + data: bytes.Repeat(hexToBytes("00"), MaxDataCarrierSizeV0-1), + expected: p("RETURN PUSHDATA1 0xff 0x00{255}"), + err: nil, + typ: STNullData, + }, { + name: "max allowed size", + data: bytes.Repeat(hexToBytes("00"), MaxDataCarrierSizeV0), + expected: p("RETURN PUSHDATA2 0x0001 0x00{256}"), + err: nil, + typ: STNullData, + }, { + name: "too big", + data: bytes.Repeat(hexToBytes("00"), MaxDataCarrierSizeV0+1), + expected: nil, + err: ErrTooMuchNullData, + typ: STNonStandard, + }} + + for _, test := range tests { + script, err := ProvablyPruneableScriptV0(test.data) + if !errors.Is(err, test.err) { + t.Errorf("%q: unexpected error - got %v, want %v", test.name, err, + test.err) + continue + } + + // Ensure the expected script was generated. + if !bytes.Equal(script, test.expected) { + t.Errorf("%q: unexpected script -- got: %x, want: %x", test.name, + script, test.expected) + continue + } + + // Ensure the script has the correct type. + scriptType := DetermineScriptType(scriptVersion, script) + if scriptType != test.typ { + t.Errorf("%q: unexpected script type -- got: %v, want: %v", + test.name, scriptType, test.typ) + continue + } + } +} + // expectedAtomicSwapDataV0 is a convenience function that converts the passed // parameters into an expected version 0 atomic swap data pushes structure. func expectedAtomicSwapDataV0(recipientHash, refundHash, secretHash string, secretSize, lockTime int64) *AtomicSwapDataPushesV0 {