Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Impl of OP_CTV #1

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
92 changes: 92 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

// ScriptVerifyCheckTemplateVerify defines whether to verify that the
// script being executed is a valid template utxo.
// This is BIP0119.
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,87 @@ 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)
err := binary.Write(&outputs, binary.LittleEndian, txOut.Value)
if err != nil {
return nil, err
}
}
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