Skip to content

Reject invalid introspected transaction values - #128

Merged
chris-ricketts merged 2 commits into
masterfrom
fix/reject-invalid-amounts-in-value-introspection-opcodes
Aug 6, 2026
Merged

chris-ricketts merged 2 commits into
masterfrom
fix/reject-invalid-amounts-in-value-introspection-opcodes

Conversation

@msinkec

@msinkec msinkec commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Closes #124.

Rejects missing previous outputs and transaction values outside Bitcoin's monetary range before either value-introspection opcode converts them to an unsigned script number. Both OP_INSPECTINPUTVALUE and OP_INSPECTOUTPUTVALUE share the same range check.

Tests cover -1, exactly MaxSatoshi, MaxSatoshi + 1, and a missing previous output.

Validation:

  • make test from a clean clone
  • go vet ./... in pkg/arkade
  • make integrationtest against a clean rebuilt regtest stack (376.148s)

Summary by CodeRabbit

  • Bug Fixes
    • Improved validation of transaction input and output amounts.
    • Invalid amounts, including negative, oversized, or missing values, are now rejected.
    • Valid amounts up to the maximum supported satoshi value continue to be accepted.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b15f671-7b88-4d52-9709-22842394d749

📥 Commits

Reviewing files that changed from the base of the PR and between 79b6ccb and 7d6d248.

📒 Files selected for processing (3)
  • pkg/arkade/go.mod
  • pkg/arkade/opcode.go
  • pkg/arkade/opcode_test.go

Walkthrough

The change validates input and output satoshi amounts before opcode stack conversion. It rejects missing previous outputs, negative values, and values above btcutil.MaxSatoshi. Tests cover boundary, invalid, and property-based cases.

Changes

Satoshi value validation

Layer / File(s) Summary
Opcode value validation
pkg/arkade/go.mod, pkg/arkade/opcode.go
Adds btcutil as a direct dependency. Centralizes satoshi validation for input and output inspection. Rejects missing previous outputs and values outside the valid range.
Boundary and property tests
pkg/arkade/opcode_test.go
Adds tests for maximum, negative, oversized, and missing-prevout values. Property checks enforce the valid satoshi range.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: louisinger

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes rejection of invalid transaction values, which is the primary change.
Linked Issues check ✅ Passed The changes validate nil prevouts and Bitcoin value ranges for both introspection opcodes and add the required focused tests.
Out of Scope Changes check ✅ Passed The dependency update, implementation changes, and tests directly support the linked issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/reject-invalid-amounts-in-value-introspection-opcodes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@arkana-ai-bot arkana-ai-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review — #128

Summary: Two valid safety fixes — a nil-dereference panic removed from opcodeInspectInputValue, and a silent sign-extend/wrap-around bug removed from both value opcodes. The go.mod promotion is correct. No cross-repo breakage: go-sdk, ts-sdk, and compiler consumers operate on well-formed Bitcoin transactions and will not be affected by the tighter bounds.


Bugs / Must-fix

pkg/arkade/opcode.go — wrong error code in pushSatoshiValue

// opcode.go (new helper)
func pushSatoshiValue(value int64, vm *Engine) error {
    if value < 0 || value > btcutil.MaxSatoshi {
        return scriptError(txscript.ErrInvalidIndex, "value out of range")  // ← wrong
    }
    return vm.dstack.PushBigNum(BigNumFromUint64(uint64(value)))
}

txscript.ErrInvalidIndex is reserved for bounds violations on indices. Using it here means any caller that dispatches on the error kind to distinguish "bad index" from "bad monetary value" will misclassify this. The natural choice for "value too large to represent" is txscript.ErrNumberTooBig, which is already used at opcode.go:1618 and 1628 for analogous BigNum overflow cases. Same applies to the prevOut == nil sentinel — that could legitimately be ErrInvalidIndex (the outpoint doesn't resolve), so the nil-check error code is defensible; the range error is not.


Security observations

Nil-dereference panic — fixed correctly

Old opcode.go:2028 accessed prevOut.Value without a nil guard. The test infrastructure (opcode_test.go:5375, engine_fuzz_test.go:257) both defensively check != nil before using the result, confirming the fetcher can return nil in practice. A malformed transaction referencing an unknown outpoint could have caused a panic (script-execution DoS). The added guard at the new opcode.go:2028–2030 closes that.

Silent uint64 wrap-around — fixed correctly

Before this PR, uint64(vm.tx.TxOut[index].Value) with a negative int64 would silently produce a value near 2^64 − 1 and push it as a BigNum, making value-comparison covenants trivially bypassable for any output carrying a dust-negative value. Same issue existed for input values via prevOut.Value. Both are now gated by pushSatoshiValue.


Minor observations

pkg/arkade/opcode_test.go — missing_prevout vector relies on mock-fetcher nil semantics

The missing_prevout invalid vector sets w.tx.TxIn[0].PreviousOutPoint = wire.OutPoint{} and expects txscript.ErrInvalidIndex. This only fires if the test's mock fetcher returns nil for an unregistered outpoint (confirmed at opcode_test.go:5375). The production ArkPrevOutFetcher contract (via txscript.PrevOutputFetcher) does not mandate nil returns; btcd's built-in fetchers return &wire.TxOut{}. Worth a comment in the test clarifying the assumption, or a companion test that sets w.prevFetcher to a fetcher whose FetchPrevOutput always returns &wire.TxOut{} with Value = 0 and verifies the opcode still succeeds (Value 0 is in range). Not a correctness bug today, but fragile if a new fetcher implementation follows btcd convention.

go.mod — correct

btcutil moves from indirect to direct; version v1.1.5 was already pinned in the module graph, so no new dependency enters the build.


Cross-repo impact

go-sdk/vhtlc/noninteractive.go:61–63 and ts-sdk tests construct scripts that call OP_INSPECTOUTPUTVALUE / OP_INSPECTINPUTVALUE on real Bitcoin VTXOs. Those VTXOs carry values in [0, MaxSatoshi] by construction, so the new range gate is a no-op for legitimate transactions. No SDK or compiler changes needed.


Required before merge: Fix the txscript.ErrInvalidIndex → txscript.ErrNumberTooBig (or a purpose-specific error code) in pushSatoshiValue for the value-out-of-range path. Everything else is clean.

@chris-ricketts chris-ricketts left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM - updated the property checkers and ran FuzzOpcodes for 10 minutes

@chris-ricketts
chris-ricketts merged commit d9530c8 into master Aug 6, 2026
1 check passed

@arkana-ai-bot arkana-ai-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pass 2 — 1 commit since last review (7d6d248); 0 of 1 prior issues fixed; 1 still open (blocker).


Prior issue status

❌ Still open — wrong error code in (opcode.go:2037)

The only item I marked Required before merge in pass 1 is unchanged. The new commit (7d6d248) touches only opcode_test.go; opcode.go is unmodified since 782208c.

// pkg/arkade/opcode.go:2035-2038  (unchanged since 782208c)
func pushSatoshiValue(value int64, vm *Engine) error {
    if value < 0 || value > btcutil.MaxSatoshi {
        return scriptError(txscript.ErrInvalidIndex, "value out of range")  // ← still wrong
    }

ErrInvalidIndex means a script-index operand was out of bounds. Returning it for a monetary-value range violation means any downstream code that dispatches on error kind to distinguish the two conditions will misclassify this. txscript.ErrNumberTooBig (already used at lines 1619, 1629, 1645, 2482, 2886 for analogous overflow cases) is the correct sentinel. This stays a blocker.


New findings — 7d6d248 (opcode_test.go only)

The 8 added lines instrument both property checkers (inspectInputPropertyChecker and inspectOutputPropertyChecker) to assert that a successfully-pushed value satisfies 0 ≤ result ≤ btcutil.MaxSatoshi.

Correctness of the new assertions — sound. Cmp returns −1/0/+1, so:

  • require.LessOrEqual(t, topBigNum.Cmp(BigNumFromUint64(btcutil.MaxSatoshi)), 0) → value ≤ MaxSatoshi ✓
  • require.GreaterOrEqual(t, topBigNum.Cmp(BigNumFromUint64(0)), 0) → value ≥ 0 ✓

Tautology today, useful regression guard — because pushSatoshiValue already rejects out-of-range values before any push happens, a successful execution will always satisfy the new asserts. The value is that if a future change weakens the guard in pushSatoshiValue, the property tests will catch it. This is fine and worth keeping.

Minor (non-blocking): the want calculation at opcode_test.go:5117 still does BigNumFromUint64(uint64(prevOut.Value)) with a raw int64→uint64 cast. For in-range values (the only ones that reach this path) the cast is safe. No action needed.


No new findings.

Fix required before merge: change txscript.ErrInvalidIndex → txscript.ErrNumberTooBig at pkg/arkade/opcode.go:2037.

@arkana-ai-bot arkana-ai-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pass 2 — 1 commit since last review (7d6d248); 0 of 1 prior issues fixed; 1 still open (blocker).


Prior issue status

❌ Still open — wrong error code in pushSatoshiValue (opcode.go:2037)

The only item I marked Required before merge in pass 1 is unchanged. The new commit (7d6d248) touches only opcode_test.go; opcode.go is unmodified since 782208c.

// pkg/arkade/opcode.go:2035-2038  (unchanged since 782208c)
func pushSatoshiValue(value int64, vm *Engine) error {
    if value < 0 || value > btcutil.MaxSatoshi {
        return scriptError(txscript.ErrInvalidIndex, "value out of range")  // ← still wrong
    }

ErrInvalidIndex means a script-index operand was out of bounds. Returning it for a monetary-value range violation means any downstream code that dispatches on error kind to distinguish the two conditions will misclassify this. txscript.ErrNumberTooBig (already used at opcode.go:1619, 1629, 1645, 2482, 2886 for analogous overflow cases) is the correct sentinel. This stays a blocker.


New findings — 7d6d248 (opcode_test.go only)

The 8 added lines instrument both property checkers (inspectInputPropertyChecker and inspectOutputPropertyChecker) to assert that a successfully-pushed value satisfies 0 ≤ result ≤ btcutil.MaxSatoshi.

Correctness of the new assertions — sound. Cmp returns −1/0/+1, so:

  • require.LessOrEqual(t, topBigNum.Cmp(BigNumFromUint64(btcutil.MaxSatoshi)), 0) → value ≤ MaxSatoshi ✓
  • require.GreaterOrEqual(t, topBigNum.Cmp(BigNumFromUint64(0)), 0) → value ≥ 0 ✓

Tautology today, useful regression guard — because pushSatoshiValue already rejects out-of-range values before any push happens, a successful execution will always satisfy the new asserts. The value is that if a future change weakens the guard in pushSatoshiValue, the property tests will catch it. Fine; worth keeping.

Minor (non-blocking): the want calculation at opcode_test.go:5117 still does BigNumFromUint64(uint64(prevOut.Value)) with a raw int64→uint64 cast. For in-range values (the only ones that reach this path after pushSatoshiValue gates them) the cast is safe. No action needed.


No new findings beyond the above.

Fix required before merge: change txscript.ErrInvalidIndex → txscript.ErrNumberTooBig at pkg/arkade/opcode.go:2037.

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.

Reject invalid amounts in value introspection opcodes

3 participants