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

txscript/engine: add execution StepCallback #1980

Merged
merged 2 commits into from
Aug 15, 2023
Merged
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
98 changes: 98 additions & 0 deletions txscript/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,33 @@ type Engine struct {
witnessProgram []byte
inputAmount int64
taprootCtx *taprootExecutionCtx

// stepCallback is an optional function that will be called every time
// a step has been performed during script execution.
//
// NOTE: This is only meant to be used in debugging, and SHOULD NOT BE
// USED during regular operation.
stepCallback func(*StepInfo) error
}

// StepInfo houses the current VM state information that is passed back to the
// stepCallback during script execution.
type StepInfo struct {
// ScriptIndex is the index of the script currently being executed by
// the Engine.
ScriptIndex int

// OpcodeIndex is the index of the next opcode that will be executed.
// In case the execution has completed, the opcode index will be
// incrementet beyond the number of the current script's opcodes. This
// indicates no new script is being executed, and execution is done.
OpcodeIndex int

// Stack is the Engine's current content on the stack:
Stack [][]byte

// AltStack is the Engine's current content on the alt stack.
AltStack [][]byte
}

// hasFlag returns whether the script engine instance has the passed flag set.
Expand Down Expand Up @@ -1023,6 +1050,17 @@ func (vm *Engine) Step() (done bool, err error) {
return false, nil
}

// copyStack makes a deep copy of the provided slice.
func copyStack(stk [][]byte) [][]byte {
c := make([][]byte, len(stk))
for i := range stk {
c[i] = make([]byte, len(stk[i]))
copy(c[i][:], stk[i][:])
}

return c
}

// Execute will execute all scripts in the script engine and return either nil
// for successful validation or an error if one occurred.
func (vm *Engine) Execute() (err error) {
Expand All @@ -1033,6 +1071,22 @@ func (vm *Engine) Execute() (err error) {
return nil
}

// If the stepCallback is set, we start by making a call back with the
// initial engine state.
var stepInfo *StepInfo
if vm.stepCallback != nil {
stepInfo = &StepInfo{
ScriptIndex: vm.scriptIdx,
OpcodeIndex: vm.opcodeIdx,
Stack: copyStack(vm.dstack.stk),
AltStack: copyStack(vm.astack.stk),
}
err := vm.stepCallback(stepInfo)
if err != nil {
return err
}
}

done := false
for !done {
log.Tracef("%v", newLogClosure(func() string {
Expand Down Expand Up @@ -1060,6 +1114,31 @@ func (vm *Engine) Execute() (err error) {

return dstr + astr
}))

if vm.stepCallback != nil {
scriptIdx := vm.scriptIdx
opcodeIdx := vm.opcodeIdx

// In case the execution has completed, we keep the
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
// current script index while increasing the opcode
// index. This is to indicate that no new script is
// being executed.
if done {
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
scriptIdx = stepInfo.ScriptIndex
opcodeIdx = stepInfo.OpcodeIndex + 1
}

stepInfo = &StepInfo{
ScriptIndex: scriptIdx,
OpcodeIndex: opcodeIdx,
Stack: copyStack(vm.dstack.stk),
AltStack: copyStack(vm.astack.stk),
}
err := vm.stepCallback(stepInfo)
if err != nil {
return err
}
}
}

return vm.CheckErrorCondition(true)
Expand Down Expand Up @@ -1549,3 +1628,22 @@ func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags

return &vm, nil
}

// NewEngine returns a new script engine with a script execution callback set.
// This is useful for debugging script execution.
func NewDebugEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int,
flags ScriptFlags, sigCache *SigCache, hashCache *TxSigHashes,
inputAmount int64, prevOutFetcher PrevOutputFetcher,
stepCallback func(*StepInfo) error) (*Engine, error) {

vm, err := NewEngine(
scriptPubKey, tx, txIdx, flags, sigCache, hashCache,
inputAmount, prevOutFetcher,
)
if err != nil {
return nil, err
}

vm.stepCallback = stepCallback
return vm, nil
}
178 changes: 178 additions & 0 deletions txscript/engine_debug_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright (c) 2013-2023 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package txscript

import (
"testing"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/wire"
"github.com/stretchr/testify/require"
)

// TestDebugEngine checks that the StepCallbck called during debug script
// execution contains the expected data.
func TestDebugEngine(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

t.Parallel()

// We'll generate a private key and a signature for the tx.
privKey, err := btcec.NewPrivateKey()
require.NoError(t, err)

internalKey := privKey.PubKey()

// We use a simple script that will utilize both the stack and alt
// stack in order to test the step callback, and wrap it in a taproot
// witness script.
builder := NewScriptBuilder()
builder.AddData([]byte{0xab})
builder.AddOp(OP_TOALTSTACK)
builder.AddData(schnorr.SerializePubKey(internalKey))
builder.AddOp(OP_CHECKSIG)
builder.AddOp(OP_VERIFY)
builder.AddOp(OP_1)
pkScript, err := builder.Script()
require.NoError(t, err)

tapLeaf := NewBaseTapLeaf(pkScript)
tapScriptTree := AssembleTaprootScriptTree(tapLeaf)

ctrlBlock := tapScriptTree.LeafMerkleProofs[0].ToControlBlock(
internalKey,
)

tapScriptRootHash := tapScriptTree.RootNode.TapHash()
outputKey := ComputeTaprootOutputKey(
internalKey, tapScriptRootHash[:],
)
p2trScript, err := PayToTaprootScript(outputKey)
require.NoError(t, err)

testTx := wire.NewMsgTx(2)
testTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Index: 1,
},
})
txOut := &wire.TxOut{
Value: 1e8, PkScript: p2trScript,
}
testTx.AddTxOut(txOut)

prevFetcher := NewCannedPrevOutputFetcher(
txOut.PkScript, txOut.Value,
)
sigHashes := NewTxSigHashes(testTx, prevFetcher)

sig, err := RawTxInTapscriptSignature(
testTx, sigHashes, 0, txOut.Value,
txOut.PkScript, tapLeaf,
SigHashDefault, privKey,
)
require.NoError(t, err)

// Now that we have the sig, we'll make a valid witness
// including the control block.
ctrlBlockBytes, err := ctrlBlock.ToBytes()
require.NoError(t, err)
txCopy := testTx.Copy()
txCopy.TxIn[0].Witness = wire.TxWitness{
sig, pkScript, ctrlBlockBytes,
}

expCallback := []StepInfo{
// First callback is looking at the OP_1 witness version.
{
ScriptIndex: 1,
OpcodeIndex: 0,
Stack: [][]byte{},
AltStack: [][]byte{},
},
// The OP_1 witness version is pushed to stack,
{
ScriptIndex: 1,
OpcodeIndex: 1,
Stack: [][]byte{{0x01}},
AltStack: [][]byte{},
},
// Then the taproot script is being executed, starting with
// only the signature on the stacks.
{
ScriptIndex: 2,
OpcodeIndex: 0,
Stack: [][]byte{sig},
AltStack: [][]byte{},
},
// 0xab is pushed to the stack.
{
ScriptIndex: 2,
OpcodeIndex: 1,
Stack: [][]byte{sig, {0xab}},
AltStack: [][]byte{},
},
// 0xab is moved to the alt stack.
{
ScriptIndex: 2,
OpcodeIndex: 2,
Stack: [][]byte{sig},
AltStack: [][]byte{{0xab}},
},
// The public key is pushed to the stack.
{
ScriptIndex: 2,
OpcodeIndex: 3,
Stack: [][]byte{
sig,
schnorr.SerializePubKey(internalKey),
},
AltStack: [][]byte{{0xab}},
},
// OP_CHECKSIG is executed, resulting in 0x01 on the stack.
{
ScriptIndex: 2,
OpcodeIndex: 4,
Stack: [][]byte{
{0x01},
},
AltStack: [][]byte{{0xab}},
},
// OP_VERIFY pops and checks the top stack element.
{
ScriptIndex: 2,
OpcodeIndex: 5,
Stack: [][]byte{},
AltStack: [][]byte{{0xab}},
},
// A single OP_1 push completes the script execution (note that
// the alt stack is cleared when the script is "done").
{
ScriptIndex: 2,
OpcodeIndex: 6,
Stack: [][]byte{{0x01}},
AltStack: [][]byte{},
},
}

stepIndex := 0
callback := func(s *StepInfo) error {
require.Less(
t, stepIndex, len(expCallback), "unexpected callback",
)

require.Equal(t, &expCallback[stepIndex], s)
stepIndex++
return nil
}

// Run the debug engine.
vm, err := NewDebugEngine(
txOut.PkScript, txCopy, 0, StandardVerifyFlags,
nil, sigHashes, txOut.Value, prevFetcher,
callback,
)
require.NoError(t, err)
require.NoError(t, vm.Execute())
}
Loading