224 lines
6.6 KiB
Go
224 lines
6.6 KiB
Go
package txscript
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"unicode/utf8"
|
|
|
|
"github.com/lbryio/lbcd/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 <Name> <Value> OP_2DROP OP_DROP <P2PKH>
|
|
// OP_SUPPORTCLAIM <Name> <ClaimID> OP_2DROP OP_DROP <P2PKH>
|
|
// OP_UPDATECLAIM <Name> <ClaimID> <Value> OP_2DROP OP_2DROP <P2PKH>
|
|
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
|
|
}
|