From 8533d513f53c4f3476ef234c2ec7f300bfd5597f Mon Sep 17 00:00:00 2001 From: ben2077 Date: Fri, 22 Mar 2024 19:12:13 +0800 Subject: [PATCH] add impl of op_ctv --- btcutil/psbt/go.sum | 1 + txscript/data/script_tests.json | 9 ++-- txscript/engine.go | 88 +++++++++++++++++++++++++++++++++ txscript/error.go | 2 + txscript/opcode.go | 50 +++++++++++++++++-- txscript/opcode_test.go | 12 ++++- 6 files changed, 153 insertions(+), 9 deletions(-) diff --git a/btcutil/psbt/go.sum b/btcutil/psbt/go.sum index a901223de4..4a874c628e 100644 --- a/btcutil/psbt/go.sum +++ b/btcutil/psbt/go.sum @@ -30,6 +30,7 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= diff --git a/txscript/data/script_tests.json b/txscript/data/script_tests.json index 5c054ed3e8..f9ddda8490 100644 --- a/txscript/data/script_tests.json +++ b/txscript/data/script_tests.json @@ -232,8 +232,8 @@ ["'abcdefghijklmnopqrstuvwxyz'", "HASH256 0x4c 0x20 0xca139bc10c2f660da42666f72e89a225936fc60f193c161124a672050c434671 EQUAL", "P2SH,STRICTENC", "OK"], -["1","NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP4 NOP5 NOP6 NOP7 NOP8 NOP9 NOP10 1 EQUAL", "P2SH,STRICTENC", "OK"], -["'NOP_1_to_10' NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP4 NOP5 NOP6 NOP7 NOP8 NOP9 NOP10","'NOP_1_to_10' EQUAL", "P2SH,STRICTENC", "OK"], +["1","NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP5 NOP6 NOP7 NOP8 NOP9 NOP10 1 EQUAL", "P2SH,STRICTENC", "OK"], +["'NOP_1_to_10' NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP5 NOP6 NOP7 NOP8 NOP9 NOP10","'NOP_1_to_10' EQUAL", "P2SH,STRICTENC", "OK"], ["1", "NOP", "P2SH,STRICTENC,DISCOURAGE_UPGRADABLE_NOPS", "OK", "Discourage NOPx flag allows OP_NOP"], @@ -857,8 +857,8 @@ ["2 2 LSHIFT", "8 EQUAL", "P2SH,STRICTENC", "DISABLED_OPCODE", "disabled"], ["2 1 RSHIFT", "1 EQUAL", "P2SH,STRICTENC", "DISABLED_OPCODE", "disabled"], -["1", "NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP4 NOP5 NOP6 NOP7 NOP8 NOP9 NOP10 2 EQUAL", "P2SH,STRICTENC", "EVAL_FALSE"], -["'NOP_1_to_10' NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP4 NOP5 NOP6 NOP7 NOP8 NOP9 NOP10","'NOP_1_to_11' EQUAL", "P2SH,STRICTENC", "EVAL_FALSE"], +["1", "NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP5 NOP6 NOP7 NOP8 NOP9 NOP10 2 EQUAL", "P2SH,STRICTENC", "EVAL_FALSE"], +["'NOP_1_to_10' NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP5 NOP6 NOP7 NOP8 NOP9 NOP10","'NOP_1_to_11' EQUAL", "P2SH,STRICTENC", "EVAL_FALSE"], ["Ensure 100% coverage of discouraged NOPS"], ["1", "NOP1", "P2SH,DISCOURAGE_UPGRADABLE_NOPS", "DISCOURAGE_UPGRADABLE_NOPS"], @@ -881,7 +881,6 @@ ["1", "IF 0xba ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE", "opcodes above NOP10 invalid if executed"], ["1", "IF 0xbb ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], ["1", "IF 0xbc ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], -["1", "IF 0xbd ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], ["1", "IF 0xbe ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], ["1", "IF 0xbf ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], ["1", "IF 0xc0 ELSE 1 ENDIF", "P2SH,STRICTENC", "BAD_OPCODE"], diff --git a/txscript/engine.go b/txscript/engine.go index 30206152b8..50538f1dbe 100644 --- a/txscript/engine.go +++ b/txscript/engine.go @@ -8,6 +8,7 @@ package txscript import ( "bytes" "crypto/sha256" + "encoding/binary" "fmt" "math/big" "strings" @@ -48,6 +49,11 @@ const ( // being spent. This is BIP0112. ScriptVerifyCheckSequenceVerify + // ScriptVerifyTemplateVerify defines whether to verify that the + // script being executed is a valid template script. + // This is BIP0199. + ScriptVerifyCheckTemplateVerify + // ScriptVerifyCleanStack defines that the stack must contain only // one stack element after evaluation and that the element must be // true if interpreted as a boolean. This is rule 6 of BIP0062. @@ -286,6 +292,8 @@ type Engine struct { inputAmount int64 taprootCtx *taprootExecutionCtx + preComputedData map[string][]byte + // stepCallback is an optional function that will be called every time // a step has been performed during script execution. // @@ -1647,3 +1655,83 @@ func NewDebugEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, vm.stepCallback = stepCallback return vm, nil } + +// GetDefaultCheckTemplatePrecomputedData calculates and caches precomputed data for current MsgTx. +func (vm *Engine) GetDefaultCheckTemplatePrecomputedData() (map[string][]byte, error) { + if vm.preComputedData != nil { + return vm.preComputedData, nil + } + + result := make(map[string][]byte) + var scriptSigs, sequences, outputs bytes.Buffer + + for _, txIn := range vm.tx.TxIn { + err := wire.WriteVarBytes(&scriptSigs, wire.ProtocolVersion, txIn.SignatureScript) + if err != nil { + return nil, err + } + err = binary.Write(&sequences, binary.LittleEndian, txIn.Sequence) + if err != nil { + return nil, err + } + } + if scriptSigs.Len() > 0 { + hash := chainhash.HashB(scriptSigs.Bytes()) + result["scriptSigs"] = hash + } + + if sequences.Len() > 0 { + seqHash := chainhash.HashB(sequences.Bytes()) + result["sequences"] = seqHash + } + + for _, txOut := range vm.tx.TxOut { + outputs.Write(txOut.PkScript) + } + if outputs.Len() > 0 { + outHash := chainhash.HashB(outputs.Bytes()) + result["outputs"] = outHash + } + + vm.preComputedData = result + + return result, nil +} + +// GetDefaultCheckTemplateHash calculates the default check template hash for a given MsgTx. +func (vm *Engine) GetDefaultCheckTemplateHash(nIn int) ([]byte, error) { + preComputedData, err := vm.GetDefaultCheckTemplatePrecomputedData() + + var result bytes.Buffer + err = binary.Write(&result, binary.LittleEndian, vm.tx.Version) + if err != nil { + return nil, err + } + err = binary.Write(&result, binary.LittleEndian, vm.tx.LockTime) + if err != nil { + return nil, err + } + + if scriptSigs, ok := preComputedData["scriptSigs"]; ok { + result.Write(scriptSigs) + } + + err = binary.Write(&result, binary.LittleEndian, uint32(len(vm.tx.TxIn))) + if err != nil { + return nil, err + } + result.Write(preComputedData["sequences"]) + + err = binary.Write(&result, binary.LittleEndian, uint32(len(vm.tx.TxOut))) + if err != nil { + return nil, err + } + result.Write(preComputedData["outputs"]) + + err = binary.Write(&result, binary.LittleEndian, nIn) + if err != nil { + return nil, err + } + + return chainhash.HashB(result.Bytes()), nil +} diff --git a/txscript/error.go b/txscript/error.go index 1f046b9612..3f2d032a5b 100644 --- a/txscript/error.go +++ b/txscript/error.go @@ -411,6 +411,8 @@ const ( // numErrorCodes is the maximum error code number used in tests. This // entry MUST be the last entry in the enum. numErrorCodes + + ErrTemplateMismatch ) // Map of ErrorCode values back to their constant names for pretty printing. diff --git a/txscript/opcode.go b/txscript/opcode.go index 4918b991c5..5199de472b 100644 --- a/txscript/opcode.go +++ b/txscript/opcode.go @@ -220,7 +220,7 @@ const ( OP_CHECKLOCKTIMEVERIFY = 0xb1 // 177 - AKA OP_NOP2 OP_NOP3 = 0xb2 // 178 OP_CHECKSEQUENCEVERIFY = 0xb2 // 178 - AKA OP_NOP3 - OP_NOP4 = 0xb3 // 179 + OP_CHECKTEMPLATEVERIFY = 0xb3 // 179 - AKA OP_NOP4 OP_NOP5 = 0xb4 // 180 OP_NOP6 = 0xb5 // 181 OP_NOP7 = 0xb6 // 182 @@ -422,6 +422,7 @@ var opcodeArray = [256]opcode{ OP_RETURN: {OP_RETURN, "OP_RETURN", 1, opcodeReturn}, OP_CHECKLOCKTIMEVERIFY: {OP_CHECKLOCKTIMEVERIFY, "OP_CHECKLOCKTIMEVERIFY", 1, opcodeCheckLockTimeVerify}, OP_CHECKSEQUENCEVERIFY: {OP_CHECKSEQUENCEVERIFY, "OP_CHECKSEQUENCEVERIFY", 1, opcodeCheckSequenceVerify}, + OP_CHECKTEMPLATEVERIFY: {OP_CHECKTEMPLATEVERIFY, "OP_CHECKTEMPLATEVERIFY", 1, opcodeCheckTemplateVerify}, // Stack opcodes. OP_TOALTSTACK: {OP_TOALTSTACK, "OP_TOALTSTACK", 1, opcodeToAltStack}, @@ -505,7 +506,6 @@ var opcodeArray = [256]opcode{ // Reserved opcodes. OP_NOP1: {OP_NOP1, "OP_NOP1", 1, opcodeNop}, - OP_NOP4: {OP_NOP4, "OP_NOP4", 1, opcodeNop}, OP_NOP5: {OP_NOP5, "OP_NOP5", 1, opcodeNop}, OP_NOP6: {OP_NOP6, "OP_NOP6", 1, opcodeNop}, OP_NOP7: {OP_NOP7, "OP_NOP7", 1, opcodeNop}, @@ -819,7 +819,7 @@ func opcodeN(op *opcode, data []byte, vm *Engine) error { // the flag to discourage use of NOPs is set for select opcodes. func opcodeNop(op *opcode, data []byte, vm *Engine) error { switch op.value { - case OP_NOP1, OP_NOP4, OP_NOP5, + case OP_NOP1, OP_NOP5, OP_NOP6, OP_NOP7, OP_NOP8, OP_NOP9, OP_NOP10: if vm.hasFlag(ScriptDiscourageUpgradableNops) { @@ -1195,6 +1195,49 @@ func opcodeCheckSequenceVerify(op *opcode, data []byte, vm *Engine) error { wire.SequenceLockTimeIsSeconds, sequence&lockTimeMask) } +func opcodeCheckTemplateVerify(op *opcode, data []byte, vm *Engine) error { + if !vm.hasFlag(ScriptVerifyCheckTemplateVerify) { + if vm.hasFlag(ScriptDiscourageUpgradableNops) { + return scriptError(ErrDiscourageUpgradableNOPs, + "OP_NOP4 reserved for soft-fork upgrades") + } + return nil + } + + if vm.dstack.Depth() < 1 { + str := fmt.Sprintf("stack has %d items, not enough to "+ + "execute OP_CHECKTEMPLATEVERIFY", vm.dstack.Depth()) + return scriptError(ErrInvalidStackOperation, str) + } + + topData, err := vm.dstack.PeekByteArray(0) + if err != nil { + return err + } + + // CTV only verifies the hash against a 32 byte argument + if len(topData) == 32 { + // Ensure the precomputed data required for anti-DoS is available, or cache it on first use + if vm.preComputedData == nil { + vm.preComputedData, err = vm.GetDefaultCheckTemplatePrecomputedData() + if err != nil { + return err + } + } + + // Compare the top stack item with the computed hash + computedHash, err := vm.GetDefaultCheckTemplateHash(vm.txIdx) + if err != nil { + return err + } + + if !bytes.Equal(topData, computedHash) { + return scriptError(ErrTemplateMismatch, "CTV hash mismatch") + } + } + return nil // Act as NOP after successful execution +} + // opcodeToAltStack removes the top item from the main data stack and pushes it // onto the alternate data stack. // @@ -2468,4 +2511,5 @@ func init() { OpcodeByName["OP_TRUE"] = OP_TRUE OpcodeByName["OP_NOP2"] = OP_CHECKLOCKTIMEVERIFY OpcodeByName["OP_NOP3"] = OP_CHECKSEQUENCEVERIFY + OpcodeByName["OP_NOP4"] = OP_CHECKTEMPLATEVERIFY } diff --git a/txscript/opcode_test.go b/txscript/opcode_test.go index 15c62907aa..cd0a53bfed 100644 --- a/txscript/opcode_test.go +++ b/txscript/opcode_test.go @@ -124,7 +124,14 @@ func TestOpcodeDisasm(t *testing.T) { // OP_UNKNOWN#. case opcodeVal >= 0xbb && opcodeVal <= 0xf9 || opcodeVal == 0xfc: - expectedStr = "OP_UNKNOWN" + strconv.Itoa(opcodeVal) + switch opcodeVal { + // OP_UNKOWN189 a.k.a 0xbd is now OP_CHECKTEMPLATEVERIFY. + case 0xbd: + expectedStr = "OP_CHECKTEMPLATEVERIFY" + + default: + expectedStr = "OP_UNKNOWN" + strconv.Itoa(opcodeVal) + } } var buf strings.Builder @@ -184,6 +191,9 @@ func TestOpcodeDisasm(t *testing.T) { case 0xb2: // OP_NOP3 is an alias of OP_CHECKSEQUENCEVERIFY expectedStr = "OP_CHECKSEQUENCEVERIFY" + case 0xbd: + expectedStr = "OP_CHECKTEMPLATEVERIFY" + // OP_UNKNOWN189 a.k.a 0xbd is now OP_CHECKTEMPLATEVERIFY. default: val := byte(opcodeVal - (0xb0 - 1)) expectedStr = "OP_NOP" + strconv.Itoa(int(val))