Skip to content

Hardening follow-ups: silent-placeholder error, locktime/feeUtxo footguns, Java funding parity#139

Open
icellan wants to merge 12 commits into
remediation/design-ports-pr2from
remediation/hardenings-pr3
Open

Hardening follow-ups: silent-placeholder error, locktime/feeUtxo footguns, Java funding parity#139
icellan wants to merge 12 commits into
remediation/design-ports-pr2from
remediation/hardenings-pr3

Conversation

@icellan

@icellan icellan commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Hardening follow-ups: silent-placeholder error, locktime/feeUtxo footguns, Java funding parity

Small, mostly-advisory hardenings surfaced by the adversarial reviews during the #106#134 remediation (#136, #138). No consensus/bytecode behavior changes for existing contracts — zero conformance golden churn.

H1 — load_prop with no constructor slot now hard-errors (all 7 tiers) — #119 tail

The stack lowerer's load_prop fallback silently coerced a property with no deploy-time slot onto constructor slot 0 — pushing an unrelated arg's placeholder (a silent-wrong-code path). It now fails loudly with a diagnostic naming the property. Proven unreachable by any current fixture (instrumented: 4,852 fallback hits, none on the error path), so zero golden churn; legitimate constructor-param and mutable-slot properties are unaffected. Ported to all 6 non-TS tiers.

  • Python note: a pre-existing Python-parser gap (inline constructor(readonly n) param-properties never enter the property list) means Python errors only when the ctor-param list is non-empty — the real ghost case still errors; the parser gap is flagged as a separate follow-up.

H2 — locktime soundness warning (all 7 tiers) — #131 follow-up

A method reading extractLocktime (or currentBlockHeight()) is only consensus-enforced if the covenant also asserts extractSequence(preimage) < 0xffffffff (else a hand-built all-final-sequence tx mines immediately despite a future nLockTime — the #131 consensus-review finding). The compiler now emits an advisory warning (transitive through private helpers) when the sequence guard is missing. Warning-only → no bytecode change. Ported to all 6 non-TS tiers.

H3 — feeUtxo oversize warning + docs — #118 follow-up

CallOptions.feeUtxo is consumed entirely as the miner fee (no change output — inherent to the exact-output covenant binding). The SDK now warns when a feeUtxo dwarfs the estimated fee (best-effort, never blocks the call), and the feeUtxo doc + a contract-patterns callout document both this and the H2 locktime/sequence pairing. (TS SDK; 6-SDK warning port is a minor follow-up.)

H4 — Java SDK funding parity — #134 / #118

Verification (all green, zero golden churn)

  • Conformance fold-OFF 64/64; parser-only matrix 576/576 on all 7 tiers — the hardenings don't perturb any fixture.
  • TS packages (runar-compiler 3586, runar-sdk 513) + all 6 native compiler suites (Go/Rust/Python 1109/Zig 714/Ruby/Java 639) + Java SDK (471) — 0 failed.
  • H1 errors consistently and H2 warns consistently across all 7 tiers; one test per hardening per tier.

icellan added 12 commits July 8, 2026 07:02
lowerLoadProp's placeholder fallback coerced a property with no matching
constructor slot (paramIndex === -1) onto slot 0, silently splicing an
unrelated constructor argument's deploy-time bytes into the locking
script. Turn that silent-wrong-code path into a hard error with a clear
diagnostic and source location. Real constructor-param properties
(readonly, or mutable state fields whose initial value is spliced at
deploy) have paramIndex >= 0 and are unaffected — zero golden churn.

Behavior-changing: needs a 6-tier port (stack lowering).
…ptions (#134, #118, #131)

Gap 1: add DeployOptions (fundingSigner + satoshis/changeAddress) and a
deploy(Provider, Signer, DeployOptions) overload. TransactionBuilder signs the
deploy funding inputs with fundingSigner while the funding-UTXO lookup and
default change address stay on the deploy signer, mirroring the TS SDK's
DeployOptions.fundingSigner. Unset => deploy signer (zero behaviour change).

Gap 2: thread fundingSigner (#134), feeUtxo (#118), and sequence (#131) through
the multi-signer prepareTerminalCall path so it matches the primary
callTerminal path. With a deterministic (RFC 6979) signer the prepared/finalized
terminal tx is byte-identical to the primary callWithOptions tx.
A locktime gate (extractLocktime) is only consensus-enforced if the
covenant also asserts the spending tx is non-final
(extractSequence < 0xffffffff); otherwise an all-final-sequence tx
bypasses the timelock. Add an advisory validator WARNING (non-fatal,
via the existing diagnostics channel) when a public method reads the tx
locktime — directly or transitively through a private helper — but never
asserts a sequence-finality guard. Also covers the currentBlockHeight()
sugar, which desugars to extractLocktime. Warning-only: no bytecode or
golden change.

Needs a 6-tier port (validators).
A CallOptions.feeUtxo is consumed entirely as fee — the terminal covenant
binds the exact output set, so there is no change output and any excess
is burned. Add a runtime console.warn (SDK logging convention) when the
supplied feeUtxo is > 5x the terminal tx's estimated fee AND more than
~1000 sats of excess would be burned. Advisory only; best-effort (a
fee-rate lookup failure skips the warning, never blocks the call). Does
not add a change output (would break the covenant).

TS SDK only — 6-SDK port is a follow-up.
(a) CallOptions.feeUtxo doc comment: the whole UTXO is spent as fee (no
change) — size it to the intended fee; excess is burned. Note the SDK's
oversize warning.
(b) contract-patterns.md: a locktime-gated covenant must pair
extractLocktime with an extractSequence < 0xffffffff assertion to be
consensus-enforced — an all-final-sequence tx bypasses nLockTime
otherwise.
H1 (#119 tail): stack lowering lowerLoadProp panics when a load_prop
names a property with no constructor slot (Go's scan loop never breaks,
paramIndex == count of ctor-params) instead of silently splicing an
unrelated constructor argument onto slot 0. Legit ctor-param props
unaffected — zero golden churn.

H2 (#131): validator emits an advisory warning when a public method
reads extractLocktime (or currentBlockHeight) transitively but never
asserts extractSequence < 0xffffffff. Warning-only, no bytecode change.

Ports TS commits 482bcc5 (H1) and 8ff8f19 (H2) to the Go tier.
H1 (#119 tail): _lower_load_prop raises when a load_prop names a
property with no constructor slot (find_index returns nil) instead of
silently coercing it onto slot 0. Legit ctor-param props unaffected —
zero golden churn.

H2 (#131): validator emits an advisory warning when a public method
reads extractLocktime (or currentBlockHeight) transitively but never
asserts extractSequence < 0xffffffff. Warning-only, no bytecode change.

Ports TS commits 482bcc5 (H1) and 8ff8f19 (H2) to the Ruby tier.
H1 (#119 tail): lower_load_prop resolves the constructor slot via a new
ctor_param_index_or_panic helper used by both placeholder arms; it
panics when position() finds no slot instead of coercing onto slot 0.
Legit ctor-param props unaffected — zero golden churn.

H2 (#131): validator emits an advisory warning when a public method
reads extractLocktime (or currentBlockHeight) transitively but never
asserts extractSequence < 0xffffffff. Warning-only, no bytecode change.

Ports TS commits 482bcc5 (H1) and 8ff8f19 (H2) to the Rust tier.
H1 (#119 tail): lowerLoadProp throws when a load_prop names a property
with no constructor slot (ctorProps.indexOf < 0) instead of silently
coercing it onto slot 0. Legit ctor-param props unaffected — zero
golden churn.

H2 (#131): validator emits an advisory warning when a public method
reads extractLocktime (or currentBlockHeight) transitively but never
asserts extractSequence < 0xffffffff. Warning-only, no bytecode change.

Ports TS commits 482bcc5 (H1) and 8ff8f19 (H2) to the Java tier.
H1 (#119 tail): _lower_load_prop raises when a load_prop names a
property absent from a non-empty registered constructor-param list
instead of silently coercing it onto an unrelated slot. The raise is
gated on ctor_params being non-empty because the Python TS-format
parser drops inline 'readonly n' parameter-properties (they never enter
ContractNode.properties); gating preserves the historical slot-0
placeholder for that parser-gap case and keeps zero golden churn while
still catching the real ghost case. Legit ctor-param props unaffected.

H2 (#131): validator emits an advisory warning when a public method
reads extractLocktime (or currentBlockHeight) transitively but never
asserts extractSequence < 0xffffffff. Warning-only, no bytecode change.

Ports TS commits 482bcc5 (H1) and 8ff8f19 (H2) to the Python tier.
H1 (#119 tail): lowerPropertyRead returns LowerError.LoadPropNoConstructorSlot
when a load_prop names a property with no constructor slot (loop never
breaks, found == false) instead of silently emitting a placeholder for
an unrelated slot. Legit ctor-param props unaffected — zero golden churn.

H2 (#131): validator emits an advisory warning when a public method
reads extractLocktime (or currentBlockHeight) transitively but never
asserts extractSequence < 0xffffffff. Bound accepts literal_int as well
as literal_bigint since the Zig frontend lowers small bigints to
literal_int. Warning-only, no bytecode change.

Ports TS commits 482bcc5 (H1) and 8ff8f19 (H2) to the Zig tier.
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.

1 participant