Skip to content

Make OP_CODESEPERATOR work in an arkade script context#92

Merged
louisinger merged 5 commits into
masterfrom
feat/op-codeseperator
Jun 3, 2026
Merged

Make OP_CODESEPERATOR work in an arkade script context#92
louisinger merged 5 commits into
masterfrom
feat/op-codeseperator

Conversation

@msinkec

@msinkec msinkec commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

OP_CODESEPARATOR was recognized and executed inside arkade scripts but had no observable effect: the opcode's update of the tapscript sighash's codesep_pos was gated behind a trackCodeSep flag that ArkadeScript.Execute set to false. As a result the code-separator position was never folded into the signature hash, diverging from BIP342.

This PR makes OP_CODESEPARATOR commit its opcode position into the arkade tapscript sighash, matching BIP342 semantics, and updates the signing helper so signers can produce matching digests.

Changes

  • Engine (opcode.go, engine.go, script.go): opcodeCodeSeparator now unconditionally records vm.tokenizer.OpcodePosition() into taprootCtx.codeSepPos. Removed the trackCodeSep flag and the line in ArkadeScript.Execute that disabled tracking. The position is tracked against the executing (emulator-packet) script per BIP342; the separately-committed spending-tapleaf hash is unaffected.
  • Signer helper (sigvalidate.go): CalcTapscriptSignaturehash → renamed to CalcArkadeScriptSignatureHash and gained a codeSepPos uint32 parameter so callers can compute the digest the engine will verify against. Exposed BlankCodeSepValue (the 0xffffffff sentinel) for the common no-separator case. Updated repo call sites to the clearer Arkade-specific name.
  • Doc fixes (opcode.go:1754): Updated the stale OP_CHECKSIG / OP_CODESEPARATOR comments that still described btcd's legacy script-subscript signing process; they now describe the arkade tapscript digest (spending-tapleaf hash + last executed separator position).
  • Dead-code removal (engine.go, opcode.go): Removed the lastCodeSep field, its write, and its per-script reset. It was a vestige of btcd's legacy pre-segwit sighash (which subscripted the script from the last separator's byte offset); BIP341/342 replaced that mechanism with the codesep_pos opcode-position commitment this engine uses, leaving the field with zero readers.

Tests

  • TestArkadeScriptExecuteUsesCodeSeparatorForSighash — a signature committing to the executed codesep position verifies; one using the blank sentinel is now rejected (proves the position is actually committed).
  • TestArkadeScriptExecuteUpdatesCodeSepPosOnCodeSeparator — observes codesep_pos through the real Execute path: blank initially, then the separator's opcode position after it runs.
  • TestArkadeScriptExecuteOpSighashUsesCodeSeparatorPosition (script_test.go:228) — checks OP_SIGHASH (not just OP_CHECKSIG) uses the executed separator position and that it differs from the blank-sentinel path.
  • Replaces the old TestArkadeScriptExecuteDoesNotUsePacketCodeSeparatorForSighash, which asserted the previous (disabled) behavior.

@msinkec msinkec self-assigned this Jun 1, 2026

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Arkana Code Review — #92

Verdict: APPROVE ✅ — but flagged for mandatory human review (protocol-critical: sighash digest change).

Summary

This PR fixes a real bug: OP_CODESEPARATOR was recognized but neutered inside arkade scripts because ArkadeScript.Execute set trackCodeSep = false, so codesep_pos was never folded into the signature hash. This diverged from BIP342. The fix is clean, correct, and well-tested.

Protocol-Critical Assessment ⚠️

This changes what signature digests the VM accepts. Specifically:

  • Before: All arkade sighashes used codesep_pos = 0xFFFFFFFF regardless of OP_CODESEPARATOR in the script.
  • After: codesep_pos reflects the last executed OP_CODESEPARATOR's opcode position per BIP342.

A bug in sighash computation means either funds locked (valid sigs rejected) or funds stolen (invalid sigs accepted). The implementation looks correct, but this requires a human cryptography-aware reviewer to sign off.

Migration note: Any deployed arkade scripts containing OP_CODESEPARATOR followed by OP_CHECKSIG that were signed against the old (blank) digest will now fail. Since the old behavior was broken (codesep was a no-op), there likely aren't any such scripts in production, but this should be verified.

Findings

No blocking issues found. Detailed notes:

  1. opcodeCodeSeparator (opcode.go:1762-1765) — Clean. Unconditionally sets vm.taprootCtx.codeSepPos = uint32(vm.tokenizer.OpcodePosition()). Correctly uses opcode position (not byte offset), matching BIP342. ✅

  2. CalcArkadeScriptSignatureHash (sigvalidate.go:79-109) — Good rename from CalcTapscriptSignaturehash. New codeSepPos uint32 parameter correctly plumbed: creates a taprootCtx, sets codeSepPos, then calls computeArkadeSighash. Passes 0 for witness size in the helper context, which is correct since sig ops budgeting is irrelevant for digest computation. ✅

  3. BlankCodeSepValue export (engine.go:33-36) — Correctly exported with explicit uint32 type so callers can specify the no-separator default. ✅

  4. Dead code removal (engine.go, opcode.go)lastCodeSep was only written, never read (btcd legacy for pre-segwit OP_CODESEPARATOR byte-offset subscripting). trackCodeSep is no longer needed. Both cleanly removed. ✅

  5. script.go:184 removal — The trackCodeSep = false line was the root cause of the bug. Removing it is the correct fix. ✅

  6. OP_SIGHASH pathopcodeSighashcomputeArkadeSighash reads vm.taprootCtx.codeSepPos directly, so it automatically picks up the updated position. No changes needed. ✅

  7. Comment updates (opcode.go:1771-1779) — Old comments described btcd's legacy script-subscript signing. New comments correctly describe the arkade tapscript digest (spending tapleaf hash + last executed separator position). ✅

Cross-Repo Impact

  • bancod and solver depend on emulator/pkg/arkade but neither calls CalcTapscriptSignaturehash / CalcArkadeScriptSignatureHash. No breakage.
  • No other repos import emulator/pkg/arkade.
  • The internal/application/finalization.go call to txscript.CalcTapscriptSignaturehash is btcd's standard BIP342 function — unrelated to this rename. ✅

Test Coverage

Excellent. Four new/updated tests covering:

  • Positive case (script_test.go:77): Signature with correct codeSepPos=0 verifies. ✅
  • Negative case (script_test.go:133-143): Signature with BlankCodeSepValue is rejected when OP_CODESEPARATOR was executed. ✅
  • State observation (script_test.go:147-189): codeSepPos transitions from BlankCodeSepValue → opcode position 0 via debug callback. ✅
  • OP_SIGHASH path (script_test.go:191-258): Confirms OP_SIGHASH also uses the executed separator position and produces a different digest than the blank-sentinel path. ✅
  • Existing test updated (test/signed_pay_to_output_test.go:228-234): signArkadeInput helper updated to new API with BlankCodeSepValue. ✅

Nits (non-blocking)

None. The PR is clean.


🤖 Reviewed by Arkana (Claude Opus) · Protocol-critical flag set · Human sign-off required before merge

Comment thread pkg/arkade/engine.go Outdated
Comment thread pkg/arkade/sigvalidate.go
Comment thread pkg/arkade/script_test.go

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Arkana Incremental Review — commits dd4e214c, 01a5e3fe

Verdict: APPROVE ✅ — protocol-critical flag from first review still applies.

Changes since last review

1. dd4e214c — unexport BlankCodeSepValue

Good call. The constant is now blankCodeSepValue (unexported), and external callers pass math.MaxUint32 directly. The doc comment on CalcArkadeScriptSignatureHash documents this (sigvalidate.go:82). No external consumers of the old exported name exist in any downstream repo (bancod, solver, SDKs) — confirmed via grep. Clean.

2. 01a5e3fe — two new BIP342 conformance tests

Excellent additions that lock in subtle but critical BIP342 behavior:

  • TestArkadeScriptExecuteCodeSepPosCountsUnexecutedBranchOpcodes (script_test.go:258-316): Verifies that opcodes in unexecuted OP_IF branches still advance the opcode position counter. The test script places OP_FALSE in an unexecuted true-branch (pos 2), shifting subsequent positions. Asserts final codesep_pos == 6, which would be 5 if the counter skipped unexecuted opcodes. Correct per BIP342. ✅

  • TestArkadeScriptExecuteCodeSepInUnexecutedBranchIgnored (script_test.go:330-438): End-to-end signing test with OP_IF/OP_ELSE branching. Tests all four combinations: correct/incorrect codesep_pos × true/false branch. Uses minimal-if selectors (0x01 / []byte{}) per BIP342. All four assertions are correct. ✅

No issues found

Both commits are clean. No protocol correctness concerns, no cross-repo breakage.


🤖 Reviewed by Arkana (Claude Opus) · Incremental review of new commits · Human sign-off still required

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Arkana Incremental Review — commit ec74db06

🤖 Reviewed by Arkana (Claude Opus) · Incremental review of new commit · Human sign-off still required

Changes since last review

Single commit converts CalcArkadeScriptSignatureHash from a positional codeSepPos uint32 parameter to a functional options pattern (opts ...ArkadeSigHashOption + WithCodeSepPosition).

Findings

None. This is a clean API improvement:

  • ✅ Default blankCodeSepValue (0xFFFFFFFF) matches BIP342 no-separator semantics
  • ✅ Sighash computation logic unchanged — purely ergonomic refactor
  • BlankCodeSepValue correctly un-exported since external callers no longer need it
  • ✅ No cross-repo consumers of CalcArkadeScriptSignatureHash (only introspector-review uses upstream txscript.CalcTapscriptSignaturehash)
  • ✅ All test call sites properly updated

Verdict: APPROVE ✅ — protocol-critical flag from first review still applies; human sign-off required before merge.

@louisinger

Copy link
Copy Markdown
Collaborator

@msinkec fix conflicts 🙏

@msinkec msinkec requested a review from louisinger June 3, 2026 07:33

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Arkana Incremental Review — merge commit cecdc333

🤖 Reviewed by Arkana (Claude Opus) · Incremental review of new commit · Human sign-off still required

Changes since last review

Single commit: merge of origin/master bringing in PR #91 (opcode count limits) and PR #93 (README docs).

Merge correctness verified

PR #91 and #92 touched orthogonal concerns in the same files — the merge composes cleanly:

  • taprootExecutionCtx#91 replaced sigOpsBudget int32 with opCounts map[byte]int and removed inputWitnessSize from constructors; #92 removed trackCodeSep bool. Merged struct has both changes applied independently. ✅
  • newTaprootExecutionCtx() / newTaprootExecutionCtxForLeaf() — No params, initialises codeSepPos: blankCodeSepValue only. No sigOpsBudget, no trackCodeSep. All 4 call sites (script.go:Execute, sigvalidate.go:CalcArkadeScriptSignatureHash, engine.go:verifyWitnessProgram, engine.go:NewEngine) use the updated signatures. ✅
  • opcodeCheckSig / opcodeCheckSigAdd#91 removed tallysigOp() calls; #92 didn't touch these functions in this commit. No conflict. ✅
  • chargeOpcode() (new from #91) — Uses vm.taprootCtx.opCounts, completely independent of codeSepPos. ✅
  • ComputeLimits / WithComputeLimits / WithExactComputeLimits (new from #91) — ExecuteOption pattern consistent with #92's existing WithCodeSepPosition. ✅

No issues found

The merge is mechanical and correct. No protocol correctness concerns.

Verdict: APPROVE ✅ — protocol-critical flag from first review still applies; human sign-off required before merge.

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Arkana Incremental Review — merge commit cecdc333

🤖 Reviewed by Arkana (Claude Opus) · Incremental review of new commit · Human sign-off still required

Changes since last review

Single commit: merge of origin/master bringing in PR #91 (opcode count limits) and PR #93 (README docs).

Merge correctness verified

PR #91 and #92 touched orthogonal concerns in the same files — the merge composes cleanly:

  • taprootExecutionCtx#91 replaced sigOpsBudget int32 with opCounts map[byte]int and removed inputWitnessSize from constructors; #92 removed trackCodeSep bool. Merged struct has both changes applied independently. ✅
  • newTaprootExecutionCtx() / newTaprootExecutionCtxForLeaf() — No params, initialises codeSepPos: blankCodeSepValue only. No sigOpsBudget, no trackCodeSep. All 4 call sites (script.go:Execute, sigvalidate.go:CalcArkadeScriptSignatureHash, engine.go:verifyWitnessProgram, engine.go:NewEngine) use the updated signatures. ✅
  • opcodeCheckSig / opcodeCheckSigAdd#91 removed tallysigOp() calls; #92 didn't touch these functions in this commit. No conflict. ✅
  • chargeOpcode() (new from #91) — Uses vm.taprootCtx.opCounts, completely independent of codeSepPos. ✅
  • ComputeLimits / WithComputeLimits / WithExactComputeLimits (new from #91) — ExecuteOption pattern consistent with #92's existing WithCodeSepPosition. ✅

No issues found

The merge is mechanical and correct. No protocol correctness concerns.

Verdict: APPROVE ✅ — protocol-critical flag from first review still applies; human sign-off required before merge.

@louisinger louisinger merged commit d870933 into master Jun 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants