diff --git a/txscript/nameclaim.go b/txscript/nameclaim.go new file mode 100644 index 00000000..bcdf01eb --- /dev/null +++ b/txscript/nameclaim.go @@ -0,0 +1,224 @@ +package txscript + +import ( + "bytes" + "fmt" + "unicode/utf8" + + "github.com/btcsuite/btcd/wire" +) + +const ( + // MinFeePerNameclaimChar is the minimum claim fee per character in the name of an OP_CLAIM_NAME + // command that must be attached to transactions for it to be accepted into the memory pool. + // Rationale: current implementation of the claim trie uses more memory for longer name claims + // due to the fact that each chracater is assigned a trie node regardless of whether it contains + // any claims or not. In the future, we can switch to a radix tree implementation where empty + // nodes do not take up any memory and the minimum fee can be priced on a per claim basis. + MinFeePerNameclaimChar int64 = 200000 + + // 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 +) + +var ( + // ErrNotClaimScript is returned when the script does not have a ClaimScript Opcode. + ErrNotClaimScript = fmt.Errorf("not a claim script") + + // ErrInvalidClaimScript is returned when a script has a ClaimScript Opcode, + // but does not conform to the format. + ErrInvalidClaimScript = fmt.Errorf("invalid claim script") +) + +// ClaimNameScript ... +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() +} + +// SupportClaimScript ... +func SupportClaimScript(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() +} + +// UpdateClaimScript ... +func UpdateClaimScript(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() +} + +// DecodeClaimScript ... +func DecodeClaimScript(script []byte) (*ClaimScript, error) { + if len(script) == 0 { + return nil, ErrNotClaimScript + } + op := script[0] + if op != OP_CLAIMNAME && op != OP_SUPPORTCLAIM && op != OP_UPDATECLAIM { + return nil, ErrNotClaimScript + } + pops, err := parseScript(script) + if err != nil { + return nil, err + } + if isClaimName(pops) || isSupportClaim(pops) || isUpdateClaim(pops) { + cs := &ClaimScript{op: op, pops: pops} + if cs.Size() > MaxClaimScriptSize { + log.Infof("claim script of %d bytes is larger than %d", cs.Size(), MaxClaimScriptSize) + return nil, ErrInvalidClaimScript + } + return cs, nil + } + return nil, ErrInvalidClaimScript +} + +// ClaimScript ... +// OP_CLAIMNAME OP_2DROP OP_DROP +// OP_SUPPORTCLAIM OP_2DROP OP_DROP +// OP_UPDATECLAIM OP_2DROP OP_2DROP +type ClaimScript struct { + op byte + pops []parsedOpcode +} + +// Opcode ... +func (cs *ClaimScript) Opcode() byte { + return cs.op +} + +// Name ... +func (cs *ClaimScript) Name() []byte { + return cs.pops[1].data +} + +// ClaimID ... +func (cs *ClaimScript) ClaimID() []byte { + if cs.op == OP_CLAIMNAME { + return nil + } + return cs.pops[2].data +} + +// Value ... +func (cs *ClaimScript) Value() []byte { + if cs.pops[0].opcode.value == OP_CLAIMNAME { + return cs.pops[2].data + } + return cs.pops[3].data +} + +// Size ... +func (cs *ClaimScript) Size() int { + ops := 5 + if cs.pops[0].opcode.value == OP_UPDATECLAIM { + ops++ + } + size := 0 + for _, op := range cs.pops[:ops] { + if op.opcode.length > 0 { + size += op.opcode.length + continue + } + size += 1 - op.opcode.length + len(op.data) + } + return size +} + +// StripClaimScriptPrefix ... +func StripClaimScriptPrefix(script []byte) []byte { + cs, err := DecodeClaimScript(script) + if err != nil { + return script + } + return script[cs.Size():] +} + +// claimNameSize returns size of the name in a claim script or 0 if script is not a claimtrie transaction. +func claimNameSize(script []byte) int { + cs, err := DecodeClaimScript(script) + if err != nil { + return 0 + } + return len(cs.Name()) +} + +// CalcMinClaimTrieFee calculates the minimum fee (mempool rule) required for transaction. +func CalcMinClaimTrieFee(tx *wire.MsgTx, minFeePerNameClaimChar int64) int64 { + var minFee int64 + for _, txOut := range tx.TxOut { + // TODO maybe: lbrycrd ignored transactions that weren't OP_CLAIMNAME + minFee += int64(claimNameSize(txOut.PkScript)) + } + return minFee * minFeePerNameClaimChar +} + +func isClaimName(pops []parsedOpcode) bool { + return len(pops) > 5 && + pops[0].opcode.value == OP_CLAIMNAME && + // canonicalPush(pops[1]) && + len(pops[1].data) <= MaxClaimNameSize && + // canonicalPush(pops[2]) && + pops[3].opcode.value == OP_2DROP && + pops[4].opcode.value == OP_DROP +} + +func isSupportClaim(pops []parsedOpcode) bool { + prefixed := len(pops) > 5 && + pops[0].opcode.value == OP_SUPPORTCLAIM && + // canonicalPush(pops[1]) && + len(pops[1].data) <= MaxClaimNameSize && + // canonicalPush(pops[2]) && + len(pops[2].data) == 160/8 + + if prefixed && pops[3].opcode.value == OP_2DROP && pops[4].opcode.value == OP_DROP { + return true + } + if prefixed && pops[4].opcode.value == OP_2DROP && pops[5].opcode.value == OP_2DROP { + return len(pops[3].data) > 0 // is this robust enough? + } + return false +} + +func isUpdateClaim(pops []parsedOpcode) bool { + return len(pops) > 6 && + pops[0].opcode.value == OP_UPDATECLAIM && + // canonicalPush(pops[1]) && + len(pops[1].data) <= MaxClaimNameSize && + // canonicalPush(pops[2]) && + len(pops[2].data) == 160/8 && + // canonicalPush(pops[3]) && + pops[4].opcode.value == OP_2DROP && + pops[5].opcode.value == OP_2DROP +} + +const illegalChars = "=&#:*$@%?/;\\\b\n\t\r\x00" + +func AllClaimsAreSane(script []byte, enforceSoftFork bool) error { + cs, err := DecodeClaimScript(script) + if err != ErrNotClaimScript { + if err != nil { + return fmt.Errorf("invalid claim script: %s", err.Error()) + } + if cs.Size() > MaxClaimScriptSize { + return fmt.Errorf("claimscript exceeds max size of %v", MaxClaimScriptSize) + } + if len(cs.Name()) > MaxClaimNameSize { + return fmt.Errorf("claim name exceeds max size of %v", MaxClaimNameSize) + } + 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/nameclaim_test.go b/txscript/nameclaim_test.go new file mode 100644 index 00000000..ed1a07ce --- /dev/null +++ b/txscript/nameclaim_test.go @@ -0,0 +1,87 @@ +package txscript + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCreationParseLoopClaim(t *testing.T) { + + r := require.New(t) + + claim, err := ClaimNameScript("tester", "value") + r.NoError(err) + parsed, err := parseScript(claim) + r.NoError(err) + r.True(isClaimName(parsed)) + r.False(isSupportClaim(parsed)) + r.False(isUpdateClaim(parsed)) + script, err := DecodeClaimScript(claim) + r.NoError(err) + r.Equal([]byte("tester"), script.Name()) + r.Equal([]byte("value"), script.Value()) +} + +func TestCreationParseLoopUpdate(t *testing.T) { + + r := require.New(t) + + claimID := []byte("12345123451234512345") + claim, err := UpdateClaimScript("tester", claimID, "value") + r.NoError(err) + parsed, err := parseScript(claim) + r.NoError(err) + r.False(isSupportClaim(parsed)) + r.False(isClaimName(parsed)) + r.True(isUpdateClaim(parsed)) + script, err := DecodeClaimScript(claim) + + r.NoError(err) + r.Equal([]byte("tester"), script.Name()) + r.Equal(claimID, script.ClaimID()) + r.Equal([]byte("value"), script.Value()) +} + +func TestCreationParseLoopSupport(t *testing.T) { + + r := require.New(t) + + claimID := []byte("12345123451234512345") + claim, err := SupportClaimScript("tester", claimID, []byte("value")) + r.NoError(err) + parsed, err := parseScript(claim) + r.NoError(err) + r.True(isSupportClaim(parsed)) + r.False(isClaimName(parsed)) + r.False(isUpdateClaim(parsed)) + script, err := DecodeClaimScript(claim) + + r.NoError(err) + r.Equal([]byte("tester"), script.Name()) + r.Equal(claimID, script.ClaimID()) + r.Equal([]byte("value"), script.Value()) + + claim, err = SupportClaimScript("tester", claimID, nil) + r.NoError(err) + script, err = DecodeClaimScript(claim) + r.NoError(err) + + r.Equal([]byte("tester"), script.Name()) + r.Equal(claimID, script.ClaimID()) + r.Nil(script.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)) + } +}