github.com/lbryio/lbcd@v0.22.119/txscript/claimscript.go (about)

     1  package txscript
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"unicode/utf8"
     7  )
     8  
     9  const (
    10  	// MaxClaimScriptSize is the max claim script size in bytes, not including the script pubkey part of the script.
    11  	MaxClaimScriptSize = 8192
    12  
    13  	// MaxClaimNameSize is the max claim name size in bytes, for all claim trie transactions.
    14  	MaxClaimNameSize = 255
    15  
    16  	ClaimIDLength = 160 / 8
    17  
    18  	claimScriptVersion = 0
    19  )
    20  
    21  // These constants are used to identify a specific claim script Error.
    22  // The error code starts from 200, which leaves enough room between the rest
    23  // of script error codes (numErrorCodes)
    24  const (
    25  	// ErrNotClaimScript is returned when the script does not have a ClaimScript Opcode.
    26  	ErrNotClaimScript ErrorCode = iota + 200
    27  
    28  	// ErrInvalidClaimNameScript is returned a claim name script does not conform to the format.
    29  	ErrInvalidClaimNameScript
    30  
    31  	// ErrInvalidClaimSupportScript is returned a claim support script does not conform to the format.
    32  	ErrInvalidClaimSupportScript
    33  
    34  	// ErrInvalidClaimUpdateScript is returned a claim update script does not conform to the format.
    35  	ErrInvalidClaimUpdateScript
    36  
    37  	// ErrInvalidClaimName is returned when the claim name is invalid.
    38  	ErrInvalidClaimName
    39  )
    40  
    41  func claimScriptError(c ErrorCode, desc string) Error {
    42  	return Error{ErrorCode: c, Description: desc}
    43  }
    44  
    45  // ClaimNameScript creates a claim name script.
    46  func ClaimNameScript(name string, value string) ([]byte, error) {
    47  	return NewScriptBuilder().AddOp(OP_CLAIMNAME).AddData([]byte(name)).AddData([]byte(value)).
    48  		AddOp(OP_2DROP).AddOp(OP_DROP).AddOp(OP_TRUE).Script()
    49  }
    50  
    51  // ClaimSupportScript creates a support claim script.
    52  func ClaimSupportScript(name string, claimID []byte, value []byte) ([]byte, error) {
    53  	builder := NewScriptBuilder().AddOp(OP_SUPPORTCLAIM).AddData([]byte(name)).AddData(claimID)
    54  	if len(value) > 0 {
    55  		return builder.addData(value).AddOp(OP_2DROP).AddOp(OP_2DROP).AddOp(OP_TRUE).Script()
    56  	}
    57  	return builder.AddOp(OP_2DROP).AddOp(OP_DROP).AddOp(OP_TRUE).Script()
    58  }
    59  
    60  // ClaimUpdateScript creates an update claim script.
    61  func ClaimUpdateScript(name string, claimID []byte, value string) ([]byte, error) {
    62  	return NewScriptBuilder().AddOp(OP_UPDATECLAIM).AddData([]byte(name)).AddData(claimID).AddData([]byte(value)).
    63  		AddOp(OP_2DROP).AddOp(OP_2DROP).AddOp(OP_TRUE).Script()
    64  }
    65  
    66  // ClaimScript represents of one of the ClaimNameScript, ClaimSupportScript, and ClaimUpdateScript.
    67  type ClaimScript struct {
    68  	Opcode  byte
    69  	Name    []byte
    70  	ClaimID []byte
    71  	Value   []byte
    72  	Size    int
    73  }
    74  
    75  // ExtractClaimScript exctracts the claim script from the script if it has one.
    76  // The returned ClaimScript is invalidated if the given script is modified.
    77  func ExtractClaimScript(script []byte) (*ClaimScript, error) {
    78  
    79  	var cs ClaimScript
    80  
    81  	tokenizer := MakeScriptTokenizer(claimScriptVersion, script)
    82  	if !tokenizer.Next() {
    83  		return nil, claimScriptError(ErrNotClaimScript, "not a claim script opcode")
    84  	}
    85  
    86  	cs.Opcode = tokenizer.Opcode()
    87  
    88  	switch tokenizer.Opcode() {
    89  	case OP_CLAIMNAME:
    90  		// OP_CLAIMNAME <Name> <Value> OP_2DROP OP_DROP <P2PKH>
    91  		if !tokenizer.Next() || len(tokenizer.Data()) > MaxClaimNameSize {
    92  			str := fmt.Sprintf("name size %d exceeds limit %d", len(tokenizer.data), MaxClaimNameSize)
    93  			return nil, claimScriptError(ErrInvalidClaimNameScript, str)
    94  		}
    95  		cs.Name = tokenizer.data
    96  
    97  		if !tokenizer.Next() {
    98  			return nil, claimScriptError(ErrInvalidClaimNameScript, "expect value")
    99  		}
   100  		cs.Value = tokenizer.Data()
   101  
   102  		if !tokenizer.Next() || tokenizer.Opcode() != OP_2DROP ||
   103  			!tokenizer.Next() || tokenizer.Opcode() != OP_DROP {
   104  			return nil, claimScriptError(ErrInvalidClaimNameScript, "expect OP_2DROP OP_DROP")
   105  		}
   106  
   107  		cs.Size = int(tokenizer.ByteIndex())
   108  		if cs.Size > MaxClaimScriptSize {
   109  			str := fmt.Sprintf("script size %d exceeds limit %d", cs.Size, MaxClaimScriptSize)
   110  			return nil, claimScriptError(ErrInvalidClaimNameScript, str)
   111  		}
   112  
   113  		return &cs, nil
   114  
   115  	case OP_SUPPORTCLAIM:
   116  		// OP_SUPPORTCLAIM <Name> <ClaimID>         OP_2DROP OP_DROP <P2PKH>
   117  		// OP_SUPPORTCLAIM <Name> <ClaimID> <Value> OP_2DROP OP_2DROP <P2PKH>
   118  		if !tokenizer.Next() || len(tokenizer.Data()) > MaxClaimNameSize {
   119  			str := fmt.Sprintf("name size %d exceeds limit %d", len(tokenizer.data), MaxClaimNameSize)
   120  			return nil, claimScriptError(ErrInvalidClaimSupportScript, str)
   121  		}
   122  		cs.Name = tokenizer.data
   123  
   124  		if !tokenizer.Next() || len(tokenizer.Data()) != ClaimIDLength {
   125  			str := fmt.Sprintf("expect claim id length %d, instead of %d", ClaimIDLength, len(tokenizer.data))
   126  			return nil, claimScriptError(ErrInvalidClaimSupportScript, str)
   127  		}
   128  		cs.ClaimID = tokenizer.Data()
   129  
   130  		if !tokenizer.Next() {
   131  			return nil, claimScriptError(ErrInvalidClaimSupportScript, "incomplete script")
   132  		}
   133  
   134  		switch {
   135  		case tokenizer.Opcode() == OP_2DROP:
   136  			// Case 1: OP_SUPPORTCLAIM <Name> <ClaimID> OP_2DROP OP_DROP <P2PKH>
   137  			if !tokenizer.Next() || tokenizer.Opcode() != OP_DROP {
   138  				return nil, claimScriptError(ErrInvalidClaimSupportScript, "expect OP_2DROP OP_DROP")
   139  			}
   140  
   141  		case len(tokenizer.Data()) != 0:
   142  			// Case 2: OP_SUPPORTCLAIM <Name> <ClaimID> <Dummy Value> OP_2DROP OP_2DROP <P2PKH>
   143  			//         (old bug: non-length size dummy value?)
   144  			cs.Value = tokenizer.Data()
   145  			if !tokenizer.Next() || tokenizer.Opcode() != OP_2DROP ||
   146  				!tokenizer.Next() || tokenizer.Opcode() != OP_2DROP {
   147  				return nil, claimScriptError(ErrInvalidClaimSupportScript, "expect OP_2DROP OP_2DROP")
   148  			}
   149  		default:
   150  			return nil, claimScriptError(ErrInvalidClaimSupportScript, "expect OP_2DROP OP_DROP")
   151  		}
   152  
   153  		cs.Size = int(tokenizer.ByteIndex())
   154  		if cs.Size > MaxClaimScriptSize {
   155  			str := fmt.Sprintf("script size %d exceeds limit %d", cs.Size, MaxClaimScriptSize)
   156  			return nil, claimScriptError(ErrInvalidClaimSupportScript, str)
   157  		}
   158  
   159  		return &cs, nil
   160  
   161  	case OP_UPDATECLAIM:
   162  		// OP_UPDATECLAIM <Name> <ClaimID> <Value> OP_2DROP OP_2DROP <P2PKH>
   163  		if !tokenizer.Next() || len(tokenizer.Data()) > MaxClaimNameSize {
   164  			str := fmt.Sprintf("name size %d exceeds limit %d", len(tokenizer.data), MaxClaimNameSize)
   165  			return nil, claimScriptError(ErrInvalidClaimUpdateScript, str)
   166  		}
   167  		cs.Name = tokenizer.data
   168  
   169  		if !tokenizer.Next() || len(tokenizer.Data()) != ClaimIDLength {
   170  			str := fmt.Sprintf("expect claim id length %d, instead of %d", ClaimIDLength, len(tokenizer.data))
   171  			return nil, claimScriptError(ErrInvalidClaimUpdateScript, str)
   172  		}
   173  		cs.ClaimID = tokenizer.Data()
   174  
   175  		if !tokenizer.Next() {
   176  			str := fmt.Sprintf("expect value")
   177  			return nil, claimScriptError(ErrInvalidClaimUpdateScript, str)
   178  		}
   179  		cs.Value = tokenizer.Data()
   180  
   181  		if !tokenizer.Next() || tokenizer.Opcode() != OP_2DROP ||
   182  			!tokenizer.Next() || tokenizer.Opcode() != OP_2DROP {
   183  			str := fmt.Sprintf("expect OP_2DROP OP_2DROP")
   184  			return nil, claimScriptError(ErrInvalidClaimUpdateScript, str)
   185  		}
   186  
   187  		cs.Size = int(tokenizer.ByteIndex())
   188  		if cs.Size > MaxClaimScriptSize {
   189  			str := fmt.Sprintf("script size %d exceeds limit %d", cs.Size, MaxClaimScriptSize)
   190  			return nil, claimScriptError(ErrInvalidClaimUpdateScript, str)
   191  		}
   192  
   193  		return &cs, nil
   194  
   195  	default:
   196  		return nil, claimScriptError(ErrNotClaimScript, "")
   197  	}
   198  }
   199  
   200  // StripClaimScriptPrefix strips prefixed claim script, if any.
   201  func StripClaimScriptPrefix(script []byte) []byte {
   202  	cs, err := ExtractClaimScript(script)
   203  	if err != nil {
   204  		return script
   205  	}
   206  	return script[cs.Size:]
   207  }
   208  
   209  const illegalChars = "=&#:$%?/;\\\b\n\t\r\x00"
   210  
   211  func AllClaimsAreSane(script []byte, enforceSoftFork bool) error {
   212  	cs, err := ExtractClaimScript(script)
   213  	if IsErrorCode(err, ErrNotClaimScript) {
   214  		return nil
   215  	}
   216  	if err != nil {
   217  		return err
   218  	}
   219  	if enforceSoftFork {
   220  		if !utf8.Valid(cs.Name) {
   221  			return claimScriptError(ErrInvalidClaimName, "claim name is not valid UTF-8")
   222  		}
   223  		if bytes.ContainsAny(cs.Name, illegalChars) {
   224  			str := fmt.Sprintf("claim name has illegal chars; it should not contain any of these: %s", illegalChars)
   225  			return claimScriptError(ErrInvalidClaimName, str)
   226  		}
   227  	}
   228  
   229  	return nil
   230  }