V03State.programs is gone; deployed programs now live directly in
public_state, keyed by AccountId::from(program_id) same as any other
account. insert_program sets program_owner to a new reserved sentinel,
PROGRAM_STORAGE_OWNER, instead of leaving it at the default.
That ownership choice is load-bearing now in a way it wasn't before:
once program accounts share the same map as everything else, they're
reachable through ordinary dispatch, so program_owner determines
whether they're claimable/writable. Left unclaimed, a program
invocation could legitimately claim a program's storage account via
the normal claim path and then rewrite its elf; self-ownership has
the same flaw, since it authorizes exactly the program whose own
invocation would touch its own storage account. The reserved sentinel
makes every program account unwritable by construction, since no real
chained_call.program_id will ever derive to it.
programs() is removed; dispatch and the deployment-existence check go
through get_account_by_id_ref like any other account lookup.
genesis_fingerprint drops its separate program-hashing loop, since
program accounts now fall out of the existing public_state loop.
Rebuilt all guest artifacts and the test fixture via just
build-artifacts as a precaution, since V03State's Borsh shape changed
even though Account's did not.
Account.program_owner is now AccountId-typed instead of ProgramId,
via a new bijective From<ProgramId> for AccountId / From<AccountId>
for ProgramId conversion pair (pure byte reinterpretation, not a
hash - both types are exactly 32 bytes). Adds DEFAULT_PROGRAM_OWNER
as the AccountId-typed counterpart to DEFAULT_PROGRAM_ID, used at
every program_owner comparison/claim site instead of an inline
AccountId::default().
Touches every call site across lee_core, lee (including the
guest-side privacy-preserving circuit), all 16 deployed guest
programs, wallet/wallet-ffi, indexer_ffi/indexer_service/
indexer_service_protocol, sequencer_core, testnet_initial_state,
system_accounts, cross_zone, storage, cycle_bench, and
integration_tests - mostly mechanical .into() conversions, plus two
simplifications: wallet's manual base58 encode/decode of
program_owner was dead code once it's AccountId (which already has
Display/FromStr), and the FFI crates' program_owner field now reuses
the existing generic FfiBytes32 wrapper instead of the now-unused
FfiProgramId one.
Rebuilds every guest ELF artifact and the prebuilt sequencer test
fixture via just build-artifacts, since execute_and_prove runs
against the checked-in precompiled privacy_preserving_circuit.bin,
which isn't rebuilt automatically by cargo test/check.
- RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID: restore the SHA256 derivation
docstring that explains the constant instead of leaving it as an opaque
byte array, and switch the array itself to a hex literal (hex-literal,
already a workspace dependency) for readability.
- Rename the loader/loader_core crate to program_loader/program_loader_core
to disambiguate it, across the directory, package name, workspace
members/dependency alias, both dependent crates, and every call site.
deploy_pda_seed's first paragraph just restated its parameter names;
kept only the domain-separation rationale. chained_call_forwarder.rs's
doc comment was floating over an unrelated type alias instead of being
a module-level //! doc.
Removed explanatory comments that restated context better left to commit
history/PR description across the deploy-dispatch and program-storage code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The AccountId migration (marvin/program-as-account-3-1) fixed dispatch
locating a Deploy-created PDA-addressed program, but that was never
what blocked this test: the forwarder exhausts its 32M public-execution
cycle budget just carrying the target bytecode through its own
instruction_data before dispatch's addressing even matters. Chain-called
Deploy needs a mechanism where bytecode reaches Deploy without being
copied through an intermediary guest's interpreted execution, deferred
to a future PR. Deploy as the top-level entry point, natively emitting
its own follow-up chained calls after deploying, is the supported
pattern today.
chained_call_forwarder.rs and the deploy tests in sequencer_core were
added on top of the pre-migration ProgramId-typed ChainedCall/Message/
ProgramInput/ProgramOutput API and didn't conflict during the rebase
since they're new code, so they needed the same field rename/.into()
treatment already applied everywhere else on -3-1.
Adds tests for the remaining execute_deploy failure modes (invalid
bytecode, wrong target account, wrong number of accounts) and a
generic chained_call_forwarder test guest to exercise Deploy invoked
via a chained call rather than only top-level.
The chained-call test is #[ignore]d: it surfaces a real limitation,
not a bug in the test. The forwarding program has to carry the
deployed bytecode through its own instruction_data to build the
chained call, which blows the interpreted 32M-cycle public-execution
cap for any realistically-sized program — the native Deploy fast-path
only covers the loader's own execution, not the caller's. Root cause
is ChainedCall/Message still referencing programs by ProgramId rather
than AccountId, which also means dispatch can't locate a
Deploy-created (PDA-addressed) program at all. Tracked for
marvin/program-as-account-3-1.
Deploy is not intended to ever run privately, and public dispatch
already always takes the native fast-path for it, so the guest ELF
had no real execution path left. Removes lez/programs/loader's guest
binary crate, its [[bin]] wiring, programs::loader(), and the
now-pointless guest-vs-native equivalence test along with the
test-only Program::execute_for_test it depended on.
loader_core (Instruction, ProgramData, execute_deploy) stays — native
dispatch still calls it directly. Deploy is now honestly just native
dispatch logic with a program-shaped interface, not a program with a
guest binary nobody executes.
Introduces a new loader_program/loader_core crate pair implementing a
Deploy instruction that claims a program's ProgramData PDA account
(image_id, segment_number, update_auth, elf_segment), unifying
deployment with ordinary PublicTransaction dispatch instead of the
separate ProgramDeploymentTransaction path.
Measured against every real program in this repo, computing a
program's image_id inside the zkVM costs ~1,400-1,500 cycles per byte
of deployed bytecode, pushing real deployments to 500M-900M cycles
against the 32M public-execution cap (vs. ~27ms natively, since
ProgramDeploymentTransaction's equivalent check runs as a plain host
function today). To keep the unified dispatch path viable, Deploy is
special-cased in from_public_transaction: calls targeting the reserved
RESERVED_DEPLOYMENT_PROGRAM_ACCOUNT_ID run loader_core::execute_deploy
natively instead of through the interpreted guest executor, wrapped in
catch_unwind since the shared execute_deploy logic validates via
assert!/expect() like every other guest program, relying on that
boundary instead of the zkVM's own panic-to-Result conversion.
The loader guest binary is kept buildable and covered by a test that
runs it for real and asserts its output matches the native path
exactly, so the two can't silently drift apart.
V03State.programs is gone; deployed programs now live directly in
public_state, keyed by AccountId::from(program_id) same as any other
account. insert_program sets program_owner to a new reserved sentinel,
PROGRAM_STORAGE_OWNER, instead of leaving it at the default.
That ownership choice is load-bearing now in a way it wasn't before:
once program accounts share the same map as everything else, they're
reachable through ordinary dispatch, so program_owner determines
whether they're claimable/writable. Left unclaimed, a program
invocation could legitimately claim a program's storage account via
the normal claim path and then rewrite its elf; self-ownership has
the same flaw, since it authorizes exactly the program whose own
invocation would touch its own storage account. The reserved sentinel
makes every program account unwritable by construction, since no real
chained_call.program_id will ever derive to it.
programs() is removed; dispatch and the deployment-existence check go
through get_account_by_id_ref like any other account lookup.
genesis_fingerprint drops its separate program-hashing loop, since
program accounts now fall out of the existing public_state loop.
Rebuilt all guest artifacts and the test fixture via just
build-artifacts as a precaution, since V03State's Borsh shape changed
even though Account's did not.
ProgramInput/ProgramOutput.self_program_id/caller_program_id, and the
dispatcher's CallerData.program_id, now carry AccountId (renamed to
self_account_id/caller_account_id) instead of ProgramId. These fields
are self-reported/cross-checked dispatch bookkeeping, not RISC0 image
identity, and AccountId already crosses the guest/host boundary this
way via every pre_state.account_id.
ProgramId is now confined to what's actually image-id-keyed:
env::verify, Program.id (from compute_image_id()), and the
for_public_pda/for_private_pda derivation formulas, each recovering
the real ProgramId from AccountId via the existing bijection exactly
where needed.
Rebuilds artifacts and the prebuilt sequencer db fixture to match.
Account.program_owner is now AccountId-typed instead of ProgramId,
via a new bijective From<ProgramId> for AccountId / From<AccountId>
for ProgramId conversion pair (pure byte reinterpretation, not a
hash - both types are exactly 32 bytes). Adds DEFAULT_PROGRAM_OWNER
as the AccountId-typed counterpart to DEFAULT_PROGRAM_ID, used at
every program_owner comparison/claim site instead of an inline
AccountId::default().
Touches every call site across lee_core, lee (including the
guest-side privacy-preserving circuit), all 16 deployed guest
programs, wallet/wallet-ffi, indexer_ffi/indexer_service/
indexer_service_protocol, sequencer_core, testnet_initial_state,
system_accounts, cross_zone, storage, cycle_bench, and
integration_tests - mostly mechanical .into() conversions, plus two
simplifications: wallet's manual base58 encode/decode of
program_owner was dead code once it's AccountId (which already has
Display/FromStr), and the FFI crates' program_owner field now reuses
the existing generic FfiBytes32 wrapper instead of the now-unused
FfiProgramId one.
Rebuilds every guest ELF artifact and the prebuilt sequencer test
fixture via just build-artifacts, since execute_and_prove runs
against the checked-in precompiled privacy_preserving_circuit.bin,
which isn't rebuilt automatically by cargo test/check.
Corrects lee/state_machine internals for the Program-as-Account migration
and fixes SeenShard::MAX_DELIVERIES, which was still calibrated for the
old 100 KiB DATA_MAX_LENGTH instead of the current 700 KiB cap. Rebuilds
program artifacts and the sequencer test fixture to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ping_receiver's renounce_authority/update_sources, wrapped_token's
renounce_authority/update_sources, authority_proxy, and
cross_zone_state_machine integration tests all landed on dev after this
branch's self/caller-identity and ChainedCall/Message AccountId migrations,
so they still referenced the old ProgramId-based fields.
dev grew new self_program_id/caller_program_id and ChainedCall.program_id
call sites (sequencer_stake's rewritten main.rs, gossip validation,
cross_zone's shared ping_emission test helper, and three guest test
programs) after this branch's ProgramId->AccountId migration commits
were originally written, so they predated the self_account_id/
caller_account_id/program_account_id rename and didn't conflict during
the rebase - they just still assumed the old ProgramId-typed fields.
Converts all of them, recovering ProgramId via the existing bijection
only where the RISC0 image id is actually needed, and rebuilds
artifacts against the fixed source.
ProgramInput/ProgramOutput.self_program_id/caller_program_id, and the
dispatcher's CallerData.program_id, now carry AccountId (renamed to
self_account_id/caller_account_id) instead of ProgramId. These fields
are self-reported/cross-checked dispatch bookkeeping, not RISC0 image
identity, and AccountId already crosses the guest/host boundary this
way via every pre_state.account_id.
ProgramId is now confined to what's actually image-id-keyed:
env::verify, Program.id (from compute_image_id()), and the
for_public_pda/for_private_pda derivation formulas, each recovering
the real ProgramId from AccountId via the existing bijection exactly
where needed.
Rebuilds artifacts and the prebuilt sequencer db fixture to match.
Chained-call and public-transaction dispatch now address the target
program directly by AccountId instead of routing through ProgramId and
converting internally, closing the gap that blocked PDA-addressed
program invocation. The field is named program_account_id (not
account_id) to stay unambiguous next to the account_ids list it sits
beside in the same structs.
Execution/PDA-derivation logic that fundamentally needs the RISC0 image
id (self_program_id, caller_program_id, env::verify, PDA seed
derivation) stays ProgramId-typed, recovering it from the dispatched
AccountId via the existing bijection where needed.
V03State.programs is gone; deployed programs now live directly in
public_state, keyed by AccountId::from(program_id) same as any other
account. insert_program sets program_owner to a new reserved sentinel,
PROGRAM_STORAGE_OWNER, instead of leaving it at the default.
That ownership choice is load-bearing now in a way it wasn't before:
once program accounts share the same map as everything else, they're
reachable through ordinary dispatch, so program_owner determines
whether they're claimable/writable. Left unclaimed, a program
invocation could legitimately claim a program's storage account via
the normal claim path and then rewrite its elf; self-ownership has
the same flaw, since it authorizes exactly the program whose own
invocation would touch its own storage account. The reserved sentinel
makes every program account unwritable by construction, since no real
chained_call.program_id will ever derive to it.
programs() is removed; dispatch and the deployment-existence check go
through get_account_by_id_ref like any other account lookup.
genesis_fingerprint drops its separate program-hashing loop, since
program accounts now fall out of the existing public_state loop.
Rebuilt all guest artifacts and the test fixture via just
build-artifacts as a precaution, since V03State's Borsh shape changed
even though Account's did not.
Account.program_owner is now AccountId-typed instead of ProgramId,
via a new bijective From<ProgramId> for AccountId / From<AccountId>
for ProgramId conversion pair (pure byte reinterpretation, not a
hash - both types are exactly 32 bytes). Adds DEFAULT_PROGRAM_OWNER
as the AccountId-typed counterpart to DEFAULT_PROGRAM_ID, used at
every program_owner comparison/claim site instead of an inline
AccountId::default().
Touches every call site across lee_core, lee (including the
guest-side privacy-preserving circuit), all 16 deployed guest
programs, wallet/wallet-ffi, indexer_ffi/indexer_service/
indexer_service_protocol, sequencer_core, testnet_initial_state,
system_accounts, cross_zone, storage, cycle_bench, and
integration_tests - mostly mechanical .into() conversions, plus two
simplifications: wallet's manual base58 encode/decode of
program_owner was dead code once it's AccountId (which already has
Display/FromStr), and the FFI crates' program_owner field now reuses
the existing generic FfiBytes32 wrapper instead of the now-unused
FfiProgramId one.
Rebuilds every guest ELF artifact and the prebuilt sequencer test
fixture via just build-artifacts, since execute_and_prove runs
against the checked-in precompiled privacy_preserving_circuit.bin,
which isn't rebuilt automatically by cargo test/check.
- Use FIXME instead of TODO for the temporary ProgramId->AccountId
conversion, per review convention for patches guaranteed to be
fixed later.
- Derive cross_zone_inbox's MAX_DELIVERIES from DATA_MAX_LENGTH
instead of a hand-recomputed literal, so it stays in sync
automatically the next time the cap changes.
Corrects lee/state_machine internals for the Program-as-Account migration
and fixes SeenShard::MAX_DELIVERIES, which was still calibrated for the
old 100 KiB DATA_MAX_LENGTH instead of the current 700 KiB cap. Rebuilds
program artifacts and the sequencer test fixture to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
usize::try_from isn't const-stable yet, so the as-cast is unavoidable
here; mirrors the identical #[expect(...)] already used for
SeenShard::MAX_DELIVERIES's const cast in cross_zone_inbox/core.
DATA_MAX_LENGTH_BYTES is a real const (ByteSize::as_u64 is const fn,
so no fallible conversion needed), replacing the private
data_max_length() helper and every duplicate
usize::try_from(DATA_MAX_LENGTH.as_u64()).expect(...) call site
across data.rs and its two external consumers, per PR #756 review nit.
Same derived-Serialize word-per-byte overhead Data had, on the same
guest-boundary path (both only ever reach a real, packed decode as the
prover's own PrivacyPreservingCircuitInput — never decoded from untrusted
bytes by a verifier, same audit as Data's). No new length cap added here,
unlike Data: neither type has one today, and tests deliberately construct
malformed-length values to exercise later, use-site rejection.
Trimmed comment prose, but restore the "why" for the RISC0 binary path's
post-hoc (not preventive) cap check — the answer to "why isn't this a DoS
bug" is the load-bearing part.
Data's serde impls went through the generic per-element seq path, costing a
full 4-byte word per byte when crossing risc0_zkvm::serde (guest I/O, journal
commits, proving) — real cycle cost on every execution/proof touching an
account with non-trivial data, not just a wire-size issue. Routes through
serialize_bytes/deserialize_bytes instead, packing 4 bytes/word (~4x fewer
words), while keeping the existing incremental DATA_MAX_LENGTH check for the
human-readable (JSON) path and staying wire-compatible with the one
production JSON consumer (getAccount).
Rebuilds all committed guest artifacts and the test fixture dump, required
since the encoding change touches the guest-embedded wire format itself.
This PR introduces the initial Cucumber-based integration test framework for the LEZ, building on the testing-framework integration work started by @andrussal, adds the first set of Cucumber integration scenarios and establishes reusable infrastructure for future Cucumber scenarios.
---------
Co-authored-by: Andrus Salumets <salumets.andrus@gmail.com>
Co-authored-by: Sergio Chouhy <sergio.chouhy@gmail.com>
Co-authored-by: Sergio Chouhy <41742639+schouhy@users.noreply.github.com>
Co-authored-by: Roman <zajic@zajic.net>
Co-authored-by: Daniil Polyakov <arjentix@gmail.com>
Co-authored-by: Moudy <m.ellaz@hotmail.com>
Co-authored-by: andrussal <salumets.andrus@gmail.com>