Skip to content

Commit

Permalink
add impl of op_ctv
Browse files Browse the repository at this point in the history
  • Loading branch information
ben2077 committed Mar 23, 2024
1 parent 7520523 commit 8533d51
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 9 deletions.
1 change: 1 addition & 0 deletions btcutil/psbt/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
9 changes: 4 additions & 5 deletions txscript/data/script_tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],

Expand Down Expand Up @@ -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"],
Expand All @@ -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"],
Expand Down
88 changes: 88 additions & 0 deletions txscript/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package txscript
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"fmt"
"math/big"
"strings"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
//
Expand Down Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions txscript/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
50 changes: 47 additions & 3 deletions txscript/opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
//
Expand Down Expand Up @@ -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
}
12 changes: 11 additions & 1 deletion txscript/opcode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit 8533d51

Please sign in to comment.