Skip to content

feat(shielded-pool-extension): PIR + epoch-nullifier extension (PoC)#74

Merged
Meyanis95 merged 22 commits into
masterfrom
feat/shielded-pool-extension-impl
Jun 5, 2026
Merged

feat(shielded-pool-extension): PIR + epoch-nullifier extension (PoC)#74
Meyanis95 merged 22 commits into
masterfrom
feat/shielded-pool-extension-impl

Conversation

@Meyanis95

@Meyanis95 Meyanis95 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Implements the (already-reviewed) SPEC.

Here: extended circuits (deposit, chain_update IVC, transfer/withdraw, insertion), ShieldedPoolExt.sol two-proof spend path, the Rust off-chain stack (state replica, bb-prover, alloy adapter, SimplePIR commitment-path client), and a self-contained on-chain e2e (deposit → rollover → two-proof transfer with real proofs, ~50 s).

Test:

cd pocs/private-payment/shielded-pool-extension
forge test && nargo test --workspace && cargo test
cargo test --test integration -- --ignored   # on-chain e2e (~50 s)

Shortcuts (see README): SimplePIR stands in for InsPIRe (no portable Rust impl; not silent-preprocessing); KYC attestation relaxed in the deposit circuit; light-client root verification (Slice 3) and withdraw's k=1 insertion verifier are follow-ups. Research prototype, not production.

Meyanis95 added 20 commits June 1, 2026 14:50
@Meyanis95 Meyanis95 requested review from oskarth and rymnc June 2, 2026 16:18
address withdrawV = address(new WithdrawVerifier());
address insertionV = address(new InsertionVerifier());
MockERC20 token = new MockERC20("USD Coin", "USDC", 6);
// withdrawInsertionVerifier (k=1) has no circuit yet; reuse the k=2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wires the k=2 InsertionVerifier into the withdrawInsertionVerifier slot. withdraw() supplies only one nullifier to _verifyInsertionAndAdvance, so the verifier receives 4 public inputs, but the generated k=2 insertion verifier expects 5 (NUMBER_OF_PUBLIC_INPUTS - PAIRING_POINTS_SIZE). With this deployment, any real withdraw path will revert inside the verifier before returning false.

Since the README/contract describe withdraw as part of the spend path, this should either deploy a real k=1 insertion verifier or keep withdraw out of the real-verifier deployment until that circuit exists.

@rymnc rymnc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

general pass; didn't inspect logic too deeply, focused on minor things

will do a bigger review a little later today or tomorrow

/// by feeding it directly into the commitment preimage, so a deposit always
/// mints a note tagged with the live epoch.
///
/// Implementation shortcut: the parent's KYC attestation-membership check is

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this a shortcut, seems simple enough to add here

/// Forge auto-deploys + links the contract libraries (ZKTranscriptLib in the
/// verifiers; PoseidonT3 + LeanIMT in the pool). Writes `deployments.toml`
/// for the test to read.
contract Deploy is Script {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this file?

IShieldedPoolExt::new(self.pool, &self.provider)
}

fn convert_receipt(receipt: &alloy::rpc::types::TransactionReceipt) -> TxReceipt {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe replace with

impl From<alloy::rpc::types::TransactionReceipt> for TxReceipt {
     fn from(alloy_receipt: alloy::rpc::types::TransactionReceipt) -> Self {
        /// whatever code u have here
    }
}

Comment on lines +15 to +18
//! PoC note: `root()` and `merkle_path()` recompute the occupied levels on each
//! call (O(occupied) hashing); fine at PoC scale, not optimized for Visa-scale
//! epochs.
//!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the suggested solution for this? would be nice to include it with the comment

Comment on lines +17 to +18
/// PoC: the cleartext adapter sends the leaf index to the server, which learns
/// it. Slice 2 swaps in a real PIR backend that computes the sibling-node offsets

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is slice 2? probably not best to send leaf index to the server

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Comment on lines +27 to +31
pub trait RootVerifier {
/// Verify `proof` (an `eth_getProof` result for one slot) and return the
/// verified storage value.
fn verify_storage(&self, proof: &EIP1186AccountProofResponse) -> Result<alloy::primitives::B256, RootVerifierError>;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just an engineering note: ports should rely on types declared in the same file, or in the domains module. here we are depending on an external crate in the args and return type; and here you are creating a contract that the adapter has to use this crate too - fine for poc, but something to note

@@ -0,0 +1,82 @@
//! Light-client root-verification integration test (Slice 3).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we remove comments with "Slice *", confusing to read

const DEV_KEY: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";

#[tokio::test]
#[ignore = "spawns anvil; run with --ignored"]

@rymnc rymnc Jun 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should remove this #ignore attribute

@Meyanis95

Copy link
Copy Markdown
Contributor Author

Thanks for the pass! Pushed 2 commits (e403c07, 4a75622) addressing this round.

@oskarth (withdraw insertion verifier): Fixed. Pulled the insertion loop into a shared insertion_core lib generic over the insertion count, instantiated at k=2 (insertion) and k=1 (insertion_withdraw), generated the real WithdrawInsertionVerifier, and wired it into the withdraw slot. Added a withdraw_flow_verifies_on_chain e2e: a real on-chain withdraw through the k=1 verifier now passes (it reverted inside the k=2 verifier before).

@rymnc (resolved in code): deleted the 5 unused stub files; removed the "Slice N" comments; convert_receipt became impl From<TransactionReceipt>; zeroize on SpendingKey; Deploy.s.sol now inherits the forge-std Config base; added the production fix to the indexed_merkle_tree note.

@rymnc (answers to the questions/notes):

  • KYC shortcut: this extension composes with (doesn't replace) the parent. Attestation is orthogonal to the PIR/epoch-nullifier work and stays fully enforced in the parent's deposit circuit. Sharpened the comment + README.
  • salt [5..]: impl choice. 27 random bytes stay < BN254 without rejection sampling; the SPEC only requires a random Field. Comment clarified.
  • ciphertext size: the encrypted note rides in the event log (not storage); mitigations are a compact format / off-chain note log + FMD-OMR. Comment added.
  • Commitment/Epoch pub fields: kept as transparent domain newtypes (the pub field is the construction + .0 access used throughout). Happy to add accessors if you'd prefer.
  • root_verifier external type: agreed a domain proof type is cleaner; kept the alloy type for PoC pragmatism for now.
  • #[ignore] on the light-client test: kept. It spawns Anvil like the proving/e2e tests, and CI runs no cargo tests so it wouldn't run there regardless (--ignored to run it locally).

@Meyanis95 Meyanis95 requested review from oskarth and rymnc June 4, 2026 13:03
@Meyanis95 Meyanis95 merged commit 00130c8 into master Jun 5, 2026
2 checks passed
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.

3 participants