Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stdscript: Add provably pruneable script support. #2803

Merged
merged 3 commits into from
Nov 17, 2021
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
16 changes: 16 additions & 0 deletions internal/staging/stdscript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,22 @@ need to return interfaces that callers would have to type assert based on the
version thereby defeating the original intention of using a version-agnostic
method to begin with.

### Provably Pruneable Scripts

A provably pruneable script is a public key script that is of a specific form
that is provably unspendable and therefore is safe to prune from the set of
unspent transaction outputs. They are primarily useful for anchoring
commitments into the blockchain and are the preferred method to achieve that
goal.

This package provides the version-specific `ProvablyPruneableScriptV0` method
for this purpose.

Note that no version-agnostic variant of the method that accepts a dynamic
version is provided since the exact details of what is considered standard is
likely to change between scripting language versions, so callers will
necessarily have to ensure appropriate data is provided based on the version.

### Additional Convenience Methods

As mentioned in the overview, standardness only applies to public key scripts.
Expand Down
5 changes: 5 additions & 0 deletions internal/staging/stdscript/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions internal/staging/stdscript/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func TestErrorKindStringer(t *testing.T) {
{ErrUnsupportedScriptVersion, "ErrUnsupportedScriptVersion"},
{ErrTooManyRequiredSigs, "ErrTooManyRequiredSigs"},
{ErrPubKeyType, "ErrPubKeyType"},
{ErrTooMuchNullData, "ErrTooMuchNullData"},
}

for i, test := range tests {
Expand Down
28 changes: 25 additions & 3 deletions internal/staging/stdscript/scriptv0.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import (
"github.com/decred/dcrd/txscript/v4"
)

const (
// MaxDataCarrierSizeV0 is the maximum number of bytes allowed in pushed
// data to be considered a standard version 0 provably pruneable nulldata
// script.
MaxDataCarrierSizeV0 = 256
)

// ExtractCompressedPubKeyV0 extracts a compressed public key from the passed
// script if it is a standard version 0 pay-to-compressed-secp256k1-pubkey
// script. It will return nil otherwise.
Expand Down Expand Up @@ -408,7 +415,7 @@ func IsNullDataScriptV0(script []byte) bool {
// OP_RETURN <optional data>
//
// Thus, it can either be a single OP_RETURN or an OP_RETURN followed by a
// canonical data push up to MaxDataCarrierSize bytes.
// canonical data push up to MaxDataCarrierSizeV0 bytes.

// The script can't possibly be a null data script if it doesn't start
// with OP_RETURN. Fail fast to avoid more work below.
Expand All @@ -421,12 +428,12 @@ func IsNullDataScriptV0(script []byte) bool {
return true
}

// OP_RETURN followed by a canonical data push up to MaxDataCarrierSize
// OP_RETURN followed by a canonical data push up to MaxDataCarrierSizeV0
// bytes in length.
const scriptVersion = 0
tokenizer := txscript.MakeScriptTokenizer(scriptVersion, script[1:])
return tokenizer.Next() && tokenizer.Done() &&
len(tokenizer.Data()) <= txscript.MaxDataCarrierSize &&
len(tokenizer.Data()) <= MaxDataCarrierSizeV0 &&
isCanonicalPushV0(tokenizer.Opcode(), tokenizer.Data())
}

Expand Down Expand Up @@ -715,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 {
Expand Down
81 changes: 80 additions & 1 deletion internal/staging/stdscript/scriptv0_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down