Repository navigation
Conversation
Update the vendored libsimplicity to 4e0beb5947b7e75ee7974d82a1447e89a0b4b5d6 and regenerate the jet files with update_jets.sh. At this revision GenRustJets emits the Bitcoin jet CMRs and costs that were previously replaced by an unimplemented!() stub, so src/jet/init/bitcoin.rs now carries the real CMR and cost tables for all 428 Bitcoin jets, and update_jets.lock records their hashes rather than a hand-written value. The vendored C tree is unchanged: the only difference between the previous revision and this one is the generator guard that suppressed Bitcoin output.
Add src/jet/bitcoin/spec_tests.rs, asserting the CMR and cost of every Bitcoin jet against the values in libsimplicity's primitiveJetNode.inc. Both tables are generated from that file by update_jets.sh, so these tests detect drift between the vendored C data and the generated Rust: if the vendored revision changes and the regenerated tables move, the tests fail rather than silently changing consensus behaviour.
Add Cost::get_padding_size, which returns the chain-agnostic byte length of the witness stack item required to bring a program cost within budget. The required length depends only on the item's serialized size, not its content, and is the same for Bitcoin and Elements. Add Cost::get_padding_bytes, which returns the chain-specific bytes that fill that length: - Bitcoin (cfg bitcoin, not elements): a plain all-zero item. A leading 0x50 byte would be misread as an annex, so Bitcoin must not use one. - Elements (cfg elements): an annex item: [0x50] followed by zeros. Deprecate Cost::get_padding (Elements annex form), which now delegates to get_padding_bytes. Make get_budget and is_budget_valid chain-agnostic by computing the witness-stack serialized length with explicit CompactSize math instead of the elements consensus encoder. The padding arithmetic itself is unchanged; only the surrounding API is split. Extend the Bitcoin test with the CompactSize boundary deficits (254, 255, 65 539, 65 540), where the length prefix grows and the smallest covering item is one byte larger than the deficit would suggest.
Declare the FFI bindings for the Bitcoin jets whose C implementations read the transaction environment (sig_all_hash, check_lock_*, current_index, num_inputs, tx_hash, tap_env_hash, annex_hash, the tapbranch/tapleaf/taptweak builders, and the remaining tx-context jets in bitcoinJets.c). These match the rustsimplicity_0_8_bitcoin_* symbols already generated in jets_ffi for the elements backend. c_jet_ptr itself is unchanged: the Bitcoin backend routes every jet, including these, through the existing jets_wrapper wrappers, which forward the Rust CTxEnv reference to the same C symbols. The new declarations are not separately bound in the generated jet table. Add src/jet/bitcoin/tests.rs, which exercises the environment path end to end against a real transaction: - c_jet_env_is_the_tx_env checks that the JetEnvironment hands out the same CTxEnv the backend was built with, rather than a unit value. - env_jets_execute_with_real_environment runs an env-context jet through the Rust Bit Machine and checks it observes the live environment. - bip_0340_verify_ptr_resolves covers the one env-free crypto jet, which resolves through the shared wrapper. - check_lock_height_executes_against_env executes a jet that reads the environment and asserts on its result. These guard the property that RedeemNode::prune() depends on: the Rust Bit Machine must be able to execute Bitcoin jets against a real transaction environment in order to rewrite dead case branches into asserts.
a717ac1 to
b4cc29f
Compare
|
Rebased to revendor libsimplicity |
| /// identically: a CompactSize item count, followed by each item as a | ||
| /// CompactSize length prefix and its bytes. The math is therefore | ||
| /// chain-agnostic, and this method is available under the `bitcoin` feature | ||
| /// (which `elements` implies) so it can be shared by both. |
There was a problem hiding this comment.
In c0bfd57
The elements feature does currently imply bitcoin, but that might not always be the case, so I think it would be better to add elements here as well.
This is a nit though, as it will show up in compiler problems if the dependency is removed.
| stack item needed to bring a cost within the witness budget, and `Cost::get_padding_bytes`, | ||
| returning the chain-specific bytes that fill it (an all-zero item on Bitcoin, an annex on | ||
| Elements). Deprecate `Cost::get_padding` in favour of these. | ||
| * `Cost::get_budget` and `Cost::is_budget_valid` now take `&[Vec<u8>]` instead of `&Vec<Vec<u8>>` |
There was a problem hiding this comment.
LLMs tend to add random information to the CHANGELOG. This is a private method so I'd vote to remove it. Fine to do in follow up.
|
ACK b4cc29f; Ran through local tests and fuzzing |
There was a problem hiding this comment.
Note
🤖 Claude Opus 5.5 responding on behalf of @KyrylR
Superseded by the inline review: #405 (review)
|
Ran some of my AIs on the PR This is a pilot test, so if you think that this "batch" is bad and distributing or you would like to see it in a different way, pls let me know for the future |
KyrylR
left a comment
There was a problem hiding this comment.
Note
🤖 Claude Opus 5.5 responding on behalf of @KyrylR
@KyrylR authorized posting this review. It was written by AI reviewers: ten independent reviews of b4cc29f, each in its own worktree. Each inline comment ends with an "Agreed by" line naming the AI reviewers that reported it independently. Opus 5.5 = Claude Opus 5.5 (×2), Mythos = Claude Mythos 5.2, Fable = Claude Fable 5.2, Astra = GPT-6 Astra (×3), Sol = GPT-6.1 Sol (×3).
The generated Bitcoin CMR and cost tables are correct; the padding API and the hand-written FFI block need changes, as described in the 9 inline comments.
Checked and correct:
- All 428 CMRs and costs match the vendored
bitcoin/primitiveJetNode.inc, with 0 mismatches across independent parsers. 3f0bb69..4e0beb5changes onlyGenRustJets.hs, andupdate_jets.sh --checkpasses.- Elements budget and padding results are identical to the old encoder-based code.
- Tests pass for every feature combination and on MSRV 1.74; clippy
-D warnings, fmt and docs are clean.
The tables commit (ffd1c22) could land on its own.
| /// This is the Bitcoin-specific form: the padding is a single all-zero | ||
| /// witness stack item. (Bitcoin has no annex, so the padding must be a | ||
| /// regular stack item; a leading `0x50` byte would be misread as an annex.) | ||
| #[cfg(all(feature = "bitcoin", not(feature = "elements")))] |
There was a problem hiding this comment.
Note
🤖 Claude Opus 5.5 responding on behalf of @KyrylR
In c0bfd57
Major: get_padding_bytes returns different bytes depending on which Cargo features are enabled.
Cost::from_milliweight(56_000).get_padding_bytes(&[]) returns [00, 00, 00, 00] with --no-default-features --features bitcoin, but [50, 00, 00, 00] with default features.
elements is a default feature, and any crate in the dependency graph can enable it, so a Bitcoin user can silently get the annex form. A build with both features cannot get the Bitcoin form at all.
Suggest two explicitly named methods, or a chain parameter, rather than selecting behaviour with cfg(not(feature = "elements")).
Agreed by: Opus 5.5 ×2, Mythos, Fable, Astra ×3, Sol ×1
| /// witness stack item. (Bitcoin has no annex, so the padding must be a | ||
| /// regular stack item; a leading `0x50` byte would be misread as an annex.) |
There was a problem hiding this comment.
Note
🤖 Claude Opus 5.5 responding on behalf of @KyrylR
In c0bfd57
Major: the all-zero padding item produces invalid witnesses, and the premise in this doc comment is wrong.
- Appending it breaks the witness.
test_get_padding_bitcoinappends the padding item, which puts it where Taproot expects the control block. With witness[0x51 script, valid 33-byte control block]and cost 90,000 mWU, this returns[0, 0]andis_budget_validthen reportstrue, butControlBlock::decodeon the last item fails withInvalidControlBlockSize(2). - "Bitcoin has no annex" is incorrect. BIP341 defines the annex on Bitcoin, and this crate extracts it in
src/jet/bitcoin/c_env.rsand hasAnnexHash/CurrentAnnexHashjets. - The Elements advice (L232-L235) gives an invalid spend. Elements
interpreter.cpprequiresstack.size() == 2after removing annex, control block and script, and otherwise fails withSCRIPT_ERR_SIMPLICITY_WRONG_LENGTH.
Suggest using annex padding on both chains, or dropping the Bitcoin variant until Bitcoin Simplicity defines a witness layout. Tests should parse the full witness rather than only check the budget.
Agreed by: Opus 5.5 ×2, Mythos, Fable, Astra ×3, Sol ×3 (Elements stack.size() == 2: Mythos, Fable)
| // Bitcoin jets whose C implementations read the transaction environment. | ||
| // Each has signature `bool f(frameItem* dst, frameItem src, const txEnv* env)` | ||
| // (note `src` is passed *by value*, matching the C definitions in bitcoinJets.c). | ||
| #[link_name = "rustsimplicity_0_8_bitcoin_sig_all_hash"] | ||
| pub fn bitcoin_sig_all_hash(dst: *mut CFrameItem, src: CFrameItem, env: *const CTxEnv) -> bool; | ||
| #[link_name = "rustsimplicity_0_8_bitcoin_check_lock_height"] | ||
| pub fn bitcoin_check_lock_height( | ||
| dst: *mut CFrameItem, | ||
| src: CFrameItem, | ||
| env: *const CTxEnv, | ||
| ) -> bool; | ||
| #[link_name = "rustsimplicity_0_8_bitcoin_check_lock_distance"] | ||
| pub fn bitcoin_check_lock_distance( | ||
| dst: *mut CFrameItem, | ||
| src: CFrameItem, | ||
| env: *const CTxEnv, | ||
| ) -> bool; | ||
| #[link_name = "rustsimplicity_0_8_bitcoin_check_lock_duration"] | ||
| pub fn bitcoin_check_lock_duration( | ||
| dst: *mut CFrameItem, | ||
| src: CFrameItem, | ||
| env: *const CTxEnv, | ||
| ) -> bool; | ||
| #[link_name = "rustsimplicity_0_8_bitcoin_current_index"] | ||
| pub fn bitcoin_current_index(dst: *mut CFrameItem, src: CFrameItem, env: *const CTxEnv) | ||
| -> bool; | ||
| #[link_name = "rustsimplicity_0_8_bitcoin_num_inputs"] | ||
| pub fn bitcoin_num_inputs(dst: *mut CFrameItem, src: CFrameItem, env: *const CTxEnv) -> bool; | ||
| #[link_name = "rustsimplicity_0_8_bitcoin_tx_hash"] | ||
| pub fn bitcoin_tx_hash(dst: *mut CFrameItem, src: CFrameItem, env: *const CTxEnv) -> bool; | ||
| #[link_name = "rustsimplicity_0_8_bitcoin_tap_env_hash"] | ||
| pub fn bitcoin_tap_env_hash(dst: *mut CFrameItem, src: CFrameItem, env: *const CTxEnv) -> bool; |
There was a problem hiding this comment.
Note
🤖 Claude Opus 5.5 responding on behalf of @KyrylR
In b4cc29f
Major: these 8 declarations, and the wrappers at L173-L198, are unused and conflict with the WASM ABI.
- Unused.
c_jet_ptralready sends every Bitcoin jet through the generatedjets_wrapper::bitcoin_*. Nothing references these, and the newjet::bitcointests still pass with this file reverted to base. - WASM ABI. They pass
frameItem srcby value, which is whatWRAP_exists to avoid ("Only the latter is permissible in WASM").cargo +1.88.0 build --target wasm32-unknown-unknown -p simplicity-sysgives 8wasm_c_abiwarnings here and 0 on base. They also skip theWRAP_postcondition assert ondst->offset. - Crash from safe code. They are 8 more safe
pub fns that can be crashed withoutunsafe: passingCFrameItem { edge: null(), len: 0 }tocurrent_indexsegfaults. The generated wrappers have the same signature; this adds more of them. - Commit message mismatch. The message names
annex_hashand the tapbranch/tapleaf/taptweak builders, but only 8 functions are declared.
Suggest deleting the block. That also resolves the code-generation question in the PR description.
Agreed by: all ten (WASM ABI: Mythos, Fable; segfault: Astra ×3, Sol ×1)
| //! Checks that the generated Bitcoin jet CMR and cost tables agree with the | ||
| //! values recorded in libsimplicity's `primitiveJetNode.inc`. |
There was a problem hiding this comment.
Note
🤖 Claude Opus 5.5 responding on behalf of @KyrylR
In 9537b3b
Minor: this is a copy of the generated table, not a check against primitiveJetNode.inc.
The 4,065 lines never read the vendored C, so they can't catch drift between the C data and the generated Rust; update_jets.sh --check already covers that. They also have to be hand-edited on every libsimplicity bump that changes a cost.
Suggest a short cfg(test) parser over the vendored .inc that iterates Bitcoin::ALL, or dropping the file. All 428 CMRs and costs do match the C today.
Agreed by: all ten
| @@ -0,0 +1,135 @@ | |||
| // SPDX-License-Identifier: CC0-1.0 | |||
|
|
|||
| //! Tests that the Bitcoin backend actually executes (and prunes) tx-context / | |||
There was a problem hiding this comment.
Note
🤖 Claude Opus 5.5 responding on behalf of @KyrylR
In b4cc29f
Minor: several tests here don't check what their names claim.
c_jet_env_is_the_tx_envcompares a value with itself, becausec_jet_envisself.c_tx_env().bip_0340_verify_ptr_resolvescan't fail:c_jet_ptrhas an arm for every jet, so "Unwired jets still panic here" (L102) is not true.TxHash,TapEnvHashandSigAllHashare only checked for output type2^256, not their values.check_lock_heightuses lock time 0 with a final sequence, so a jet that ignored the environment would also pass.- This doc says "(and prunes)", but nothing calls
prune.
Agreed by: Opus 5.5 ×2, Mythos, Fable, Astra ×3, Sol ×1
| // Note `deficit >= 2` here: the function returns early when the cost | ||
| // already fits, and a deficit of 0 or 1 cannot occur. |
There was a problem hiding this comment.
Note
🤖 Claude Opus 5.5 responding on behalf of @KyrylR
In c0bfd57
Minor: "a deficit of 0 or 1 cannot occur" is false.
Cost::from_milliweight(51_001) on an empty witness has a deficit of 1, and get_padding_size returns Some(1). On Bitcoin an empty item already satisfies is_budget_valid. Across deficits 1..70,000, this is the only case where the size is larger than needed.
Agreed by: Opus 5.5 ×2, Mythos, Fable
| // and 5 bytes for larger values. The overhead subtracted must account | ||
| // for the actual CompactSize encoding length of the resulting annex. | ||
| // for the actual CompactSize encoding length of the resulting item. | ||
| let deficit = (weight - budget).0 as usize; // cast safety: 32-bit machine or higher |
There was a problem hiding this comment.
Note
🤖 Claude Opus 5.5 responding on behalf of @KyrylR
In c0bfd57 (logic already on master)
Minor (already on master): the padding calculation ignores growth of the item-count prefix.
When the padding item takes the stack from 252 to 253 items (or from 65,535 to 65,536), the count's CompactSize grows by 2 bytes, and this calculation doesn't account for it. For example, 252 empty items with a 4 WU deficit returns 3 bytes when 1 is enough. It only over-pads, never under-pads.
The "padding must be minimal" test removes the whole item instead of shortening it, so it misses this.
Agreed by: Mythos, Fable, Astra ×3, Sol ×2
| * Add `Cost::get_padding_size`, returning the chain-agnostic length of the padding witness | ||
| stack item needed to bring a cost within the witness budget, and `Cost::get_padding_bytes`, | ||
| returning the chain-specific bytes that fill it (an all-zero item on Bitcoin, an annex on | ||
| Elements). Deprecate `Cost::get_padding` in favour of these. | ||
| * `Cost::get_budget` and `Cost::is_budget_valid` now take `&[Vec<u8>]` instead of `&Vec<Vec<u8>>` | ||
| and compute the serialized witness length directly rather than via the Elements consensus | ||
| encoder, making them available under the `bitcoin` feature (which `elements` implies) as | ||
| well as `elements`. |
There was a problem hiding this comment.
Note
🤖 Claude Opus 5.5 responding on behalf of @KyrylR
In c0bfd57
Minor: this entry is incomplete.
It omits the main change: Bitcoin::cmr() and Bitcoin::cost() no longer panic. It also omits the libsimplicity bump, and it lists the private get_budget as changed API.
Agreed by: Opus 5.5, Mythos, Fable
| let budget = serialized_len.saturating_add(50); | ||
| let budget = | ||
| u32::try_from(budget).expect("Serialized witness stack must be shorter than 2^32"); |
There was a problem hiding this comment.
Note
🤖 Claude Opus 5.5 responding on behalf of @KyrylR
In c0bfd57
Nit: this now panics where it used to saturate.
50 is added in usize before u32::try_from(...).expect(...); the old code converted first and then called saturating_add(50). Serialized lengths within 50 of u32::MAX now panic. This is only reachable with witnesses of about 4 GiB. Suggest converting first, then saturating_add(50).
Agreed by: Opus 5.5 ×2, Mythos, Fable, Astra ×2, Sol ×1
Includes and supercedes #386 #387 #388
Implements the rest of the Bitcoin jet environment from #331: the
cost()andcmr()stubs for Bitcoin jets are replaced with full tables derived from the canonicalprimitiveJetNode.incin libsimplicity, covering all 428 jets in the Bitcoin jet set.Splits
Cost::get_paddinginto a chain-agnosticget_padding_sizeplus a chain-specificget_padding_bytes. While Elements uses an annex item ([0x50]+ zeros), Bitcoin must pad with a plain all-zero stack item.Also declares the FFI bindings for the env-context Bitcoin jets (with tests exercising them against a real transaction environment) and drops the now-unnecessary
Arcs in environments.Closes the remaining gaps in
RedeemNode::prune(): the Rust Bit Machine can now execute Bitcoin jets against a realCTxEnvin order to rewrite dead case branches into asserts.Draft until I figure out how to resolve this with upstream code generation for simplicity-sys/src/c_jets/c_env/bitcoin.rs