Follow-up to #142, which fixed this in the TypeScript SDK only. The same latent bug very likely
exists in the call-transaction builders of the other six SDK tiers that support multi-contract-input
(merge) calls (Go, Rust, Python, Zig, Ruby, Java).
The bug
The funding coin-selection (selectUtxos / estimateDeployFee and equivalents) models only the P2PKH
funding inputs (148 B each) + the continuation output + change. It is blind to the contract /
covenant input(s) being spent in the same transaction. For a MERGE, each covenant input embeds both
parent transactions as method args (tens of KB), so ignoring them under-provisions the funding:
buildCallTransaction then sees change <= 0 and drops the change output, and the merge covenant —
which reconstructs [continuation][P2PKH change] unconditionally — fails its hashOutputs
OP_VERIFY. The merge transaction is invalid and the covenant becomes un-spendable via that path.
The fix (TS reference, PR #142)
- Hoist the per-input arg resolution + placeholder unlock scripts above the funding coin-selection
so their byte sizes feed the fee estimate.
- Add an
extraInputBytes parameter to estimateDeployFee / selectUtxos (default 0 — pure-deploy and
funding-only selection are unaffected).
- Size the fee against the real serialized per-input bytes of every contract input, plus a
per-contract-input overhead (≈ 2 * lockingScriptLen + buffer) covering the stateful unlock's
codePart + BIP-143 preimage scriptCode. Over-provisioning is safe (slightly larger change; no dust
limit on Chronicle); under-provisioning was the bug.
See packages/runar-sdk/src/contract.ts (prepareCall) and packages/runar-sdk/src/deployment.ts
(estimateDeployFee / selectUtxos).
Tiers to audit + candidate files
Only tiers that support additionalContractInputs (merge) calls are affected; single-input stateful/
stateless calls add a correspondingly small term and are unaffected.
- Go —
packages/runar-go/sdk_deployment.go (SelectUtxos), sdk_types.go, call builder
- Rust —
packages/runar-rs/src/sdk/contract.rs (select_utxos), mod.rs
- Python —
packages/runar-py/runar/sdk/contract.py (select_utxos), calling.py
- Zig —
packages/runar-zig/src/sdk_contract.zig, sdk_types.zig
- Ruby —
packages/runar-rb/lib/runar/sdk/types.rb (select_utxos), contract.rb
- Java —
.../runar/lang/sdk/UtxoSelector.java, FeeEstimator.java, TransactionBuilder.java
Severity / scope
HIGH for tiers used to build merges (produces an invalid, un-spendable merge tx); tiers without
merge-call support are unaffected. This is tier-local fee/coin-selection ergonomics, not a
wire-protocol primitive per CLAUDE.md — so per-tier correctness, not a cross-tier byte-parity gap. Each
affected tier should get the multi-contract-call regression coverage #142 exercised in TS.
Follow-up to #142, which fixed this in the TypeScript SDK only. The same latent bug very likely
exists in the call-transaction builders of the other six SDK tiers that support multi-contract-input
(merge) calls (Go, Rust, Python, Zig, Ruby, Java).
The bug
The funding coin-selection (
selectUtxos/estimateDeployFeeand equivalents) models only the P2PKHfunding inputs (148 B each) + the continuation output + change. It is blind to the contract /
covenant input(s) being spent in the same transaction. For a MERGE, each covenant input embeds both
parent transactions as method args (tens of KB), so ignoring them under-provisions the funding:
buildCallTransactionthen seeschange <= 0and drops the change output, and the merge covenant —which reconstructs
[continuation][P2PKH change]unconditionally — fails itshashOutputsOP_VERIFY. The merge transaction is invalid and the covenant becomes un-spendable via that path.The fix (TS reference, PR #142)
so their byte sizes feed the fee estimate.
extraInputBytesparameter toestimateDeployFee/selectUtxos(default 0 — pure-deploy andfunding-only selection are unaffected).
per-contract-input overhead (≈
2 * lockingScriptLen + buffer) covering the stateful unlock'scodePart + BIP-143 preimage scriptCode. Over-provisioning is safe (slightly larger change; no dust
limit on Chronicle); under-provisioning was the bug.
See
packages/runar-sdk/src/contract.ts(prepareCall) andpackages/runar-sdk/src/deployment.ts(
estimateDeployFee/selectUtxos).Tiers to audit + candidate files
Only tiers that support
additionalContractInputs(merge) calls are affected; single-input stateful/stateless calls add a correspondingly small term and are unaffected.
packages/runar-go/sdk_deployment.go(SelectUtxos),sdk_types.go, call builderpackages/runar-rs/src/sdk/contract.rs(select_utxos),mod.rspackages/runar-py/runar/sdk/contract.py(select_utxos),calling.pypackages/runar-zig/src/sdk_contract.zig,sdk_types.zigpackages/runar-rb/lib/runar/sdk/types.rb(select_utxos),contract.rb.../runar/lang/sdk/UtxoSelector.java,FeeEstimator.java,TransactionBuilder.javaSeverity / scope
HIGH for tiers used to build merges (produces an invalid, un-spendable merge tx); tiers without
merge-call support are unaffected. This is tier-local fee/coin-selection ergonomics, not a
wire-protocol primitive per CLAUDE.md — so per-tier correctness, not a cross-tier byte-parity gap. Each
affected tier should get the multi-contract-call regression coverage #142 exercised in TS.