Skip to content

feat(rpc): add zks_getAccountPreimage#1161

Draft
antoniolocascio-bot wants to merge 1 commit intomatter-labs:mainfrom
antoniolocascio-bot:prividium-sd-account-preimage-rpc
Draft

feat(rpc): add zks_getAccountPreimage#1161
antoniolocascio-bot wants to merge 1 commit intomatter-labs:mainfrom
antoniolocascio-bot:prividium-sd-account-preimage-rpc

Conversation

@antoniolocascio-bot
Copy link
Copy Markdown
Contributor

Summary

Adds a new zks_getAccountPreimage(address, batchNumber) -> Option<Bytes> JSON-RPC method under the existing zks namespace. It returns the 124-byte AccountProperties::encoding() blob for the given account at the end of the specified L1 batch.

Why

Any client that wants to prove claims about individual AccountProperties fields (balance, nonce, observable bytecode hash, versioning data, …) against the Merkle-proof-verified tree value needs the exact 124-byte preimage, because the state tree stores only blake2s(AccountProperties::encoding()) at the account-properties slot. The Merkle proof returned by zks_getProof covers that hash, but not the preimage it commits to.

Partial reconstruction from the existing public surface (eth_getBalance, eth_getCode, eth_getTransactionCount) is not sufficient: versioning_data, bytecode_hash, and artifacts_len are internal fields of AccountProperties that aren't otherwise exposed, and the resulting bytes wouldn't blake2s-hash to the tree value. This is particularly important for zk-based tooling (selective disclosure, light clients, cross-chain bridges reading ZKsync OS state) where trusting RPC responses directly defeats the purpose.

What

Thin wrapper around the existing

storage.state_view_at(last_block_of_batch).get_account(address)

path that eth_getBalance / eth_getCode etc. already rely on internally. The new method simply returns Bytes::copy_from_slice(&props.encoding()) instead of extracting a single field:

  • lib/rpc_api/src/zks.rs: adds the getAccountPreimage method to the ZksApi trait (under the existing zks namespace).
  • lib/rpc/src/zks_impl.rs: implements get_account_preimage_impl using ViewState::get_account, returning None when the account doesn't exist at the queried batch (same semantics as a non-existing-slot zks_getProof result).

No new deps, no new fields on existing response types, no schema changes.

Tests

  • cargo nextest run -p zksync_os_rpc -p zksync_os_rpc_api — 14/14 passing locally.
  • cargo clippy --all-targets --all-features --workspace --exclude zksync_os_integration_tests -- -D warnings — clean.
  • cargo fmt --all -- --check — clean.

I haven't added a dedicated RPC integration test in this PR because the path being exposed is the exact same one eth_getBalance / eth_getCode already cover (they go through ViewState::get_account and then extract a field), so any regression would already be caught by those paths. Happy to add one if reviewers prefer.

Context

The canonical user of this RPC is https://github.com/antoniolocascio-bot/prividium-zk-selective-disclosure — a tool that generates zero-knowledge proofs of individual Prividium account facts bound to an L1 batch commitment. That tool needs to hash the preimage bytes inside a zk circuit, so fetching the fields individually over RPC is not an option.

🤖 Generated with Claude Code

Exposes the 124-byte `AccountProperties::encoding()` preimage for a
given account at the end of a specific L1 batch, under the existing
`zks` JSON-RPC namespace.

## Motivation

Selective-disclosure tooling that runs over ZKsync OS state (and
specifically anything that wants to prove claims about individual
`AccountProperties` fields — balance, nonce, observable bytecode
hash, versioning data, etc. — inside a zk circuit) needs the full
124-byte encoded preimage of the `AccountProperties` struct. This is
because the state tree stores only `blake2s(AccountProperties::encoding())`
at the account-properties slot; the Merkle proof returned by
`zks_getProof` covers that hash, but not the preimage it commits to.
To verify any single field in-circuit against the Merkle-proof-
verified tree value, the prover has to feed the exact encoded bytes
through blake2s and compare.

Partial reconstruction from the existing public surface
(`eth_getBalance` / `eth_getCode` / `eth_getTransactionCount`) is not
sufficient: `versioning_data`, `bytecode_hash`, and `artifacts_len`
are internal fields of `AccountProperties` that are not otherwise
exposed over the JSON-RPC surface, and the resulting bytes would not
blake2s-hash to the tree value.

## Implementation

Thin wrapper around the existing
`storage.state_view_at(last_block_of_batch).get_account(address)`
path that `eth_getBalance`, `eth_getCode`, etc. already use
internally. The new method simply returns
`Bytes::copy_from_slice(&props.encoding())` instead of extracting a
single field. Returns `None` when the account has never been touched
at the queried batch, matching the semantics of a non-existing-slot
`zks_getProof` result.

Used by
https://github.com/antoniolocascio-bot/prividium-zk-selective-disclosure
to build `balance_of` / `observable_bytecode_hash` witnesses from
Prividium RPC without requiring a second trust root.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@antoniolocascio antoniolocascio marked this pull request as draft April 9, 2026 11:21
antoniolocascio-bot pushed a commit to antoniolocascio-bot/prividium-zk-selective-disclosure that referenced this pull request Apr 9, 2026
## Summary

Wires the host crate from a library-only prover/verifier into a
fully end-to-end tool that fetches witnesses from a live ZKsync OS
L2 RPC and verifies them against a live Ethereum L1 RPC, driven by
a CLI. Adds a local-setup/ directory that spins up anvil + a
patched zksync-os-server for offline testing.

## New host modules

- `rpc_wire.rs` — JSON wire-format types mirroring
  `zksync_os_rpc_api::types::BatchStorageProof` and
  `zksync_os_merkle_tree_api::flat::StorageSlotProof` one-to-one.
  We deliberately redefine these rather than taking a path dep on
  `zksync_os_rpc_api`, because its transitive dep graph pulls in a
  zksync-airbender version that conflicts with the one
  `airbender-host` uses. Four snapshot tests lock in the exact
  camelCase shape + the `type: existing | nonExisting` tag.
- `disclosure_request.rs` — high-level `DisclosureRequest` enum
  with one variant per v0 statement (BalanceOf,
  ObservableBytecodeHash, TxInclusion). This is what the CLI and
  any user of `prove_from_source` passes in.
- `witness_source.rs` — `WitnessSource` trait. Uses `impl Future +
  Send` in the trait signature to keep the returned future `Send`
  (required by `prove_from_source`'s block-on). Also ships an
  in-memory `MockWitnessSource` for tests.
- `rpc_l1.rs` — `RpcL1Source`: an `L1Source` that calls
  `diamondProxy.storedBatchHash(uint64)` via alloy. Blocks on an
  internal tokio runtime so the sync `L1Source` trait doesn't
  leak async to callers.
- `rpc_l2.rs` — `RpcWitnessSource`: a `WitnessSource` that:
  - calls `zks_getProof` to get Merkle paths + state commitment
    preimage + L1 verification data,
  - calls `zks_getAccountPreimage` (the new method from
    matter-labs/zksync-os-server#1161) to get the 124-byte
    `AccountProperties` preimage,
  - for `tx_inclusion`, walks 256 blocks via
    `eth_getBlockByNumber(full=true)`, converts each header
    field-by-field into our core `BlockHeader`, and locates the
    target tx in the rolling-hash window.

## Prover / verifier wiring

- `prover::prove_from_source<W: WitnessSource>` — new helper that
  fetches a witness via the source, blocks on its async call from
  a local tokio runtime, and feeds the resulting `ProveRequest`
  into the existing `prove()`. New `ProveFromSourceError` cleanly
  separates witness-fetch failures from prover-side failures.

## CLI

`host/src/main.rs` is now a full clap-based CLI with three
subcommands:

- `prove balance-of | observable-bytecode-hash | tx-inclusion`
  — fetches a witness via `RpcWitnessSource`, runs the airbender
  dev prover, writes the serialized `ProofBundle` to disk.
- `verify` — reads a bundle from disk, queries L1 via
  `RpcL1Source`, runs the airbender dev verifier, prints a typed
  `VerifiedDisclosure`.
- `inspect` — human-readable dump of a bundle file, no RPC calls.

## Local setup

`local-setup/run_local.sh` delegates to the server's own
`run_local.sh` pointing at `local-chains/v30.2/default`, after
checking that the sibling zksync-os-server checkout is on the
`prividium-sd-account-preimage-rpc` branch (needed until PR #1161
merges). `local-setup/README.md` documents the branch setup and
shows a full end-to-end CLI walkthrough.

## Tests

New `host/tests/witness_source.rs` — 4 integration tests that
exercise `DisclosureRequest → MockWitnessSource::fetch →
prove_from_source → verify_bundle → VerifiedDisclosure` end to
end for all three statements, plus a "not registered" error case.
Combined with the existing tests, the total is:

- 69 `prividium-sd-core` unit tests
- 18 `prividium-sd-test-fixtures` tests (9 unit + 9 integration)
- 6 host library unit tests (4 rpc_wire snapshots + 2 mock witness
  source)
- 8 `host/tests/end_to_end.rs` (pre-existing)
- 5 `host/tests/statements.rs` (pre-existing)
- 4 `host/tests/witness_source.rs` (new)
- **110 total**

All green, all RPC-free — the real `RpcL1Source` /
`RpcWitnessSource` / CLI paths are exercised manually via
`local-setup/run_local.sh` against a live local setup.

## Notes

- The bundle format is unchanged; the new RPC pipeline just
  produces the same `ProveRequest → prove() → ProofBundle` flow.
- No new dependency on any server crate: the wire types are
  redefined in `host/src/rpc_wire.rs` and snapshot-tested against
  hand-written JSON that matches the upstream shape.
- `core/` and `guest/` are untouched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

2 participants