Skip to content
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
16 changes: 4 additions & 12 deletions pkg/arkade/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ const (
)

const (
// blankCodeSepValue is the value of the code separator position in the
// tapscript sighash when no code separator was found in the script.
blankCodeSepValue = math.MaxUint32
// BlankCodeSepValue is the value of the code separator position in the
// tapscript sighash when no code separator was executed in the script.
BlankCodeSepValue uint32 = math.MaxUint32
)

// taprootExecutionCtx houses the special context-specific information we need
Expand All @@ -49,8 +49,6 @@ type taprootExecutionCtx struct {
sigOpsBudget int32

mustSucceed bool

trackCodeSep bool
}

// sigOpsDelta is both the starting budget for sig ops for tapscript
Expand All @@ -75,9 +73,8 @@ func (t *taprootExecutionCtx) tallysigOp() error {
// context.
func newTaprootExecutionCtx(inputWitnessSize int32) *taprootExecutionCtx {
return &taprootExecutionCtx{
codeSepPos: blankCodeSepValue,
codeSepPos: BlankCodeSepValue,
sigOpsBudget: sigOpsDelta + inputWitnessSize,
trackCodeSep: true,
}
}

Expand Down Expand Up @@ -140,9 +137,6 @@ type Engine struct {
// the current program counter. Note that it differs from the actual byte
// index into the script and is really only used for disassembly purposes.
//
// lastCodeSep specifies the position within the current script of the last
// OP_CODESEPARATOR.
//
// tokenizer provides the token stream of the current script being executed
// and doubles as state tracking for the program counter within the script.
//
Expand All @@ -157,7 +151,6 @@ type Engine struct {
scripts [][]byte
scriptIdx int
opcodeIdx int
lastCodeSep int
tokenizer ScriptTokenizer
dstack stack
astack stack
Expand Down Expand Up @@ -660,7 +653,6 @@ func (vm *Engine) Step() (done bool, err error) {
vm.scriptIdx++
}

vm.lastCodeSep = 0
if vm.scriptIdx >= len(vm.scripts) {
return true, nil
}
Expand Down
21 changes: 7 additions & 14 deletions pkg/arkade/opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -1751,8 +1751,8 @@ func opcodeHash256(op *opcode, data []byte, vm *Engine) error {
return nil
}

// opcodeCodeSeparator stores the current script offset as the most recently
// seen OP_CODESEPARATOR which is used during signature checking.
// opcodeCodeSeparator stores the current opcode position as the most recently
// executed OP_CODESEPARATOR for BIP342-style signature hashing.
//
// This opcode does not change the contents of the data stack.
func opcodeCodeSeparator(op *opcode, data []byte, vm *Engine) error {
Expand All @@ -1762,10 +1762,7 @@ func opcodeCodeSeparator(op *opcode, data []byte, vm *Engine) error {
return scriptError(txscript.ErrReservedOpcode, str)
}

vm.lastCodeSep = int(vm.tokenizer.ByteIndex())
if vm.taprootCtx.trackCodeSep {
vm.taprootCtx.codeSepPos = uint32(vm.tokenizer.OpcodePosition())
}
vm.taprootCtx.codeSepPos = uint32(vm.tokenizer.OpcodePosition())

return nil
}
Expand All @@ -1774,14 +1771,10 @@ func opcodeCodeSeparator(op *opcode, data []byte, vm *Engine) error {
// signature and replaces them with a bool which indicates if the signature was
// successfully verified.
//
// The process of verifying a signature requires calculating a signature hash in
// the same way the transaction signer did. It involves hashing portions of the
// transaction based on the hash type byte (which is the final byte of the
// signature) and the portion of the script starting from the most recent
// OP_CODESEPARATOR (or the beginning of the script if there are none) to the
// end of the script (with any other OP_CODESEPARATORs removed). Once this
// "script hash" is calculated, the signature is checked using standard
// cryptographic methods against the provided public key.
// The process of verifying a signature requires calculating the arkade
// tapscript signature hash in the same way the transaction signer did. The
// digest commits to the spending tapleaf hash and the opcode position of the
// last executed OP_CODESEPARATOR, or BlankCodeSepValue if none executed.
//
// Stack transformation: [... signature pubkey] -> [... bool]
func opcodeCheckSig(op *opcode, data []byte, vm *Engine) error {
Expand Down
3 changes: 0 additions & 3 deletions pkg/arkade/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,6 @@ func (s *ArkadeScript) Execute(spendingTx *wire.MsgTx, prevOutFetcher ArkPrevOut
engine.taprootCtx = newTaprootExecutionCtxForLeaf(
s.spendingTapLeaf, int32(s.witness.SerializeSize()),
)
// Arkade scripts execute from the emulator packet, not from the
// spending tapleaf whose hash the sighash commits to.
engine.taprootCtx.trackCodeSep = false

for _, opt := range opts {
opt(engine)
Expand Down
143 changes: 135 additions & 8 deletions pkg/arkade/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ func TestArkadeScriptExecuteUsesSpendingTapLeafForSighash(t *testing.T) {
txscript.NewMultiPrevOutFetcher(prevOuts), nil, nil,
)
sighashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
digest, err := CalcTapscriptSignaturehash(
digest, err := CalcArkadeScriptSignatureHash(
sighashes, txscript.SigHashDefault, tx, 0, prevOutFetcher,
closureTapLeaf,
closureTapLeaf, BlankCodeSepValue,
)
require.NoError(t, err)

Expand All @@ -74,7 +74,7 @@ func TestArkadeScriptExecuteUsesSpendingTapLeafForSighash(t *testing.T) {
require.NoError(t, script.Execute(tx, prevOutFetcher, 0))
}

func TestArkadeScriptExecuteDoesNotUsePacketCodeSeparatorForSighash(t *testing.T) {
func TestArkadeScriptExecuteUsesCodeSeparatorForSighash(t *testing.T) {
t.Parallel()

signingKey, _ := btcec.PrivKeyFromBytes([]byte{
Expand All @@ -84,6 +84,9 @@ func TestArkadeScriptExecuteDoesNotUsePacketCodeSeparatorForSighash(t *testing.T
0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40,
})
pubKeyX := schnorr.SerializePubKey(signingKey.PubKey())
// OP_CODESEPARATOR is the first opcode (position 0). Per BIP342 it sets
// codesep_pos to its own opcode position, which the following OP_CHECKSIG
// must commit to.
arkadeScript, err := txscript.NewScriptBuilder().
AddOp(OP_CODESEPARATOR).
AddData(pubKeyX).
Expand Down Expand Up @@ -111,21 +114,145 @@ func TestArkadeScriptExecuteDoesNotUsePacketCodeSeparatorForSighash(t *testing.T
txscript.NewMultiPrevOutFetcher(prevOuts), nil, nil,
)
sighashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
digest, err := CalcTapscriptSignaturehash(

const codeSepPos = uint32(0) // OP_CODESEPARATOR is opcode 0 in the script.

// A signature that commits to the executed code-separator position must
// verify.
digest, err := CalcArkadeScriptSignatureHash(
sighashes, txscript.SigHashDefault, tx, 0, prevOutFetcher,
spendingTapLeaf,
spendingTapLeaf, codeSepPos,
)
require.NoError(t, err)

sig, err := schnorr.Sign(signingKey, digest)
require.NoError(t, err)

script := &ArkadeScript{
script: arkadeScript,
witness: wire.TxWitness{sig.Serialize()},
spendingTapLeaf: spendingTapLeaf,
}
require.NoError(t, script.Execute(tx, prevOutFetcher, 0))
require.NoError(t, script.Execute(tx, prevOutFetcher, 0),
"signature committing to the executed codesep position must verify")

// A signature that ignores the code separator (blank codesep_pos, the
// pre-BIP342 behavior) must now be rejected.
staleDigest, err := CalcArkadeScriptSignatureHash(
sighashes, txscript.SigHashDefault, tx, 0, prevOutFetcher,
spendingTapLeaf, BlankCodeSepValue,
)
require.NoError(t, err)
staleSig, err := schnorr.Sign(signingKey, staleDigest)
require.NoError(t, err)
staleScript := &ArkadeScript{
script: arkadeScript,
witness: wire.TxWitness{staleSig.Serialize()},
spendingTapLeaf: spendingTapLeaf,
}
require.Error(t, staleScript.Execute(tx, prevOutFetcher, 0),
"signature ignoring the code separator must fail")
}

func TestArkadeScriptExecuteUpdatesCodeSepPosOnCodeSeparator(t *testing.T) {
t.Parallel()

// OP_CODESEPARATOR is opcode 0; OP_TRUE leaves a truthy stack so execution
// completes successfully and we can observe codesep_pos at each step.
arkadeScript := []byte{OP_CODESEPARATOR, OP_TRUE}

outpoint := wire.OutPoint{Hash: chainhash.Hash{0x04}, Index: 0}
tx := &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{{
PreviousOutPoint: outpoint,
Sequence: 0xffffffff,
}},
TxOut: []*wire.TxOut{{
Value: 900,
PkScript: []byte{OP_TRUE},
}},
}
prevOuts := map[wire.OutPoint]*wire.TxOut{
outpoint: {Value: 1_000, PkScript: []byte{OP_1, 0x20}},
}
prevOutFetcher := newTestArkPrevOutFetcher(
txscript.NewMultiPrevOutFetcher(prevOuts), nil, nil,
)

script := &ArkadeScript{
script: arkadeScript,
spendingTapLeaf: txscript.NewBaseTapLeaf([]byte{OP_TRUE}),
}

var seen []uint32
err := script.Execute(tx, prevOutFetcher, 0,
WithDebugCallback(func(_ *StepInfo, e *Engine) error {
seen = append(seen, e.taprootCtx.codeSepPos)
return nil
}),
)
require.NoError(t, err)

// The callback fires once for the initial state, then after each step.
require.GreaterOrEqual(t, len(seen), 2)
require.Equal(t, BlankCodeSepValue, seen[0],
"codesep_pos must start at the blank sentinel")
require.Equal(t, uint32(0), seen[1],
"codesep_pos must equal the OP_CODESEPARATOR opcode position after it executes")
}

func TestArkadeScriptExecuteOpSighashUsesCodeSeparatorPosition(t *testing.T) {
t.Parallel()

spendingTapLeaf := txscript.NewBaseTapLeaf([]byte{OP_TRUE})

outpoint := wire.OutPoint{Hash: chainhash.Hash{0x05}, Index: 0}
tx := &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{{
PreviousOutPoint: outpoint,
Sequence: 0xffffffff,
}},
TxOut: []*wire.TxOut{{
Value: 900,
PkScript: []byte{OP_TRUE},
}},
}
prevOuts := map[wire.OutPoint]*wire.TxOut{
outpoint: {Value: 1_000, PkScript: []byte{OP_1, 0x20}},
}
prevOutFetcher := newTestArkPrevOutFetcher(
txscript.NewMultiPrevOutFetcher(prevOuts), nil, nil,
)
sighashes := txscript.NewTxSigHashes(tx, prevOutFetcher)

expectedDigest, err := CalcArkadeScriptSignatureHash(
sighashes, txscript.SigHashDefault, tx, 0, prevOutFetcher,
spendingTapLeaf, 0,
)
require.NoError(t, err)
blankDigest, err := CalcArkadeScriptSignatureHash(
sighashes, txscript.SigHashDefault, tx, 0, prevOutFetcher,
spendingTapLeaf, BlankCodeSepValue,
)
require.NoError(t, err)
require.NotEqual(t, blankDigest, expectedDigest,
"test requires the code separator position to affect OP_SIGHASH")

arkadeScript, err := txscript.NewScriptBuilder().
AddOp(OP_CODESEPARATOR).
AddOp(OP_0).
AddOp(OP_SIGHASH).
AddData(expectedDigest).
AddOp(OP_EQUAL).
Script()
require.NoError(t, err)

script := &ArkadeScript{
script: arkadeScript,
spendingTapLeaf: spendingTapLeaf,
}
require.NoError(t, script.Execute(tx, prevOutFetcher, 0),
"OP_SIGHASH must include the last executed OP_CODESEPARATOR position")
}

func TestReadArkadeScriptRejectsNonBaseSpendingTapLeafVersion(t *testing.T) {
Expand Down
14 changes: 10 additions & 4 deletions pkg/arkade/sigvalidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,22 @@ func computeArkadeSighash(vm *Engine,
return digest[:], nil
}

// CalcTapscriptSignaturehash returns the non-standard arkade tapscript
// CalcArkadeScriptSignatureHash returns the non-standard arkade tapscript
// signature hash used by OP_CHECKSIG and OP_SIGHASH inside arkade scripts.
//
// The byte layout mirrors BIP342's tapscript sigMsg with arkade's witness
// masking and "ArkadeTapSighash" final tag. Callers should pass the active
// Bitcoin spending tapleaf whose hash the signature commits to.
func CalcTapscriptSignaturehash(
// Bitcoin spending tapleaf whose hash the signature commits to. Pass
// BlankCodeSepValue when no OP_CODESEPARATOR executed before the signature
// opcode.
func CalcArkadeScriptSignatureHash(
sigHashes *txscript.TxSigHashes,
hashType txscript.SigHashType,
tx *wire.MsgTx,
idx int,
prevOutFetcher ArkPrevOutFetcher,
tapLeaf txscript.TapLeaf,
codeSepPos uint32,
) ([]byte, error) {
if sigHashes == nil {
return nil, fmt.Errorf("nil sighash cache")
Expand All @@ -97,12 +100,15 @@ func CalcTapscriptSignaturehash(
return nil, fmt.Errorf("nil prevout fetcher")
}

taprootCtx := newTaprootExecutionCtxForLeaf(tapLeaf, 0)
taprootCtx.codeSepPos = codeSepPos

vm := &Engine{
tx: *tx,
txIdx: idx,
hashCache: sigHashes,
prevOutFetcher: prevOutFetcher,
taprootCtx: newTaprootExecutionCtxForLeaf(tapLeaf, 0),
taprootCtx: taprootCtx,
}

return computeArkadeSighash(vm, hashType)
Expand Down
4 changes: 2 additions & 2 deletions test/signed_pay_to_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,9 @@ func signArkadeInput(
PrevOutputFetcher: txscript.NewMultiPrevOutFetcher(prevouts),
}
sighashes := txscript.NewTxSigHashes(ptx.UnsignedTx, prevOutFetcher)
message, err := arkade.CalcTapscriptSignaturehash(
message, err := arkade.CalcArkadeScriptSignatureHash(
sighashes, txscript.SigHashDefault, ptx.UnsignedTx,
inputIndex, prevOutFetcher, tapLeaf,
inputIndex, prevOutFetcher, tapLeaf, arkade.BlankCodeSepValue,
)
require.NoError(t, err)

Expand Down
Loading