From 6d80d906a8e169791494d065e3029777cc168380 Mon Sep 17 00:00:00 2001 From: Roy Lee Date: Thu, 14 Jun 2018 19:13:08 -0700 Subject: [PATCH] [lbry] txscript: introduce claim script Co-authored-by: Brannon King --- txscript/claimscript.go | 216 +++++++++++++++++++++++++++++++++++ txscript/claimscript_test.go | 80 +++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 txscript/claimscript.go create mode 100644 txscript/claimscript_test.go diff --git a/txscript/claimscript.go b/txscript/claimscript.go new file mode 100644 index 00000000..1737ff60 --- /dev/null +++ b/txscript/claimscript.go @@ -0,0 +1,216 @@ +package txscript + +import ( + "bytes" + "fmt" + "unicode/utf8" +) + +const ( + // MaxClaimScriptSize is the max claim script size in bytes, not including the script pubkey part of the script. + MaxClaimScriptSize = 8192 + + // MaxClaimNameSize is the max claim name size in bytes, for all claim trie transactions. + MaxClaimNameSize = 255 + + ClaimIDLength = 160 / 8 + + claimScriptVersion = 0 +) + +// These constants are used to identify a specific claim script Error. +// The error code starts from 200, which leaves enough room between the rest +// of script error codes (numErrorCodes) +const ( + // ErrNotClaimScript is returned when the script does not have a ClaimScript Opcode. + ErrNotClaimScript ErrorCode = iota + 200 + + // ErrInvalidClaimNameScript is returned a claim name script does not conform to the format. + ErrInvalidClaimNameScript + + // ErrInvalidClaimSupportScript is returned a claim support script does not conform to the format. + ErrInvalidClaimSupportScript + + // ErrInvalidClaimUpdateScript is returned a claim update script does not conform to the format. + ErrInvalidClaimUpdateScript +) + +func claimScriptError(c ErrorCode, desc string) Error { + return Error{ErrorCode: c, Description: desc} +} + +// ClaimNameScript creates a claim name script. +func ClaimNameScript(name string, value string) ([]byte, error) { + return NewScriptBuilder().AddOp(OP_CLAIMNAME).AddData([]byte(name)).AddData([]byte(value)). + AddOp(OP_2DROP).AddOp(OP_DROP).AddOp(OP_TRUE).Script() +} + +// ClaimSupportScript creates a support claim script. +func ClaimSupportScript(name string, claimID []byte, value []byte) ([]byte, error) { + builder := NewScriptBuilder().AddOp(OP_SUPPORTCLAIM).AddData([]byte(name)).AddData(claimID) + if len(value) > 0 { + return builder.addData(value).AddOp(OP_2DROP).AddOp(OP_2DROP).AddOp(OP_TRUE).Script() + } + return builder.AddOp(OP_2DROP).AddOp(OP_DROP).AddOp(OP_TRUE).Script() +} + +// ClaimUpdateScript creates an update claim script. +func ClaimUpdateScript(name string, claimID []byte, value string) ([]byte, error) { + return NewScriptBuilder().AddOp(OP_UPDATECLAIM).AddData([]byte(name)).AddData(claimID).AddData([]byte(value)). + AddOp(OP_2DROP).AddOp(OP_2DROP).AddOp(OP_TRUE).Script() +} + +// ClaimScript represents of one of the ClaimNameScript, ClaimSupportScript, and ClaimUpdateScript. +type ClaimScript struct { + Opcode byte + Name []byte + ClaimID []byte + Value []byte + Size int +} + +// ExtractClaimScript exctracts the claim script from the script if it has one. +// The returned ClaimScript is invalidated if the given script is modified. +func ExtractClaimScript(script []byte) (*ClaimScript, error) { + + var cs ClaimScript + + tokenizer := MakeScriptTokenizer(claimScriptVersion, script) + if !tokenizer.Next() { + return nil, claimScriptError(ErrNotClaimScript, "not a claim script opcode") + } + + cs.Opcode = tokenizer.Opcode() + + switch tokenizer.Opcode() { + case OP_CLAIMNAME: + // OP_CLAIMNAME OP_2DROP OP_DROP + if !tokenizer.Next() || len(tokenizer.Data()) > MaxClaimNameSize { + str := fmt.Sprintf("name size %d exceeds limit %d", len(tokenizer.data), MaxClaimNameSize) + return nil, claimScriptError(ErrInvalidClaimNameScript, str) + } + cs.Name = tokenizer.data + + if !tokenizer.Next() { + return nil, claimScriptError(ErrInvalidClaimNameScript, "expect value") + } + cs.Value = tokenizer.Data() + + if !tokenizer.Next() || tokenizer.Opcode() != OP_2DROP || + !tokenizer.Next() || tokenizer.Opcode() != OP_DROP { + str := fmt.Sprintf("expect OP_2DROP OP_DROP") + return nil, claimScriptError(ErrInvalidClaimNameScript, str) + } + + cs.Size = int(tokenizer.ByteIndex()) + return &cs, nil + + case OP_SUPPORTCLAIM: + // OP_SUPPORTCLAIM OP_2DROP OP_DROP + // OP_SUPPORTCLAIM OP_2DROP OP_2DROP + if !tokenizer.Next() || len(tokenizer.Data()) > MaxClaimNameSize { + str := fmt.Sprintf("name size %d exceeds limit %d", len(tokenizer.data), MaxClaimNameSize) + return nil, claimScriptError(ErrInvalidClaimSupportScript, str) + } + cs.Name = tokenizer.data + + if !tokenizer.Next() || len(tokenizer.Data()) != ClaimIDLength { + str := fmt.Sprintf("expect claim id length %d, instead of %d", ClaimIDLength, len(tokenizer.data)) + return nil, claimScriptError(ErrInvalidClaimSupportScript, str) + } + cs.ClaimID = tokenizer.Data() + + if !tokenizer.Next() { + return nil, claimScriptError(ErrInvalidClaimSupportScript, "incomplete script") + } + + switch { + case tokenizer.Opcode() == OP_2DROP: + // Case 1: OP_SUPPORTCLAIM OP_2DROP OP_DROP + if !tokenizer.Next() || tokenizer.Opcode() != OP_DROP { + str := fmt.Sprintf("expect OP_2DROP OP_DROP") + return nil, claimScriptError(ErrInvalidClaimSupportScript, str) + } + + case len(tokenizer.Data()) != 0: + // Case 2: OP_SUPPORTCLAIM OP_2DROP OP_2DROP + // (old bug: non-length size dummy value?) + cs.Value = tokenizer.Data() + if !tokenizer.Next() || tokenizer.Opcode() != OP_2DROP || + !tokenizer.Next() || tokenizer.Opcode() != OP_2DROP { + str := fmt.Sprintf("expect OP_2DROP OP_2DROP") + return nil, claimScriptError(ErrInvalidClaimSupportScript, str) + } + default: + str := fmt.Sprintf("expect OP_2DROP OP_DROP") + return nil, claimScriptError(ErrInvalidClaimSupportScript, str) + } + + cs.Size = int(tokenizer.ByteIndex()) + return &cs, nil + + case OP_UPDATECLAIM: + + // OP_UPDATECLAIM OP_2DROP OP_2DROP + if !tokenizer.Next() || len(tokenizer.Data()) > MaxClaimNameSize { + str := fmt.Sprintf("name size %d exceeds limit %d", len(tokenizer.data), MaxClaimNameSize) + return nil, claimScriptError(ErrInvalidClaimUpdateScript, str) + } + cs.Name = tokenizer.data + + if !tokenizer.Next() || len(tokenizer.Data()) != ClaimIDLength { + str := fmt.Sprintf("expect claim id length %d, instead of %d", ClaimIDLength, len(tokenizer.data)) + return nil, claimScriptError(ErrInvalidClaimUpdateScript, str) + } + cs.ClaimID = tokenizer.Data() + + if !tokenizer.Next() { + str := fmt.Sprintf("expect value") + return nil, claimScriptError(ErrInvalidClaimUpdateScript, str) + } + cs.Value = tokenizer.Data() + + if !tokenizer.Next() || tokenizer.Opcode() != OP_2DROP || + !tokenizer.Next() || tokenizer.Opcode() != OP_2DROP { + str := fmt.Sprintf("expect OP_2DROP OP_2DROP") + return nil, claimScriptError(ErrInvalidClaimUpdateScript, str) + } + + cs.Size = int(tokenizer.ByteIndex()) + return &cs, nil + + default: + return nil, claimScriptError(ErrNotClaimScript, "") + } +} + +// StripClaimScriptPrefix strips prefixed claim script, if any. +func StripClaimScriptPrefix(script []byte) []byte { + cs, err := ExtractClaimScript(script) + if err != nil { + return script + } + return script[cs.Size:] +} + +const illegalChars = "=&#:*$%?/;\\\b\n\t\r\x00" + +func AllClaimsAreSane(script []byte, enforceSoftFork bool) error { + cs, err := ExtractClaimScript(script) + if IsErrorCode(err, ErrNotClaimScript) { + return nil + } + if err != nil { + return err + } + if enforceSoftFork { + if !utf8.Valid(cs.Name) { + return fmt.Errorf("claim name is not valid UTF-8") + } + if bytes.ContainsAny(cs.Name, illegalChars) { + return fmt.Errorf("claim name has illegal chars; it should not contain any of these: %s", illegalChars) + } + } + + return nil +} diff --git a/txscript/claimscript_test.go b/txscript/claimscript_test.go new file mode 100644 index 00000000..ef2d2b02 --- /dev/null +++ b/txscript/claimscript_test.go @@ -0,0 +1,80 @@ +package txscript + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCreationParseLoopClaim(t *testing.T) { + + r := require.New(t) + + // OP_CLAIMNAME OP_2DROP OP_DROP + script, err := ClaimNameScript("tester", "value") + r.NoError(err) + cs, err := ExtractClaimScript(script) + r.NoError(err) + r.Equal(byte(OP_CLAIMNAME), cs.Opcode) + r.Equal([]byte("tester"), cs.Name) + r.Equal([]byte("value"), cs.Value) +} + +func TestCreationParseLoopUpdate(t *testing.T) { + + r := require.New(t) + + claimID := []byte("12345123451234512345") + claim, err := ClaimUpdateScript("tester", claimID, "value") + r.NoError(err) + cs, err := ExtractClaimScript(claim) + r.NoError(err) + r.Equal(byte(OP_UPDATECLAIM), cs.Opcode) + r.Equal([]byte("tester"), cs.Name) + r.Equal(claimID, cs.ClaimID) + r.Equal([]byte("value"), cs.Value) +} + +func TestCreationParseLoopSupport(t *testing.T) { + + r := require.New(t) + + claimID := []byte("12345123451234512345") + + // case 1: OP_SUPPORTCLAIM OP_2DROP OP_DROP + script, err := ClaimSupportScript("tester", claimID, nil) + r.NoError(err) + cs, err := ExtractClaimScript(script) + r.NoError(err) + + r.Equal(byte(OP_SUPPORTCLAIM), cs.Opcode) + r.Equal([]byte("tester"), cs.Name) + r.Equal(claimID, cs.ClaimID) + r.Nil(cs.Value) + + // case 2: OP_SUPPORTCLAIM OP_2DROP OP_2DROP + script, err = ClaimSupportScript("tester", claimID, []byte("value")) + r.NoError(err) + cs, err = ExtractClaimScript(script) + r.NoError(err) + + r.Equal(byte(OP_SUPPORTCLAIM), cs.Opcode) + r.Equal([]byte("tester"), cs.Name) + r.Equal(claimID, cs.ClaimID) + r.Equal([]byte("value"), cs.Value) + +} + +func TestInvalidChars(t *testing.T) { + r := require.New(t) + + script, err := ClaimNameScript("tester", "value") + r.NoError(err) + r.NoError(AllClaimsAreSane(script, true)) + + for i := range []byte(illegalChars) { + script, err := ClaimNameScript("a"+illegalChars[i:i+1], "value") + r.NoError(err) + r.Error(AllClaimsAreSane(script, true)) + } +}