diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ed82a363..7093df2e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,6 +162,9 @@ jobs: - name: Checking code format rust run: pnpm rust:lint + - name: Compile contracts for ABI catalog tests + run: pnpm evm:build + - name: Run Unit Tests run: 'cargo test --lib && cargo test --doc' @@ -172,7 +175,7 @@ jobs: GH_TOKEN: ${{ github.token }} rust_integration_tests: - needs: [detect_changes] + needs: [detect_changes, integration_prebuild] if: needs.detect_changes.outputs.rust_integration_tests == 'true' timeout-minutes: 45 runs-on: @@ -211,9 +214,22 @@ jobs: - name: 'Install the dependencies' run: 'pnpm install --frozen-lockfile' + - name: Download source-aligned ZK fixtures + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: integration-noir-artifacts + path: .ci-noir/ + + - name: Stage source-aligned circuit fixtures + run: | + chmod +x .ci-noir/bin/bb + mkdir -p dist/circuits/insecure-512 + cp -R .ci-noir/circuits/insecure-512/minimum dist/circuits/insecure-512/ + - name: Run Integration Tests env: - BENCHMARK_PROOF_AGGREGATION: 'false' + CIPHERNODE_SKIP_PROOF_AGGREGATION: 'true' + E3_CUSTOM_BB: ${{ github.workspace }}/.ci-noir/bin/bb run: 'cargo test --test integration -- --nocapture' zk_prover_integration: @@ -376,6 +392,12 @@ jobs: echo "## Lint results" >> $GITHUB_STEP_SUMMARY echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + - name: 'Enforce production contract size budget' + run: 'pnpm --filter @interfold/contracts size:check' + + - name: 'Validate upgrade storage layouts' + run: 'pnpm --filter @interfold/contracts validate:upgrade' + - name: 'Test the contracts and generate the coverage report' run: 'pnpm coverage' @@ -438,6 +460,22 @@ jobs: with: toolchain: ${{ env.NOIR_TOOLCHAIN }} + - name: Cache Barretenberg binary + id: cache-integration-bb + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: /usr/local/bin/bb + key: bb-bin-${{ env.BB_VERSION }}-amd64-linux + + - name: Install Barretenberg (bb) + if: steps.cache-integration-bb.outputs.cache-hit != 'true' + run: | + curl -fsSL "https://github.com/gnosisguild/aztec-packages/releases/download/v${{ env.BB_VERSION }}/barretenberg-amd64-linux.tar.gz" -o bb.tar.gz + mkdir -p bb_extract && tar -xzf bb.tar.gz -C bb_extract + BB_BIN=$(find bb_extract -name bb -type f | head -n 1) + sudo mv "$BB_BIN" /usr/local/bin/bb + chmod +x /usr/local/bin/bb + - name: Install solc run: | sudo add-apt-repository ppa:ethereum/ethereum \ @@ -465,6 +503,15 @@ jobs: target/debug/pack_e3_params if-no-files-found: error + - name: Upload source-aligned ZK fixtures + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: integration-noir-artifacts + include-hidden-files: true + path: tests/integration/.interfold/noir/ + retention-days: 1 + if-no-files-found: error + check-nix-flake: runs-on: ubuntu-latest steps: @@ -515,6 +562,11 @@ jobs: with: name: build-artifacts path: target/debug/ + - name: 'Download source-aligned ZK fixtures' + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: integration-noir-artifacts + path: tests/integration/.interfold/noir/ - name: 'Download interfold binary' uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: @@ -538,11 +590,11 @@ jobs: chmod +x target/debug/fake_encrypt chmod +x target/debug/pack_e3_params chmod +x ~/.cargo/bin/interfold - - name: 'Setup ZK prover' - run: | - interfold noir setup + chmod +x tests/integration/.interfold/noir/bin/bb - name: 'Run ${{ matrix.test-suite }} tests' - run: 'pnpm test:integration ${{ matrix.test-suite }} --no-prebuild' + run: | + INTERFOLD_BIN="$HOME/.cargo/bin/interfold" \ + pnpm test:integration ${{ matrix.test-suite }} --no-prebuild - name: 'Add test summary' run: | echo "## Test results for ${{ matrix.test-suite }}" >> $GITHUB_STEP_SUMMARY @@ -578,7 +630,7 @@ jobs: && sudo apt-get install -y solc protobuf-compiler - name: Build interfold CLI - run: cargo install --locked --path crates/cli --bin interfold + run: cargo install --locked --path crates/cli --bin interfold --features test-only-skip-proof-aggregation - name: Upload interfold binary uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 diff --git a/Cargo.lock b/Cargo.lock index 45c390228..0c68128cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3201,6 +3201,7 @@ version = "0.3.0" dependencies = [ "alloy", "hex", + "serde", ] [[package]] @@ -3387,6 +3388,7 @@ dependencies = [ "chrono", "derivative", "e3-ciphernode-builder", + "e3-committee-hash", "e3-crypto", "e3-data", "e3-events", @@ -3437,6 +3439,7 @@ dependencies = [ "hex", "num-bigint", "serde", + "serde_json", "tokio", "tracing", "tracing-subscriber", @@ -3567,9 +3570,11 @@ name = "e3-keyshare" version = "0.3.0" dependencies = [ "actix", + "alloy", "anyhow", "async-trait", "bincode 1.3.3", + "e3-committee-hash", "e3-config", "e3-crypto", "e3-data", @@ -3610,9 +3615,11 @@ name = "e3-multithread" version = "0.3.0" dependencies = [ "actix", + "alloy", "anyhow", "bincode 1.3.3", "e3-bfv-client", + "e3-committee-hash", "e3-crypto", "e3-data", "e3-events", diff --git a/agent/ARCHITECTURE.md b/agent/ARCHITECTURE.md index c11b03c73..1a64a6e3e 100644 --- a/agent/ARCHITECTURE.md +++ b/agent/ARCHITECTURE.md @@ -175,7 +175,7 @@ error handling, read the clock, or mutate actor state. Protocol-specific invariants must be named and tested. Important examples include: -- runtime `party_id` is derived from the finalized committee normalized by ascending ticket score; +- runtime `party_id` is derived from the finalized committee normalized by ascending address; - the active aggregator is the lowest non-expelled `party_id`; - the DKG aggregation circuit receives exactly `H` canonical honest NodeFold proofs and exactly `N` ordered committee addresses; diff --git a/agent/CRATES_ARCHITECTURE.md b/agent/CRATES_ARCHITECTURE.md index c7b75fca1..9d80535df 100644 --- a/agent/CRATES_ARCHITECTURE.md +++ b/agent/CRATES_ARCHITECTURE.md @@ -101,7 +101,9 @@ flowchart TD SDK --> Indexer SDK --> EvmHelpers SDK --> FheParams + Indexer --> BfvClient Indexer --> EvmHelpers + Indexer --> FheParams EvmHelpers --> Utils Wasm --> BfvClient Wasm --> FheParams diff --git a/agent/flow-trace/00_INDEX.md b/agent/flow-trace/00_INDEX.md index ae3aec44d..2f140a9d2 100644 --- a/agent/flow-trace/00_INDEX.md +++ b/agent/flow-trace/00_INDEX.md @@ -8,7 +8,7 @@ | 2 | [02_TOKENS_AND_ACTIVATION.md](02_TOKENS_AND_ACTIVATION.md) | FOLD license bonding, USDC→tFOLD ticket purchasing, unbonding, burning, exit queue, claiming. Activation thresholds and the `_updateOperatorStatus` formula. | | 3 | [03_E3_REQUEST_AND_COMMITTEE.md](03_E3_REQUEST_AND_COMMITTEE.md) | E3 request on-chain flow, fee payment, committee request, IMT snapshot. Rust-side sortition (score-based), on-chain ticket submission, committee finalization, `CiphernodeSelected` event. | | 4 | [04_DKG_AND_COMPUTATION.md](04_DKG_AND_COMPUTATION.md) | Full DKG with ZK proof pipeline: BFV keygen → C0 proof → encryption key exchange → TrBFV share generation → C1/C2/C3 proofs → share verification → Shamir secret sharing → encrypted share broadcast → C4 proofs → decryption key reconstruction. C5 proof for PK aggregation. Ciphertext output → C6 proof for decryption shares → C7 proof for plaintext → rewards. | -| 5 | [05_FAILURE_REFUND_SLASHING.md](05_FAILURE_REFUND_SLASHING.md) | Timeout-based failure detection, `markE3Failed`, `processE3Failure`. Refund calculation (work-value allocation). Off-chain AccusationManager quorum protocol (proof failure → accusation → voting → quorum). Lane A (attestation-based, atomic) and Lane B (evidence-based, with appeals) slashing. Ticket/license slashing. Slashed funds escrow and routing. | +| 5 | [05_FAILURE_REFUND_SLASHING.md](05_FAILURE_REFUND_SLASHING.md) | Timeout-based failure detection, `markE3Failed`, `processE3Failure`. Fault-attributed refunds: requester/DP/CP failures pay completed work from fee escrow; supplier/ciphernode failures return all fee escrow and compensate honest nodes from ticket slashes. Off-chain accusation, Lane A/B slashing, and slashed-fund routing. | | 6 | [06_DEACTIVATION_AND_COMPLETION.md](06_DEACTIVATION_AND_COMPLETION.md) | Voluntary deactivation (ticket/license withdrawal), full deregistration (IMT removal), E3 happy-path completion, node shutdown, sync/restart, exit queue timing, ban/unban. | --- @@ -125,14 +125,18 @@ 16. PROCESS Anyone calls processE3Failure(e3Id) → Payment transferred to E3RefundManager - → Work-value allocation calculated (BPS-based) + → FailureReason mapped to Requester or Ciphernodes liability -17. REFUND Requester claims proportional refund - Honest nodes claim proportional compensation - Protocol treasury gets 5% +17. REFUND Requester/DP/CP fault: + → completed work + protocol share paid from fee escrow + → requester claims the unspent remainder + Supplier/ciphernode fault: + → requester claims 100% of fee escrow + → no protocol fee or base fee-funded node payment 18. SLASHED $ If slashed funds escrowed: - → Failure: requester filled FIRST, surplus → honest nodes + → Failure: actual slash asset → active honest nodes + → No honest nodes: slash asset → snapshotted treasury → Success: nodes + treasury split (successSlashedNodeBps) ``` @@ -149,10 +153,10 @@ │ │ (fault, penalties) │ │ (USDC wrapper) │ │ └─────────┬──────────┘ └──────────────────┘ │ │ - │ │ escrowSlashedFundsToRefund: - │ │ BondingRegistry.redirectSlashedTicketFunds - │ │ → ticketToken.payout(refundMgr, USDC) - │ │ Interfold.escrowSlashedFunds + │ │ retryable reserved-fund route: + │ │ BondingRegistry.reserveSlashedTicketFunds + │ │ → redirectReservedSlashedTicketFunds + │ │ → Interfold.escrowSlashedFunds │ ▼ ├────→ E3Program (validate, verify computation) ├────→ DecryptionVerifier (verify plaintext) @@ -170,37 +174,54 @@ _Found during source-code cross-referencing of these trace documents._ ### Critical Doc Inaccuracies (now fixed) -| # | Description | Where | Fix Applied | -| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | --------------- | -| 1 | `addTicketBalance` does NOT multiply by `ticketPrice` — raw stablecoin units are passed directly to `ticketToken.depositFrom()`. `ticketPrice` is only used in the activation check. | BondingRegistry.sol:371 | 02_TOKENS | -| 2 | `removeTicketBalance` does NOT multiply by `ticketPrice` — raw amount passed to `ticketToken.burnTickets()`. | BondingRegistry.sol:395 | 02_TOKENS | -| 3 | `gracePeriod` is NOT added to deadline checks in `_checkFailureCondition()`. All timeout checks compare `block.timestamp` directly against the raw deadline. `gracePeriod` is only validated in `_setTimeoutConfig` but never referenced in failure detection. | Interfold.sol:860-887 | 05_FAILURE | -| 4 | `activate()` calls `register()` → `registerOperator()` which has `require(!registered, AlreadyRegistered())`. So activate **reverts** for already-registered operators. It only works for re-registration after deregistration. | BondingRegistry.sol:308 | 01_REGISTRATION | -| 5 | `E3Requested` event is `(uint256 e3Id, E3 e3, IE3Program indexed e3Program)` — seed and params are inside the E3 struct, not separate parameters. | IInterfold.sol:82 | 03_E3_REQUEST | -| 6 | `finalizeCommittee()` checks `>=` deadline, not `>`. | CiphernodeRegistryOwnable.sol | 03_E3_REQUEST | -| 7 | `publishCommittee()` is now permissionless. The effective access control is DKG proof verification plus the single-publish guard `publicKeyHashes[e3Id] == 0`; the old `onlyOwner` note is obsolete. | CiphernodeRegistryOwnable.sol | 04_DKG | -| 8 | `CommitteePublished` event emits `(e3Id, nodes, publicKey, pkCommitment, proof)` — full PK bytes, pkCommitment, and proof bytes (DkgAggregator when proof aggregation is enabled), not just pkHash. | CiphernodeRegistryOwnable.sol | 04_DKG | -| 9 | `_validateNodeEligibility` calls `bondingRegistry.getTicketBalanceAtBlock()` (not `ticketToken.getPastVotes()` directly). | CiphernodeRegistryOwnable.sol:668 | 03_E3_REQUEST | -| 10 | Lane A slashing uses **attestation-based** verification (committee quorum votes), not direct ZK proof re-verification on-chain. `proposeSlash()` decodes voter addresses, agrees, data hashes, and ECDSA signatures — not ZK proofs. | SlashingManager.sol | 05_FAILURE | +| # | Description | Where | Fix Applied | +| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | --------------- | +| 1 | `addTicketBalance` does NOT multiply by `ticketPrice` — raw stablecoin units are passed directly to `ticketToken.depositFrom()`. `ticketPrice` is only used in the activation check. | BondingRegistry.sol:371 | 02_TOKENS | +| 2 | `removeTicketBalance` does NOT multiply by `ticketPrice` — raw amount passed to `ticketToken.burnTickets()`. | BondingRegistry.sol:395 | 02_TOKENS | +| 3 | `gracePeriod` is NOT added to deadline checks in `_checkFailureCondition()`. All timeout checks compare `block.timestamp` directly against the raw deadline. `gracePeriod` is only validated in `_setTimeoutConfig` but never referenced in failure detection. | Interfold.sol:860-887 | 05_FAILURE | +| 4 | `activate()` calls `register()` → `registerOperator()` which has `require(!registered, AlreadyRegistered())`. So activate **reverts** for already-registered operators. It only works for re-registration after deregistration. | BondingRegistry.sol:308 | 01_REGISTRATION | +| 5 | `E3Requested` event is `(uint256 e3Id, E3 e3, IE3Program indexed e3Program)` — seed and params are inside the E3 struct, not separate parameters. | IInterfold.sol:82 | 03_E3_REQUEST | +| 6 | `finalizeCommittee()` checks `>=` deadline, not `>`. | CiphernodeRegistryOwnable.sol | 03_E3_REQUEST | +| 7 | `publishCommittee()` is now permissionless. The effective access control is DKG proof verification plus the single-publish guard `publicKeyHashes[e3Id] == 0`; the old `onlyOwner` note is obsolete. | CiphernodeRegistryOwnable.sol | 04_DKG | +| 8 | `CommitteePublished` emits `(e3Id, nodes, publicKey, pkCommitment, proof)`. The full PK bytes are an untrusted transport hint that the indexer decodes and checks against the on-chain commitment before storage; that commitment is C5-proven by the mandatory final DKG proof. | CiphernodeRegistryOwnable.sol | 04_DKG | +| 9 | `_validateNodeEligibility` calls `bondingRegistry.getTicketBalanceAtBlock()` (not `ticketToken.getPastVotes()` directly). | CiphernodeRegistryOwnable.sol:668 | 03_E3_REQUEST | +| 10 | Lane A slashing uses **attestation-based** verification (committee quorum votes), not direct ZK proof re-verification on-chain. `proposeSlash()` decodes voter addresses, agrees, data hashes, and ECDSA signatures — not ZK proofs. | SlashingManager.sol | 05_FAILURE | ### Protocol Design Concerns -| # | Concern | Severity | Detail | -| --- | --------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | **Deregister-before-slash race** | Accepted | SlashingManager Lane B (evidence+appeal) has a window during which the operator can deregister and claim their exit. If they do, the slash executes against 0 funds. The contract comments acknowledge this as an accepted tradeoff for the appeal window design. | -| 2 | **Committee publication decentralized** | Resolved | `publishCommittee()` is permissionless. Off-chain role selection chooses the active aggregator, while on-chain C5 proof verification and the single-publish guard prevent invalid or duplicate committee publication. | -| 3 | **`gracePeriod` is dead code** | Medium | `gracePeriod` is stored and validated during config updates but never actually used in any timeout check. Either the deadlines already bake in sufficient buffer, or this is a missing feature. | -| 4 | **`activate` CLI command is misleading** | Low | Named "activate" but actually calls "register" — will fail for already-registered operators. There's no standalone way to trigger re-evaluation of active status; instead, `_updateOperatorStatus()` runs automatically inside `addTicketBalance()`, `bondLicense()`, etc. | -| 5 | **Active-job load balancing bug fixed** | Info | The Rust `NodeStateStore.available_tickets()` subtracts `active_jobs` from total tickets, reducing the chance of busy nodes being selected for new E3s. Previously, the `Sortition` actor's `Handler` was missing match arms for `E3Failed` and `E3StageChanged`, causing these events to fall to the default `_ => ()` — the typed handlers for decrementing jobs were dead code. This has been fixed: E3Failed and E3StageChanged are now routed to their handlers, and `finalized_committees` is cleaned up in `decrement_jobs_for_e3` to prevent unbounded memory growth. | -| 6 | **Committee member expulsion** | Resolved | `SlashingManager` can call `expelCommitteeMember()` mid-DKG. The `Sortition` actor enriches the raw `CommitteeMemberExpelled` event with the expelled member's `party_id` (resolved from its stored `Committee` list) and re-publishes it. `ThresholdKeyshare` uses the enriched `party_id` to update its collectors. The public-key keyshare gate and aggregation reducer normalize both expulsion records and self-reported keyshare nodes to `alloy::Address`, so an expelled member cannot re-enter by changing address casing. `ThresholdKeyshare` itself does not hold committee state. | -| 7 | **ProofRequestActor failure bridge fixed** | Info | `ProofRequestActor` no longer leaves proof publication suppressed under log-only "will not be published" exits. `ComputeRequestError` and local proof-signing failures for DKG-path proofs (`C0` through `C5`) now emit `E3Failed { failed_at_stage: CommitteeFinalized, reason: DKGInvalidShares }`, while decryption-path proofs (`C6` and `C7`) emit `E3Failed { failed_at_stage: CiphertextReady, reason: DecryptionInvalidShares }`. | -| 8 | **Settlement receipts isolated** | Resolved | Durable EVM receipts such as `RewardCredited` and `RewardClaimed` are global audit/projection facts. The E3 router no longer sends them into a completed per-E3 context, so reward fan-out cannot reopen a finished E3 or produce false `AlreadyCompleted` failures. | -| 9 | **Replay-safe compute effects** | Resolved | `ComputeEffectGate` subscribes before EventStore replay, buffers `ComputeRequest`s while effects are disabled, deduplicates equivalent requests, prefers the newest hydrated retry, cancels terminal E3 work, and releases pending effects only after `EffectsEnabled`. This closes the mid-E3 compute-loss window without changing durable event order. | -| 10 | **Restarted active aggregator decryption recovery** | Resolved | A node killed after key publication but before ciphertext publication could miss the one-shot `AggregatorChanged` role event before its plaintext buffer existed, lack recovered committee dependencies when ciphertext arrived, or rehydrate without the per-E3 `CommitmentConsistencyChecker` needed to answer C6 verification gates. `ThresholdPlaintextAggregatorExtension` records aggregator role, remembers pending ciphertext until committee facts arrive, recovers full committee data from public-key state/replayed public events, recovers the honest subset from public-key or threshold-keyshare state, `CommitmentConsistencyCheckerExtension` recreates its per-E3 checker from recovered meta, and restart recovery never blocks the router on synchronous store reads. | -| 11 | **Large EventStore replay startup failure** | Resolved | Restart replay now awaits each EventBus handler result before submitting the next persisted event. A backlog larger than the 2,560-entry EventBus mailbox no longer fails startup with `SendError::Full`; progress is logged every 10,000 EventBus-handled events. The EventBus still fans out with unacknowledged `do_send`, and replay still materializes the full range in memory, so end-to-end queue and memory bounds remain open. | -| 12 | **Canonical DKG H/N aggregation shape** | Resolved | The Rust DKG aggregation path no longer asserts that honest proof count `H` equals full committee size `N`. Every supported preset has `H < N`; validation now requires exactly `H` NodeFold proofs with unique in-range party IDs and exactly `N` ordered committee addresses, matching the producer and compiled Noir witness. The removed assertion affected debug/test binaries; circuit semantics and public inputs are unchanged. | -| 13 | **Forward-compatible E3 version skew** | Resolved | A well-formed `E3Requested` carrying an unsupported committee-size or BFV-preset enum is now classified as a benign skip for that older binary. The parser emits an internal `Processed` marker so historical ordering and startup can advance without dispatching the E3. ABI decode failures and missing indexed topics remain rejected and fail the chain pipeline closed. | -| 14 | **Post-startup network lag recovery** | Resolved | `NetEventBuffer` still fails readiness closed if its broadcast input lags during startup buffering, because historical/live reconciliation would be incomplete. Once `SyncEnded` has moved it to `Running`, a `Lagged(n)` signal is warned and skipped, and the ingress receive loop continues from the oldest retained event instead of stopping the buffer and permanently deafening gossip consumers. | -| 15 | **Local/network event redelivery collision** | Resolved | EventStore now treats the same HLC timestamp plus stable event ID and equal payload as an idempotent duplicate even when transport context differs (`Local` versus `Net`). It retains the first stored context and still fails closed when different payloads claim the same timestamp, preserving collision detection without panicking on sync/resync redelivery. | -| 16 | **Crash-torn event-log tail** | Resolved | Startup validates active-segment frames against the commitlog index, truncates only an unindexed CRC/length-invalid physical suffix, and restores complete CRC-valid/decodable records whose tail index entry was lost. Indexed corruption remains fatal. Runtime reads return correlated errors instead of panicking query handlers, and `node validate --repair` exposes the same narrowly bounded recovery explicitly. | -| 17 | **DAppNode v0.1.8 schema upgrade** | Resolved | DAppNode v0.2.3 is shipped as the required compatibility bridge. Its entrypoint atomically renames the legacy `/data/.enclave` state root to `/data/.interfold` (and refuses ambiguous dual roots), preserves existing encrypted identities, and then relies on the v0.2.3 release's one-time schema-1 stamp. Later fail-closed binaries therefore see a proven marker instead of permanently rejecting the shipped v0.1.8 datastore. | +| # | Concern | Severity | Detail | +| --- | --------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | **Deregister-before-slash race** | Resolved | One unresolved-proposal counter covers both lanes. Every authorized current or retained historical slashing manager participates in the BondingRegistry exit gate, so rotation cannot release collateral belonging to an in-flight E3. Ticket withdrawal, license unbonding, deregistration, and exit claims remain blocked until execution, upheld appeal, or permissionless expiry terminates the proposal. | +| 2 | **Committee publication decentralized** | Resolved | `publishCommittee()` is permissionless. Off-chain role selection chooses the active aggregator, while on-chain C5 proof verification and the single-publish guard prevent invalid or duplicate committee publication. | +| 3 | **`gracePeriod` is dead code** | Medium | `gracePeriod` is stored and validated during config updates but never actually used in any timeout check. Either the deadlines already bake in sufficient buffer, or this is a missing feature. | +| 4 | **`activate` CLI command is misleading** | Low | Named "activate" but actually calls "register" — will fail for already-registered operators. There's no standalone way to trigger re-evaluation of active status; instead, `_updateOperatorStatus()` runs automatically inside `addTicketBalance()`, `bondLicense()`, etc. | +| 5 | **Active-job load balancing bug fixed** | Info | The Rust `NodeStateStore.available_tickets()` subtracts `active_jobs` from total tickets, reducing the chance of busy nodes being selected for new E3s. Previously, the `Sortition` actor's `Handler` was missing match arms for `E3Failed` and `E3StageChanged`, causing these events to fall to the default `_ => ()` — the typed handlers for decrementing jobs were dead code. This has been fixed: E3Failed and E3StageChanged are now routed to their handlers, and `finalized_committees` is cleaned up in `decrement_jobs_for_e3` to prevent unbounded memory growth. | +| 6 | **Committee member expulsion** | Resolved | `SlashingManager` can call `expelCommitteeMember()` mid-DKG. The `Sortition` actor enriches the raw `CommitteeMemberExpelled` event with the expelled member's `party_id` (resolved from its stored `Committee` list) and re-publishes it. `ThresholdKeyshare` uses the enriched `party_id` to update its collectors. The public-key keyshare gate and aggregation reducer normalize both expulsion records and self-reported keyshare nodes to `alloy::Address`, so an expelled member cannot re-enter by changing address casing. `ThresholdKeyshare` itself does not hold committee state. | +| 7 | **ProofRequestActor failure bridge fixed** | Info | `ProofRequestActor` no longer leaves proof publication suppressed under log-only "will not be published" exits. `ComputeRequestError` and local proof-signing failures for DKG-path proofs (`C0` through `C5`) now emit `E3Failed { failed_at_stage: CommitteeFinalized, reason: DKGInvalidShares }`, while decryption-path proofs (`C6` and `C7`) emit `E3Failed { failed_at_stage: CiphertextReady, reason: DecryptionInvalidShares }`. | +| 8 | **Settlement receipts isolated** | Resolved | Durable EVM receipts such as `RewardCredited` and `RewardClaimed` are global audit/projection facts. The E3 router no longer sends them into a completed per-E3 context, so reward fan-out cannot reopen a finished E3 or produce false `AlreadyCompleted` failures. | +| 9 | **Replay-safe compute effects** | Resolved | `ComputeEffectGate` subscribes before EventStore replay, buffers `ComputeRequest`s while effects are disabled, deduplicates equivalent requests, prefers the newest hydrated retry, cancels terminal E3 work, and releases pending effects only after `EffectsEnabled`. This closes the mid-E3 compute-loss window without changing durable event order. | +| 10 | **Restarted active aggregator decryption recovery** | Resolved | A node killed after key publication but before ciphertext publication could miss the one-shot `AggregatorChanged` role event before its plaintext buffer existed, lack recovered committee dependencies when ciphertext arrived, or rehydrate without the per-E3 `CommitmentConsistencyChecker` needed to answer C6 verification gates. `ThresholdPlaintextAggregatorExtension` records aggregator role, remembers pending ciphertext until committee facts arrive, recovers full committee data from public-key state/replayed public events, recovers the honest subset from public-key or threshold-keyshare state, `CommitmentConsistencyCheckerExtension` recreates its per-E3 checker from recovered meta, and restart recovery never blocks the router on synchronous store reads. | +| 11 | **Large EventStore replay startup failure** | Resolved | Restart replay now awaits each EventBus handler result before submitting the next persisted event. A backlog larger than the 2,560-entry EventBus mailbox no longer fails startup with `SendError::Full`; progress is logged every 10,000 EventBus-handled events. The EventBus still fans out with unacknowledged `do_send`, and replay still materializes the full range in memory, so end-to-end queue and memory bounds remain open. | +| 12 | **Canonical DKG H/N aggregation shape** | Resolved | The Rust DKG aggregation path no longer asserts that honest proof count `H` equals full committee size `N`. Every supported preset has `H < N`; validation now requires exactly `H` NodeFold proofs with unique in-range party IDs and exactly `N` ordered committee addresses, matching the producer and compiled Noir witness. The removed assertion affected debug/test binaries; circuit semantics and public inputs are unchanged. | +| 13 | **Forward-compatible E3 version skew** | Resolved | A well-formed `E3Requested` carrying an unsupported committee-size or BFV-preset enum is now classified as a benign skip for that older binary. The parser emits an internal `Processed` marker so historical ordering and startup can advance without dispatching the E3. ABI decode failures and missing indexed topics remain rejected and fail the chain pipeline closed. | +| 14 | **Post-startup network lag recovery** | Resolved | `NetEventBuffer` still fails readiness closed if its broadcast input lags during startup buffering, because historical/live reconciliation would be incomplete. Once `SyncEnded` has moved it to `Running`, a `Lagged(n)` signal is warned and skipped, and the ingress receive loop continues from the oldest retained event instead of stopping the buffer and permanently deafening gossip consumers. | +| 15 | **Local/network event redelivery collision** | Resolved | EventStore now treats the same HLC timestamp plus stable event ID and equal payload as an idempotent duplicate even when transport context differs (`Local` versus `Net`). It retains the first stored context and still fails closed when different payloads claim the same timestamp, preserving collision detection without panicking on sync/resync redelivery. | +| 16 | **Crash-torn event-log tail** | Resolved | Startup validates active-segment frames against the commitlog index, truncates only an unindexed CRC/length-invalid physical suffix, and restores complete CRC-valid/decodable records whose tail index entry was lost. Indexed corruption remains fatal. Runtime reads return correlated errors instead of panicking query handlers, and `node validate --repair` exposes the same narrowly bounded recovery explicitly. | +| 17 | **DAppNode v0.1.8 schema upgrade** | Resolved | DAppNode v0.2.3 is shipped as the required compatibility bridge. Its entrypoint atomically renames the legacy `/data/.enclave` state root to `/data/.interfold` (and refuses ambiguous dual roots), preserves existing encrypted identities, and then relies on the v0.2.3 release's one-time schema-1 stamp. Later fail-closed binaries therefore see a proven marker instead of permanently rejecting the shipped v0.1.8 datastore. | +| 18 | **Exit-queue stale-head cap (AUD M-01)** | Resolved | `ExitQueueLib` caps the explicit number of non-empty tranches instead of deriving a shared count from the minimum of two asset-specific heads. Fully drained ticket-only or license-only tranches release capacity. | +| 19 | **Dual-role refund claims (AUD M-02)** | Resolved | `E3RefundManager` tracks requester-refund and honest-node claims in independent ledgers. An account that legitimately holds both roles can claim both allocations once, and callers can query either role explicitly. | +| 20 | **Sale deployment front-running (AUD L-01)** | Resolved | `InterfoldTokenSaleDeployer` snapshots the factory deployer as its immutable deployment operator. Only that wallet can consume a sale config, preventing copied mempool calldata from triggering the one-shot launch while preserving the configured Safe as FOLD owner. | +| 21 | **Invalid verifier trust anchors (AUD L-02)** | Resolved | BFV verifier constructors now require a deployed circuit-verifier contract and nonzero recursive VK hashes. Invalid production parameters fail at deployment instead of creating permanently unusable or incorrectly anchored wrappers. | +| 22 | **Core contract bytecode headroom (AUD L-03)** | Resolved | Contracts CI now measures the production runtime bytecode for `Interfold`, `DkgAggregatorVerifier`, and `DecryptionAggregatorVerifier`, failing releases that leave less than 256 bytes below the EIP-170 limit. | +| 23 | **Bonding asset rotation (AUD M-08)** | Resolved | Ticket and license assets can rotate only after all balances denominated in the old asset have drained. New assets must be deployed contracts, with a narrowly scoped initial zero FOLD placeholder retained only for circular deployment. | +| 24 | **Mutable eligibility policy (AUD M-03)** | Resolved | Governance may update ticket price, license thresholds, and minimum-ticket policy. Each effective update advances a policy version and invalidates cached activity in O(1); registered operators fail closed until a permissionless refresh re-evaluates them under the new version, keeping on-chain counts and Rust sortition state synchronized. | +| 25 | **Requester fee consent (AUD H-02)** | Accepted | Requesters can call `getE3Quote` immediately before `request`; the existing input-window start bounds when a request can execute. The protocol intentionally does not add per-request fee-slippage or duplicate deadline fields to the core request ABI. | +| 26 | **Deferred slash collateral gate (AUD H-03)** | Resolved | One unresolved-proposal counter now covers both slashing lanes. Ticket withdrawals, license unbonding, deregistration, and queued exit claims remain blocked until execution, an upheld appeal, or the permissionless appeal-expiry path terminates every proposal. | +| 27 | **Upgradeable storage baselines (AUD H-04)** | Resolved | The remediated pre-deployment layouts are committed as the first production baselines with source/build provenance. The read-only CI gate fails on missing baselines, compiler drift, nested layout incompatibility, or bad gap consumption; baseline creation is a separate explicit maintainer command. Initial pricing is now passed as a typed initializer struct and written through the same validated setter path, with no hard-coded storage-slot writer. | +| 28 | **Per-E3 settlement policy (AUD M-07)** | Resolved | Each request snapshots the refund/slash work allocation, treasury, and policy version. Later governance updates cannot retroactively alter failure refunds, success-path slash splits, dust, residual routing, or orphan recovery for an in-flight E3. | +| 29 | **In-flight dependency rotation (AUD M-04)** | Resolved | Each request freezes the complete lifecycle dependency graph across Interfold, committee registry, slashing manager, bonding registry, and refund manager. Rotations set defaults for later E3s while existing E3s continue callbacks, verification, rewards, failure settlement, expulsion, and slash routing through the original deployments. | +| 30 | **Retryable slashed-fund routing (AUD M-05)** | Resolved | Every ticket slash reserves its underlying asset against treasury withdrawal and records a durable proposal-scoped route before escrow is attempted. Temporary payout or accounting failures leave the route permissionlessly retryable; transfer, escrow accounting, and route consumption remain atomic and completed retries are idempotent. | +| 31 | **Fault-attributed, token-aware failure settlement (AUD H-01)** | Resolved | `FailureReason` is exhaustively classified as requester-side or supplier/ciphernode-side. Requester/DP/CP failures pay completed work from the request-time fee allocation. Supplier/ciphernode failures return 100% of fee escrow with no protocol cut; honest nodes are compensated only from actual ticket slashes. Slash assets retain their ERC-20 denomination in independent pull claims, so no oracle conversion or relabeling occurs. Failure-triggering slash policies must expel the faulty operator before honest recipients are resolved. | +| 32 | **Proof-disabled publication bypass (AUD C-02)** | Fixed | E3 requests no longer carry a proof-aggregation switch and both final verifier calls are mandatory. Only binaries compiled with the `test-only-skip-proof-aggregation` Cargo feature can honor the ciphernode `skip_proof_aggregation` test/CI setting; production builds reject it. Feature-gated test nodes use non-empty C5/C7 placeholders accepted by mock deployments, while production verifiers reject those placeholders. | +| 33 | **Client public-key commitment binding (AUD C-01)** | Resolved | Serialized public-key event bytes are treated as an untrusted transport hint. Rust indexers decode the key and store it only when its recomputed circuit commitment equals the on-chain value; the TypeScript SDK exposes the event commitment and a semantic validator, which the default app runs before encryption. Calldata substitution therefore cannot become a different first-party client encryption key. The commitment itself is C5-proven by the mandatory final DKG proof. | +| 34 | **Cross-E3 decryption-proof replay (AUD C-03)** | Resolved | Every secret-bearing C6 proof commits to a domain over `(chainId, Interfold address, e3Id, committeeHash, ciphertextOutputHash, committeePublicKey)`. C6 folding requires one common domain, the final DecryptionAggregator proof exposes it, and the BFV wrapper rejects any domain that differs from the value recomputed by `Interfold`. This prevents cross-chain, cross-deployment, cross-E3, cross-committee, cross-ciphertext, and cross-key replay without a global consumed-proof storage ledger. | diff --git a/agent/flow-trace/02_TOKENS_AND_ACTIVATION.md b/agent/flow-trace/02_TOKENS_AND_ACTIVATION.md index 7d231c9b1..2362c7371 100644 --- a/agent/flow-trace/02_TOKENS_AND_ACTIVATION.md +++ b/agent/flow-trace/02_TOKENS_AND_ACTIVATION.md @@ -92,6 +92,15 @@ Before a node can register, it must stake two types of collateral: └───────────────────────────────────────────────────────────┘ ``` +Bonding-asset rotation is liability-gated. A replacement ticket wrapper cannot be configured while +the old wrapper has issued tickets or a payable balance. The registry tracks `totalLicenseLiability` +across active FOLD bonds, queued exits, and slashed funds; it decreases only when a claim or +treasury withdrawal actually consumes an obligation. Unsolicited old-token dust is therefore +distinguishable from operator liabilities and can be sent to `slashedFundsTreasury` with +`sweepLicenseSurplus()` before rotation. The FOLD license token still cannot change until its raw +registry balance is zero. Replacement assets must be deployed contracts; the only zero exception is +the one-time license-token placeholder used to resolve the circular FOLD/BondingRegistry deployment. + --- ## Step 1: Bond License (`interfold ciphernode license bond`) @@ -124,9 +133,10 @@ User runs: interfold ciphernode license bond --amount 50000 │ │ │ → FOLD _update can see the pre-recorded bond │ │ │ │ and enforce locked-floor accounting │ │ │ │ → FOLD tokens move from operator → contract │ -│ │ │ 4. _updateOperatorStatus(msg.sender) │ +│ │ │ 4. totalLicenseLiability += amount │ +│ │ │ 5. _updateOperatorStatus(msg.sender) │ │ │ │ → May activate if all conditions now met │ -│ │ │ 5. Emit LicenseBondUpdated(msg.sender, newBond) │ +│ │ │ 6. Emit LicenseBondUpdated(msg.sender, newBond) │ │ │ │ } │ │ │ └──────────────────────────────────────────────────────┘ │ │ @@ -166,6 +176,15 @@ _updateOperatorStatus(operator): emit OperatorActivationChanged(operator, true) ``` +Governance may update `ticketPrice`, `licenseRequiredBond`, `licenseActiveBps`, and +`minTicketBalance`; `minTicketBalance` must remain nonzero. Every effective update advances +`eligibilityConfigurationVersion`, resets `numActiveOperators`, and makes all previously cached +operator statuses fail closed. Operators or governance then call `refreshOperatorStatus` (or its +batch form) to re-evaluate registered operators under the new policy. Only operators refreshed into +the current version count as active, so committee requests cannot rely on status cached under an +older policy. The Rust sortition state consumes the same `ConfigurationUpdated` event and marks its +chain-local operators inactive until matching `OperatorActivationChanged` refresh events arrive. + --- ## Step 2: Buy Tickets (`interfold ciphernode tickets buy`) @@ -345,12 +364,9 @@ User runs: interfold ciphernode license claim [--max-ticket 50] [--max-license 1 │ │ │ │ │ underlying.safeTransfer(to, amount) │ │ │ │ │ │ └────────────────────────────────────────┘ │ │ │ │ │ -│ │ │ 3. licenseAmount = _claimLicenseExits( │ -│ │ │ msg.sender, maxLicense │ -│ │ │ ) │ -│ │ │ → Each FOLD source pays its withdrawalAddress │ -│ │ │ → Receiver callback gets (operator, amount, │ -│ │ │ sourceId) when supported │ +│ │ │ 3. if licenseAmount > 0: │ +│ │ │ totalLicenseLiability -= licenseAmount │ +│ │ │ licenseToken.safeTransfer(msg.sender, amount) │ │ │ │ → Pending FOLD is removed from totalBonded() │ │ │ │ as returned FOLD reaches the wallet │ │ │ │ } │ diff --git a/agent/flow-trace/03_E3_REQUEST_AND_COMMITTEE.md b/agent/flow-trace/03_E3_REQUEST_AND_COMMITTEE.md index b30c5ea65..a34508c2d 100644 --- a/agent/flow-trace/03_E3_REQUEST_AND_COMMITTEE.md +++ b/agent/flow-trace/03_E3_REQUEST_AND_COMMITTEE.md @@ -51,10 +51,15 @@ Requester calls: Interfold.request({ │ ├─ E3 CREATION: │ ├─ e3Id = nexte3Id++ +│ ├─ Snapshot Interfold dependencies for this E3: +│ │ registry, refund manager, and slashing manager +│ │ → later global rotations apply only to new requests +│ ├─ snapshottedRefundManager.snapshotE3Policy(e3Id, registry) +│ │ → freezes refund/slash allocation, treasury, policy version, +│ │ request-time Interfold, and request-time committee registry │ ├─ seed = uint256(keccak256(block.prevrandao, e3Id)) -│ │ → On chains without `prevrandao`, the value is still deterministic -│ │ per-block; downstream sortition relies on the per-E3 snapshot of -│ │ ticket balances at `requestBlock - 1` for manipulation resistance. +│ │ → Shared per-E3 ticket-scoring input only; not BFV key material and +│ │ not relied upon for cryptographic unpredictability. │ │ │ ├─ encryptionSchemeId = e3Program.validate( │ │ e3Id, seed, e3ProgramParams, computeProviderParams, customParams @@ -82,10 +87,14 @@ Requester calls: Interfold.request({ │ │ │ │ │ │ │ │ │ requestCommittee(e3Id, seed, threshold) { │ │ │ │ │ 1. require(!committees[e3Id].initialized) │ -│ │ │ │ 2. require(threshold[1] <= │ +│ │ │ │ 2. Snapshot request-time Interfold, bonding, │ +│ │ │ │ and slashing manager for this committee │ +│ │ │ │ → ask SlashingManager to snapshot its │ +│ │ │ │ bonding, registry, Interfold, refund routes │ +│ │ │ │ 3. require(threshold[1] <= │ │ │ │ │ bondingRegistry.numActiveOperators()) │ │ │ │ │ → Enough active nodes must exist │ -│ │ │ │ 3. committees[e3Id] = Committee { │ +│ │ │ │ 4. committees[e3Id] = Committee { │ │ │ │ │ initialized: true, │ │ │ │ │ seed: seed, │ │ │ │ │ requestBlock: block.timestamp, // H-26 │ @@ -93,10 +102,10 @@ Requester calls: Interfold.request({ │ │ │ │ block.timestamp + sortitionWindow, │ │ │ │ │ threshold: threshold │ │ │ │ │ } │ -│ │ │ │ 4. roots[e3Id] = ciphernodes._root() │ +│ │ │ │ 5. roots[e3Id] = ciphernodes._root() │ │ │ │ │ → SNAPSHOT the IMT root at this moment │ │ │ │ │ → Only nodes in tree at request time eligible │ -│ │ │ │ 5. Emit CommitteeRequested(e3Id, seed, threshold,│ +│ │ │ │ 6. Emit CommitteeRequested(e3Id, seed, threshold,│ │ │ │ │ requestBlock, committeeDeadline) │ │ │ │ │ } │ │ │ │ └─────────────────────────────────────────────────────┘ @@ -326,7 +335,7 @@ CiphernodeRegistrySolReader decodes SortitionCommitteeFinalized event ├─ Sortition actor: │ └─ Stores finalized committee as a `Committee` struct in persistent map │ → Provides O(1) address→party_id lookup for later expulsion handling -│ → `CommitteeFinalized` is normalized into ascending ticket-score order before storage +│ → `CommitteeFinalized` is normalized into ascending address order before storage │ ├─ CiphernodeSelector: │ ├─ Checks if this node's address is in the committee list @@ -339,7 +348,7 @@ CiphernodeRegistrySolReader decodes SortitionCommitteeFinalized event │ │ Publishes AggregatorChanged { │ │ e3_id, │ │ is_aggregator = (my node has the lowest non-expelled party_id in the -│ │ score-sorted finalized committee) +│ │ address-sorted finalized committee) │ │ } │ └─ If NO: does nothing for this E3 │ @@ -381,14 +390,13 @@ If any deadline is missed → anyone can call markE3Failed() 2. **Snapshot-based eligibility**: Ticket balances are checked at `requestBlock - 1`, preventing front-running manipulation. -3. **Runtime committee order**: the Rust runtime normalizes `CommitteeFinalized` into ascending - ticket-score order before `Sortition` and `CiphernodeSelector` derive `party_id`. That makes - `party_id` in the runtime equivalent to score order, even though the raw on-chain `topNodes` - array is not itself score-sorted. +3. **Runtime committee order**: both the on-chain registry and Rust runtime normalize the finalized + committee into ascending address order before deriving `party_id`. This keeps party IDs, + aggregator failover, proof inputs, and `CommitteeHashLib.hash(topNodes)` aligned. 4. **Active aggregator selection**: `CiphernodeSelector` derives `AggregatorChanged` from the finalized committee plus enriched `CommitteeMemberExpelled` events. The active aggregator is the - lowest non-expelled `party_id` in the score-sorted runtime committee. + lowest non-expelled `party_id` in the address-sorted runtime committee. 5. **Permissionless finalization**: Anyone can call `finalizeCommittee()` after the deadline — no single point of failure. @@ -396,6 +404,14 @@ If any deadline is missed → anyone can call markE3Failed() 6. **IMT root snapshot**: The Merkle tree root is captured at request time. Nodes that join/leave after the request don't affect this E3's committee. +7. **Dependency graph snapshot**: Each E3 drains through its request-time registry, bonding, + slashing, refund, and Interfold relationships. Admin rotation changes defaults for later E3s but + cannot redirect or brick committee callbacks, proof checks, failure settlement, rewards, or + slashed-fund routing for an in-flight E3. A request atomically records the complete graph before + committee formation begins. Because applying a new graph requires several governance + transactions, request-time validation rejects every intermediate state; a requester can only + freeze the fully old or fully new graph. + --- ## Cluster 7 audit additions (post-fix semantics) diff --git a/agent/flow-trace/04_DKG_AND_COMPUTATION.md b/agent/flow-trace/04_DKG_AND_COMPUTATION.md index e2f9d32ea..e05d56ae4 100644 --- a/agent/flow-trace/04_DKG_AND_COMPUTATION.md +++ b/agent/flow-trace/04_DKG_AND_COMPUTATION.md @@ -637,6 +637,10 @@ ThresholdKeyshare receives AllThresholdSharesCollected ├─ Requires EffectsEnabled ├─ Requires active_aggregators[e3_id] == true ├─ Reads chain state to confirm committee public key is still unset + ├─ Encodes the DkgAggregator proof in production + ├─ Feature-gated test/CI nodes with `skip_proof_aggregation` reuse the non-empty C5 proof as a + │ mock-verifier placeholder; this does not bypass contract verification + │ and every node in a test swarm must use the same flag value └─ Calls contract.publishCommittee(e3_id, publicKey, pkCommitment, proof) │ │ ┌─── ON-CHAIN (CiphernodeRegistryOwnable) ──────────┐ @@ -646,7 +650,7 @@ ThresholdKeyshare receives AllThresholdSharesCollected │ │ 2. require(c.publicKey == 0) — publish once │ │ │ 3. committeeHash = keccak256(abi.encodePacked(c.topNodes)) │ │ │ c.committeeHash = committeeHash │ - │ │ 4. When proofAggregationEnabled: │ + │ │ 4. require(proof.length > 0) │ │ │ e3.pkVerifier.verify( │ │ │ e3Id, committeeRoot, c.topNodes, │ │ │ pkCommitment, committeeHash, proof │ @@ -658,6 +662,7 @@ ThresholdKeyshare receives AllThresholdSharesCollected │ │ [2+H] & [3+H]) vs committeeHash │ │ │ • last PI == pkCommitment │ │ │ • M-35: revert on failure (no `bool false`) │ + │ │ and verify/store per-node fold attestations │ │ │ 5. c.publicKey = pkCommitment │ │ │ publicKeyHashes[e3Id] = pkCommitment │ │ │ 6. interfold.onCommitteePublished(e3Id, pkCommitment) │ @@ -676,6 +681,15 @@ ThresholdKeyshare receives AllThresholdSharesCollected │ └─────────────────────────────────────────────────────┘ ``` +The serialized `publicKey` event field is a transport hint, not on-chain authority. Before +`e3-indexer` stores it in `E3.committee_public_key`, it decodes the BFV key, recomputes the +circuit's public-key commitment using the request's parameter set, and requires equality with the +event's on-chain `pkCommitment`. TypeScript event consumers receive the same `pkCommitment` and use +`InterfoldSDK.validatePublicKeyCommitment()` before accepting the bytes; the default application +does this before advancing to encryption. Malformed bytes or bytes for a different key fail closed +and never reach first-party encryption clients. Production verifies the C5-backed final DKG proof +on-chain; the explicit test/CI skip mode works only with mock verifiers that trust its placeholder. + > **C-08 (BfvPkVerifier domain binding) — implemented** The wrapper exposes a > `verify(e3Id, committeeRoot, sortedNodes, pkCommitment, committeeHash, proof)` signature. > `committeeHash` (computed on-chain as `keccak256(abi.encodePacked(c.topNodes))`) is split into @@ -684,6 +698,10 @@ ThresholdKeyshare receives AllThresholdSharesCollected > params `(e3Id, committeeRoot, sortedNodes)` are forwarded for interface compatibility and future > circuit-level binding. +> **Verifier deployment anchors:** `BfvPkVerifier` and `BfvDecryptionVerifier` constructors reject +> zero/EOA circuit-verifier addresses and zero recursive VK hashes. Production deployment tooling +> additionally compares the immutable VK hashes with the version-controlled circuit artifacts. + --- ## Phase 3: Encrypted Computation @@ -776,6 +794,11 @@ InterfoldSolReader decodes CiphertextOutputPublished event │ │ → Circuit: ThresholdShareDecryption (C6) │ │ → Proves decryption share was correctly computed from │ │ sk_poly_sum, es_poly_sum, and ciphertext + │ │ → Publicly commits to the E3 decryption-domain limbs: + │ │ keccak256(abi.encode( + │ │ chainId, Interfold address, e3Id, committeeHash, + │ │ ciphertextOutputHash, committeePublicKey + │ │ )) │ │ → Fiat-Shamir transcript absorbs full `d` (all coefficients per CRT limb) │ ├─ ZkActor generates proof via bb binary │ ├─ Signs proof @@ -868,8 +891,11 @@ InterfoldSolReader decodes CiphertextOutputPublished event │ │ ├─ Dispatches ComputeRequest::zk(ZkRequest::DecryptionAggregation { │ │ │ c6_total_slots, jobs, params_preset │ │ │ }) -│ │ ├─ Each job folds the selected C6 proofs for one ciphertext index and checks them -│ │ │ against the matching C7 proof inside `DecryptionAggregator` +│ │ ├─ Each job folds the selected C6 proofs for one ciphertext index, requires every +│ │ │ C6 leaf to carry the same E3 domain, and checks them against the matching C7 proof +│ │ │ inside `DecryptionAggregator` +│ │ ├─ `DecryptionAggregator` exposes that C6-authenticated domain as two public +│ │ │ 128-bit limbs in the final EVM proof │ │ ├─ Tracks the in-flight correlation id │ │ ├─ ComputeRequestError, missing C6 inner proofs, or C7/decryption-aggregator proof-count │ │ │ mismatches now emit @@ -884,6 +910,10 @@ InterfoldSolReader decodes CiphertextOutputPublished event ├─ Requires EffectsEnabled ├─ Requires active_aggregators[e3_id] == true ├─ Reads chain state to confirm plaintextOutput is still empty + ├─ Encodes the final DecryptionAggregator proof in production + ├─ Feature-gated test/CI nodes with `skip_proof_aggregation` reuse the non-empty C7 proof as a + │ mock-verifier placeholder; this does not bypass contract verification + │ and every node in a test swarm must use the same flag value └─ Calls contract.publishPlaintextOutput(e3Id, output, proof) │ │ ┌─── ON-CHAIN (Interfold.sol) ─────────────────────────┐ @@ -891,17 +921,21 @@ InterfoldSolReader decodes CiphertextOutputPublished event │ │ publishPlaintextOutput(e3Id, output, proof) { │ │ │ 1. require(stage == CiphertextReady) │ │ │ 2. require(now <= decryptionDeadline) │ - │ │ 3. e3.plaintextOutput = output │ - │ │ 4. decryptionVerifier.verify( │ - │ │ e3Id, committeeRoot, │ - │ │ committeeNodes, ciphertextOutput, │ - │ │ committeePublicKey, │ - │ │ keccak256(output), proof │ + │ │ 3. require(proof.length > 0), recompute │ + │ │ decryptionDomain = keccak256(abi.encode( │ + │ │ chainId, address(this), e3Id, │ + │ │ committeeHash, ciphertextOutput, │ + │ │ committeePublicKey │ + │ │ )), then call decryptionVerifier.verify( │ + │ │ decryptionDomain, keccak256(output), │ + │ │ committeeHash, proof │ │ │ ) │ + │ │ → C-03: final proof domain must match the │ + │ │ domain already committed by every C6 leaf. │ │ │ → M-34: c6Fold / C7 VK hashes are immutable. │ │ │ → M-35: revert path only (no `bool false`). │ - │ │ 5. stage = Complete │ - │ │ 6. _distributeRewards(e3Id) │ + │ │ 4. stage = Complete │ + │ │ 5. _distributeRewards(e3Id) │ │ │ │ │ │ │ │ ┌─ Reward Distribution (pull, H-01/M-02) ┐ │ │ │ │ │ 1. Get active committee nodes: │ │ diff --git a/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md b/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md index 809087071..29886bd93 100644 --- a/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md +++ b/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md @@ -2,9 +2,16 @@ ## Overview -An E3 can fail at any stage due to timeouts, insufficient participants, or misbehavior. When failure -is detected, the protocol refunds the requester (proportional to work not completed), compensates -honest nodes, and slashes misbehaving operators. +An E3 can fail at any stage due to timeouts, insufficient participants, or misbehavior. Settlement +first attributes economic responsibility from `FailureReason`: + +- Requester, data-provider, or compute-provider failures pay completed work and the protocol share + from fee escrow; the requester receives the unspent remainder. +- Supplier/ciphernode failures return 100% of fee escrow to the requester with no protocol cut. + Honest nodes are compensated from actual ticket collateral slashed from faulty nodes. + +Slashed assets remain token-specific pull claims. Compensation is therefore limited to collateral +actually slashed and does not require an oracle or relabel one ERC-20 as another. --- @@ -103,30 +110,38 @@ Anyone calls: Interfold.processE3Failure(e3Id) │ │ │ │ ┌─── E3RefundManager.calculateRefund() ────────────────┐ │ │ │ │ -│ │ │ 1. Determine work completed based on failure stage: │ +│ │ │ 1. Read FailureReason and call getFailurePayer(): │ +│ │ │ │ +│ │ │ Requester liability: │ +│ │ │ NoInputsReceived, ComputeTimeout, │ +│ │ │ ComputeProviderExpired/Failed, RequesterCancelled │ │ │ │ │ -│ │ │ Stage at Failure │ Work Done │ Work Left │Proto │ -│ │ │ ─────────────────────┼───────────┼───────────┼────── │ -│ │ │ Requested / None │ 0 BPS │ 9500 BPS │ 500 │ -│ │ │ (no committee yet) │ (0%) │ (95%) │ (5%) │ -│ │ │ CommitteeFinalized │ 1000 BPS │ 8500 BPS │ 500 │ -│ │ │ (DKG failed) │ (10%) │ (85%) │ (5%) │ -│ │ │ KeyPublished │ 4000 BPS │ 5500 BPS │ 500 │ -│ │ │ (compute failed) │ (40%) │ (55%) │ (5%) │ -│ │ │ CiphertextReady │ 4000 BPS │ 5500 BPS │ 500 │ -│ │ │ (decryption failed) │ (40%) │ (55%) │ (5%) │ +│ │ │ Ciphernodes/supply liability: │ +│ │ │ CommitteeFormationTimeout, │ +│ │ │ InsufficientCommitteeMembers, DKGTimeout, │ +│ │ │ DKGInvalidShares, DecryptionTimeout, │ +│ │ │ DecryptionInvalidShares, VerificationFailed │ │ │ │ │ -│ │ │ NOTE: KeyPublished and CiphertextReady have the SAME │ -│ │ │ work-completed value (4000 BPS). The decryptionBps │ -│ │ │ (5500) is NOT added for CiphertextReady — decryption │ -│ │ │ work is not counted as completed until E3 is Complete.│ +│ │ │ None, _MAX_FAILURE_REASON, and future unclassified │ +│ │ │ reasons revert InvalidFailureReason (fail closed). │ │ │ │ │ -│ │ │ 2. Calculate amounts: │ -│ │ │ honestNodeAmount = payment * workDoneBps / 10000 │ -│ │ │ requesterAmount = payment * workLeftBps / 10000 │ -│ │ │ protocolAmount = payment - honest - requester │ +│ │ │ 2a. Ciphernodes/supply liability: │ +│ │ │ requesterAmount = payment (100%) │ +│ │ │ honestNodeAmount = 0 │ +│ │ │ protocolAmount = 0 │ +│ │ │ → honest-node compensation can come only from │ +│ │ │ actual slashed ticket collateral │ │ │ │ │ -│ │ │ 3. Credit protocol amount to treasury pull ledger │ +│ │ │ 2b. Requester liability: use request-time work BPS: │ +│ │ │ KeyPublished / CiphertextReady defaults: │ +│ │ │ honestNodeAmount = payment * 4000 / 10000 │ +│ │ │ requesterAmount = payment * 5500 / 10000 │ +│ │ │ protocolAmount = remaining 500 / 10000 │ +│ │ │ → if no honest recipient exists, fold the work │ +│ │ │ share back into requesterAmount │ +│ │ │ │ +│ │ │ 3. Credit any requester-fault protocol amount to the │ +│ │ │ snapshotted treasury pull ledger │ │ │ │ │ │ │ │ 4. Store RefundDistribution { │ │ │ │ honestNodeAmount, requesterAmount, │ @@ -135,24 +150,13 @@ Anyone calls: Interfold.processE3Failure(e3Id) │ │ │ originalPayment, perNodeAmount: 0 │ │ │ │ } │ │ │ │ │ -│ │ │ H-08: if honestNodes.length == 0 and │ -│ │ │ honestNodeAmount > 0, fold honestNodeAmount back │ -│ │ │ into requesterAmount before storing — the work- │ -│ │ │ completed share would otherwise be stranded forever │ -│ │ │ (claimHonestNodeReward requires honestNodeCount>0, │ -│ │ │ withdrawOrphanedSlashedFunds only drains │ -│ │ │ _pendingSlashedFunds). │ -│ │ │ │ -│ │ │ 5. Drain pending slashed funds queue: │ -│ │ │ pending = _pendingSlashedFunds[e3Id] │ -│ │ │ if pending > 0: │ -│ │ │ _applySlashedFunds(e3Id, pending) │ -│ │ │ (see "Slashed Funds Routing" section below) │ -│ │ │ → Handles slashes that arrived BEFORE │ -│ │ │ processE3Failure was called │ +│ │ │ 5. Preserve slashed assets as separate claims: │ +│ │ │ → New escrows are keyed by (e3Id, actual token) │ +│ │ │ → A pending entry matching paymentToken may settle│ +│ │ │ now; other tokens remain permissionlessly │ +│ │ │ settleable without being relabeled │ │ │ │ │ -│ │ │ M-09: snapshot perNodeAmount AFTER the pending │ -│ │ │ drain so it reflects the final post-escrow pool: │ +│ │ │ M-09: snapshot the base fee-token per-node payout: │ │ │ │ if honestNodeCount > 0: │ │ │ │ dist.perNodeAmount = │ │ │ │ honestNodeAmount / honestNodeCount │ @@ -176,10 +180,10 @@ REQUESTER claims: │ ├─ require(distribution calculated) ├─ require(msg.sender == requester from Interfold) -├─ require(!already claimed) -├─ requesterAmount includes BOTH: -│ • Base refund (from work-value BPS allocation) -│ • Slashed funds (requester filled first, up to originalPayment) +├─ require(!requester refund already claimed) +├─ requesterAmount is either: +│ • 100% of fee escrow for ciphernodes/supply liability, or +│ • the unspent request-time work allocation for requester liability ├─ Transfer requesterAmount in the per-E3 fee token └─ Emit RefundClaimed(e3Id, requester, amount) @@ -188,23 +192,31 @@ HONEST NODE claims: │ ├─ require(distribution calculated) ├─ require(msg.sender is in honestNodes[e3Id]) -├─ require(!already claimed by this node) -├─ honestNodeAmount includes BOTH: -│ • Base compensation (from work-value BPS allocation) -│ • Slashed funds surplus (after requester is made whole) +├─ require(!honest-node reward already claimed by this node) +│ → This ledger is independent from the requester-refund claim ledger, so a +│ requester who is also an honest node can receive both entitlements +├─ honestNodeAmount exists only for requester-attributable failures +│ → ciphernodes/supply failures set this base amount to zero +│ → honest nodes claim later ticket slashes through claimSlashedFunds ├─ perNodeAmount = honestNodeAmount / honestNodeCount -│ • SNAPSHOTTED at calculateRefund (M-09); also re-snapshotted -│ inside _applySlashedFunds while _claimCount == 0 so pre-first- -│ claim escrows are reflected. Post-first-claim escrows route to -│ _pendingSlashedFunds and never mutate the snapshot. +│ • SNAPSHOTTED at calculateRefund (M-09) and never changed by +│ slashed assets, even when the slash token equals the fee token. ├─ Last claimer routes the residual dust to _pendingTreasury via │ TreasurySlashedCredited (pull); the last node never gets a │ silently-inflated payout, and no per-claim dust is stranded. ├─ Transfer directly to node (not via BondingRegistry) └─ Emit RefundClaimed(e3Id, node, amount) + +SLASH RECIPIENT claims a token-specific entitlement: + E3RefundManager.claimSlashedFunds(e3Id, actualToken) +│ +├─ Read _pendingSlashedClaims[e3Id][actualToken][caller] +├─ Clear the claim and reduce actualToken's protected liability +├─ Transfer that exact token; base refunds never consume the protected reserve +└─ Emit SlashedFundsClaimed(e3Id, caller, actualToken, amount) ``` -### Refund Example (Base Only) +### Refund Example: Requester/Compute-Provider Fault ``` Scenario: E3 fails at KeyPublished stage (compute timeout) @@ -216,38 +228,32 @@ Scenario: E3 fails at KeyPublished stage (compute timeout) Protocol fee: 5% → protocolAmount = 50,000 Each honest node claims: 400,000 / 3 = 133,333 - Last honest node claims: 133,333 + 1 (dust) = 133,334 + The 1-unit division dust is credited to the treasury pull ledger Requester claims: 550,000 - Treasury receives: 50,000 (immediately) + Treasury claims: 50,001 ``` -### Refund Example (With Slashed Funds) +### Refund Example: Ciphernode Fault ``` -Same scenario as above, then 2 nodes are slashed for 300,000 each: - - Before slash: - requesterAmount = 550,000 - honestNodeAmount = 400,000 - originalPayment = 1,000,000 - - Slash #1: 300,000 escrowed to refund pool - Requester gap = 1,000,000 - 550,000 = 450,000 - toRequester = min(300,000, 450,000) = 300,000 - toHonestNodes = 300,000 - 300,000 = 0 - → requesterAmount = 850,000, honestNodeAmount = 400,000 - - Slash #2: 300,000 escrowed to refund pool - Requester gap = 1,000,000 - 850,000 = 150,000 - toRequester = min(300,000, 150,000) = 150,000 - toHonestNodes = 300,000 - 150,000 = 150,000 - → requesterAmount = 1,000,000, honestNodeAmount = 550,000 - - Final: - Requester claims: 1,000,000 (fully made whole) - Each honest node gets: 550,000 / 3 = 183,333 - Treasury received: 50,000 (at processE3Failure time) +Scenario: E3 fails during DKG because one member supplied invalid shares + Fee escrow: 1,000,000 USDC + Honest nodes after expulsion: 2 + Faulty node ticket slash: 300,000 TICKET-USD + + Base fee-token claims: + requester: 1,000,000 USDC (100%) + honest nodes: 0 USDC + protocol: 0 USDC + + Separate slash-token claims: + honest node 1: 150,000 TICKET-USD + honest node 2: 150,000 TICKET-USD + requester: 0 TICKET-USD + + If no ticket collateral is actually slashed, honest-node compensation is + zero; the requester refund never waits for or depends on slash execution. ``` --- @@ -547,16 +553,17 @@ Anyone calls: SlashingManager.proposeSlash(e3Id, operator, proof) │ ticketAmount: policy.ticketPenalty, │ licenseAmount: policy.licensePenalty, │ proofVerified: true, // Lane A marker -│ executableAt: block.timestamp, // immediate +│ executableAt: block.timestamp + policy.appealWindow, │ banNode: policy.banNode, │ affectsCommittee: policy.affectsCommittee, │ failureReason: policy.failureReason │ } │ → Policy values snapshotted at proposal time │ → Prevents execution drift if policy changes later +│ → Increment unresolved financial proposal count for operator │ -└─ 8. IMMEDIATELY execute: - _executeSlash(proposalId) +└─ 8. If appealWindow == 0, immediately execute; otherwise leave + the proposal deferred and appealable until executableAt │ │ (see "Slash Execution" below) ``` @@ -573,12 +580,16 @@ SLASHER_ROLE calls: SlashingManager.proposeSlashEvidence( │ require(!policy.requiresProof) → evidence-based only │ → reason is an explicit bytes32, not derived from proof │ -├─ 2. Replay protection: +├─ 2. Require the snapshotted E3 dependency graph exists and +│ registry.isCommitteeMember(e3Id, operator) +│ → Evidence cannot slash an unrelated operator into another E3's escrow +│ +├─ 3. Replay protection: │ evidenceHash = keccak256(abi.encode(e3Id, operator, keccak256(evidence))) │ require(!evidenceConsumed[evidenceHash]) │ evidenceConsumed[evidenceHash] = true │ -├─ 3. Create proposal with SNAPSHOTTED policy values: +├─ 4. Create proposal with SNAPSHOTTED policy values: │ proposal = SlashProposal { │ e3Id, operator, reason, │ ticketAmount: policy.ticketPenalty, @@ -590,8 +601,9 @@ SLASHER_ROLE calls: SlashingManager.proposeSlashEvidence( │ failureReason: policy.failureReason │ } │ → NOT executed immediately +│ → Increment the same unresolved financial proposal count │ -└─ 4. Emit SlashProposed(proposalId, e3Id, operator, reason) +└─ 5. Emit SlashProposed(proposalId, e3Id, operator, reason) ─── APPEAL WINDOW OPENS ───────────────────────────────────── @@ -600,8 +612,6 @@ Operator (accused) calls: SlashingManager.fileAppeal(proposalId, evidence) ├─ require(msg.sender == proposal.operator) ├─ require(block.timestamp < proposal.executableAt) │ → Must appeal before window closes -├─ require(!proposal.proofVerified) -│ → Cannot appeal proof-based slashes ├─ require(!proposal.appealed) │ → Only one appeal per proposal ├─ proposal.appealed = true @@ -615,20 +625,26 @@ GOVERNANCE_ROLE resolves: SlashingManager.resolveAppeal( ├─ require(proposal.appealed && !proposal.resolved) ├─ proposal.resolved = true ├─ proposal.appealUpheld = upheld +├─ If upheld: decrement unresolved proposal count └─ Emit AppealResolved(proposalId, upheld, resolution) +If governance does not resolve a filed appeal by +`executableAt + APPEAL_RESOLUTION_GRACE`, anyone may call `expireAppeal`. +Expiry conclusively upholds the appeal and releases the collateral gate. + ─── AFTER APPEAL WINDOW ────────────────────────────────────── Anyone calls: SlashingManager.executeSlash(proposalId) │ -├─ require(!proposal.executed && !proposal.proofVerified) +├─ require(!proposal.executed) ├─ require(block.timestamp >= proposal.executableAt) ├─ If appealed: │ require(proposal.resolved) │ require(!proposal.appealUpheld) │ → If appeal was upheld, slash is cancelled │ -└─ _executeSlash(proposalId, policy) +├─ Decrement unresolved proposal count (reverts atomically on failure) +└─ _executeSlash(proposalId, lane) ``` ### Slash Execution (Both Lanes) @@ -726,18 +742,30 @@ _executeSlash(proposalId): │ │ Always escrows — regardless of E3 stage. │ │ Destination decided later at terminal state. │ │ -│ │ Self-call for atomicity: -│ │ try this.escrowSlashedFundsToRefund(e3Id, actualTicketSlashed) +│ ├─ Reserve and record BEFORE attempting the route: +│ │ bondingRegistry.reserveSlashedTicketFunds(amount) +│ │ pendingSlashRoutes[proposalId] = { +│ │ e3Id, token: ticketToken.underlying(), amount, pending: true +│ │ } +│ │ → Generic redirect and treasury withdrawal cannot spend reserve +│ │ → Emit SlashRoutePending +│ │ +│ │ Bounded self-call for initial atomic attempt: +│ │ try this.routePendingSlashFunds(proposalId) │ │ │ -│ │ │ ┌─── escrowSlashedFundsToRefund() ───────────────────┐ +│ │ │ ┌─── routePendingSlashFunds() ───────────────────────┐ │ │ │ │ require(msg.sender == address(this)) │ │ │ │ │ → Self-call only (for try/catch atomicity) │ +│ │ │ │ require(route.pending) │ +│ │ │ │ route.pending = false before interactions │ +│ │ │ │ → Callback cannot consume the reserve twice │ +│ │ │ │ → Any later revert restores pending=true │ │ │ │ │ │ │ │ │ │ Step A: Move USDC from BondingRegistry │ -│ │ │ │ bondingRegistry.redirectSlashedTicketFunds( │ +│ │ │ │ bondingRegistry.redirectReservedSlashedTicketFunds( │ │ │ │ e3RefundManager, amount │ │ │ │ │ ) │ -│ │ │ │ │ │ +│ │ │ │ ├─ reservedSlashedTicketBalance -= amount │ │ │ │ │ ├─ slashedTicketBalance -= amount │ │ │ │ │ └─ ticketToken.payout(e3RefundManager, amount) │ │ │ │ │ → Transfers UNDERLYING USDC (not ticket │ @@ -746,32 +774,33 @@ _executeSlash(proposalId): │ │ │ │ burnTickets() during slashTicketBalance │ │ │ │ │ │ │ │ │ │ Step B: Update escrow accounting │ -│ │ │ │ interfold.escrowSlashedFunds(e3Id, amount) │ -│ │ │ │ → e3RefundManager.escrowSlashedFunds(e3Id, amt) │ +│ │ │ │ interfold.escrowSlashedFunds(e3Id, token, amount)│ +│ │ │ │ → e3RefundManager.escrowSlashedFunds( │ +│ │ │ │ e3Id, token, amount) │ │ │ │ │ │ │ │ │ │ │ ├─ If refund distribution NOT yet calculated: │ -│ │ │ │ │ _pendingSlashedFunds[e3Id] += amount │ -│ │ │ │ │ → Queued until terminal state is reached │ +│ │ │ │ │ _pendingSlashedByToken[e3Id][token] += amt │ +│ │ │ │ │ tokenLiability[token] += amount │ +│ │ │ │ │ → Require balance >= protected liability │ │ │ │ │ │ │ │ │ │ │ └─ If refund distribution IS calculated: │ -│ │ │ │ require(no claims started yet) │ -│ │ │ │ _applySlashedFunds(e3Id, amount) │ -│ │ │ │ (see priority logic below — failure path) │ +│ │ │ │ settle token-specific pull claims now │ +│ │ │ │ → Never mutate fee-token refund buckets │ │ │ │ │ │ │ │ │ │ If EITHER step reverts → both revert together │ -│ │ │ │ → Funds stay in BondingRegistry for treasury │ +│ │ │ │ → Route remains pending and funds stay reserved │ │ │ │ │ → Slash itself still proceeds │ +│ │ │ │ On success emit SlashRouteCompleted │ │ │ │ └────────────────────────────────────────────────────┘ │ │ │ └─ catch: emit RoutingFailed(e3Id, actualTicketSlashed) -│ → Slash is NOT rolled back, only fund escrowing fails +│ → Slash is NOT rolled back; anyone may retry the route │ -├─ 6. proposal.executed = true -│ → Set AFTER the two bondingRegistry.slash* calls (and AFTER ban -│ update), so an OOG / revert during slashing leaves `executed` -│ false and the proposal can be retried (audit H-21b, defence in -│ depth). Reentrancy is already blocked by `_executeSlash` itself -│ being reachable only through nonReentrant entry points. +├─ 6. PERMISSIONLESS ROUTE RETRY (only after an initial failure): +│ anyone calls retrySlashRoute(proposalId) +│ ├─ pending == false → return false (idempotent no-op) +│ └─ pending == true → self-call routePendingSlashFunds +│ → transfer + accounting succeed atomically, or all state reverts │ └─ 7. Emit SlashExecuted(proposalId, e3Id, operator, reason, ticketSlashed, licenseSlashed, banned) @@ -783,82 +812,72 @@ _executeSlash(proposalId): > short-pays the treasury. Booking has already been zeroed before the transfer; the event exists for > indexer-side reconciliation (audit M-13). -### Slashed Funds Priority Logic (Failure Path): \_applySlashedFunds() +### Token-Aware Slashed Funds Settlement (Failure Path) ``` -_applySlashedFunds(e3Id, amount): +settleSlashedFunds(e3Id, actualToken): │ -├─ Priority: MAKE REQUESTER WHOLE FIRST +├─ Read and clear _pendingSlashedByToken[e3Id][actualToken] │ -├─ requesterGap = originalPayment - dist.requesterAmount -│ → How much more the requester needs to reach their original payment +├─ Read current active committee nodes from the request-time registry +│ → Faulty operators expelled by failure-triggering policies are excluded │ -├─ toRequester = min(amount, requesterGap) -│ → Fill requester up to originalPayment, no more +├─ If active honest nodes exist: +│ divide the whole actualToken amount among them +│ → deterministic last-node dust assignment │ -├─ toHonestNodes = amount - toRequester -│ → Surplus (after requester is whole) goes to honest nodes +├─ Otherwise: +│ credit the whole actualToken amount to the snapshotted treasury +│ → no requester windfall beyond return of their original fee escrow │ -├─ H-08: if dist.honestNodeCount == 0 and toHonestNodes > 0, -│ route toHonestNodes to the treasury pull-credit pool -│ (_pendingTreasury[treasury][feeToken]) and emit -│ TreasurySlashedCredited. The requester cap (originalPayment) -│ is preserved; the honest-node bucket would otherwise be -│ unclaimable since `claimHonestNodeReward` reverts when -│ honestNodeCount == 0. +├─ Credit _pendingSlashedClaims[e3Id][actualToken][recipient] +│ → Base fee-token RefundDistribution fields never change +│ → Decimal/unit differences cannot corrupt the base refund │ -├─ dist.requesterAmount += toRequester -├─ dist.honestNodeAmount += toHonestNodes -├─ dist.totalSlashed += amount -│ -├─ M-09: if honestNodeCount > 0, re-snapshot -│ dist.perNodeAmount = honestNodeAmount / honestNodeCount. -│ escrowSlashedFunds gates this path on _claimCount == 0, so the -│ snapshot only moves before any claim has landed; later escrows -│ land in _pendingSlashedFunds and surface via -│ withdrawOrphanedSlashedFunds. -│ -└─ Emit SlashedFundsApplied(e3Id, toRequester, toHonestNodes) +└─ Emit SlashedFundsApplied(e3Id, actualToken, + 0, toHonestNodes) Design rationale: - The requester PAID for the computation and got nothing. They should - be made whole before honest nodes receive any slash-based bonus. - Honest nodes already receive compensation via the base BPS allocation - for work they completed. + Supplier/ciphernode failures already return 100% of the requester's fee + escrow. Ticket slashes compensate honest service providers in the slash + asset itself. No trusted conversion price is needed, and requester-fault + failures do not gain a slash-funded rebate for costs they caused. ``` ### Slashed Funds Distribution (Success Path): distributeSlashedFundsOnSuccess() ``` -distributeSlashedFundsOnSuccess(e3Id, activeNodes, paymentToken): +distributeSlashedFundsOnSuccess(e3Id, paymentToken): │ ├─ Called by Interfold._distributeRewards() when E3 completes successfully │ -├─ escrowed = _pendingSlashedFunds[e3Id] -│ if escrowed == 0: return (nothing to distribute) -│ -├─ _pendingSlashedFunds[e3Id] = 0 -├─ _slashedSuccessToken[e3Id] = paymentToken // snapshot for later claims +├─ Mark success settlement ready +├─ Every explicitly recorded token settles independently via +│ settleSlashedFunds(e3Id, actualToken) │ -├─ Split using WorkValueAllocation.successSlashedNodeBps (default 5000): +├─ Load the immutable E3PolicySnapshot captured by Interfold.request +│ (allocation, treasury, Interfold, registry, policy version) +├─ Read activeNodes from the request-time registry at settlement time +│ → Expelled nodes cannot receive a later slash-funded bonus +├─ Split using snapshot.allocation.successSlashedNodeBps (default 5000): │ toNodes = escrowed * successSlashedNodeBps / 10000 │ toTreasury = escrowed - toNodes │ ├─ Credit (pull-payment, H-01/M-02) — funds are NOT pushed here: │ for node in activeNodes: │ perNode = toNodes / activeNodes.length (dust → last node) -│ _pendingSlashedSuccess[e3Id][node] += perNode -│ Emit SlashedFundsCredited(e3Id, node, paymentToken, perNode) +│ _pendingSlashedClaims[e3Id][actualToken][node] += perNode +│ Emit SlashedFundsCredited(e3Id, node, actualToken, perNode) │ ├─ Credit treasury for protocol share: -│ _pendingTreasury[treasury][paymentToken] += toTreasury -│ Emit TreasurySlashedCredited(treasury, paymentToken, toTreasury) +│ _pendingTreasury[snapshot.treasury][actualToken] += toTreasury +│ Emit TreasurySlashedCredited(snapshot.treasury, actualToken, toTreasury) │ -└─ Emit SlashedFundsDistributedOnSuccess(e3Id, toNodes, toTreasury) +└─ Emit SlashedFundsDistributedOnSuccess(e3Id, actualToken, + toNodes, toTreasury) Claim flow (separate transactions, pull-only): - honest node → e3RefundManager.claimSlashedFundsOnSuccess(e3Id) - / claimSlashedFundsOnSuccessBatch(e3Ids[]) + honest node → e3RefundManager.claimSlashedFunds(e3Id, actualToken) → Emits SlashedFundsClaimed(e3Id, node, token, amt) protocol treasury → e3RefundManager.treasuryClaim(token) → Emits TreasurySlashedClaimed(treasury, token, amt) @@ -869,39 +888,62 @@ Design rationale: a slashed peer) and the protocol treasury. Both shares use a per-recipient pull ledger so a single failing recipient (e.g. blacklisted ERC-20 address) cannot brick the success-path or strand other claimants' funds. + Governance changes to the live allocation or treasury increment policyVersion + and apply only to later E3 requests; existing snapshots never migrate implicitly. ``` +### In-flight dependency rotation (AUD M-04) + +Every slash and settlement route resolves the dependency graph frozen when the E3 was requested: + +- `Interfold` uses the per-E3 registry, refund manager, and slashing manager for callbacks, + committee reads, verification, rewards, failure settlement, and slash escrow. +- `CiphernodeRegistryOwnable` uses the per-E3 Interfold, bonding registry, and slashing manager for + ticket eligibility, committee callbacks, and expulsion authorization. +- `SlashingManager` uses the per-E3 bonding registry, ciphernode registry, Interfold, and refund + manager for attestations, penalties, expulsion, failure callbacks, and fund routing. +- `E3RefundManager` accepts lifecycle calls from the Interfold recorded in the E3 policy snapshot. +- `E3RefundManager` reads slash recipients from the committee registry recorded in that snapshot. +- `BondingRegistry` retains replaced slashing managers as authorized until governance explicitly + revokes them, so an old manager can finish snapshotted penalties and remains part of the exit + gate. + +Admin setters update the live defaults for future requests only. Each E3 must have a complete +request-time snapshot; lifecycle calls fail closed if that invariant is not satisfied. Governance +must revoke a replaced slashing manager only after all E3s, proposals, and pending slash routes that +depend on it are terminal. + ### Slashed Funds Ordering: Escrow → Terminal State Resolution ``` -Slashing always escrows funds in _pendingSlashedFunds[e3Id], -regardless of the current E3 stage. The destination is decided -only when the E3 reaches a terminal state (Complete or Failed). +Slashing always escrows funds in +_pendingSlashedByToken[e3Id][ticketUnderlying], regardless of the +current E3 stage. Settlement never substitutes the E3 fee token. ── FAILURE PATH ────────────────────────────────────────────── Case 1: Slash happens BEFORE processE3Failure → escrowSlashedFunds sees !dist.calculated - → Funds queued in _pendingSlashedFunds[e3Id] + → Funds queued under their actual token and liability protected → When processE3Failure → calculateRefund runs: - drains pending queue via _applySlashedFunds - (requester filled first, surplus to honest nodes) + matching fee-token escrows may settle immediately; every other + recorded token is permissionlessly settled with settleSlashedFunds Case 2: Slash happens AFTER processE3Failure → escrowSlashedFunds sees dist.calculated - → _applySlashedFunds runs immediately - → require(no claims started) — reverts if too late + → token-specific honest-node/treasury pull credits are created immediately + → Base refund claims may already have started; ledgers remain independent Case 3: Multiple slashes on same E3 (failure) → Each slash independently escrows funds - → Priority logic runs per-slash: requester filled first each time - → totalSlashed accumulates across all slashes + → Every token retains an independent pending/claim/liability ledger ── SUCCESS PATH ────────────────────────────────────────────── Case 4: E3 completes successfully with escrowed slashed funds → _distributeRewards calls distributeSlashedFundsOnSuccess - → _pendingSlashedFunds[e3Id] split between nodes and treasury + → Enables per-token permissionless settlement + → Reads active nodes from the request-time registry when each token settles → Nodes receive successSlashedNodeBps portion (default 50%) → Treasury receives the remainder ``` @@ -915,16 +957,19 @@ SlashPolicy { requiresProof: bool // Lane A (true) or Lane B (false) proofVerifier: address // verifier address (Lane A: used in policy lookup) banNode: bool // permanently ban operator - appealWindow: uint256 // seconds for appeal (Lane B only, 0 for Lane A) + appealWindow: uint256 // seconds; required for Lane B, optional for Lane A enabled: bool // policy active affectsCommittee: bool // expel from E3 committee failureReason: uint8 // FailureReason enum (0 = no E3 failure) } Constraints: -- If requiresProof: appealWindow must be 0 (atomic execution, no appeal) +- If requiresProof: appealWindow may be 0 (atomic) or > 0 (deferred challenge) - If !requiresProof: appealWindow must be > 0 (delayed execution, with appeal) - At least one penalty must be non-zero +- A nonzero failureReason must be below `_MAX_FAILURE_REASON` +- A policy with nonzero failureReason must set affectsCommittee=true + → The proven-faulty operator is expelled before honest slash recipients are resolved Slash Reasons (derived from ProofType for Lane A): reason = keccak256(abi.encodePacked(proofType)) @@ -995,7 +1040,8 @@ Slash Reasons (derived from ProofType for Lane A): │ └─ Slashed USDC escrowed in E3RefundManager │ │ │ │ 10. FUND DISTRIBUTION (at E3 terminal state) │ -│ ├─ Failure: requester refunded first, surplus to honest │ +│ ├─ Failure: fee refund is fault-attributed; slashes pay │ +│ │ active honest nodes │ │ └─ Success: nodes + treasury split │ └─────────────────────────────────────────────────────────────────┘ ``` @@ -1009,36 +1055,38 @@ state: ``` STEP 1: ESCROWING (always, at slash time) - Triggered by: _executeSlash → escrowSlashedFundsToRefund + Triggered by: _executeSlash → reserve + routePendingSlashFunds When: Any slash with actualTicketSlashed > 0, regardless of E3 stage - Flow: BondingRegistry.redirectSlashedTicketFunds(refundManager, amount) + Flow: BondingRegistry.redirectReservedSlashedTicketFunds(refundManager, amount) → ticketToken.payout(refundManager, amount) - → USDC moves to E3RefundManager - → _pendingSlashedFunds[e3Id] += amount (if not yet calculated) + → actual ticket underlying moves to E3RefundManager + → _pendingSlashedByToken[e3Id][actualToken] += amount + → tokenLiability[actualToken] protects the balance Effect: slashedTicketBalance goes UP (during slash) then DOWN (during redirect) - -STEP 2a: E3 FAILS → Requester-first distribution - Triggered by: processE3Failure → calculateRefund drains pending queue - OR: escrowSlashedFunds after distribution is calculated - Flow: _applySlashedFunds(e3Id, amount) - → Requester filled up to originalPayment first - → Surplus to honest nodes - Claims: requester and honest nodes claim via claimRequesterRefund / claimHonestNodeReward + Failure: route stays pending and the same amount remains reserved against + generic redirects/treasury withdrawal until permissionless retry succeeds + +STEP 2a: E3 FAILS → Token-specific compensation + Triggered by: terminal escrow or permissionless settleSlashedFunds + Flow: settleSlashedFunds(e3Id, actualToken) + → The entire actual-token slash is divided among active honest nodes + → If there are no honest recipients, the slash goes to the + snapshotted treasury + → Credits stay in actualToken; fee-token refund buckets are unchanged + Claims: claimSlashedFunds(e3Id, actualToken) STEP 2b: E3 SUCCEEDS → Nodes + Treasury split Triggered by: _distributeRewards → distributeSlashedFundsOnSuccess - Flow: _pendingSlashedFunds[e3Id] split by successSlashedNodeBps + Flow: each actualToken amount split by successSlashedNodeBps → Nodes receive their share evenly (with dust to last) → Treasury receives the remainder - Effect: _pendingSlashedFunds[e3Id] set to 0 + Effect: only _pendingSlashedByToken[e3Id][actualToken] is cleared -FALLBACK: TREASURY WITHDRAWAL - Triggered by: Owner calls BondingRegistry.withdrawSlashedFunds() - When: Escrowing failed (catch block) or leftover balance - Flow: BondingRegistry sends to slashedFundsTreasury - → ticketToken.payout(treasury, ticketAmount) - → licenseToken.safeTransfer(treasury, licenseAmount) - Effect: slashedTicketBalance decremented +FALLBACK: RETRY, NOT OWNER RELABELING + Failed routes remain reserved in BondingRegistry and retryable by anyone. + E3RefundManager has no owner function that accepts an arbitrary token to + relabel an untyped amount. Its transfer helper preserves each token's + protected slash liability after base refunds and treasury claims. License bond slashes always go to treasury (no escrow routing for FOLD). ``` @@ -1138,21 +1186,27 @@ Applied audit findings: **C-05, H-05, H-06, H-07, H-09, H-10, H-24, M-14, M-15, is emitted. The operator can call `fileAppeal` during that window; otherwise anyone may call `executeSlash` once it elapses. -### Lane B open-proposal gate (H-05) - -- `SlashingManager` tracks `_openLaneBCount[operator]`: `proposeSlashEvidence` increments, - `executeSlash` decrements before `_executeSlash`, and `resolveAppeal(upheld)` unwinds the counter. -- `hasOpenLaneBProposal(operator)` is exposed as a public view. -- `BondingRegistry.deregisterOperator()` reverts `OperatorUnderSlash()` while this gate is true, - preventing escape during an active Lane B proceeding. Lane A is intentionally not gated because it - is atomic (or short-windowed via H-06) and self-clears. - -### Pull-payment slashed funds (H-07, H-09) - -- Slashed funds are routed through the same pull-payment pull-pool as E3 rewards (Cluster 3 / H-08 - path). Recipients claim via `claimReward(e3Id)`; failed-transfer attackers cannot grief the whole - distribution. Late credits (e.g. `_applySlashedFunds` racing a prior reward claim) are accumulated - rather than lost. +### Unified open-proposal collateral gate (H-05, AUD H-03) + +- `SlashingManager` tracks `_openProposalCount[operator]` for both Lane A and Lane B. Proposal + creation increments it; successful execution, an upheld appeal, or terminal appeal expiry + decrements it. `hasOpenSlashProposal` exposes the unified semantics. +- `BondingRegistry` reverts `OperatorUnderSlash()` on `removeTicketBalance`, `unbondLicense`, + `deregisterOperator`, and `claimExits` while the gate is raised. Both active collateral and assets + already queued for exit therefore remain slashable. +- That check covers every authorized current or retained historical slashing manager. Rotation + therefore cannot release collateral for an old manager's in-flight proposal. Governance revokes an + old manager only after its E3s, proposals, and pending routes are terminal. +- A filed appeal cannot freeze collateral indefinitely: after the policy appeal window plus the + seven-day governance resolution grace, `expireAppeal` permissionlessly upholds it and clears its + gate. + +### Pull-payment slashed funds (H-01, H-07, H-09) + +- Slashed funds use their own `(e3Id, token, recipient)` pull-payment ledger rather than the normal + reward or refund bucket. Recipients call `claimSlashedFunds(e3Id, token)`; failed-transfer + recipients cannot grief other claims, different token decimals never mix, and late credits remain + independently claimable. ### Two-step ban (M-14, M-15) diff --git a/agent/flow-trace/06_DEACTIVATION_AND_COMPLETION.md b/agent/flow-trace/06_DEACTIVATION_AND_COMPLETION.md index 7939e6c5a..8ab1a5eaa 100644 --- a/agent/flow-trace/06_DEACTIVATION_AND_COMPLETION.md +++ b/agent/flow-trace/06_DEACTIVATION_AND_COMPLETION.md @@ -163,10 +163,9 @@ publishPlaintextOutput() succeeds │ │ │ _pendingTreasury[snapshottedTreasury][token] += protocolAmount │ │ ├─ _creditRewards(e3Id, nodes, amounts, token) │ │ │ → Credits pull-payment rewards to each registered operator -│ │ ├─ e3RefundManager.distributeSlashedFundsOnSuccess( -│ │ │ e3Id, activeNodes, paymentToken -│ │ │ ) +│ │ ├─ e3RefundManager.distributeSlashedFundsOnSuccess(e3Id, paymentToken) │ │ │ → If any escrowed slashed funds exist for this E3: +│ │ │ read the currently active committee from the request-time registry │ │ │ split by successSlashedNodeBps (default 50%) │ │ │ nodes portion distributed evenly to activeNodes │ │ │ remainder sent to protocol treasury @@ -505,12 +504,12 @@ GOVERNANCE lifts ban: ## Cluster 6 Audit Addendum (deregistration & bans) -- **Deregistration is blocked while a Lane B slash is open** (H-05). - `BondingRegistry.deregisterOperator()` calls - `ISlashingManager(sm).hasOpenLaneBProposal(msg.sender)` and reverts `OperatorUnderSlash()` until - `executeSlash` or `resolveAppeal(upheld)` unwinds the open-proposal counter. Lane A is permitted - to proceed through the normal exit queue because Lane A is either atomic or closes within the H-06 - challenge window. +- **Collateral exit is blocked while a slash is open** (H-05, AUD H-03). + `BondingRegistry` checks `hasOpenSlashProposal(operator)` on every authorized current or retained + historical manager and reverts `OperatorUnderSlash()` from ticket withdrawal, license unbonding, + deregistration, and exit claims. Execution, an upheld appeal, or permissionless appeal expiry + unwinds the counter. After manager rotation, governance must retain the old manager until every + E3 and proposal that depends on it is terminal, then explicitly revoke it. - **Two-step ban** (M-14, M-15): bans now require `proposeBan` → `confirmBan` from a **distinct** signer holding `GOVERNANCE_ROLE`. `cancelBan` rescinds an unconfirmed proposal. Legacy direct-set diff --git a/circuits/benchmarks/README.md b/circuits/benchmarks/README.md index dd89b271d..17cdf3894 100644 --- a/circuits/benchmarks/README.md +++ b/circuits/benchmarks/README.md @@ -70,15 +70,16 @@ switch; manual edits to those files will trip the check. ### Proof aggregation and folding (integration) -The gas / integration stage always runs `cargo test -p e3-tests test_trbfv_actor` with proof -aggregation enabled (`E3Requested.proof_aggregation_enabled = true`): per-node `ZkNodeDkgFold`, fold -attestations (EIP-712 against `DkgFoldAttestationVerifier`), and exported folded `dkg_aggregator` / -`decryption_aggregator` proofs for Π_DKG / Π_dec on-chain gas. - -| Flag / env | Effect | -| ------------------------------------------------------- | ------------------------------------------------------------------------------------ | -| `BENCHMARK_MULTITHREAD_JOBS=N` / `--multithread-jobs N` | Rayon concurrent ZK jobs (default `1`) | -| `BENCHMARK_DKG_FOLD_ATTESTATION_VERIFIER=0x…` | EIP-712 verifying contract for fold attestations (default: localhost deploy address) | +The gas / integration stage always runs `cargo test -p e3-tests test_trbfv_actor` with the +ciphernode test/CI skip flag off: per-node `ZkNodeDkgFold`, fold attestations (EIP-712 against +`DkgFoldAttestationVerifier`), and exported folded `dkg_aggregator` / `decryption_aggregator` proofs +for Π_DKG / Π_dec on-chain gas. + +| Flag / env | Effect | +| ------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `CIPHERNODE_SKIP_PROOF_AGGREGATION=true` | Test/CI-only: skip recursive aggregation; benchmark scripts explicitly set it to `false` | +| `BENCHMARK_MULTITHREAD_JOBS=N` / `--multithread-jobs N` | Rayon concurrent ZK jobs (default `1`) | +| `BENCHMARK_DKG_FOLD_ATTESTATION_VERIFIER=0x…` | EIP-712 verifying contract for fold attestations (default: localhost deploy address) | **Output directories** (under `circuits/benchmarks/`): diff --git a/circuits/benchmarks/results_insecure_minimum/integration_summary.json b/circuits/benchmarks/results_insecure_minimum/integration_summary.json index 83fd87c6b..e8b8d17ab 100644 --- a/circuits/benchmarks/results_insecure_minimum/integration_summary.json +++ b/circuits/benchmarks/results_insecure_minimum/integration_summary.json @@ -6,7 +6,7 @@ "bfv_preset": "InsecureThreshold512", "lambda": 2, "proof_aggregation_enabled": true, - "multithread_concurrent_jobs": 13, + "multithread_concurrent_jobs": 4, "committee_h": 2, "committee_n": 3, "committee_t": 1, @@ -17,234 +17,61 @@ "proof_aggregation_enabled": true, "dkg_fold_attestation_verifier": "0x7969c5eD335650692Bc04293B07F5BF2e7A673C0", "multithread": { - "rayon_threads": 13, - "max_simultaneous_rayon_tasks": 13, - "cores_available": 14 + "rayon_threads": 4, + "max_simultaneous_rayon_tasks": 4, + "cores_available": 8 }, "operation_timings": [ - { - "name": "CalculateDecryptionKey", - "avg_seconds": 0.003846458, - "runs": 3, - "total_seconds": 0.011539375 - }, - { - "name": "CalculateDecryptionShare", - "avg_seconds": 0.021596236, - "runs": 3, - "total_seconds": 0.064788708 - }, - { - "name": "CalculateThresholdDecryption", - "avg_seconds": 0.021793167, - "runs": 1, - "total_seconds": 0.021793167 - }, - { - "name": "GenEsiSss", - "avg_seconds": 0.006900819, - "runs": 3, - "total_seconds": 0.020702458 - }, - { - "name": "GenPkShareAndSkSss", - "avg_seconds": 0.01082625, - "runs": 3, - "total_seconds": 0.03247875 - }, - { - "name": "NodeDkgFold/c2ab_fold", - "avg_seconds": 18.129162805, - "runs": 3, - "total_seconds": 54.387488416 - }, - { - "name": "NodeDkgFold/c3a_fold", - "avg_seconds": 71.186463291, - "runs": 3, - "total_seconds": 213.559389875 - }, - { - "name": "NodeDkgFold/c3ab_fold", - "avg_seconds": 7.876658916, - "runs": 3, - "total_seconds": 23.62997675 - }, - { - "name": "NodeDkgFold/c3b_fold", - "avg_seconds": 70.951759472, - "runs": 3, - "total_seconds": 212.855278417 - }, - { - "name": "NodeDkgFold/c4ab_fold", - "avg_seconds": 7.872269472, - "runs": 3, - "total_seconds": 23.616808416 - }, - { - "name": "NodeDkgFold/node_fold", - "avg_seconds": 18.285537124, - "runs": 3, - "total_seconds": 54.856611374 - }, - { - "name": "ZkDecryptedSharesAggregation", - "avg_seconds": 1.53384025, - "runs": 1, - "total_seconds": 1.53384025 - }, - { - "name": "ZkDecryptionAggregation", - "avg_seconds": 46.886299625, - "runs": 1, - "total_seconds": 46.886299625 - }, - { - "name": "ZkDkgAggregation", - "avg_seconds": 5.350731209, - "runs": 1, - "total_seconds": 5.350731209 - }, - { - "name": "ZkDkgShareDecryption", - "avg_seconds": 1.001284562, - "runs": 6, - "total_seconds": 6.007707375 - }, - { - "name": "ZkNodeDkgFold", - "avg_seconds": 105.400482805, - "runs": 3, - "total_seconds": 316.201448416 - }, - { - "name": "ZkNodesFoldStep", - "avg_seconds": 5.041216854, - "runs": 2, - "total_seconds": 10.082433708 - }, - { - "name": "ZkPkAggregation", - "avg_seconds": 0.408154917, - "runs": 1, - "total_seconds": 0.408154917 - }, - { - "name": "ZkPkBfv", - "avg_seconds": 0.217929541, - "runs": 3, - "total_seconds": 0.653788625 - }, - { - "name": "ZkPkGeneration", - "avg_seconds": 2.247310694, - "runs": 3, - "total_seconds": 6.741932083 - }, - { - "name": "ZkShareComputation", - "avg_seconds": 2.368657458, - "runs": 6, - "total_seconds": 14.211944749 - }, - { - "name": "ZkShareEncryption", - "avg_seconds": 4.003050597, - "runs": 24, - "total_seconds": 96.073214335 - }, - { - "name": "ZkThresholdShareDecryption", - "avg_seconds": 3.246631902, - "runs": 3, - "total_seconds": 9.739895708 - }, - { - "name": "ZkVerifyShareDecryptionProofs", - "avg_seconds": 0.082421444, - "runs": 3, - "total_seconds": 0.247264333 - }, - { - "name": "ZkVerifyShareProofs", - "avg_seconds": 0.253021858, - "runs": 5, - "total_seconds": 1.265109292 - } + {"name": "CalculateDecryptionKey", "avg_seconds": 0.002493002, "runs": 3, "total_seconds": 0.007479008}, + {"name": "CalculateDecryptionShare", "avg_seconds": 0.014546000, "runs": 3, "total_seconds": 0.043638000}, + {"name": "CalculateThresholdDecryption", "avg_seconds": 0.014658909, "runs": 1, "total_seconds": 0.014658909}, + {"name": "GenEsiSss", "avg_seconds": 0.003312954, "runs": 3, "total_seconds": 0.009938863}, + {"name": "GenPkShareAndSkSss", "avg_seconds": 0.006545443, "runs": 3, "total_seconds": 0.019636331}, + {"name": "NodeDkgFold/c2ab_fold", "avg_seconds": 15.542266178, "runs": 3, "total_seconds": 46.626798535}, + {"name": "NodeDkgFold/c3a_fold", "avg_seconds": 67.103157296, "runs": 3, "total_seconds": 201.309471890}, + {"name": "NodeDkgFold/c3ab_fold", "avg_seconds": 8.157004177, "runs": 3, "total_seconds": 24.471012532}, + {"name": "NodeDkgFold/c3b_fold", "avg_seconds": 66.292620289, "runs": 3, "total_seconds": 198.877860868}, + {"name": "NodeDkgFold/c4ab_fold", "avg_seconds": 8.877390382, "runs": 3, "total_seconds": 26.632171146}, + {"name": "NodeDkgFold/node_fold", "avg_seconds": 20.289683136, "runs": 3, "total_seconds": 60.869049409}, + {"name": "ZkDecryptedSharesAggregation", "avg_seconds": 1.746945975, "runs": 1, "total_seconds": 1.746945975}, + {"name": "ZkDecryptionAggregation", "avg_seconds": 77.416901137, "runs": 1, "total_seconds": 77.416901137}, + {"name": "ZkDkgAggregation", "avg_seconds": 8.409547509, "runs": 1, "total_seconds": 8.409547509}, + {"name": "ZkDkgShareDecryption", "avg_seconds": 0.927539390, "runs": 6, "total_seconds": 5.565236343}, + {"name": "ZkNodeDkgFold", "avg_seconds": 146.711601900, "runs": 3, "total_seconds": 440.134805700}, + {"name": "ZkNodesFoldStep", "avg_seconds": 8.132157674, "runs": 2, "total_seconds": 16.264315348}, + {"name": "ZkPkAggregation", "avg_seconds": 0.678950539, "runs": 1, "total_seconds": 0.678950539}, + {"name": "ZkPkBfv", "avg_seconds": 0.243462617, "runs": 3, "total_seconds": 0.730387852}, + {"name": "ZkPkGeneration", "avg_seconds": 1.639551785, "runs": 3, "total_seconds": 4.918655355}, + {"name": "ZkShareComputation", "avg_seconds": 1.182185354, "runs": 6, "total_seconds": 7.093112124}, + {"name": "ZkShareEncryption", "avg_seconds": 1.861872787, "runs": 24, "total_seconds": 44.684946896}, + {"name": "ZkThresholdShareDecryption", "avg_seconds": 4.559046281, "runs": 3, "total_seconds": 13.677138845}, + {"name": "ZkVerifyShareDecryptionProofs", "avg_seconds": 0.041032219, "runs": 3, "total_seconds": 0.123096659}, + {"name": "ZkVerifyShareProofs", "avg_seconds": 0.148539630, "runs": 5, "total_seconds": 0.742698153} ], - "operation_timings_total_seconds": 1098.460620331, + "operation_timings_total_seconds": 1181.068453926, "operation_timings_metric": "tracked_job_wall", "phase_timings": [ - { - "label": "Starting trbfv actor test", - "seconds": 0e-9, - "metric": "wall_clock" - }, - { - "label": "Setup completed", - "seconds": 0.9476265, - "metric": "wall_clock" - }, - { - "label": "Committee Setup Completed", - "seconds": 7.02067775, - "metric": "wall_clock" - }, - { - "label": "Committee Finalization Complete", - "seconds": 0.003086, - "metric": "wall_clock" - }, - { - "label": "Aggregator P2: PkAggregation pending -> PublicKeyAggregated (wall)", - "seconds": 120.643449, - "metric": "wall_clock" - }, - { - "label": "ThresholdShares -> PublicKeyAggregated", - "seconds": 132.69507875, - "metric": "wall_clock" - }, - { - "label": "E3Request -> PublicKeyAggregated", - "seconds": 133.201828291, - "metric": "wall_clock" - }, - { - "label": "Application CT Gen", - "seconds": 0.009273208, - "metric": "wall_clock" - }, - { - "label": "Running FHE Application", - "seconds": 0.000058375, - "metric": "wall_clock" - }, - { - "label": "Aggregator P4: Aggregation pending -> PlaintextAggregated (wall)", - "seconds": 48.434143, - "metric": "wall_clock" - }, - { - "label": "Ciphertext published -> PlaintextAggregated", - "seconds": 51.995006459, - "metric": "wall_clock" - }, - { - "label": "Entire Test", - "seconds": 193.176080792, - "metric": "wall_clock" - } + {"label": "Starting trbfv actor test", "seconds": 0.000000000, "metric": "wall_clock"}, + {"label": "Setup completed", "seconds": 0.800449549, "metric": "wall_clock"}, + {"label": "Committee Setup Completed", "seconds": 7.024013699, "metric": "wall_clock"}, + {"label": "Committee Finalization Complete", "seconds": 0.001819861, "metric": "wall_clock"}, + {"label": "Aggregator P2: PkAggregation pending -> PublicKeyAggregated (wall)", "seconds": 134.166717000, "metric": "wall_clock"}, + {"label": "ThresholdShares -> PublicKeyAggregated", "seconds": 241.734708588, "metric": "wall_clock"}, + {"label": "E3Request -> PublicKeyAggregated", "seconds": 242.237859363, "metric": "wall_clock"}, + {"label": "Application CT Gen", "seconds": 0.011831894, "metric": "wall_clock"}, + {"label": "Running FHE Application", "seconds": 0.000089208, "metric": "wall_clock"}, + {"label": "Aggregator P4: Aggregation pending -> PlaintextAggregated (wall)", "seconds": 77.109859000, "metric": "wall_clock"}, + {"label": "Ciphertext published -> PlaintextAggregated", "seconds": 83.937900581, "metric": "wall_clock"}, + {"label": "Entire Test", "seconds": 334.013529569, "metric": "wall_clock"} ], "folded_artifacts": { "dkg_aggregator": { - "proof_hex": "0x000000000000000000000000000000000000000000000001e75e6e6bf316ec7900000000000000000000000000000000000000000000000e5e66538093519ec700000000000000000000000000000000000000000000000ea1d99185bf57a67100000000000000000000000000000000000000000000000000027a81f6b520c100000000000000000000000000000000000000000000000300a975ba6f13dbee000000000000000000000000000000000000000000000004a477a61f80a9e6d100000000000000000000000000000000000000000000000fd219fd84fb4defd70000000000000000000000000000000000000000000000000002fef0aaa80f5700000000000000000000000000000000000000000000000587f4bf0b7f2c5edb00000000000000000000000000000000000000000000000e89f1f317a49cf14f00000000000000000000000000000000000000000000000176869939c2c359c0000000000000000000000000000000000000000000000000000129747f04f15f000000000000000000000000000000000000000000000002d178d9b11de53f8f0000000000000000000000000000000000000000000000009044587fc21f92c1000000000000000000000000000000000000000000000009fdf61347211a781000000000000000000000000000000000000000000000000000008cdfd42e77de1980c43e1a2bdb19cf0131ff4fad06d6f11d7fd3279bd66410f2388c088f70a31e30bb64c3895db6a3301e9be27e79c07a3027013b0e68eb9f15025acf14e1972ecf23db1d3f80e4db8d3a8ce5e61a085775421a4ef5a8e770f626dc0fce3db71d0f0ff62f59bef95993de4ff0fc655fba80d47d6dd49ac6a99222e6b74f833800c35d744198e39965e71b95e55f6b34b670494a4279fb653f46cdce9e7d9e910a06fdd4d860032388619541f32f14d021660e8fa5497b849a703d63a58e930a195f63db46d4043c35c1fc90f21d2251f27e90874b191252eea17c8946d69d060959a597a665f3f225822e7cd537ed3be57beb9ea1ca04fc1baf6135541631ab1500bf70062fecf66545666e5f092e35c6655aa6eaf8d8bf20727575f7fc6c2c26c51b65b1a2161c60550ff91579866608e5f84a22bd9d525db9c3c0910d5a9d257d8b9fb3278bf7e3983384d4569ef101962e973fdc50af677e61249f7659180a6357f6d55ff9c3d0c3ded3bbcb37b1c49d08a76bba3d8ade40e097310dc2d2162b769f4d49f4a18af470a46bbe04ee29c054b4d644f034bed1b330b7f8624916ab19c29d424d52039e2f724aece13e882b30e012c588d515212a73f8539ef1167250177895980aea7471392e3ba028ec0715fae1cf2539f351dcf30b5d48790b4c6789f819bad95e55d1789e239dbd28d7cdcc56ad0bc1359ba6868a569d5d0bee03d62aec658dfd96444f35a5254fb150e5273acc5c643f0c6232b71c27d7109e197faa332c23d8b7c3f590a4a9972c5dc7f072121d62db7e63389c59c03726436b338944ea9825f719c9763ef1c8633e4ba2ee18556f162d735ae3c56d9e09e8ac442f6d947881166eef26a7b6fa538cbd84f3eebe8dbb6853e7399ec4520677fd7801b63bd7fa27430251fa14ea07fc97371a20021362a971be8ff709462c2e2be304a8baf3c6f57cecfb7e2dcb804656a16b3533262110bfd6bb8983cb16f23b3025152a5a67b3ba9d5f073fde73e67ff05ee11a29924d74412cf1555709498cba2023bf4d40d0fdbc85670c51082e006b173761c4183d75815fb12cda2b696fad8923295ae21e8958d220435e0a1b7bed0da50cee047f64c599848aab17beebb6cc9e802e65596603823a70658891f4d5da2d768e5acba89a796f3b8b3034f1a2c01add7ccb7fed20cc7068b76c2738cdc9f4c0cb7b752dba7393173a28f16aafb2f8423dc1c661fe8bfb5339cc45482f1df073b995b7e97f2a6c2a5824adc1dc1cfcdfeb2a64daef86e37da39bc9c8197716f12be4903f3228fbaab02bd7959a5ef295739062e15917703d8fd32f6a2a7c7667561282eb80fdb3fb6b2b123ef2586fd658281d49dca1b320e2b10851631c9b11f42550bf7a0bf737c3005918962545897986fb73a7cfacd6960fdef24f2a08d0d9909425f1b401e0572f6c2d7f7f1b16621e22948e190ffada5611905e724e14031cf80a38fd2431500edd1c857e0d04bfe118ba0e6620899d7e77a2d87d2a74adcca704a1b2c0186526acfe031abc4da8b5acfa7d50b2c69790ebf6c47f70cc3f43c6984704592a4e28316ef07835a8b598db038d52417ec5f81bf7f1b3697f5f1bc45e95a34112300f0b77c2213b2a827c30939cfd9282d84dcaa386c25bee8d7da7ca98b02968b31e1c714facf0d484cf017398d694cc979513571261b7df8c6b2c32a28820004f27df14770753cc82ad82fc23820185c486444867a8080bb57075d9c3cf32e23d1d643fda915e0ad53332c302f5e2804bdc2581874b0104eedc116923256237f41adb04fe2fb7c053b4716f2bbfada9b87cc1b4c22af9b77248e0a26bb622c3521f0e4244c66a7af714d4f840273335d1aa8d2e3159cddfd166e3d567c9268b472dc2d46705f03557130da85c7d69b53de20961e3850b6e8fb8591beaa86d0fd10b765c64e3a18d3a3171538b4a236aa5a0598d8e0678617a8cb90c62c8f941010680b196b8fefd31a1948ffda6faa57093c0e5bdb15ab793644f278ceb0ff43f12090bdafbc4674113de9a445ce1aaa725587a8e3d5aaa1ba49ff4930ec7447e0dbc9bfbdceafd180cfa38214128df7f02b436865f4ab2e5dc449557d5386c301affca4aeac69ec4f8167d8db4ad57d0e4ea4b4e60ac2f62187edfef5efbc7b2009d2829cf84a14526960e56b022d48ede9b144f6440242904da9cacb7f8db180bbed0140d8a53ef6cebbf37bb856bf73bd8fa5317e59044f72aa111b65483550994ca20ca1b0b1236fc724c0863806e5f7382ad322d66b21a8e1756f01ad84504a8c28f257e89a6369320f29a2369a69ea93a8a5afc6781483cbf31c33091a400513cca8308b55905d0cbdb38a453c8db8833cdba8c6ae2ff057fcd60e43a561ddfc683a046f7584dc4e2861eb9fcce302bf211e9693589c6e77a11cccedefb2dd16cf71ac62163775ddb1c3832a7b7ec669461968961ceccb928d387ee35e10a040c3f1cb63e84e1d359e36fe76052c8c44d4c7448e50cdd148dccfd5868a009d8b8e6fb0ca9f7e41ff668cde4ebe82df366177918e950849d8107e9a338c822b015efb43ba64949f8bcb267a544cd2e2485d66cc1525ee4e54f0d3dd0b3221e77b6ca1ad4a95d905c06145fd1a9d2821708e1ae3000e868b4f4707f38af1404d399bb89c3f4d4c4a5730271048b5d96072f147d106ac16cc13f20367e67b8142a20c858c21ee4e5e081ea05813323e4b76020518dd83c4e3eb040aac1ba862a4870e2f07eb53071ef39daa2cd981381a6adc2fe595944b16b8f3f487a870f2dfb23a1114ebbd6a5de16cc1f0fb09af8d174afba1902c520a90117ecb45afc04f60d3d3105d013af7696668a02aa56e31ac9cc3670265bb8a0d2103cfc3d072cc2af914aa524cdbf165a4f3190afc28b5810774e6c6b77669f24c3b35fc3f4119e0006c49411d8ae9918d6c3ff4207bea8185e836c24ba66c4f6c6caea6d341386396dde428fe86014248ef438f5ab688a18dbb82b516e360a706f83e414f119573fe6cc28a0d6d52ec6c133bdfcecfd4e547d43816e6fa064b6d7e93f532825ab2a81e8db45f8efb64ae9972a7cca6a163df9f2033de5a2b79240304f782d2dd93fa04491da05424e25dc8c6cbefcbffae80b6c8e792c0e5f43edc39dd7df23f9d0f14541bafae611a59f5c8aa26e03380b723934162c19a459608d98b347299086598038e2a91c73b7e0782ce39331f6d8512fbe07a7fc7b08c9b98f35082cbe354c1fb739964ac30f61d4b1f71d08a8b6a196574934238a87028427dfec27fc4436566de283beb42dbdc55e87188162e72844fcba35e452d654f06d80840a0f19ab6ecb4bab013f2e50d3069b47664158b0c63fe98f22dda6ce21cc87ad039f328fbc56d4394b0c8d0c42d6acd596912931ed363a6054458ae54789198e2d3bbc4b536f974d0b8bd865e9909e2e5bd297d45b64da2dfd9ae5c9a3a687c208e2a4ff1134488d967ddbc64b3107a9b23e7b7ca83402be13743a67d66c9a5e18418af5a327d3d8ddcd5e15421465c4b889bfa0e31c1126a743aa2f515d3c8b2a2f5598ad35c7bde623ae2bd70be128018bab38332186f3c8ea5b0ed790046421e604996d3d77c0257ce4598a66ffa6c39c755cc330dd0f827298ff230a11502223b2d5017042fb406f1cd386e642521d9f761c3c6d99b16cd03b8253982d331303a9d35dd122d9f49cc010e33c00eb51cf199abdcaf56f8648edea566a574d2e69611d85210c46ad4702245353f3616f899c74847da4456bd3fc09b84d396e234d15ca8b10caf34411fa4d9b16c44c0ae469bdeef548b7ad3a26c772810cf225a4af0997e2f4c9145a0b0e91ab7a1cc519dd271a6c3b58f2bc45c24a790bcb0bebca54fbef6125eb192cb92b1fa3c6bcb0a61c73d1efbd57d138b402953e5100ae1c1e7d107647636bee7a9309e1b1eb66e1cc1faf8ab5e8f77ddc06121a3f0244f4f4304b06453fdc75fa41db3e1b15bc1b04256ae0896b00f3268f67ff511f363f7632cadbbbac7997eb2722ddfc9d59e21c8d842b5251c4d3d76aed8fc50042272b8de59329dff728a309ff3d13bccc8913f2a664e2ba6fd8bdc0d0cbf42d453ece719b816784dc9aebe3e3e9053b6c22d14aabbec84bd9fd670e1ccbe7055be7145f3995da1a2187f9f16cbcd1b3d757624bedbf93346352a131c1840f187b4964ab692aad4d6107b75b2d22cdf9efeb5536c24a0ce84b4a57d30b42262d404e0f3178cbc291d51a96ba7ee2f7e14025059d8ff407aac7d03758f4c4260114dda8a818c704dfd406e98d804128cab1c04f35bb681799ee4a30b656ec202fb62bccc933a1416ad7b85aad49eb3180167ca52842b89ba82ac83b8d6554950ac7cf180f14c8bf1fb3b4362be359524d47bfac31b78affb5dc5c6d3d3026ef181ea0ec299ebdf35bf046ad6e9c45b6f18a4010de317e26295723ba5743fd700d3ad9a07c060be8eaf1c903412063aece07c05331fd2dfa95ef1060d2f601fd08bf69a7b0a6f2a4527320ca4f4969abce4099492c007166abe37419d20826cc1f0a166adb3e36c8f5a8e072a4aff8ca9bec1181beaa2b87e9a0f70f1c18c2db10c751f37041d1362319dcfb04fc5793083903e75cb4b510b777e81fc4ae7e3d27d853dc7a2f500f08e29ec406bd29da17b3c2477fc65aa413c13ae19d66f9b120edca4a240d2c8848d9fe4e33f695538043af26e4f6f3f19fed0f13f01ec1f103eb9cd40eafecb1d2fa1baab9e43ae7d150426827416f64a93865dc1f97b0610eb497f1d8364fdf8cb9a23478d77d66aa4a120752fa843f0c7dd4b37ed3d3d8117864f2c913d928bee9f700024ce3e597abd468289f4ba5acf754817f751a831e80aa16566b4f413037a9b7fd9fdd21400a23091807b764e3010226586b9ab929c21f256d595a95444a4e09cc4e7a5ff59d047ea54260df1d4a7a603a3a18a108984ca89e76f6ffc7369eb3215e572e865e2c05ca1c108dd3e592371a7f7bb626a0dc75a6b3d61c907c9d3f35895b219ba5d50f573e739942737cacfcaef3f70fea9682b9104dae31342cfafc5832621000bc409403687fef64c904c7d4e9a3249c19a2b07c46eb1d0667837580a380a5bf8d4501792b51fdbd8e215e44e307251e8b962cc1c1cbe05e792db1e0d4870bceacd545709283af3a9b5c3e917c690e4cff620d02eda82401c1349026a6a9809c49f899d86499d1c7900523454b7623569eeca508db51e54938b1ff954bd5abfd4b2ab5c7f67e7a11adb60119a07f0bfdb09da284b1450ba5b230fff60c7f4be779eb8d3cde7c0580fb5205f4802e0b8d987dfa249b777f42cd3d213f5945759a31c79211426f0bae9a138c9b90892c8f6912ac582724062ee8b6192acb157553f51f6317fd6ebb50af0e51419e6f25c2fbeb3e2814a21016c8489f37fe016d82b563409565692bedab6449f7095c24e77e51ee36c1d4daf80c9ef363a2f9b3673a0b3da7ae46993667adfabb47d80d6d7a3d15e84d0c42cffbdf6a2e75dc2815f6d5e0a1f64aa7d3dd22a838b11f1f6293522395fc29b60109bbadcf3de1d3a461d75131580ed56675b7648064e12626c930ca131799f18d1a050b8f2bf5e84366b8ae90d4dc77f15e98a84a3a290b13dde5201fa4ba401b2959be9d60793da5cafc8c11cf3e891aea323b5bf15d0e24a049c82597ac0781bab9dd41edfb58268a690625911be4f01c7cb94d934c1b25176d969dfe9b34d6fed67f9ae47d57d833d9af528dbaaf91285ddc45eaf9122dbe16539cf24a0c3dacc44ec98f8d88ab7adcb74bfee1a543c88f4e2e62d022541213eaeef891e4debbe5ea26f90f3474e55099d4b7f5ee86eef7772989fe1d1c25d9420a2e0d332e755af7b835a05e47a493edbd291f92126cf8f1855f5c1a244c568838828c4d494aaf5b88e13acbd8c3d7c4be49b0b3fd8b7ab4c9fb630e8afa9e4bf719bf5632d8910b7fb79b4cbba77f5810c1e6fe7401b5b41b91c61f9a5940657a8bcbafcf34afc0e9a434569a4f1919307a325dd86c8c10e1aa2a133fd94a58aac7c3be622a6f994261eac893484f9aea9826feaf71c375f073520cc58a69a35ceb6dbb2c1c65df95947118cd406728a12eb3854ee662074247b417b5a11b5daf6791ed17986906542a20f94515f08f218a8ab502e1b11e0aac770081d293b47933c806076012139832a8e64423c19a1074e9e0de79531196f2ea0bfcc8bc150670f2be8274e8ae922f2bbfad37eebc9077f98aa4ee75a28f4d7905a64017a72c409e12e9cf6ada08ac13b2dba318d32bd083c672a6c614d0bf9822bf7306835cc4c6de854e0d688c9f9cbb2e1232afd03be44541fa60eaac5c8a1f63971da1ccfa9936179c5019c8ff81a58b59edf5999d04508ed13c89645c6613ab155d18e2619002fd0a72004b09d224cb4148f271591cb2a4426ed904fb2d2212ad06ad516d91c6bf5327f0eb369bce8c8b21e7565d02cea64b2d7f3c34c91a5a0c816b61884489c8b3276e5561350da01cb1a119a6731003f2b17e86c1b30bdc6dc3b26d9151f5431e081c634ea1a9b614d7ae882a25d795af76b912eb841c150e88557ca4b44af991b82f7de755d4194ffd7451cdce97a08e72519e2bda060743c8fdba876bba64d622d3a327b26d7b2afa91a01a50d1d01d185360b8ae0170c51968c9f474009653bd419140cad4617e717a1bfa931648040b94e327da062e27394520550f3306ef2e382620771b6f8fbeee6cd62843a3250439de314019c4b41cc44de9007f07a2e5080ab05ac265f887744c70327792f6049a1fcccb27da87d4ab427b9b4c85af0927600f93fdc236d2289da94542315d39b7bdb768060b03b1d14a9cdc60b1557c6fd8b6bac4542231bca105e635f3281e7c62cc2b1f48c7159380fd0d6216d9554aba836c201b849f2a605040e9121314be4b866d1a2fae8c523bbdff12f569e21fce88ffa46cb9ced93c840921992dea2efd19460757f5c2f64cd24c7715a82de62612724ae70c8ca5d3cc85b63cc9c7fc299ce7233846d0922dcc43f9add1a83bbef63484e773b158ee55a5d30f4578266b1dfa2373becaaff6cf6b7170b4c2085fc791eff1de8917f17c9431bf4cc8ee95f61713c630086899abd3d7f20e0e8268815fd7983d5f3537e1c3c468c2c230cd39f0139087845c94cb7b81f812f650f9380f4cb21477b3a38b803134e9a22c24f67d29dfa788f5121f5f0d9eb5525984578f241189170113154c11b2254cab4bc5341007ddf7059c3331775cfb74f91f6191831e9cb87307dd294081121ffada8aaa073cc372846273c03b391c56748ddf263f87d4a60fc4b995ae8cb2b20221305b1aa0cc91d7da4aab51b807dde0675cc29c484c3c02c3456307b7eeb667a831d523c695ef210c609ba4cba27c99da6e51dd302b8fdae82d6d9f6a9aa396a6c3e30795648859df738869d97752561a1d88841995749366464fdfcbd661586ceadb144bb1217b3c7c2f44ecbe837472b2664dc8332fef90f9510cc2487d88b5927c2433456419d301db11a097d3a25997a3d2eeefbef2a7a8db4f2c7245b8485e932c50919631c4ce6b5ce255fb3a5cc858c3e3544c506117a619f707bc19c3efba1d95b287af15f587081deebabcdfb4c4f908b3b8c8693691b096df4f6dc52a41305543367023d60fd114cc4bf0affd9b5d9ffa07ae31d8c7f25cc412930046350c203cf0ae151b4ca11593001a15458bbce8485d4482fce3d2cc3403c04c77aa2f20209eaf549ebd9cc1a981171ababf3ec2ba1ed8e65ca6551c7788d52d515a0b089e0b1524de80ab0fd091d1214e4d5bdba3565a7b0efcc62d8e0fa9f404dd21f4dec3ff782e6ab2b5ae3ee011ee9d11a0d1a78b1e210366fd5b3f8edc769c2db8fe731d318984acb0ac6f4623ee3da9efdc6f9ff6ff3ba48567db397df4ea289db1c6510d93632c53679ff55101b789440208421573fd0c444df98007f3492ae39f895998fbf498f649ed257cda5f907d6ed197e2fff53a5419db41ac81a50d483775cecb08ae4c0bf98c73fced128605c54cdc366204ea89b06d2334a7ac22c92bd95e7c976ce697aa0c4f2dd1c14f78a292df8a922e8fef5e70f3a71e6e12a3ecba8a005ff5b5f0907cde6addd618f87dc3af2282c7fd224c4b81489e5c21e04a303f14fd115821b8d2c9c0c87ddef37c2f8b75a38706ace5b753a14cd404d0070d8090b5b1a0c558531a7626b40b0793f8ba8222bf81c038a3873d56881446ccea637d62a1649fb1ef247d0a342eb4a80b80e50d9b3bde954cb36da862292fc562e6ea29b4e4540670f6091f8e208f3654dc8775fe7cb2d0396c9098912f3841a820514ab2888d1336989d54d731f343bc117e3e5ea9c44273500bdf4c1ade9f6df986799bdea8d7f971c6914e70d406ca623d827a38c06e8b3824e9792853e50587a4b6d3dd71d34debc7e16ca04ca6b341b156b7393f409ded9f6f2c15cdf3390434653ccc1001e4122760cedec3a26aef2447d1ea5ee1af1035e28e1c6bdfeb3b32736479f04b19a4a1145dc0c458661d5b7d1318bb173d15385635180271dc91dd237698a29c921bd0894c593b7baa8ae6a7acd3611b5d0f4412852d476bb32f265ad24625d3e0682a8287d30a28e80d927243ebd4d31f233fc390176f79482c591fb0dc511b571e2c5f3542d52f32262ce0dd0226d4a0289de5c40550e19c3a7ae98cb29bc08cf3b6839097693797d5ca83fe66d684f44f9e9044232f95842896713cf8d85fadad0cd8292644b1a544e5a1313668f27af4f17cd600b28b913f5decc071740c52d7743edc53cda78277d9aff4e5ee8155c0854b4f135d1f7ac80785ddf791093fd7f722f19b2adcfd954f7aa838f2fe64c22214ce19c24064dc86e1c55333c3c32439bc8e8a71e8124af4078a7e9e50762bd0f4331327baf15995e2559ba95f44067b3e51a2038379b66ca898763030856451c29b2ea529401bcaf41b1b223ef9984d1c50793dfbb0744b871ac7385b1bed63140508a63c8806d470c88868cd900442157edcab4575b1a4a4841dc59e05610a17881163a53193b4f46038ec16d3a2acbfb22a828e1a66624123169cb5a837c2b2011ad3085e87c50e9c2090861347696909b69c5e58b7815ddcd0643ba8878b1e8f036cee3022f1da9114688c17a2ebf421eaafd7361287c4c521181d54cba83b3817308c53918ee0943f330dbbc80cf4cc32b9ba6b6a4c062a8f43b4376f1d14b52559c7ad92155bd042d3c5eebfb3507f296f6d43c94c22498614015eff3695f70e0ff18ee349bdf38f952f695bf514c1df01bf656937c556ac772dd985c440922409ee81e639296fdc8c12260c32447264782affadcc5bf4a79dd22811caab34269b381426ce376540fc588cf3619c022e7eadc42b5922677c22c2ff713ca3cf107db447a08cf0613c5bc706a050c83c06471958fa6469a56d77e3f45e8d3c331a535823cee65ac7c8c4afdb19949f24181d7c7f21e1aa4799980622831538c8151cc5566f52a7b13833b80415eac513ec1c379d80deb19bd856e201a18dfebc2eee4f168918883e7340b94437016cfa000c73edfdcb415138dd4f2e7d1f1b5500564a9eac0cf699202924402233d3cf17fb0cc71c2d36a5756e6c9a66a4d0e82e54d53a42d0f6c6e68b4fc6201138af40afdafdb86dd19c3743f5c1bd756b3a0129e5d2912421adc8599fd2153517102b90b3e61e6057536869bc28db874754062177ae022f1f1b0d740d39449cd7180f6e9e5a1ae0f570ab236b4296d4306c1a3fd3f59866db6bb3853c890f8993b31fadd42ad1c44c9c054f18a41db4facf2a7a8a5a2fc146bd14040dcd9dee96424fcff3a3d942093bb298b8a23f9721ee12abfb3f889e0bf1fdb61379cf4b45319e071b6e2a6d6c37be28099addceef0a0b65e63b0f17706ad47fb2cc73a40de4dfe018a4e3fa93c7b873a2456c35610a1272f488091e34387fd1db5b9e5b3fa673dd169652af9eafd7070bd4e353ce961b0b0f58259760aba424f2216f090ca085bfcce32299bce70cf9b1c4167f08ec2842f0022eeb47eccd16484a32e08299e9f7f6688c3d21d74780c284d98af5d22077f2b2d8792de1960e21ed61e1af6df5948ffb2f92d82cd8d00efae054c6730f75d06e2287eba7416dcefe8bddc2fc68b222de614f5642a8a1787b4bf652411071c6e00b058da7aedceb2923578667f49be6451e2f68336c6a63a3f1c8dbbf2126794ee71fd02d449d1b0febcdfd42713447a8023cd91866df9eb83cb0c84b2ea1de622cb3cf144613d6f7a002d159653c9f96d02de3d21bc23192c9f78f911f2c41152c4066d401b95984d314f961f38b6743f440a3f04550d4d4e150b7110d1b91be8944aa03a26fa598db4a5a0f69e75b533a2207c46bc39c351c9ba7f223fda2324aea7101eafad809321f8415dbab2aff593cb49291b7e8945641d8f425cb352ec3a15786cdc3597cd032670839a4c551ab6574edb82c6e76dcba9333268c043a2982fa0d4317237502c90a1200ee11e53feb3637ec36f551f122e1e50083ae2b39be1cd219b0d78c6958e6b5cfd88996fd748a14e99a6d2fc2962bc114690032e6d81fa9d2ac6aed38b94d92af501ff32c9b3ab5aee0b0e76d84d5cd235088e4efd4d536a88ce0b512f98777f9b0198e44e865876669cd498d3a55ab0dd10534d18582c409a543c9c9f87a00a79e62382382765eba9bef2b1c5e91be0031fd9f3b1fb2d76b661d1d0f7b9a0cd08ad31881809c625913a6eb18fe23f50f119e9782aecc36fee9ac6f56a3b3fe0c5240297a01afc7cb0f0675af297a790755a4ccbcd4f1866e1f561b5f03e8a9448e57528caf7148b9725cac6831ca982740bc1864a832bf1c7df354412f5e31780a951386ff364ea1f01ce880fc974c176e1209efff827a6e99ff93a5626451cbf51c4bc89b2f14e5aa376ad61e9fc22f7fd42b0f4378cd0326b21031805568ac8fd7d64ccf4e872e7cdff021f48f7d162205911801d4a6ac2cd45882dc7e02c465034d976c358a781079831f87a889242de0921ca6e9ec53f785ec9b695b3cd0bb080a6109a2d5158e9d9573bf42ab2c6f3fb93baeb4cb45d84d16ff83ebcf879cacef6a2c47f6eddd47f08cb9649b0b1ab3c4334de9a157252c42ed94d64ce65ab4666fdd06e256489ccef93a9fe5041380b0aba4610c03a7cad4f8d205c8c7477cf65e79edf520e85f0752dc4f9427bc42b9359c1a8498f935dc47fbc843d4fff6eaa3d0eb3a361edfe1e31a10502f073561801dae02a117682fcbd044310886caf0f756bee1388d1c260d1e0dd320cc47a959354d31a31e6f8aa49c74a388f2e6515dfb55b712cfc9e54397d24918b47a2576194194a6efcdc203bb3e67524336a62e288fb251ba07d348954ef92fea075d7e50785d56376bcff1157cc85e2670076c58a9d2ce39cb1be88abbc00413d4ccd164c11e57b2f8c0e122cebd75ad03376dc7da3355b0ccb87dc125a31c3176bfdd507b2fe2eab4a020b0bb85a2e75a40b46f7e25b4b7df5dd0a92ccd01bb041eb5abfce247954c34fa5a4b5cbec62d0cd0860db0f1a6cbf9d5f106e202c3c76759bd8ed2d8b45dee6b1470428729ea03d0dac9a65e9da2048c7f99ce04c3329559fa0290eac1a9a42e1bae853424cfd3fada637d50067aa58d1d4d943015b03c2309ccaca5a209459c4bf2c301e4274b1e3045453a918d8a2e214db618dff718f0e8287927480d5a3ae118be697a50cff71b45211e65eacf0357d8d8084a5a7663313d67403bd7ecb9140c76fbaf2387e480f89302b47bcc31d1e6d7005b0b9e5c5d626e9a6619c4ce5587637b45c44a486f7c62e075adc727eb01cb2213bcb92edf2e81be9f2a49ad512fea35fd981727f3140cc114f24f28e9ce950f6e1d41030ca825b20c02bafa07da5283d31726260659ea0575a470e9f3862612594ae7da751efe342fbf919a5ac503522de63fd4cdb644d7560d435f237f7c22eabffdcda551aacaa33bfbfd7b264b0de538421e87c5b6112d4c10fa837a670872ab3fd883ca6be74ec4a41888ea76efccbda23a48f3876c20007bb5e8fd6608bcf1f22036ccf763f19291926e1ae21061c1381f42bb82af11bc3bb8f34f1d25c566966289b286af824e626be44fa609fb9f8e5c280b11ab25da9eae47547d1134ba4a20c37fa9f9c9119b3f0946765dc30ae229db3828469d37c4af2a3d43255889f4da75691e9009aed63ec762f684754cc16909e6f55c5269ff4ed7626917c8a264883124b54e4c661f42400179867e83256dd6c8b5c70a6eecf33f69fd2e3c04920a72f5f2249b63e1e08881e8273da02e502582e13dbe9d0b5d79b1b226a8698609fc8716a362b03401118b9ffd1a8d4151e25d3aab6864751a3f82db0affa91866f5748099a7a04ecf6c582401622c3aee7b56e9926ed231141c26f1034408199b216c95816741706ac3a241e10725fb30888123fbcd7609ae4cc233041e412d88d21b96c2c899f9d3464d3be3ada75bfa0a939522cb1222f6fd2d201250460ce0f1a7fb9074a0f6fa3b172e262e1724003855416d8485c2d780f07f1195cae1b14cd3e8b70a23cf4667c9913433b4f2ca4488685c0d83f58f24902023ce9091da8bd491e977134e6d24ba3e0d62afba9b9f0c9596ce5e207a75f02e05a488621b6b1b2f82eab998582f6da1b507d69d16de20539f5ea50ca1e89bf3043c3d2bb158ea3be35187e000b92298549448cd4d5b896af4c10ff7c292a9b6234888e4059dd68e03f9d2f6ba50284c55a63594c727efccb615a7dc0336d3641caec9c366f569ff0f2cecb46452264a800c5963aafcc862f4ef840e4839dc1c25c64913bb18e4943f1bca953e7bc4ca7238ffb61f87ccb9b4f411cca65172d82a9b37ff2faf9238ab9a8cd548f01a5c06a2f0c852b173ed8bb2f16e4e49e7491a0841bc464caab75ddb9afac16a3c4c9979a16067072e41ec38ac45384fc7db175ce8432e258234b709460386d667b25e97f32010e41cec2cab1c959c68d23b09d014d1421ec2b3fecd14fa25e9c288f5e508fa5606c89a8678bf87d7b7c938002486328b9b29b627d0f6ebf8a77cb2e8a8038986c07da3a8b718c3fb80602705be98a904bb2f54859cc16434cde0b7e8a6de36b5ec8a6ea22f10f227be5b060f1dcb994fad2f44350307e50d5481ce1a3cde7147806383acb56cf1cb59bd25244acc1d62e14a82954b17b519a1cb03b5d4103beb62feb96e13350c9c7fa5bb07bd3a2c8ea44417d8a935a9e6099406c7b36215c35285a0614326bf40fccab901a9a0fcb2319ffbade7f8d15ced7f3f51f979809c892ac9f6bfd4c9a2a1a5aa04a128248d3cb749415d418a4384da9bebaeca135d0f28ba169e4947a16ca1f52b82b51b5263c99d50552714a9a2fea1a5d0bd680d2fe441f8abe121960053680300e3f56615a750fc1d8e8bbdc1a74feb42de4f686920a55b33c9d37364c1261f9637c6e7ca508cd210914189000700c7299658e9f007bacaa37980f64bd6cb249cc42f44e8f86a0afca5ea0f982453b72208aa9fbb69df5b2d91e1cbcefebc21027e3be6e9bafc8efa9000169e7fdc38fe27e534e786e5b363fd2fb3c706ca24bb1c43e8e4d331239503d6c0820a75a673526b2b4c8e33d810f0cd432caeac2961cae418d33d3f5fc24a83452e897af28d3d2cb3e94d177fd1eccfd030ebc1186974863f6fbeed1d5b16ff8eb4fff18629f25349caeda0f588bee9bf6342c42f5b264f5b6c882fa61c8e9d80c4fd68019c6220c9c04a4fb50fda44c1294f0417e5c37beb768112252becc0cb87f4f7b3668e7acf882e119b83517186944b6402d290f7c84aca9901c8c2e39bf76cdc6e6dc58affb9a31cc2eda3b04ffb86700a2cffaa7330da0604b73af6be18bb8c028958709e814773086372e6f300e1a31df307488514ba7a83b1776aadac1d20c306b12776e8050edc6d21e3cd6e23910a90de67c3d41d621aa294394bfad19c0cade227c268ab93bd9c69fea9d554c4253a34614ea3eb7565f89a7b76c6294edc4359819409b436da27f11206bd097b26c2e4d4e0b551afde56514b47c71407c1b1faf7470958c3cd29ddc98d4d57782f6fafc9c9b673e30ba8387fc5d09b2d6af4fb50260aad9cd5efbc5232f9c79b30612b7f03f037f16a8a5b587aa1556ceacd8c1e3cb7a936a7dc132341ecb5d0067578f8637ba3fdb938deedfe74d89f87891021566578a7f2c238794d73d0571a77b344f2989ebe251e7e1ff9029bd686e1608af6d7c5b275671f67d2083830134ab23a331c5f5d8208748b39f75da694aa518406fcb20273227668c7e598121a8f82dfb5d84f876fba9a258d1a1817968395ee75da396f59abbd3bc8d3143a11a434282eeac18c25ed67af3b2733e69375ecc8b0e604e168f2f6bdf41a4c9f0ba4b7b622754e332144a32a7678575beeffc8cd293964509b5d9bc054a0792e2d967ff97e72a34fcde286456d9a94e248963c6fd50338647d8996d23f27298118e03f44699f28d7ff31f31c5cc651b47df9ec3b9b869ac0bc4f66bef4e5b14c0e186b0227bba2e618527749c1c041b9e4b5c39abb3a77e9328fcbc0cb025d4225f3bb62ce3504f467d07fc5ece3b1ba8a5ad7682a503bc7a6502f37f4f0fe13106355c8bda71d12797aaca34008c3a006fdd066b64da246671b4d0f37152bd4", - "public_inputs_hex": "0x25832c071d6d99ad42c7b885e48b10100726dc0de03c0a779b282c519ccf538a25fc312a5f0009edd084811b739bcbd529987df1cea36f5a24515386662573960000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000c946233fabee579beb961008103a4c8d00000000000000000000000000000000026a297ba8fb72294cced25b49dae7c92208484402633db0cfcb2af11d3b66ff07cd7b6b12c3a35e9581183e399d53f72b942ad53baa7c6e1a7616e4f0aa2eedf7d67eacc63696345525988225847479273fc58215abae6a73503410e8209a542c92bbea2ba8e6fb129c971def4ca8452ddcd2c1e0b08c268d942b52a49e7597873ccad9df1b02ea1ab854e7111eb2dd188f793a89424a83a3e746a72d4cdfd5702a7944deb0fa2ee04f84489f4af3c22f5ffac9352fe35d950a4ce548798309f5d896dd11f304f93d83cdfe284b37a6" + "proof_hex": "0x0000000000000000000000000000000000000000000000038e146b049f5a2ebc0000000000000000000000000000000000000000000000018fbadc502ad14036000000000000000000000000000000000000000000000008a2cf332394158a790000000000000000000000000000000000000000000000000001f710378be6e900000000000000000000000000000000000000000000000c08a0a49dcc1784f700000000000000000000000000000000000000000000000479a1f04a4849464900000000000000000000000000000000000000000000000ecdd4f70261b68b8e0000000000000000000000000000000000000000000000000002bdc67e780e270000000000000000000000000000000000000000000000068c088e96890e815e0000000000000000000000000000000000000000000000019927ec72a8a0dea300000000000000000000000000000000000000000000000bc30ab5296a7e92200000000000000000000000000000000000000000000000000001b1768dcaf05300000000000000000000000000000000000000000000000514324a6e0f591c0a000000000000000000000000000000000000000000000002e31090bcd0a36cca00000000000000000000000000000000000000000000000eee316dba6c9c144300000000000000000000000000000000000000000000000000000fea047711602896aebdb264dac06725b3966345b9e4ef9019cf6b810f3dcc51860c5764920f2014eca6c2f04e7cf3b91e2628946b6a3718640954ab71d723bde7bf502a9d1a2f34db5ecb0806f11af806f3bb030c60338ec88b64ffe73e839f5c5870542b041441320f7b67e34ad210322d2f5fb516ac74a4f27d68106a4b6bb8862f403fea0fde735d225bf57a92f107fcaf3280cbae16c9c7029987ff0a7f4ee7a20274e10bff5a51024341ee9a5663df40a9283f6b04fc17bae3b7bb5a1581785ff55d700c1e06cfb9c565754142bcaa7b5b4cbdfe8d1a68400b427ad5a3859a91572c9a07739d0572ceabbe0d52bd4b99c0f620ca8a5321b1b804a8a139818f04a251b12a1ddf209a0d7b853ec134beb579539c5f1c5c12272c86207ade7f36723a0c1808564b49f500341dc967a931ef95cdf14fe2d6e213f59a4c08241a0a630c0a20094d9edbb54cc50d8085bd1ae3f24c32b350465caa4ee2c1f3f0e98d99ef1f280d566dbffac05a37803f1edd0b55a4915e64dd5cc8040dfa02d698b48ed45c2b0512682837918095625fc2f6808d921af9f7809b5769a1d571d5ae35c1ecedd22eb10c42bf1697e343247de4678b4739d8468c15ff49d0789daad865c2a4dfd520a710c69acd71d2e39b1b66bca361a77383686b095267d483e9a65a7106fbc129b9018e43e5304f0560ef448d298fd8c68d0bbfa7f415422f83b3a7c0c1149d100a6294b4a17081217254d550f784ac02a667e1d23540040f976e34a6d2059b0ee597f3b9db445f113f30e5498a99ff0bbc642ec6ec2902fcf42cfc0dc9c8ce290fc0e267a0be0f576ff0af58e4a68a01ccc44047c891eff4f54421844362872cc5f85a0b7a4a2c5683e6927c0129ebf5aabd8df471e02ae39565e11f3ce0831c28465d49309ddf5bce3974f68fefaa445b99e5aec1d308991ef090362bc9611951db034f99dd86a24366d10c24747aa78338a374f51595dec1de2f4e35604f0fda9c6dc2f4a1d965603db25df75368cfdcaf4a4e5c8defa96b67e839008bf02d308407c26a577d97b478ea360252b08fae8d535c3c54649a262d2c9725450e1db0494dbc8db5b571cb6e856c23cbd330154826c02ce5ba29b4bb2720ae713e0e751541f9b404bc211cacc9691d3c0917a149cb51e4ac5ddd944b7d1bc9668d2bc5615a7d472255e69005a613c4f949a3bab81819ae67ec962bbcc2635ab94e1dce72569ae638d012c6ca6cf21634eb535f80177106ee176eea6d1a4c3c3f032e3839c2b0f5c9165b77e0af36691c7451341d88be0a519f7a87b72170406ea908f56f676ef12fdbd5cdd673d77a9fe1106d29f5be7ab396ef83fc3cdbbf70152b69c3d9cf3c2da2f6e32224297dfd0d86e41bcbd3a04343c5c05547f1fa94412933a7e458060e19ce1faf9db3c9ca6e069a30c1c4ee3368ea06f3f29022bfdb118c7245fc64000f57ffdb78ef63bc87ee99108ec5ae0f3b79c18eb49a5a87e8174a8fdb7cacc0af74eb2668d45b1d999a92c3ac7234b0c504b22981c0f44fce2d45cd8817e21eea6a471a53ceb7aad4e2f20efe82889e70284075c63df31f1802f49619073b093b611c5d89f0b69bbe6510cd5c0098862491d94c229b9fefeb301a1359c653d0d5a264cc62a8ae6c08b988e588ea96f11355bdf497f26207530a887415ad40f0da37e84607ef267fef2a4392874e3e035db64ed8b4e804a5bf27ca07f83779e8ae2cf14654363fe4f88f25093ed819e433a9510367bea91d9f2db1e26383a4c8a0df489007f12ea00f91d6d00a39eef342cfce4049cb1005331a81e08d651c53177fee573a87fb0652bf0f8b6ae2eabdab0cd53a222a97bb1f2599e409604b872622a24d9f0eda3e56a7cc78ec9780f28a329e531e56b416540277caf2f5367e0c6ab8463bb850496fb67960433bd6fbfdc0f384b3734ce64902f8aaa512adc680bf8b4ee6e8805006c28ac3fe58625d3eff6c55ed1418b2d80af9a08acd5b9cb6bc4dc70c131282f2aaa7b77da31a88736f45781f136f23841e512e5a081aa544a6d5b4b7da8fcfb871abc7c2aeffb7d5c5f36c3a47ef542d0a2f0e77853444ef1bd396138b4692c68bcbc71b49273d6b85c43bd3d636d8171f9b8f9e1a6cce6b155ca1b8941e29f9384fb8675df1b925fe9d049934b57d2e0344065e99856c0f58560c7b61969d8f8a1b22185d7cee49e155dc41eeddaf392055b9fbff12a248c53aa009b31d8d898fa411dc0ae2f8832e8286f7a2502af118c76caca54feb76a5660e2e925f0a2fc3e76a09df485fad25d6454f2078345d247e5986d23f8130dde014cc70ce4d6600102584d37d2ae1ee3339cca8ebaacd1b0bc9c2cfeba8bf0d26baf663766f1a41511b6643f268b8a4b18e72dfc133af07299dd37a8e816a3301dfeb7186b727fcf0d937e36aedf38061ec699e43aa9622ba36764425a1f2d8e63cf07b25d5eea48a81658ceafff1eb99284fd40ba0ea0e8070b0f18b508f8dc8b73ad007a52a97b95997da9c88e51d725fb0322dfcb52b15678857422cd69a0a38d0b739c4640cb3a65bb1387a18b9c597802a7008b307335192168f583e82957952e50c1bb0872a59617318249d8f63aa6db33db7d90fa9601322e78210e75087f0f74d98f6de55c577d149bb5323f10274cc323a571e1a58d0bd353b5cea492d943f2b9ce4a7c1c597290edf32132364b69af83381298d05687cd395bfa6845e299ba59cfc67314a07cc4347892ef618d0f36a3fb62442db09f8bd01600b9d4797b1a1855cdd9c2fcbe56ddcf7aa3c9a6f7808d7d602c5ecf4a2882da2d6b21cb057b030bf46f1c58e6177a97ba2aed6b67bdbf42715a3daede130d745cf8524e663319b106661a34762cedd498bc1f8670ea78a042158e87a0661db99505ee3a6aae62d8ca251044674fde2521bbe3dd236f6c35a2d7f3bed582584f5791362da4c8b52a5c85a7d9d698e0d2ca59ea2fc4aea21c2256ee0cbe2b992e43e977d082c1f7979c49189644d6e4f56a293347de835599703e0ada4a22af6d5fde5b44821ad479e26d73e1af7994d26d04794cbeb8ec61c05d6afa779be840e98bd1c3ed40836c5741c99fc55edb8f7203823f6a50dd44e19c061d28ad101a955c8358078cd5490855af88af9eb62704daeea6382ddc35400d11c2b43f4373b045010f285bd78c8912d74b74f7d294126ac5e72155dcea405e9031605c04becdb06f8b84e244e6baad4146c7dab4572f9d1d4ba038d452018a742401211aea375ff3f2b56acfdb64639a433002aca37cf20cebb3be5c60715ef324c76ea04192181fa5121703321e84713478c02f63a9192f50067ae13421931e93801854687d8e60ddd847100095fcd9bb2ede66d370e647fe4f0d7fc3a2f363c468eef65a3793cd4bfb3e56d79ab68a3b09be67ab2bc3c2da6cf38eb631310185da9dfb841b5467e8183bc21132f88582589c25856117e167d80dd692c076cbda0ce6d6d2d5dcfd89311658456916c62de398c5638e43a843366d817cc0ddc7d7de125fc1c474608004580282688063f5aca6f06690dfc491a8ae9c5c02ecb8837abd0b8af52d74854a8f15b57dad2ce4346c3a57b9f16e8708730a6cb064b6fe9fb0089a65dd35b6aa970b411c78f7b2aa5b9400d87fcc1acacb3e369171c47f3230a9f6a371d420d11eafa30fd4658145178f4dcf5c28c34bb19e1a123b681a4887939e0a351aaf8fe25680434596806ca11ea526051c082247bb87a209e2e9c586721be984a8f864c9d3be80c8c7523c21113e1b18b3b706aa7d8c21d657c054d8769747e011d716dddfbefdc0cc7fa8d471e1c58ca31cf89c1d196218dc8b201131c3a34389ce3fee2ec9ed61ff3959c451e5a0f020c8033efcdfb0b5bcf51236b0b6541029662b8a3c50376f053a85124291f173411ced41748f009c22127832ab9f36659c9c87f116365b90af9e2392f5e92086b801e6fe6376a2ac461be885eea8608ddbfa4db732feb4e936819e6446f9697249b38e1f716292348b3287168eb24314e2425373dd6756868bf66717995f106cd9c871a5a68572cee61ede06b11f37d231a609bea864675dec910763e710754c461b778f906bd095df757103c44f4bdebdbc1409647a6892f67ceadc6bad48589824e199226432fafb8eda60a1202f598713bcfcb94681048b4980b98760455343316b5b35a9b295941a67f7b4ac4f1a24c8b3ed045247feb50d3670a5a918355ad64e5cf7e302775f423dd2acf738d94430a22803d157ac60ae399707c08adcec7d41e03d118052a31d8a16b8a3aef7c9830df6815906f0440531b33a10571f1ed27d84cb75510495df8efcb714197c94f8c9fd53b198efe93334064d84b5c6c22c822c643d02c3a053da66d5c4cc774b5fd04740d89c3839be5157aa9ad2d2169bdecfb020c002199232930c8eb7fd2e59c70ca689f838a282b03d2372e35f3b52fb332d8e7118f60a499489c36626769c99700537dd8f5e80422d92dd0683b5efc3f21690e288089688a1421854d4653247bb20bed1dccd20b56c9dc4daa52cab2a984fa03024b5ad9af64d50703a4de10752bf3a82f835938640c7ab80e0a0190b0a2261d2ab6a42d129566586bf0db21334d4336c2754b368cd5ce3bce6196007c51a2b52cb2a7f29f13b502784ba00d49726cdd8f2315488c2511acb7f64acb5e86a6fd2506d1d03a141d4247fee5336d6210577a55cb80c60607c3ddf33d9e3ca1c46f0d62d50ce69948edfbb234bbf9216a1aff75647b311f859620385c0acaa4193f2e5f309fabc40436ac440f39da8db73698e0a85edafc0836ff33e234321278352394536d28d050f56da00b11fbb40fd5f8a4566c72be8e5ecfacaa7d38c2cf3e0f9e2384d56265c064dc420fdbc6a73f8c0e6e04b644e9cbc61df97d88e9c529145184b011f9eb91fc5096842a3aaec5b6caee0899dbc249e19b2d0a176d35b82cb08a43f16b3b82da140b313618120d6906492df6fe2f189703dcb70023b64c28fe97dc5ee19d42a34cc5e7abadd24ffd49c250e34a4741c8956e6616ec6efe20ae8c503e52ce778350c20b65b6f09a5051a7e95694565ea4bf3cf1f7a93dba00e28eaf5dbcae67ebfdba29b7068b913ea43126ce9a9a39170ce0e05aff43bc00119d5e9e43798de0b003442bbf4b3e07af206186e1a96eb324bb77eb0f8c242b4d00117d41cb40900378b7235ea285c4f1dd4cc09843501eb4e301e1a7fe7606f17485221a20da729dca07ec410db76f9c69f7b723e209e79a605b5fbfdc431f6420d8788a71c1af68e3f2d8a68768e632346dd1cd22d6a4a2c6268c6224810247c6cc0c8cd333a78b736a02f0d029e760f7f68589298a4919b407290a97dd2083d9af9cb142c55efde8b4c69193ac087c976a0dee9ee7b5f2630707a1632209af5ed7b86d5f08d8abcde1dc1ff387ad7a31e9388ca089e2f5f8340d54aeb0232f5ad00ce41cec1f6647b1edd63ccd207531485b06b8874ac25d769e17b22c1c8f6d4b1b5251c23e315c2f0a6b18b5521fe3ffb7fc36c156e935e34a3d918c2ab04f3bbdcb1307886b001258e4ca0efe37f22a80fbc035788ea7704d8681a4277cf318460ff17c8f2790f78a22a05264e33715a085c7429e7181944af22fb329334c5f59fed35528ec577f296de352adb6d95822e85dd1493d81be726aaef9165c3501ed7bc205db385083ad76207c9f586daae7f8c78b4ce4eb3de59338c426c7fb9191c9f897feec6174af661fd70b303f6f50a23266aa7cee73e15d320a0cad09345c02e98c86f16b6b80b94c18edf982583b4f96e047c1fbcc45c4205618b64c24cd8c14c082dc62b338b3c048a4293481ee0bef71dd3cdadd40fbd4f419a02b9f28c205241b18241d5e117306e89dd23a884a3fc54cb8079a924bebbe11deebc2e717ffce26862b8b7804bdd09db9704b0623a397c429054666741d2302640a3ab0551496af654bf68fd032082432e7e8596aefae1ed73ec7546b6fe41f13b641fdaa7396a87890b29dd70425b2c60daf38e4fac39375c7da128620a42cb6b472caa894b66488e4056e20adee4b5051750d58f3c8f9e21e5f69dcb03f0eac9e69b653a0485052211d6495de5aa9b499a123aad61eb126d430172d07e2090d8abf78ae916581c51b2e3abfa8c68740a7dd7f47dc90d8ea573a6460059a273621d0211dc09cf4274902a2c2681eb4d62fbb27ffdc5483473734e87e78621e05869d22c3adc90ced561fa5d5b023d494c41c392a690b001dedbda3dd7fdd20f479ce52973dba498a97f2358bb08cbd67c571c90d6cdc69a1fee0ad59de442bfb9bcdbd21e349693af3522519fb8c32f21a588e1f426b1d95683e4446ef340e9b664df02bf2b8ff0ad4148f50d517ff24acb4b9b33a01c3e9e0d13ebc789c0b4686f8844ffc6f638b7a90dc67bbe0a6b00d0174f3135d48eedab7fddaaf4c165a4460fca15426dde3b0718d64c4697d296912edc702e1f5258f0d26939ff4220fb00ff5792b4efb016789b009d6812f1c81d5a4dd82dbee54d9864a15068121598b4701b74147dce1853d2ce745710ef36f85432d5bee18b5fca0662be8bd0a622e484d95516e6a425a4919a4fc52c8764d106b4bcb6242dea3a8b3cb8b58306313fc37588ec9f6aa94c22ab4151908b675806ad6c9a95df02143ddc913ef054bf6e35f383708a2d5259d0a2336ddbc2a4ef93772e289a8a1710c78898caf0dc63980253765e3ec76174601394171065781698d149637a81dbf295fc6fb80020d85ea2aadf1f6b2d34f425aaf14d4a7b2d32afcd5eafa3013f9f76d12d0e70ca208763a96a78410d65d925326ac2a85f000eb77f77517672807b5b279bbba1914c7955fbdd9ba738021e16940413339f9fc19702af01758a8964a3648eb5f2b38cb8f8170de80e05194bae8de9fc3fc98e7d99456bf7ed4e9280dcd188a2909e5b07d0f283c47d1f7f9f0703a70c3d068d48040b976324abea0d70af38dad20fd871e487c34122fb7fbc2e808ce4923f17beea6b0c78baf481c0e7a7782e6301d886ffd464f271910ff70eae27784767ee6cad1b7c706568b8448f2ae1db00007db90b02058d7c9a26e29392545cc731e629682b34cc76007a0e2c672503c02b62b0a2861aa8b1541eafe096469fd0fc7671db638eac0d7a3c861af4e041b06358dd08a1d1fabf41e2caf56c93619dcaa1c152f84f123b433b5de6f526d652dfdbb3cf6ca3b495bf4e119d3192744aaa057e59b25c4dbcbcb1551531c925a1fed585cbba1e3e4832dbe93ef798ea3104aed3c15ebb35567a1b0fba49c080f1b259db8e480c19b86f74d3f12242ad8ddf276551b5579f889a925699391618b2fc3fb82abb5dabc4b164696a158efa88188d644926aa14601c5e62ef22038b12fe800ae4bbd980802c5321da39fa532fb6546a3996cd3d7c89e3a22e390bba90555e59695339ceeaa217f88e74826783c3a1efa8de6714f2275cd9067e427bd21bf502ff800c2cdef2d00ff1baab89b91b68c2c0541964433c87a099d90ddbe0a8798761cd55fee0f4d5ff34e9a58ba87decf37f3cf420687bf0371171ca8252f6e8b2f4617c0fa96b9c65542f0cb40662789e0f2e00b7f05db17c528cfe216108f9162d4eefd554a028a1db73813040a371fad8c790996a7084a20eaf0e67507873b7fa254aa0638156ce5e8b739179fc965a19dffd739a07b3a1a6a71bfb42c1b02e90a38270855f423d40e83bfa56b31993377625174ab859dd3a66a2c880c1aa42a078a47519c83c3718af991f3475f0951012ec4643a53495feb2f534715a02251c6da883227283e90750f5c68b6d14a712291ad78b16e8663ba0ada3d25809bd3d1c6bf98b92f28bc97921c7b6cf346f57797a2f7d8355f8247f29fcf1e70b2e59b61d4a90e90204e3e843a475f1525a80f27f2c139aa440bee68db3b1b272f57c8719c8d01e533e12e6d01c477823c46343278281d910d26a6c4f3c32f8a20f59a9785a883d74bfd8f621578d57a4b149b01eb7cfb4b6918dcb7b68f139b1ba1b7a052b79be15a40085bf9b91ec9937f78e54d385f2a09a747a7138f2e909b486a842c9943fac57c89900e32d4c0eccd701f3d439fe00aa9bf910d2827178724f0614e8084a6c472f74c0289623b10ba0edf264e88f45dc9d80079cb17ba5e3ece6e6c3c76f57b6b4459f8536cbb511054face7d1636809a52d7904d0bad02e0ef3e5992417d9c6c356646d4ae6f8ef5ca2f8b45d00d22e7f3f43a321d402a81c0794c86c6cbcc0b8f0af9cdf907836ecc4dcc159ac76e7e9b2d4ab90476fefeceb4acff387ae5e1eb6abe96c97be936ff5523f2b2767e3e3b1f0d4128ba97d019c749862c304731f9462a2d22583235f00df1f812f7fe18ba6afd24260768907290435becbac3106b72c5289bda8f55ac47bc0b45a0337cef3f77f51faac7cc16a64e8d42db7fe0de579a842761f013e5c1ac835ec7a7a4d5a1c50e061863ba5f8315e1d829c7c1f0c507a69075f5c5d39c659a49f0e766abbf080711f8623621c44771dc91147349f6208f5578b294a9ebb3fc3ed64da4c19c98982ef5d818424ad862d1f5dbd75d9200c40ae912792a93633ca6fbf0bb199000da10ca86481b98e624f81d5c0a4acb3bc1abc06c5d113683ec1c40e345ec9ac66607bb8da713163665fd11d4c11bc6ea47bf2647ba10234b4f4e7a20c32f8ced0d125d027030726358be036baa138cbef9ad966d57399ed14cce73aa45921d5254099bd00d5f347f292bae143b759c9448fcbcca7ea6cbee944f8a00fc8aeceae222362079175d66ee0c74e798127385d4e7ab0103a050e05e16345d82f4e3f98b1ac557f7a3dc1a6d7fda9a7b5ac1ed91ed23fbaa48afe8376e6b3c52e0c40bd10574bcd3a407a0a700420cf3ff6bf4e9d2c92603b0e3ba2fe76a86601f5b58b32b1d5c029f31a366f0a64a899988644c135db3aabe1188c1937e0e67d43784cb259c6df89d2678b2eda7d8f7c59d3742b405fc7dcbe70eb947e80cdabfe853cd0ab21de8f0cd93b0e975f77dfa85612421e01fb87848c26875a6423b34de9a820d6ca2314c7d276f56fa815f374af9a3c15ade27bfd6c74a94d8df257ec68dc401969ab545a44962b4480d7787827e9220791ccfab4ec5ed310f331ac84d41d620b975acd3d6d7244e0c9bb86cc0a45d5b86e6a8e0a8387020337602fb9664fa1f42a5e9bace6f4a0adf7a680459ac831e9148dd067aec1e6247b703bf3fd541262169439bbbced69403ebe737c5b1bf602ec254585f3533875af458d75f4b6b04904c93b04484c354815a73fa7a2f84c69b33822bd91db96ba88c06b4e7083f11ad2f5ee09720c05014344b0a80575011bfc838f5691b8401628a0bf8f137e10ad62be00adddc2e69fdc00dac0bba8a8dd99116dd8c0475879304f478e4d0b419780617665f19a173d472938bde6fc5443a221a8bb6ae4fce38148cb97e9a3c2faca00d80752a8daf6e02a7af2b014f0ad3d05f16ad4cb3155fc320b777805b1e7a4de59fd26fec3113ec4ff03fe8cace4e8a5c352032020155f6df973172802d4120638812c9ee71880c4e97cb48d2e1cddc2777d093912a2b6ee71de7d54e12e877b827611ca16f81f1dc67a480230e997c5b8437c9b4a99997b227bc034216731efbe60e9f9b3fc4a0f877a973aed8e2a29baf5de5b7a63406987c15e0670464c90090c236014d4b77d3455b4d09048f5123409f32d104cb7f6d59f3a2550f13d808471f826a1338c92e579bd02cc4f12e97833c52547dc31c1c207fb3f7024ca9687a18b57a64626be3d6c34563e9bf612404c60dff70ca16804759818329d8f0fae167bb6649e51caa8831278645f950c16888e24ececc5378976da9c82c894e200a8ef1fd2dc188d41e70e336ea373cf1ef81751147d768bf50c2aad418eb384eb68613754260e9d2bea84c850924d171bbda4c856a78b74f4bf9e5242bebf28c2c3c53437b81528c77b5e14145392417ac24b77b634ea8709293dffc03b4d4d57e67ce15a550d6d3003bc839d1beadcddd0abc5157f06c3542ecf35a030ef12b79dd6dbd1f86bafdef9613f63ad08a03cf7978dbcbcb9ee67aff50230c810be573ac148f794c0650424b0e7992692d96f482481aa5638d16c88e31812f60dcb63c920e69f88d14ff0d18d076f8fd0faa93c4b8c2bbc6f41fa2c0857f2a656284d2218612e128640002e4af05c6be31686edf9974791630d4ae3197b8211e31a04979b215d23a9834258fe87c12d45d77944db3606cdfbabdd010a05c0d3e23bc252d9cd18a0cdf2d9cc94865bfd0a803a4ec27de80a01ffa5dee9b9e0ca42cb9c506e92f339d0f1504199b9e624a71e581ea1e531e48659f75e7ed992c62e94ad931b28ae7495f5fec6b73eb59707c8cdf58149dc4ac231f7b2b770911fe287c9f702d993205d349896c2a4e491b9a6c5f923c6195ef8dd2d064cce0249c1753b3c38b42b2e67c8e75d0eba600108c3c400fbbbc2da699a5fb635a0707212d96fea2841bdb3a184a7bee263731ef6f4184fc903ae392c4012977f4e32b41f65e453c9892c2f685f43636eecf49d451735342f95e842551c9f2817dd104b05865b8c0e3fca15f8c9f236b8ac32287d9b79f1a2b2a978641c76bf86e1a280d8af6b20b402423863ffad8270c35656613c8eaa25d2bddf64ed473508f442a0ddcaf8fb65ff255b991762c99df15a0f3e406b47e367a5290e9a93502cda808f5003de148bdafa621f1056d271f7d5b3d9ed3b3564f7ca2321bcf5e9f728e15e13d7b117abfc92f42a4ab68c260e37f27187224246a90a2040d0e133ceb3b194a4dfe23338883d6cedf803623c9e7e319320fa414208a7e9e083e334e7a102ffaf3053c5ec4dbc1afc1fd0a1e2ec41fb236993d167299e4ac66fb39a6ff7b2dddf7de35183aa28dbd3e7f55085df1e85823b7a2a1415afa97ce1fba342d0c1b76ce8c291da4eab121b9bf1ed337fc3eaa48c0519f81beb378a21cf30de0211a36c491e17075f9d390f74af0c916b4b72a52cca839a8ec77453f5069a11ee606a0a221273d418f367330a250f685df6535c0f011535a661f5e0329507faba7086cb2591cb49bb2d7f9f36cb318bcb71b3a0c36a764de9159da2b359b42bfa40c2be677dbac3cbf3257f23add29381424b2c501161f347275a0eeaabfe55c84297b2d313b2aefea9bea73663caf91539278db07b06b54ade556bc5f863552870016e61cddec04e209f65b23a294dec5f51ada9a71877524071a6ea37e323abf10477b2c9ed66ea8cdd54cea94061109bb65d0c735cc408c908a3f03d4082a2208a7fc84c0a72caf1ffc0e6ed1e76508a1b12281c8c2c9c689539c18df34d9110b40a9a11243c885df0264afe639e088f4eb0b58e37f66646193e462e31328bb219dab1e270ed096487efaaeffea549d1155f6880c13af3e2cb5b0911d2a464a01415ef211740de758492628c66e9dd91e7f4e34764d67f6752df3b1d916f8d2145e0e5f0f89f34315af664917caa889c19aea42b0d33dc42316513ad09688731a9d0847762e6e23049166ec03a3806de98ca8f294bb1d5a048d4edcf28e099f109305e145b3d4eeb96745bbccf43a9539ee56cf4e98272ab0748490a629e802252bbfe3be8fe6b8385583cc012128fe518bcaa30542f39f988b9fb67442b0fb0e7504643c613e4d19af19ac28c4c5836e764648abd2e35ffa5686349d7809c70ce07a4620bc5fd09ebb0ceeab9325bab28cccbbef4dd184df360140a1d4615c202b344fe389b9dc87b319c09e0dbb55a2e43baeb87f1436120c1252986972141ba8ecc47065d5ec5f8cb112daf5090184e5feb2a314be75e75d274315c55af2213ffd564c562bc0cca937ecbdc9f30ff98354721e0dbf5cde8c067ce35cd6851ee308e251e86c55bba859435f09869c38758ea576ece6900fa2b8be2f89abd908ff57ade95004643698340099abbfd7e03c84b0318c007f4f05278d99644dd92a8c2dcc238e60a4dbf7d3d55c9944a71904123e26ffa3fb020ee020881ec78e002a17629d2c4c3964192849fda0e6adb70e34bc84f1467ebb9e22b1c565fe9e01bd5f58733c2da2eba1144d50afec878d11547b4b6c50bafd213a7dfea0081c28601df6765d0e4ee2c62ea450f53ce44526df86f32d1defd826168a72a6b920213190556ae50722f3880e4af3b1edd283ca0350c6a283fdf00ad92bdaa18bf2157c350f6ee718a1a3cde639733fc2349d24c78270fdc21200b686dad3608802168371758ded5ccc436d431141f79d62b38cef19f5a47555da701ed8ae18231825b7c906ce453bb755d3bc1676eec754c0dab65f27c40e59a55398dc0fba641a0892b81895428beefb7ed284886e138b82014911fb433cd864aa73aa714613402189ecfbeeb18f9b16b55c507adb769f2c4c5f835c7648df6b5262fb07fd0a2817fe3da73410af4651d7a63b68e23928fd166e3f267fce54cf78292a56ab39e20fb21d359fa63fb3e1ec851eca417207415d1a31c3d555e7f37ecf615cfc28880368029a43fb583cc9ec4a17a734ae0a8049756ee5ff77ff74f8111ec484ae801983d6932fb35c81a7b46486eba0913f01076f46d206f3a489a23d140886dd571cbda8df5b840b54f0a7d916fba3a2a113386c7dbe0f549148d3e707d9a9d6b6072a9b6033b5be4c53b68aa9f7b477c40091461f2e2f02e16d4415d7a2ecd09024f69b28b9794c45f96282ae002d52b4a8220df57fefd31e37c19c2fd036b7ff2cb77fa1ccfc2c67146766aa5f35a98f4dcd212d7279c899818285fbb9b83e6c259c7a955c3493e431b5bbde4227647df6a83c86e3769f1f0d9c89e2c5a480e91738e2dc7e605d15367ea67ce4c71cc11f99bc44eb2182437c564e6773e46a9a0d5a77619a1a6331ecf0ff39d75636169e1c0b4e38fcfd6ed67c848b93022faf1d8e51615c0774abeea8a79ffc4d2caec987768a90d93237d016e7713eb8118316c1bc2b8b9ccc7aa4faf4a50be342596ad7b4c8edc8ed7fe5a7ff5f5939591900df5a5284d15388b073a0af365eb1c24592b64b1c82d038ed563e553092e6d7048e132898602a07c0f960b2570b85d5fa2e1b4decabecfccdf1112b8b861bd626aa202c37ebec7772441b5f603bc932a664de98260e9d49ccb0755d9c7d822428b6d3ed257e5ac8ea166103154e36c20aa5f22c7939d0c03ddc9792cde75d4826794267b7ce2691bcf03f39f3d22b0880db9d479b70844c8a8a77fa1c60907e14d06a5afc5131c18866c61c265c54a18724f68c19c8b82709b1bf888b5c73292ba7cc7276f19d4202234cf42211e8469e760aa814e2c294e8c2382e7e99294b127ee3cc14a3c6cfe5134b9a6bade9114d816f61113f52b41198c21ff59a6c6820d1486a96ba8b765442a01b6038a1fd73d5ffaa332b3e550ee414c45e231d0e2d1423d06f425b2ce5df3a31e84885399841165521a56b4eeb752ff05e0c40310e9ce5f10b9e04a82d4b4a8ded7f583b45545e1f9d1c968ebf17db91821b9c873011955b2e280d34e3438df5326ad21c9f1ed98d6381eafec7d79a0080d0acab0b34e2d1daaf718033987daa5b33ac16ca645236f57a240a68b56bb18876f6592b1ed337f1e257d929a91f65e86638d0acab0b6888283c291fe9ef9e954ea08510e39719e12227711908eab00635ef7924f1faa04716e4a34d4c8807bedb8997256608e44ac98289e2fcd9b02c0b0ab0f8052577ff841ff5e5b79488f0b7fd432ca2ca5e920866c51eeaccec44e4058cb5117f0616d9d116aa4fc7022499ee1b10b9f5fc832077a33189c4aadd1556c83f06b77f925691ccc5686fb376c6c49a17937868fb780b8e3f922a603dcff6f04c6dcecb61c049f57ab204b26d9ede8e28d4ac29a8bb31d996cb32fa7ecc34188e070a5aabdd6cf44e901df3fe0324cd0b4d45d3abfe6f26bf5518fedaded225a1035af7ce5718cfcc45f680f1cdd5462201443bbb8b3c7a170e5a6c149204fc0aaf929c596dc73e0892cc48491a58cf1be03855cc91be6d8bf06a7fa80feddce1fd960863852ecfb7cdc46a7b286f0c29a51bf98b25b04881d1a0212e05e6969f39fa4b7952e2413bcbf31fcd2734362db6cd58cc76bf3b72c19014acff4c67ef4cdc24684f1320d63c269a189be80521208b163f2041974ed84a52863c756b994eaf8742f40c82c0cffffc9181184626865e3d548de611cb99f1a9b17e2b56f32db2f9191c07a9a604cbe44c3ad0182de47bd199a0fb45ee54a0822cd8e96d3557e8038a3d6e349979edd20261bfb92ff749ce13423317b1f5c5ea4098f22e55539d2ddb4aa1ba6629460f46976276157df52e4c2296fae7f02811a91b132aa57ebbbfcd9fefd8d553ed1a0214e0a21bf873377ecb470024fb9ecfed2e815120d5d428ca9f0b05de050081fe146e7d02a448638f8bcfea0900ff7c9f3708147674b69b2c7ca201efdf77cbb1318a2f1bde19219ba1ed0a9d8e938573fc5f080b86b7b9806980f26e202cde0805f99f2632811d2237127a1461fb0a2e7f96966c3217f6cef17878964d4858dcb3d8d40e779558453eee9fc19844fce26ad920446fbb4196dbf31525e5453e2dbf01a61cc54ed7971d0096ba4dd210313a41b3b826ddcec371851e8c1f65505a210e3e2c3916f5eafc1c6d84ee035d17bec092aca97468727448cb5af027e190a40a00", + "public_inputs_hex": "0x25832c071d6d99ad42c7b885e48b10100726dc0de03c0a779b282c519ccf538a25fc312a5f0009edd084811b739bcbd529987df1cea36f5a24515386662573960000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000cb80a0dfbeaeb79af355a70d8f19855c00000000000000000000000000000000915681ec51465d5a3404f61907428b712208484402633db0cfcb2af11d3b66ff07cd7b6b12c3a35e9581183e399d53f714ae98e2b0527277d74d7898e5a1d76041a613ee74f2db890bd6db2fd3a76e9d30329528532c70174aa6a80f652ef9fb86217d722ac4e756dac8ec699873be87009de2f6efc0e2420bebb740d52be9795045be2c08d2f61c665fd8c2946f25a70037344482bad1ff4c46d1ed3887ee2f37d4bc2dd3a1c34c3c6a0b0d75d4fa63077e46b34c8c83b27e71c711f27ad90e03f985d92330211d3eb4c6c88abb18ae" }, "decryption_aggregator": { - "proof_hex": "0x0000000000000000000000000000000000000000000000046462eded1bbde4bf000000000000000000000000000000000000000000000009f2fdc2ae91c95310000000000000000000000000000000000000000000000000b2ea26392102492c00000000000000000000000000000000000000000000000000004afc85764e79000000000000000000000000000000000000000000000004c469c087756779e800000000000000000000000000000000000000000000000d3dc12dc70ceba4e500000000000000000000000000000000000000000000000d09dfd159e66bd2360000000000000000000000000000000000000000000000000001d0742afdbf00000000000000000000000000000000000000000000000009104c8f5d91fa2bf500000000000000000000000000000000000000000000000836b1660e0992fc3c0000000000000000000000000000000000000000000000031380efa2b7ff3ce80000000000000000000000000000000000000000000000000001d837d5ff0a5500000000000000000000000000000000000000000000000db4b2385e73fbe68b00000000000000000000000000000000000000000000000e4f7388c73406d61700000000000000000000000000000000000000000000000e8623e231e09b7f3a000000000000000000000000000000000000000000000000000132c5f9251f0e1f3f0033a84e44de526080bdfdef3d2c4e9be76761bf24b2d0e31ac487ab12b12f3b1b559ce6d9ce7c97b5f31aae0e6529acf2d329858733b8c0215b3e3314c51eb174b690f7d4f832df6574dd9e4bfdd41cc47539b0301c66a88984cc0347a312b53b6e93390fa5cff0fbb34c9a55b3bfa61ab7841f31779fec5644f07e5f9c17d0eb1194403db98035cef3a0d47e70ddedfbfb47bd33c44fc0f7f4921d50180f10ba9e083af9917dde07ffbe45a53e65983092dc0f35e1c84f92b7189595390810dddf63209c3f94b856eae91571feb451a665cd1a1035b990c887ecc43cba2433686433d56359e92f6bacf0501d6d7662ce00d33aebf14dbe64164c036caf16519af50842a2f1478375a011705e1b6e3cc8d1427b463a370002e3a70896ad27996d20e8ff12e70916e72471a5dbaef7f378d7232905aba8cecc91e8c077c72a272b8af0c6bc297b6b8563e218a17a1d510b271bf12dd8d3aa90e56d6519a32d29635466a7bd4dfdf51438ccceb51d486058596bd2e201d320c9b63fe239251273008b48d56b8a01c916ad52aace95cfaaae3232df4a22af016a830f00b2280dc4090249ebc6c4f4977b3d2f3e92603ca1e86d0c65fb91f757747e5b3c64f229c98b47e1adbdae651e8c93d1b5e22b26a27a4004b26a4cc6dece078c1e1f420e928f4810f467c371466a5ad780e27de4b7cc7d34c4bcf9bc4d1a8d69382b5c26a661451205b6074193728e248ef7585707509e09eae306e78710f1fb3288251b20d0edf70c5a978f32b92a869fe73975d955f5281b23392981a6f19a2f0f58271d29171ed9a120fb70186dd4f920f56f767149d663c103ad123bb86de7d5ab1678aaa63cc7e6039049df417f336220a6115e3e860bf85aa96a44d22587562527980f69c45463860ee9df44482e0f52c5a10107aa1bd1fa8a3ffa215868740307471504d1532b017118223f73cec88bdb2514519f0eb31f1c14aa42fccc0d541f5b3ea28bd0f5a00d90defbeeb8e25bdb73835f623fa6bfee1e5ea3032e649608c59db0e1d6d963d552f4b4633c5767b0e8bddd6f350c179ed032448ecb129c15f26bb9e1ca1f3529eef918c8ff9788b597b6befad98ef7b67aa0eb9134d5b4210870b8989b0adbbeabe3dfebe6ea30bd7d60659f4e39e266184b8243691b2e0552aae18e9cd627f7c5ec8693c5fa0d609eded0c8d047ec9ea6c224418278250112560148af7398eef1f9dd48561fa9c0441801dbdd69c8bd46e5c4af99ffe4109f7ed54f9cb7f83e617e6b2f93b922bce7e1c07151e676a7ece630dd747bcc142aeccbc157a93950eed8401606e1b16e66571b7f8ef663ccf4e93fb82995212bc9e169ea1c0594356b171e607e54fc02180a1059edf5acb8983064734fd9f8209e394bb72f81f207edcfaf9f2ed0b8c0689b85c47181abcfc8d36e08b0c5ae05832a231dcb4f6da7f1ff2ac341a08b210acdf2bf48f315d5920175f3d5cec908b4961c1e62cb178fd865c31734ac843d19fe58175f36c3bd84dd20e8069f6b0a624f48a0e47210130d1e6697959e2fc351c28cdc4228fe9fe37fd5abaa897e269391d032b12bcccdfe6c5b4157526f2325f20b13b8a81f8ee1492e0008cc770dc6aa7a1be35998418d9d54e3eeb7bc87d1ae47d20d47645c4973519428a79b15d9121c942e03cc0ccd93c2158ec1e33c2b59bb09173e7b7c9662f2baba474b01d7183e0570580d82094361ca63a614ebb95d875ff403af22cabd79cc4e8f400424df7b0904cb8d32decc3e907e6a0b511bec4ce2fe9331afdf13b26b4b010b0b3f755f1a444cc372cbbb860872fa0493f8c492a7f23867ff93dda7b637a2e90efee7b19c99a3476e8f399e1c68944d4bac5b988ae883dac4f406ec18abb7ac0c45dca453ba41f5b9c28dcca7905816fc9600d2298b31a000bda8fd06731f6f26b119a02a7fc2a4ae33307e9f2798f2502ba08f0cba86fa82624386a8a7cf1a1bcc4583eb73ca4ac0b5285cef38d0a4a6302218e5ff0184f9c9908eca82c01a26e711272a4d2dc6c57ad49857ecb1b4adab7807061cb4f5edfdde5930058ee71e8bc31fedd59323de5af8e5713c7ceb21d48c3f73611979e7e2712684589c4711f24d5a2b83af9233bea1cf2268234eac399129feda79cc48c5cd77735d9b000d2d0a40be4f80ee8003228f36bd9602fff33d890bec95c8c23e00e7cae127c41075162afa6c2b18a81f299e7eae0f590c18989adf321dfd19a846924be23a7918c08a3dd27cd71b39d3e3562973ca37074ec000f69aa3824710bc344bd5ea292ae856b14a9a14635879ccbf87f082eddf2dbd240b9fa4702f5fc5d88ee72b2a096808a9862cbf8ae30240058d22dd69272b346467520339fda95a07f6a8403325be8b2eb1744fef4514ad7158c52488019b3eafa4d8cf7df694c3bf47c3a59c1a1fc4a36060bc9af14ebf2eb79d07cfda2528c398b145317c2cc84de6137d44221226dbd233ab0c7c0263ddef6811487072f0e218b94a6c72fc7e39bc27141401b11d54d55aba111509422d31d6cb92dea31534ace379c0427c6c3a92d4f90f02251ec156fd36045aa90adf78403f49e8479b8ca9778326da1425427815fbd6274bf80015317e3ecdca8795d60e025b58a56dc77f5a4b9acb453d3e78deee5c14a5cd11d5d16c49fef05d37c81548bcc270ef9f21a79b449a30d540ee0b9a8307e991ea9b46f57eb69d7c1ebe87a8fdf4f8048b4f41d4808162f8e0f7cfb3e11a5c3cdec6eecb39b1351814f82418b9170740fff3f5681d48fce7310e527b6a03d5ae1282295d1a77b314268efd8cac07fc028290a1c8657e45227f4b6ee63a24de15adf0cd89fa38336b41277d5fbb3dd19e281b815b2c0d2e9c3e5511797e00e3f664895d68974f56c39c9889994ab59f8cab6b8a8ecb2d703646a2236aa91198ae08f77f02dfc879c800f7c84e398ed8a43f20f2aea496b004cc58fcf4780b4966b97a4be732ee4358cbdfa657c322cd78549ee3780e0350374ea4a3d8900f6418d683ec071a1273be730e362daf287de8c335727321143ba40ee0115c241f9ecad9f72e9925e3de109f1e566341752dd39b45d512d2388dacf23b281ce3108c6659ba45864d7bb6836042b9097f20f3f2a053046045dc613ef501a2eea81281834f5e8f9d7140f709b79d53fd264556d70139ed47d25666027aa69c58e42b34757fdfd0b9d122191696b4b4bf4eca578e24fd9b844b34d73c31ac3ddfa801ff012ca13ccd6b0f5e561e0e6430b4be2b0b2d374ea129e470491ed7c3b13b183e35a8ce4cd1853425fe864158971415c7a65f11dfe69d5074f950809a122a28063e5d27dab57ae4f0b3ae9fc361028adf438677dfb5dfb988f8b8b6ae57a3076e5ea1a0b2f974df0ffbfe0a060e3637c3127776fa81d9b01ea0d657dabda901d6807a490c87e46e4e394192ec98b42ed04f334b5a1c0f5057e4a4c557c01a1043c947cc968219522c1abe82b20d6a4db9c2b388a24b71cced988a4b883a481d25e3810be1b850a40508bc3fa632b2e5fcff0a7b1a49340915865bd52f501f1f78f68122727eb366d619ffc67d08d4d38cf0e91ea66920699c3a2dcd37656724c44f6274e12fa976dcaa3d6cb603e2b323f8dcacbcfda2d2371a78596fd7e42e52e2ba095e30b7021a480d05dad2f2838df1fbbaf38e0df2af304c8b1175b80cb447d4e3ba6d26637a624eec49a5617a8380020addb7879bc212fe1c600425147bf057f7596b5f2c938d61049a8cd1889ab5c40ab9386a13aa7022d00ad42d0e1b1338ff64fa766858440e350fa32f5fc613ccd8b46c95090843de78343271006a68bf23479c2223f60d11d64a98c2865fd34f92e687eb8c2b7a28deca830818a2208adc5a09a52dcfdc04f6e0e53bbc232b046d60528ffb794238b3e64b5417d6dfa423205c8a1ab17a90537a19ef9e5931c33a757771a62c7288865270fa1762f450463e4256850c78446525c13e0f55928414615620e1c41fc5f2a62bbe2ec7351b1d969bf8399ed677fcdd79a6689cb8c08682aa486ed19431e2fea30326c3b9cb74eb998fdd93bd366ca54253b2ac0e213d65275b7e15be6f55b87e5326ae1e2391bfc71e63cb96fe8badbb8eea59b1bfbbc89d9fa9c76214c968a9ef10f4697708d257e2caa80176fce9c9dd0bf7f08db787efff22e8c50a2b3d851c03bc1a26a9f96c6c7519fa1f9f69e89d21f3a846cfe8c7825b32cd3b1df30dba1d5dbd1e3883130ed44f9f1708164d7c037f7110fc42a2569005b099cd6c03561737abb36b4e6751c47f50a1749e53575fb2d60336dfb156eea527c39e6044fd0fa4c2d07af7ebb89646540fb42fce47fe70faaf75eeef6741a570a90a39e2212e25594c3141f72b1caabdbcdd33ae4e941fe07d0a9fd16698c6856b74619a810c0cfff04663e7bee110fefa623a4db43fad9bd5d72829e65d9a71427a0d5c2f0bc57df006430e7edbc07970858f1f006b42a6b8e510300fc604821abe5d9ed612879f61da683c655452ab2d76bf7345c63299b02b7ccaf83dcb03552c5048020354ed28001f40fb66a7fc55ff272aa8de1844409e56a6c2a3fcb89c94218f082674c00541fe4443178d227abef44644d4abfd5e9303fff2e68bf3e1d140235910cd28a2f0ac9cd54303522a68c80e618d937536c1d26897c8f85b4acf4aa1060c9d7cd154657431320a619ea0a603fd86a52edf39d34cbfd2327b95c4d02dac2df4af3fda5d0fb1411f7499e9228e69f7baba0aeed7b1e31f6d17aad11c2c4a1fda0b5b8b20f3f9963100e0dcd1ab2346b12f602bc30a2bb453e615a7c7df7a1e209bafa91194c0e3da621aea9637db24336c649d82aed522c6ebdc9e3079df264481411d0846d3f82043e90df085bb974d2ff8c5094b56b0721e622a3ece1e2c72afee9cce381b1ab0a042d7a2a0d32bfe34d76e43ae24cfa00802038ce3482e328423d2b01a87447801dd0fc1e2c33563bf35c6bf7ee27f8ea7808e26da630559777a16193d3cbaa060b686fdf5756e395ca3c942d5415ea321930d9bc4581da056043fe7c53176c8fcd66835b9ef69bd815564d1bc7fc71c1373bfc03d480b7aacb1b71a1d5cb97773bd8420ecedb4abeac3762884e73eb9701d5e192c1e2798ea30398aa7c6aa711720294d914419284a52a9794048ad73895e21c73f2e27ee0aeb6cbfc1ed89266847420a154aa825df70f24eb1b1f0b958c046771a911baa6453cba72219ac18b8e31e8b660828ed849b2cd1b642f2494e7430d440552ccde3ff0ea6860a58505c5d2c029cbb4f5fa598fe4ac011751790e0e606322c13d130dfeb16f00ef3a180f8e4886c9a3f367a36a4b2fa215a60511d77a4d1ab2e1d16ee35174f5333bacc3170eb79641811ab8d999ca78bd6aeb778c04bfe4b022405d81d43d828fa1363837759b1d688f9bc423f18067c1e63e7ddcae77b252d284dd9ebdebff77e6f0f9e799a7461a0f4899e54c7d34d148cb21cc3d29c76292c218095c7386baa2c5304e29fd4c70bd82e6062cf160ebb110230f444c2d71f3187f7f4932fda50d850dc56b3ef554e7745311c76955f8eb116e15d3f72d81a267864acfa25775724ef1f370daa11bc9983df1f6d897ed8f040899ecaad740b34dfb34c45c325bd21c38b8634103c639653053c1a72bf63e3a074b437a978169d1d7cf13831621c414bd3d03f307e7bf3c2bc654afdd857c43c0e1e47c4e726f6703cf414f9bef59c90a72eb596013447cc3cf7a8576cecd1674d8607f22e0cf5d470df13e216be6c2e9e8ea0ff0a179c00cac95375a14e02787cacb54a901fd68c2ae7eb0ee85c442aae3d8304039a8209c8090040c83dc3e287ce5187731d9b8e516f91b73a1475c4c4077544dbd325ee6ccfa3aabd1144352afdbd457c2f39054b92189663deb023ab48081a80b8e28841188838be3981e7e93840ac35199412daa396d472a6a2cd403a92a8a80995a1092e99f0dab79a2c024258bff1009161d190a6eff111d46fd2e4fb3528475d89ed0f4acbd2a83ed488cd0d14fb2864e012328fa9fa8234ef040bd7e64c94d8e5dd435f85564aed36b3c59bf50b0611681a82df36ab4b44b009b20fe02e8689ea64df791546cd3db0588d08074c241768008e3feedafd677164b7e19c9dfb2a1f5b4c91abe6b2716dda9027a8cc020340435acab05014bc0b5fba39be8a5dfe4751ec0cd65a54e3acaf559fc45310001c7116a956f02d2c919493c1972c2a8c37c5c7018371de046603f943861d069b44e45d5c59aa7b8e14620ef592b42e43dcd8022db7fd3d652bc292fdebed066f0db566282ce6fd05432d2bf063cc05c2372a442e4eb1ae95d943750660e129ab4a81c1e699cb974b471bb885af4c42f8be4be4a44f6c16cf33b22bfacc7122532e5f78e3da9ca7b8da40ad3651c7a3cde3c028a39f76ee378a5df8e7122d117b0ada3274420f4169996ff8ad7f0644294b964bd02e43b573e027e56c31f30281ded22c23f5318715d0f675c4c697a2c257f22112738f30ba3d9abfab6ffa1b183c5a511100513b515950e7995ccf0f082cb5b0d62b57269e2c7cf3e4c0eb0bffb7398d6bcbc8c3a742ce3bfb2975d365c391409d9590552733692a1a62b7138cea6bdb50758166ea8db309d0ad52dd680140c79d82fe429ceced64c6c214103a90d27fdc490bf99777ffd713275db4329c3cc42f5d631a29af86d0e449a5298a95d8f509c980f7b11abc90efb5b6ec019ba2abe05ff97971e3cc4494a8d11c320855d17be84acbdabcc29634e10451cc55f7b18e63385dee746c70927b271c9d654c07f30212ba3d0b34ab1c7d81484353e7303ba8739122ea0b4d054c5905b3b6fc0e3482c9d1f92261d1caf129228350cf179238a409ae49a4887645050da8ffcf50fa4ef02b22e5cd98fee5625348f8dd72c4806e6a6028da2514e05314cfd201047def07dbac92fc3f31197a0f94659a6264cd08c7b7b1b73f5cc19d074a8c82be071ccd4ad401a5907a3816735668a55c2c2254cc94d9fd42d2990419d4ce2b03bae535d55557127ecd66370f5a555fa5b3d3255b0e3e18040e7c9718c174a2ecd7916c33c950e946764b5a2490252a6c8b4e8a3f4e4200160e12472f73ca99a627089fc5affdb380da4506c458ac642d6a8f151f60b09261abddb2092edcbd085881db6d5b96013108dabdc80a3b38fa8d2e638be26c78465bbeaf2c06f7a360a2cc343d6d9dc2db8f6424302e9718b8884ec3d80e99407770f64a15777f2da7f4aab9408e3ae2f3414bbffd74a290cd14195ef7fff6b31d737bc22f2a1491ff76b75114b2105c8ced13258e504a7b36c5102321daa348581a551729aa4fd8e5c8a411c89f29eb17ccbbdb02141008bbdf46c7274a2c719595fb7a2f5854c0f6543b85ec7221d1e15b8e867c80848016f25e72617f27e537cd721c05b0e96a7801ee3fc43a534ff819962b1e8282ed7fe5437df8026f12931affc92a45a488eb039c2285d69ebb9cc721d0c0481ade8e27986cc2424bd63f09ea13281fa330a68c69d21ea8262d617a8784c5c3130f9fe2638a28d3cdda46fa9c3f110d509bb1dfefd5f72c7af0c2d6cf3036ea02de515f6876005bbac8d8c51d6300ac7fc8e451e423e4ba7c25eb016814dbbe5a9847c8d72a519dee86749635c52fc29948f9db22787989d3d2630dabd1b4a74528011a8a2df437b4fa220098850a3e4753fadb485e012a8cc00da2cd5919c51598f8ff98d82ff813d03bedaedf2fea1e5e0c489ac9b0400e0d949e92115373e6350c76f49c345b79f2ff56bcd001b956017661a2f6b6b167f5f12b4894a32157334c60f29d0521166d13d15de9302c9b0c0bb5c7880f6acbf2acb78d97a66d12e898b7ec0d8c7322bf62e51aec263770b749a733e1842af368cb129d636ca2c23890cd74c44f69a427e6dcd095119e080b300e82de5fc8b08396e97d027b8e781f0cbd63489c5d496949b3daee08a8d6b94c056c4b29bdbefdfb87f715725d48e2aa2c8ecb2e72e4a6329b861c11a1733f664320f80bb5e0624c847534380dd10bf3b9d7b0b3f3cbc00ed6d8d72ea19cc0831d47f5142d7699110a13d154190e29363598288d9e87582621ec8518aa844817a33cf7385a4867a624e6d35d19f08689cb4694d9eb4068a5a89cee14fe3cfd331d9edeba664f921f4e2af1b182eff2ffca37f65408d3d1b42eb324193f27aea86433f3a6aaa1f0ccc589243d364a6d284678652a5727709a417c212fda27231f6e20f52417f284a5641a6d80362cf3dd402b81e6dda971168329fc14aa355a2874208110adb7169be2c53ef263bdf59336ba78618087e36be1f3c9277257150ffe847f6d86895f1197dcaa651987c33d02f5ba5b482311f817d2752f96ec1b41838e338b2af7008a0d5949ff9f18dc7be79ef5739211659f266d621462e12c1be7bc4b4934fdb8699f993d2caf7b1c570cf25b802c98a25d69b994073b68b01ae79b6a69eb211a914afceb059e78d2a636d304a48f6b05bc32993f1eb3e3e642813c85af8b5f1de0888cb13c53fdc5d5d479c70ccbbe61744c1239002bf3e62cebcaeb5e7303b3e12a1f30fd0db795b804875570e7fed3230bd8052871b4aea05a90ad7b63f4113cef0be4dda60693a503b2378172a58c4624a2eb2d5737e779f841a33c28eb444b62d3d23520278eb740181fda57f544f3aee7e30252680255206ff7d0a4a519afb865069d8a05da1132d795895891fa94f8f29904f4324c26aceac3104c97d39759801a293e22e65a381aa86de3b50159ca9fca09cf8f2106c1ecba895d547e1dc71ed0d9e8db14b9db90888777e29ef793b337179e7d5a6852b64d326b7fff80c4434942d3585902b1c534450562c6ffd93e8c0d157d7f9386ead62dbe90615399b0c8fb87e5fca00721a2a3fb68d14d36819b09983dca3f3792b69abc99b3f3f5afd33d28cba0999b8968f72566f05de37d660436b2b1da3001d81f914ab0dc20eb913507e7e526d5f96f842dbc6d461b96df2c259d17a61cc499cc16d0ecb910225076caef6a3d6c2fa282b2bf34a1deb87129450563c1427f7e543ebbd7aa31c01a16f7f9ef377414182129f183ba1246f91048c125b304aec8c136a897807710b6a9a73cf827b2c53e733c2eb1df3fb2fb0210be8239b1631db74571a2e30ff1dc5e77a7be632890b5bed671bdaf6e2a960bdf75213571caa91dd52fab661a9e4fa96e23b6d814748fc4bd6a032ca1cbc71a6e2915cc8eb694d63a591eae3f5aec153656fb2311b4ba339a7e871ef872702461a3b251b88bd55cea092af0c1cf7ec9f96f35e46cd359223da833d8e518990775f13a8a47b5df373d0d46a9098a0b23c1dd6aa76b286f6708e7713811025a23a510820428c3768c4dfc8965fb9d34ed574ea7ca0d2eb426afe5f8fe0681ef02a7a0700344716e9466f589aaee49d2860b604052684091a9e0ea23dc12661a18dc0d314a36ae8c600e6a886d4343a2b42f3419ba19e793ede386baaa303a402f234bee6497b9e517ac24aaab0fb01afb0b1707423554a869d1bea6a5bf743f11f32baa913c5500c9df9db6f92f49d30b7adb7cda475c5cbcfbdab514f391b12c3e705687b7ca3381e1363090cbf57ff7a86d1fc0c9b110893d6537435e223b0966964c5e464aa76eb466b04c4d5c7037f19229aa437e7e490d1bf9b19eec700cee2eaf6b694fa0b6b912b86ac7bddd35f7270baf326abc5234e02798047f6100dd53510a41dc8fe6369350a1b458bf042e8204d4453a359a5d107ad266d8bd0a393b07ac21e86143568945dea47579f961da100dcd17c89e800299d1dac22413736f9bcfe75432de59c190fec427a659e0878ae078596ccfca7ce18070706a27da89553eba9079775003ae2cb47c214b82ea2eefefe40da9638e912cfe60822ae649c2057442471a471a9f952eff46a2babf1a83e19a4ddab3794cdac562db101a09b7a278b2dd6fb6e2a9d393579c92db556817f6dbdc7733599f140cb4382e6b319f574a62dbebd2af4cc151e0773ab2fec6239c69681263c76fd83cff19220271f850682cc592aaaa6eea28d6bc0cb73007fd4bd1bd995a099bd82c024f0a2f782d188ed96e6e3bf4087c26d3d237a2b12204a57053ad307c344ae34fa4213bd4ee4f056cb822fe91f44421f97b9553e372050526ddbec9a9810f0b77cd05e7dd08def408b601eea156f02988103cdc6af0fb020894d84e00680fc6cb690af351f694b00379f093c09cf78cb4261d49857bea4be6f8dd3f4a7b32f59a230e3189ede5dddf5124a8caf0482b511ba11705b8e97b0ae8daff89770da3fb7e20cb1d9be77cebb1509930887487b1599d90171d64bffc6e929ec317bf7e5aec2f7740dbe894189de87675d7e978196e17b528ac35b67d091c6b144dccebf6770a01a0077e8d2c021ad57fb50a34e3158e55e57f94290cb6db1438e73c26dd5f03367dc364867a8930faff385c5e3805b3f5d61b5fe90d04e51b1e14373054971a06811bbc967bd9eec33c892ae9c4ef7274d5e4351abbabd2bcb803a5f5c70f02860e11bdf399a1dfe38969bf66e7da76086fd1368b68e47963db7f9565b3130d69008e708fc5f8961db2d87f77fe3aa722795a6c711cdaa2d4da8f38eaef83196be74e4cf2a837a79259f60cff97b4901f7f48941807f07784fcbf4db57ba80c897759c9ca3474d1f1c03abf18050430fe347a17998ad5498c2afd48c8a98415b32a411758c283b4c28c1d5ca62fc3dc246c62fae1b7450b168c5854c3f6f81f25ec496179eca02a8934cceb7f9ab9fc81a0b5c0982e045b5742e3d21791562bf709a403a8ff7f4c3d3ed483bd94c60426e37242a53fb501d465f6771bdd1c01e4364a0bcd48a20d4fff5363f75c85d02ce552a1c66784f8c393df093885b02286068214029f61fe4946410855f18276aac139a43f0b328f281a40f607dc65006694ad288bedeb4d931e515e3d739eb1c0cc95d7c5284f3ae66f43286a98a92480eb742d842cf382e23e07211c6767bc86b65efcaf58199ad5bbcfaeb67b8116ab42ba4929e61ef40822b47b3b77e1411f2f8dbc72c1e930cd08e4eb38f37b0bea7c2020d7ee87ea54c1b3696fdbc146aa584a23042c69e6efe33f7556c1762b3e3ba8c773badbf5104256d6a599fd5b20145beefa188347e080716230875a1f23ad5fd8f118d14793db2a001b386410d01cae4501bde987126554aa1f943809b93a34284851e0092f0ddadd9d27066c2568c8ebf91bbd5e1554c3ebbc6eb509d2ea1544f9c6b1dce97cd8584418ced461121ffcdec9e3dd1fa14f95d2e4ea08cf6a13709220c27a0a49bb9334e11e182a621cde1bde5f384b1b0ad51d19833033a68bb8ad808d6c9c7b07ab154119abbcbb85ef10d40fb251969dc2108c90221aec3e7930b31580525bb5871112787777aea6ad16614536a5a667520debea18baad0cb9f041125b510f5f1fd48f41cd60268c6ec4149908e7f9c9a1c3fcf813a79c776a2eea726579809ef20c267d839f3a208120f8736779796e9dfa7bca253a82820d90f4f5f7fb4b94a870394dbb688fdcc0bb429e43546b8b88743f3f114951cfc8e34a21110a24cfb809985668ea681e55f068d8f260436dfcbaffc91c32edff5ee1777d2ff82c80ea797077225f691258424cf5f5d64b3c312d6f48257b1ca15daafc6d830d0d38e74669add7907e870f0f8e896837af7e0104b15d103b7ad3d25f8de54bcc7ba08a0b3bb9f30e238fe4e3321a523728c554b162110489739821241ca55e4ecbcc5f6f25bfb44763a32e19fe977940ce8800db666317bdcdf1f29834dc6b2f07c60f7076ea4f4c73881e89734d46a31a160380704e0e4d0b30a60fae2bd517e3c939cf57682a076d4f6f364126bb2975f940ee8fdd0976ffd75e34827735162044bff075b04735ee3468e51d80a7a09bed81b48942205981554c5a78f504f763531fdb93d2861ef929be2f873e59dcec85631807b70484a7f6ec144b234ea375bdd48188e15ada0eb56ad1b987d75ed356bb65d5e01bdf10e80176f7be41f9d31008b8274f1b0a8e37108cfd37fa80c5be57fdc6450207b4b4209e983e6aa97e26c81afb4d57059de74276ffde689f883d5ed67abd2ccd9fbc681e291fed67130e43f755601bae4c8849909e9030484cf3c71aea07227f95a36dffb88c8d0131284fba36b597c0607f0ab3b1bb6c215a19fd80cdc92c0ae72660453b0973ebfa890718c2939f1207e3ae5ae18bde7208392f708a9a093cbd866e4426034e015fa66d8b9379509021996d41841c44374f7e4ec77a8e0bcb2604e0783d0a7981f77addc0d9d00dcdba536950e38f1893b138696ca20828cc6f843d63ce7c068774807ac2f6a52bd2adc3649c130a35b854e2965dda9a2a39eab89126cc289a350e2bf0eda765a5bc5da8640047bf12423fd54224d0d6053505f8e689a403440ad11e799b653515ad5ebadac03c3c17a2e6a40ad8b829025a520eabe6104fefc0349b83a1292f1713d4e56131708398777532728fcd2b0b712b5802b896281422c1bc219dfd0d9df3be491a20443ed5119ae62c468d2a22adda3834369b2cefc68f1033b217baa21720c4e4813cd4b52388f3bce5eb3e21ea381f1ee6305814a07f4742b52311720f7aa836766eb57fc2504d89fa68be03b747454dcc12f3b4434342eab6d95eb64c2dccd735983c553b76a009c18e6e2e8e783a9067cda1f5109f7371fcaa2cc1658c50fc513effba99a62f84479ede1c2afee100f375462e12d611ca1e58b350f4713e43436f8d44ed129d36d741772485b57e2cde8766e19956bf15fe1a3f715f462ff70e3e66911ca8b0349ccac50d0c4d59a71edabc1844172fce54e10bcba6f74cbd36c2c6cb8cdf86b07fcf272bbeae03157930ff5bf215a8fbeae00610efde36e8803758c44b71611ad8b7530f77a243c70d35bcbdf3d390e0e1f358a53bb7b271748486ae81cecf1d3bd7b312e91ec7312eb62f7f4465e9f133c90b40b5aabadf897db276170982e3682959027d4f12eee5a6720a6863c259d5569d30604063277d5afa87a04ff1db7cbbd8048b5f6ba32e0972746c42fada98ecc8139f01726b225c661b7131c962a6af431c46da1b18603580e0eb36b1b3a78803784752e95178856a52cdf293be7d20ca28afd91216a390b4c5fa200d13796d66a09e67c0dcad2e5cc6ba969843f35de110a26385709f5eaa333c3dbc672805554507e4492dd88e4e1fb37d30f00b100b0b73ef0f2805b3dcee77ee3c518d4d2b5e10a6e74139d13ec437fb20caf21f6b14258b219d2d7c874ff865b46668c9a98a6f95f7513c4840eac1fcd8c0d5983f2fc417fa028662ef9622af0c8b9a8370044569515953bc3351e7879256a1682f1913c6f71242142890f49878f50a88254967744ee98fc8f39b077df24f896475138b353992d43a58ab679391c189def4abb585c95fc78f8560ed1578334a54ef06c4e242e99763949a6214d2f1e6b2e0084716b5acb458f0132451aca43dbe9002d447989ac471fc47f751e9ab748f01dba71e41478c7ba6cba186c59ab2b22e0ae2cd982005400350f2b1fd5804f85a943324dcd60e01aec98058ec872048fc2a3aa79499437917db1f087117dc5627fb97486c24adaf4cdea43ec70a5ecb1115482b3eccdfff6508c76655387abd8db57b01b170d5e1870faa1384ba4365e7054331d92e9f9f0b1ad97b7b530669a248a4c26ceaf174cf6479efbc7da6ea54099c32957b8e46d8efb77b50d25e3c705ec7679f33abae0b097fd615c45f9d461b03dc3ca49cfa79c813d003bc5fe4ec9cfa94a1acd523d23b88d35ebb5a6e9a2fea8939d0a2391f5ba2f337b17972354cbee48f9f4316611ca4747267fb899a2c3deec9cbe9907b63384af2f8949c18e82d0599a14c794393c329254a5497322b8f054ff0c0c835e7176bef90eebe63c9caf6838cf97942832984d19839ab9a15ff2ee3f4e15a05e49a92af53f986375f372836e8b0871f4c4ec28a690e5e631d2dd1341b55a31770bbeb958ca73627dcb539875b11846f8129714880e708ee260d85ed2385e58c520b25866ee5635882ab26c9019999d6fbf748f1e799852121d87673b8e00f15076cdc02771e293c1ca49972b74ab8790702fbc2d08d89be1c26e413b5ff7ff00f30fdc45ed794caecb4f8e943d341f7ec1f9cbe686e8408302d278a2da6ae5b6328b996f93480f99dcbf66ac56244f780859f9bc5e6d61413b5b5724c4eaff5aa4d5113e19f2072cc4486528b4f864c0b2fdd8dc12c07f70799dcb83bccdf08e82e1fb2354171bec979c58e2dc82f05fb9018859906ff3803fc845ea83bfb869fca929234cedf2c7ed172a4a857031bd37d4a65886916451a01d80e3d40970692f0ab31cc53c737a8ffd3408641dd18b12b59d00880a1a51bf1201219c156f4687c89b3bba7966515c263a6da337c70219b18cbd7e1a7ca07f77287b04f62adf3ba0a00f903a4de156b402aa92faec6737f1be36050a4810eb34741a55cfc86f22b7e734e338f600f09df00574221e9f861fe9b68b5f8501ec4c809a6c85ede1d9410633478be10ac32a14f2d77664f050a16f2b0a73b470a99616c27a96a7b747c365d9191b9a0a92f96eb10e8cfd399d35a186ab9ee7f1680f3102a604fc69d1b1e5058113ac9d917ec32e1cb339755b5536e4715758f", - "public_inputs_hex": "0x1a207628cc6936816ccb62a7b56fdbbf8e975293b677c988644e018fc402e4411dfdc7cb6265f100524012f038ee1f205bf8789b70b46c57463dedb4998f7ac800000000000000000000000000000000c946233fabee579beb961008103a4c8d00000000000000000000000000000000026a297ba8fb72294cced25b49dae7c91806572c19ee2f3532e970d617919b3aa8cdc582782dfad0699badc631f75128000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000022b942ad53baa7c6e1a7616e4f0aa2eedf7d67eacc63696345525988225847479273fc58215abae6a73503410e8209a542c92bbea2ba8e6fb129c971def4ca8452ddcd2c1e0b08c268d942b52a49e7597873ccad9df1b02ea1ab854e7111eb2dd188f793a89424a83a3e746a72d4cdfd5702a7944deb0fa2ee04f84489f4af3c2000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "proof_hex": "0x00000000000000000000000000000000000000000000000b848f7067da04597300000000000000000000000000000000000000000000000cfb8eacb33dc5026500000000000000000000000000000000000000000000000a140f61a0ae7da9830000000000000000000000000000000000000000000000000001b35adc769dec000000000000000000000000000000000000000000000005529748f6a3df43120000000000000000000000000000000000000000000000041e2b70da2e142a38000000000000000000000000000000000000000000000002489ca9f8182b061f000000000000000000000000000000000000000000000000000069af3676bfe700000000000000000000000000000000000000000000000bc2f7c98198f35216000000000000000000000000000000000000000000000001de57a498803e7aa800000000000000000000000000000000000000000000000e40fd6827d61103c1000000000000000000000000000000000000000000000000000084230bc9e5b600000000000000000000000000000000000000000000000c20745ecc0a9b617d0000000000000000000000000000000000000000000000098615242b257ddaf500000000000000000000000000000000000000000000000d98b13f67a2b6a5220000000000000000000000000000000000000000000000000000df689a73e63b1a953e2b375615edc3058e7d1ca5cc4bcc12d1207ee7045581b3b6d157383acb008447d2b42adf3a25787526050a2c464130614c700a9cf0b996d758e0ea3101207ba4219ef22ce8b381e24a97633135e5ae877a3905824a6b2117d2c52adb6121f18d2e4a72128ba85103c6084065a0a957c369df32907adc47c59f3457ae3524cfaf1eb600e22c8222d5c7f3d05b89dfb6a6252ed158a263b28cf1d704e00722d53425971fa3915cb5842157f175ef3b6c978bd686b6bb911469c0a0bb04e524323ec504346fc6d320aefb7c8a5aadf1728b7a7598f38ee2595296f4aa053a27c5f74b1cb2d97320928bf8e5004eaea31e7ec579c77a628c4d06c323adffc62948c40c873573a3a100f76bfd152510de33a30d61a10d2b6d1c93de13958e1c18776cc9836534e19b35a0f44676561fe123ab6651f2926973483bed88c85e5c1ed56dc8a8651f15acc31d6d6bcf2e97b303b160aff49148eaae0a509cf30cd9294fb69b8ae360bd1e988a8fd0b940a61d4811e0e8b18fb2e137aa4e76109a151c5d96eba7b16f7543505b2e217df4e672a962b340c011df2743b79be60e0b0c05a2155c315ad8038be74112c3c0a11a2ba8bb424fe02ff440851c431f3a0117298170f8e7b02639cab117e04b8477192bc4e2feb6dc7d675bd3a0e09017ee842e52ae8728ea51e4a18da1ff28088f42d38ff18e60317977213e9c0d60b20d2828872222cc0f34e17aa5c9267d627f4ac25bf6c7969cc08f69079cc5182cf58c183cfc115d5179470fd7395d888cfca34a1913a3ac918e490bb8231b59359ef909ab3830af8794f8ad241303954d3f8c794969cf24f7f171196fd1267895e1e42daea16482d70b755aeac34e0b1442d10765de80604fff4fb678bc7f0400f5db1065748acf1169718b09f0aa0f601350f0a894f8d32b81d408c6d02b1906426515448f4e0aae668ea694fbbc2a7280df188e758dcd850ad11c7fa74268dd35f910a27b4c1b260afcbcd7f6837d48fc77d4a6da0cb7b8500247d05df4e3154c3024c6488ba33e3fd241db9e47eadb5c7ecdd8246a0532a85ea5c12ad12077537a256a2e262e3ba7450fcb7709ad3b2b9857b7326ea74d55f5ceb973b149f8cf7e0a8ab8172a80b043633b24ae10967d6489570114fe7c5bcc2419711b7d4be07121e64aa42315eaadd92cad2c7dfaba18ac683b7ba27997eec1d15edee22fd752017f1e3ea133164724fb9730c4863fac6f3c79aac2204d17c154dfeaa11ad78e1c9c38a7affeb9d26b70487b79b739b2daa0d2b89b0a57de0138d35875d5fd23095e676dc7daa828b9c87b55c7a3518b138dda5947ad0dd1b14a160547a853850ac087cc19fc4ec1f18b50f448df96d3b691645c3f02ae58749b108889cc72921344c9da4fcc45375fc89d97646e8a7d8ac9fc1507cfcb38fe716ca264c886db2146a1ccd11f884504c374b7a78a3396c92c52412791af6611b89bec51c8ba232444938535ffa6ecb26f8768571cb60fcb4e413ce7ccf34fd796bee3fbe9753322b0fa40c4ece2d11c0a0fbe4caf01ee10217d9713e3da320a49d2e44fa046fa2d95725fde9404a03323ed83df779d69954a6159ad108a0cec3860d6790373240e33e2916f05c0e22bf7ef3e1e55026f54b16bac5918c42a031e3cd75368a3ea0911eacdce98241da2874f049bf64456301ae43029d846360fc0b7157f3ccf9c2a618b5f19970b3569e8e6c296cb17ded8d661167462409d035d33f8f410a1af1c9d7c94e3a65ea5bb07307f1472f9bf7d3a77d4d738bafefbbb37b11d76a8592e9c374b65d060a6e0432b1e507bc3121a282a9098f9b1c56d75ca6cca35104415bbbb52c70c65afa557e9a5bda404374ce6d4933c90b2bb75b3ad3579f19bfe2479c82140a0b2d0dc1c358799373f4b0054141fdc63e26b4d40735cb98b412e0a92db0db0d383f17d1a3c5b32c14c7e00ecce12114b2a14c522a007a951e5d02562699300507e2202c4ec98a17de017c2a98f9f696f74f012a63e0ddea2b4c40e88b9ea9eba8c6a614ee3d17688edd8bfd425e968efd6e41f18fa4212a02c471d1fe5f93f27bbc092530c4ac6f2ad3a6560773c63da42312ebe80209dbe7af5221fa153635f616561ac1701355d572dda9f76e1858ec211a1cddd0c571472cc1524e7a358c86623765427aff53132b5a43e43735b0724cf4f7888341d96c119180e1cfed86fd01c41ab9a7b9ed10c57679055a8590d57581a6d0249971f93e12e33f3c568d000fc1ed9f0f12a741a42ec1a40e365c1ef0da413aa7292fdb61c0e994f53d3698d0be3dafb87348b8de14bef041dc9e7928909858239b791c7981e35ba93a4550b707bde9024dae26188c586bd851e350ae151480f3b787fc3db0053c854ac64e45cfd0c7526783b1d9066c34520dad62fd5c2195a94f05dcd90289201e5ef356c1ef34c84a1f3908211af44c1a9e44f63c34bc1077872f831cf2d6227dc6d78093ae08420e2c0ee695b9b8876fcbe7a5e27f7167c33cdfb5d3315023c8573bd927cd69c7dd64f6725215180bc8ec72d43dbe87904c30e01fd9e1315d81e115af8bef91661176d0d43d12c5281e09189e4fa8da6ef31867c5e49229b5e9f1e762ad2820ad879257ae7d089e7c29665fe3350109b817b2ba1cff502fc48497b60a3fcf25520e674245d397e728388b0885fb2254724111ce886c3008b363f7fa3ee256c6d2b66b2ee8ee6706129c381a305f06e0bf6427cb59d072472eff3c40da57fa40bad15e9c5fe639305d72ea09049d2a4598b72aa20bbe11559f8a819c5bc7278828b036cbf3cdbad90b31710103fa95493a3e1d95f14311b48fe60856b93baa932b1264f8203a3c78d7c04f10295c5ee7acf6d430edc2028a04b182925feeca261c0d7721177163c92b273fdc65ea9e8dc8644588ea1090684ef3bb32cd09380e7e12c59b402bd772c4418952cc8a294ff4ff1e46364041d295cc8f55949a82ae31c2f69523f349d5a258bacccd74e56231e958f8fee981ff582ff63854b92917c9bba45cf614b18c5b4632989de011ed79138ab62362d26ef1fd5eebd2afa99b676fdf9ba3ca972fba10c25aa0cb6dab126aa1a390be51ea1f33fa64b2d28c185a7a108e7dab30c1b6294c066e945d9eb6fec0dfdb9691c8121fe3c63bf90063464f2d891bdd627ae213e03b80d381676e921583af3cd21a651eee9fed187b8e69df4575045e3917026bfd4bf22a9aa5c680ce3d5cc0e0637807164449acdb6f3b12ac057411106e158e043f2ae60fa35ef009d6037eb0a92bf7d3b84212cfbf3ec6bc236b7821f0b17152ef2f1dbd536790d8060aaf81de5ee880095e83cca416a8783a531b8b9affc26f14848e3ea28aa826521b91d20a4b663e9fd51696ad6dcfd0068afb00e91ff5a813da0d09e4e27ddd02d477c294acafe501df227625d3ec378c8ebce32a57811927421e2e173efe6c0222a7e111dce77d8ff4a253b43bb277027570fc74609252c0c480a91326a694214312e2f5cab400667c72202f583340647f2910a182c44d174422c75d287fbed54792a2d54d768ea9152152da70641eabc415a78251bf2cd430f248bd5e22089e8d574231c9823edd290fb508e77034dc2f163bdc0126715cc646beef40785e43a7a6c10fad4cc07ac70c73a86c05ab097eae1fa23091df78485e6c00c7e9ab187eb8e09177ebd746640f923894114b8a1c066370d18c21cd3f0779187e04600feb1150e3812fe80bee8cd54a1622028c75ca001bb037e569533f3d9c127b688309a0026ba070ab9931c737db933bff1d7feff03fde071d5d47462704098e3f37e18e509b1f0c3190850f84d9ce5bef54dd7fc2f3c3c578ee43c40ee92e6256f605ded019e527d4b2c3f5e4e29cc85841ff0e94697affc8a31cb61062bc94a0e39bb140656f880eae3933627a1239b9b85969b64e9bffd551162916a19cd0f363259ab1a90ab085bb36bace2486c4692d034964ac64b049d51febf49b19dcfa5f99c170b17bd9df0b4b930338d1ef2d5696406b529fbd7dc830fc8c14c6bd8c16f00f1253c1e478d7e88fd2978ba0bd327c11331425cf4b985fd58b14d879b9839e0620bf079427cc9eeb05626ce30880a78ce60ae4d6ac366b9102d69765aec3752b40bc86f3d0a057736d13e3c85e4713dca46a775894be6a86afc029e0f8a76cea40f2182cf1d36ca720da6d2b5b679660c5061add32b5fc152a820b782087ba66b0a07809344a0134d129ba7f4e19956cfd5c47b410b7302525494b9a3e397b51e3039ad1ef896965dec0bff9b358868014c10ca1fb842e3b901b0e510122498551f2e9d7794aad63d358a377b42b0c6d1d78415abe68f58df1e04dc5bf11769b1051f90bc0f3e131d24df2c55c2e7050e7d33d22ad39e5db73e9af8b1c00128081b8ce0ce2a9951c146ffeff3e1fcd55fa601217e3da2193448fb858c06be8d3c1f7e565a2e821cba698bc9a13112bdfe25250d3823deb0e7a8fb419d9e772a922eb38484d054315f4418bb9a05efd6b9bfc3e5ee052f6a7fde779216815dfc4f13478e9b3d87b0a82f217cd9b0e782f61a8697d5d6707d5e0e04ae039d32acb62cbb49c04501ac0643d2524d3709215390667fd7b63a2c0c85f63f7a392074fd094f6c07f5ef4742de0a6aba56f4b16d8ce2e6f7270e5906c9cac6bbd72e0440214dd6d3ec8f200878ca27d4d36ca018bd7d9092b0aa1b86e6ea37d637778c3808dd56099b383a37fe3b60526ab524475ac22dd37d44a10380cf05c26d795edf159e18b8e9e07abf492f5ad7031ab13106b34b66be1ecc8366bb6933be7233d02250b41b4fb8f028195bc6f56fb47efbaa729680c0a89e3bab9c2d109baaf5231fed54a986f8a867b839ba0b3394cab7b638f79fb5fd0003bbb4f17236d2178b244a32093464ef748333651ad7798fefe65c89e14efea7bf094f077db3dcaaa2153a977ffc12f1b060c3fc74f4cac4c8852b1b86683e693d11c4978c2879e757056e7665ae4ec522122f631b3e7be0e247c70e498be722e31864c30b9e131f00069729385ad3114bbf616f1ee7b0da8bcacb110bf9e27cb437bedf1e283c5261008c0347f73b653e6bf9ac59c203749cca809dd1c666d128eaa074865773063400f1a2f125d9711cbd61c211db9c22d974b11205d2af4aff9f8f5795a2aa02991bc661edf40dcd7704f835795a7eaaa5d2ffca5a355400fd8eb053cd2eb201b72cecc94ff7c922e1351c74bec7068a18db3043d9048ebd87ffccbadae1ec64fa2b05d1c7370963c3ce5976f94d3c3cf4337bc97e2862c5371d7de5fbf8b08cf00a469637d69d340a3acad48ee62a6da336abe55d5a82d1c88e1a4cc0d64019010879351b300c0d4f5c8ec7593bcc6a9afa75eb0b529fe079bd4e99238be11c171ced5a0a55a72439584a10a548ff26452e842426bd0572527c6bb2e48b5f9ecb117d9f4717426d96ddc8c4e5c2d3cf707a3a44a20e5d564401a54fcbb2bec9c81527aed8153ecda2dfe77976690a75f4ad60038a20a8287fba638619b1bd8acf09bb43d9e15613ca424fb0f4b15117d8bf4cc72ceeabc45b803d6a4657ac4acf0bfbd262fd8158150068a3e2e6e6b97148231ab66fedb12b089fbf9ac106e6d70aec7ed98cc0a00c75a21498193b1249a75d4a121c8de1da14e8f03d76530171172b24f9cf58ed5bb84c96a80bff5fb44d8276815de2b11f63280aaf0910c977015b57b509baedf1a0abb96552ded4ffec30c1aa214f478ffbad0332bf585026177219291f2a7a5d6a47eb312e333eebadb57634b2c6b7b42340883b9444a9cf0d756bc8ec5020031ab0255720813527bf9ea6fa107c276178962953db9abb091a6dfccc0ee73df060ed6671445cb8ab79645d36a4c81daf02949f2ccac2f2b50d8c680c18a8038e2e4fb9e4613caec509de7e672fea040c71ade0f84ddf82442060de941d416b6cda862fb8a064d0721b031c7579c9b917480ee6e05054b3b70b73cb22a12cbebf44319d890f99ae72567d4fea41d298a200676aed5c9c440d2c1da3167a37b4a6a2a8e3cab3ab003d0a9bd75c1f3565db7440a50840afa0d820ed8e1a347f5ed5f14306fa9d6580d65de70e3d9e6ff372855141bf3baab542143e686a3af2c0ad7eec32397fea1d8261d7ad9fd280a835cfa66015b1e27517225c5c595103ebf1dae43c83a1b20d5d48a99fdd13de8804b71a36e29bb9220d2eb9e83179fb7d6d036a24a977f063539add79293bc4b8a514db5b794b578e0719e14d05a12be872f6d6d6eac49bf338195d99a4a767b10a4ef31b8df5c8b2240e27dfa9b58ab84a85882f45235480b01b40a00c94a4f7030f0963ce2a4cd8b0263bbcac11527f42563962bd99da5db443d0b1e0033795ca9a8c7903f1f339f6255933c3493c3bf79745abbd70be31483d1cac03942d2168a4f1847170fc85d302fff1ab84401d0eab0359433eeaf7cd60090c9d81e6c8e5c07e205eb049b2f10c72d392a223087ab81ca2a4f0077e20706fab5f7f305bdde4d0a9153a4915de2000df2d1e2967f24ba50b979f116f205af7492a83adf7a55eaa247ee60a2c421e43b1b9e7b656041004affd6a20ea5bb0ef63b91c467780eb487c6070fab41811ce6b8ad866d10db953803612ed1bbf5988481c603f79276edf7e871459055802f3d0053b41b8792dfdd36153e6ed6258f93c1f8e0b8ac021b9b94f525408b202a84935cd7589deadc831c1c5605db0e200fad3e10317a51d3fec67451b252c292e7b1774e660407e5a15b34a8c9f4d61f0e733d8c85ca0963b9f22a8435baa12070dbd1df83e54b0b2a1acc4171d1caee9a39fd3c7923cc6871804cd6a45241522f4d78196ee7eaffc973e1267f5890b93c6d0653bcef6aa53b62ed9b61b841e8bab815b1ded0129cb1b38c56e9a754538a399e7800b170ca36571923260a12d6bb79a7fb8e6dc8e5b7f00ca46cab952bce4483ad5cd24ffbf0f717380323122c46ba422755404ae8510b406b2fd907d9aaeb3963a3c27ececec99a33edaa9101f93327e93488b4a619d33020e760bf00158724f273434655cbb6f8d1a975c132e4be33e35e0efc792875a03699d80227c2f4a82cc1c41779b595ec6b229ca1aeb4878e91d33f5c1f8e6f4fc6dc0272fc57bc3f5695d22c8d8c48d43c1bdc31d2da24ac5e9316f10b959e21550a2cfd755abaefd3f38123733ffcd44e99736179ac0bae52fe1f58c61775f68d32f624cd87d2feb6cb4dbdbf4373a9d66b80d12eb80cbbe95ba9303626558d899af8106ced7de5ee279a67156c0a3c331aec02e87e29957b9c0ac857cce8a96e3f1df0c20d5de2919263398f66b9d600a73211678262826d92f4bcb00a9d6eb2c2eb2a7ecc4938a4014ac4c51c552607b6dc10cd7a5bd4df68a63f60e99a4217feee85660182837c340fc514b1babb8999f22252b722718ed1b4cba7feeea9e62cf1f928561264ffccd96ba25a5b2d251034805640ec7a51cd89c8c15551954728e5c4a0fb51a2eb096f42476fa59c85cede303994fab2c2b79d77b62c0e603897a38e965fefa190cb8f5a56e41f3e5d982ec1d07f555fdc8fb2ad2f4ffe98295c150230780734aa03b45309488fd2833b8381d4757d04fa792cc07684c3779d6e7925b62e7a9c7e4a480bbcb4d6fedac1f18128d5eb739d4b69e8b21071a541c26179848912a0d4cc4ff0a781cb6b31f878f02974c4f50900ed33f113e0bb0f26468012cc533c2ca3992893bab98c90ffc870737d1e3dbd4fefb3f3cad23798bd53a8582110eaffd4d743e338d4d722f37ce1ebcc857a827e17f21b11559e1967b8d77375ab7496a26c4b0b074a869f4dba80ee390a805941102fcfd60ae53451ffdea972f2540da6e0158f526d3746ad4151bbfc38f1e51d61b972ba86463812c0757425438511fb52e822cfaf6de308b220969aa9f86e81ea726dd04aec3f9d5ac8a8f17b0f20f9d609076fc00b14bd3fe02345a027a720c6e69b4a63e5e36183dbfd0f8cc4e91bbe3c3068c619a1b1d3403aff3af1b56cc7b1739136e60067ae287c98d9e2ce149030a202ac73941a6ff0f94086685820732dd374210e88b9f4b63485d7aedd71fb847236dba844137ce0450b47164a5f527c08afd78e8f640c14d87643dd72e423cf6fbba79157dcc420566c93478dd666c41ffa31c2ecd7be1dbce9d9bdcf168d4ada263e0d0f430011c777c6b3b40b46080e53d4119b244eff42c6e69ed48279af0cf4770293edaf511a16c3963ec99669ac601b463cbf66e3fab9c15d3060e000fa88e3d05bc656a1ba70800b6c5741a4423df85e4054f05520df212d9281dc34f7fb9249ecf8c3c0f4d2c8f7acc0e83aee1d251a75f8b91adcb606c62d3ede2690a94ac2278b8bb19f63535ec4947928b83cb975772c1af82294fa24b19f01347a7152ac2c3ddc62079fcae6d7b3760bb8b4c789def8284a0c967e2a4952a1a004f5f0eacd2cc75056ccd1e5bb952dff6b0d686e0518547fa090b71c61135b66143c3902bb940381e174fb8cabadfc57cf2b221173a632bb2118164ee73e7551653239d0f26b0ab0251555276a22689f249497e1aa2fdb80c1ccb10f1c9251356172afcc8daff492210ecc2afbe7e22c86e233d33094753e2d99ebb44400299bfadba8f00742d9d1a3407043af6996354516cea8df2b32a4a812e495d44034010823b33d462803f0b1a6e0bbd39623cc963d33a0a091ca88a609129f5ca0aa07564b4ddf1cf3fda284e629a09f29850659d05d665d6239a15df892595a38396e55b60e15f582ba40f41fd33fc05b2dfdcd49d4d2c57699a0ed952941b8cad789dcf07e30b681da50407bed8c5b81c23c1b3b42c545d849a58c8a573234b62976474fcf2a8fcaeb807573f4628bc97825e7f54b1a3ad043bea287e4445e53f042c4c6a15464841ae11ff27679c65cd10dc23087e03e9e29be3c1f3a2e9cb0ae4fa6854e09b9d26f1063cbac8be17a250addbc6bd7abaae71d56e594050d5c1d7df6b7442966069fd243dd956498931f6cf7cb430a0cd69c77ff8e4607c1afe006c8d74c3f813f7b41f1087a949f426d578c92f78fae3702cc06f55f93ea35fe7b8d001c8f615ce1d1ffe8754bad06ea54d0a3112a3810bf1bc5b9ae3baa0d2ab3c099718bf98bf4820658f23fbdb337379fddaaff6303ce007a89157a5a9a8b2509cb6f2e0c72114244743184f8156393fe128396af8c48d5a74dc8946b61d66137aa6ee7d4930cf264a587fdbd7aa0c068804057b2215c264c3c77663dd4cb58f2bdc2de0b5ab112b104da9bac0d9db69a448412588863ea869d4f890eb8e4faab5827299a111b42ee9463654303cd9f4dcff5e58afe88724abcd3fe4cb3168aef9b6ceb7e9b1a6156679fd2085e6db996c6540ada72d5ea0cff41cdb9483648f9f2685347e7bc512a9d6e535978c5ed0ce38f4cd903a38b3b61ddc2d79e14080166c08fb5272b52644a0ee32396a63c02d3746844ceec71816272a612ce308778c70a75119f5b0140916fafdc97b1f9dcadb8fd0735e7c5e57c9da0c017770e01e20ea42b73f771522f9921eeb1daa7e293c702d0f61b4569b5b516fca3820d31f6dd4ceda2f9728c0cfef56256dfb1a8d61cd21a3f45f7eb98e3091e05328e13e50cdbc6843ce2d8f397f0050b564b3ebd0acbafc6510b3e86e22b2d2047f8ec5257c46db621209a2a078c9dec80cc74b327642819a2f99c7be4b2e1d2199e2f2beac3dc2e5022d6d91efdb7b76b09877ceed20947ac3cab1538083c437423814930ef96dea4c15090bc6cebea4517d208e2e824892c6e16377ee7929e61d65ad7237dd82598010f7abaf49e01ec9e6e08a82c4e853284210a8c4702df5bf12123f71a1f0f5160d6794f9c7a9279b930eea4d341380919281804a4e94872273e2e5cc539c76901209f99b730c355459134fe2c0f31cf6b4588b1d44d3ebac7719797d95bd035208afabe41e1975f751fc36aaa3407e07c6d57413529e35a7c1b38491422ef1291f6a30c0c6daf41e7058c2591f6b097d9b2edde98f1b20ce6fd5aece66bea7fa2915ce7d77023ae83c91694c5f0b737add3e40e4843a6c18f6d707c3d2c85df0299529419ac1919c5bc769faedd0f12b209ccb799f5dc915a985c0708c680aa11d7895f6e24a12d2bfd0a60807702e15814b1520e87f3409de46e5209973232d01db4272a97d90adb153e7cc27dd74f4185a29398200a656d1d9dfd683aaf39313946aea52c413519d6b3bc374cc173cff10809b8909baa02b1e5159edee8bb62eac59720ed6dcacfe763a0b7cfe322855ebf7bff78973198d6938e4ffd88c8b0d32ae740ef3e56931f09c0c29f96ab6c186380f5071cec86000f7a22df3a72d0b895de61a03eac7b5558fbe848e4f10ff02e3908077f41ad02d24fd4cd0bb6b26e7e931f92d07c7fb6398efe14d166d8636b74c50f36125536e8d4e54b8ef490f9613caac20bc9595db4415c3c2220cdde4da13c96c7fc5aec1fe4d3959b9f924f963f560c6b25f8fde46885119f57a6f7371258532c6993d273a15634e653f02b022ab922892ed90592aab7ecbc248ca426f6c781699f05b94fd9699d9193d1fac31588805eb9e9c2b0f6d07d672153267fa971b65d7919fc06fed449ec9f10cfa1765e6fe32511c5cd9964110fa2e8c31067d0aca0594a8e30930f7cad4df0852eeb8f4ee0118d291d2d6bc65e845de6259392a3a23f0ebb4b8044e9b033a0e02436c782b517fd3571f5e640fc633f9eaea7afb3e5e52d7fb7ae85002bd43280e9cb71ca103b038f5440f07ffb5c07022c63c71b7b13c5d02d95611745d5e163831b7d8405c891e99b0187be7e16ee91a4df4d51a1f1e7471a4df113b506b27d0bd739504efa54410a51805c7a0c3473591bc5e1fb407b88989972bc23efa2d26ee8732f82d458d22a48f6f87698fb230510278f71e6110d3c824c27b0ad00a8cfa8560bd7d8182893afe1d9fb2977cb50d6fa56f26d1517d2f71e0ccb42622faa7016742636fb83a916407f2d4efc89f9946cc1cd1e0d1cc3807759633cb0510e8ac665d0a45803f1c9568411eef994874bfcc7931df17395ff51241ff5c124400dfcdd47389cb656691a889fe095e97f339d36646df49e57b965d8b33e214b4f694f8f0925e03fd5f40981b9286d6e407f18f92ffc8c85f83ef8d07181312b7055d28e732a517a8521eaaaa90a3ca67de07dc28e701b85c5dbdacfd5a5008b34dfe53d6fc73f30cb18c00518041bfc57d9f64b64e8678cd4c1188676e530227175b1eef5ff44203e722278bb91f98fe8b911573c39b9f761e7f73dad7a92dbf76cec5be468770a1978606c24b0d3e522f5120b693eff54d38b1d10cd80f114153657e6e4376f300f59fecc5e7709e17b7e14fe4af20642b7ad7bfe91bb0300fd5d9db6bf4df35703fe05bff89f997c274d95ccf9bf07eb5ddb8c8b8fc9c2f48ece63e29a592ea41c2685c73cd1097a2134a4c484b2b53d6b4bb234739e22abdc8030545ac4df71a37aaefb9c7a704ce293764bbbd30c4baadbcf6c369eb0ec714758cb33b4e304652545922f487f96c6a267ffd7fc0d7c11a25d6e0429608c2f6df6ef67e6390eff059329d36ba9db3eac7bca7a8f2ca56840f6141305f2c116a3db192620c0a131bbaefac4da65c54529043350d609fcad09ade9a24402e8725b3338cfb990f458ae7a4c52ccaf53ce9ae282547a422a26827d1d3dce2274e9b51112988587a3ab714f0ef42a1d876dc39e3c112a79cae3896a33d9a13072177ee6d6fea94f01eb78f935f8c9de33b62221ba77b347b9229a334d417182e59c4a6cd9fa657cc2a8667b92f6bb818b472c5d7715b61bc5989755ee041240801037f656357e6c0fa53e6b42cee535390a6876123780b29ba90d7c558da9f12d9a37b69f6f5b3e924428e8b08fc14af078a468c81f4c3c6c1e06c277de2a620a0f69e36deb8c19cddcb73fa68c8301e1ebdf39a72ade861dc02e5ab7f8ab428cab45a4ff96e733274d6d87f1df3e42f219b2d613394a1755de67872ffbeed131a535a20e7c418741bd73ca355044fa144a7936e930279ed6e3ded182d8cc61d89de26952b776cf1d17ed04d61968d07370a8d3950445b4bd9642394f003ff14a6c73f993dc9ca33bfb497cfcb7f27a2f3f2452fead8ab31a0182e3d3b70632ed9d410f61054261e79d7a3ac90233c3b473a3576fff10fcfd468e95749cbdd2add7cff11960f67ed0ef73691934d54f99e0ee94202a349f03436c695b561a81e808338a7d39286e584ac61ee5d2d94c0c22bd24bec13e7e10ef3125e2ae98301659ef85444a286a35b4837e5a034f3f1e4acfa0ae181957571788a160dec670c479afbd10fc99caf3c58705fe8f1fa7cd599743838be1e46742adc2f33a8691dd72b5b0c3d6dc48a5d7fb4bfe2e7112ab91b325348739db3d804d56d207583091d5c455a120de60438a23ac79f773c4f3a7d3f6ba689cd73e78d8fcb11c34928e9f3e2950b59524e5c978460553925ee38e93f4a36ba9fc319a2cb9554530603f73f6d43197e47255602bdbfa9d0f42763a3d7c827fe7652119d280f8bfcb1291f0c1ef60919a98e8fd034a894a276ef544c3c726a4d7765486549002b0b6401c820c3ae13077142caa1a9cf4dfeb037b575379d09bbfdfad8e9a3a65b442a2eb998ee8036dc8ee44b19765f3ad89c68250136bdaebcd6f20a64a2a996a0ee2a2e15af6c67e71250d9fb7dc0c4f5396938174d1836f598c91312cc390edbac22082e3de6dc6c42bbfc782edb4735308a414b581425c46fa1899c43dffab7ad1470b789f3a376d8f05e382e4eef5ad2f730a4c6ca71b3dc181bfedc014a3d2b00dea01112567d0465b3e605cbca12e841543e9f39ecc126724ac802fbccaa8c0238a905a0a909ba0c4d302b30dc3b77073e601e426d5ecdcd9e991f8cb9a833291480283cb121f1bdc9f638f91033e3aa6b5c64dc4006f5aae7cfd5d5bbfac70ba40e9cb25808a0a689515f811e67e7d63cc307cc75654506f44405570f452418754d1712c04993196cab35c7ea1f2144ecc1543a2c4e1591f3b94ecd495fee1c1bb846112a5aae6cb6a19faf9fb27b33277f32daa6f82c824fbb3055507bc818f96b1ec9ef2de81da729dca128fdcda775a39fab77b88be963fb242a099ccc0ece6a91d0bf6d93df0e19f975ad7da6e9f10a546949ebd6b18180ed3825a77e1c6e15d81d01398ec10c8fd8724f52c696b34de7e26ac48375189ddd503e19ed293aa1cd6eae95bcf17f3add4b4dfa09c3433fd656e0c3dadf111b8f1143a7e22516d371c2a38e182f341290fbb5056ac9f1ea51058ee260825918bb89a393502df87d475d7fc3d36b0dc606bcf6c369c51a422d9b95752543704a8d9c2ac374271e2d61cd899fffba684b47845bda1683342d76d8ee853a801288cb933b889f1446f0ad542d89e6721fae1dcf14690775cf7b2bea19715f14e2ce869bfe48871f14cafb1ebc870c16ad964b0571772d77f785dc4d116ac3ec7dd3e6e33a316d292cf4826fc7534dd40a58496a31a6698165ed2d15d6b65f4cf65a083b233081182c75c5c7256093280baf3e6039b485c966a4adc93e968128151b789e493478216822e156940e5383997307d339c3cd0f6e6b797ed3c20efa02724897beb560224117481c8293bacccec8b5930f3debc7459fa6c8e71d9f3277b14c5fa01c4311b90c808a4fdf40dc95186163f4e28cc568c5e5acbd27b7618bd0e5c9bb7efe2219af0f6204b7497060c1d9601f9efc7f317f7c6f7081ab661059f573305d9e2fd2eb51c85ac7b45341c2cc132192bc506304df61661e833aa262d3fa75e21208e014fe42ce0846f8b16168a662a57a5b55d0d27af4237a165855fd27f122851019b4c04fb5d022c676fc313ffb296a03eef785638c893853cfc602561ad32b101fd6ad5ea0fea4d4d5a492017fcad531bc307455c1b7905de3408a9d8850ce25e8e3a1e6ed3c3306eb705fe3785cb93b49278b24fb6a96793798d2f0a057052df7591a5099f3fcbb2e0e7410709cc4bbbe1b05460812d481bb5f02b59603601e677bb528c3a003004de33da4925468554eac3cb3229f138b19a1278a83d7ab15e9be54c76e61c983702c43cffc2d2e9b91907bc8d7274aa2212996a91e1c880655f31c7e5235bd1859b5362ec0d4e1c591b9da7391a509c56d6f7a331e6e7e0e8bd781ec70faec75a6bfb9b6405e5ad2ffe2acf7ab915b986a456f16b61c3f0e1244ad90e3f92e0b892ee2608af2845ec819cb196bfb2cdbda39e30bc912d81b93a3d81947e6fddc6b4f3efb313fed09e5c0d9f6a27b1e97bb5a143474d3b51a0a41cc70485edfc31889587523d0da2943be33c2ef8f1298b0a6194efa03ac29ccc15bb12ba5b38d399585be59fb9ece3a9e74f5a92249c7da5ff28469859c0770a3a910361232dad8292a99fcd4f06d55ad29a3abb22aa9de47d58d9567572260eb019429970a087a1bc0f84db9d940b67051598e083885cf0a54dd7c7f41166e9923e928060b8c82bd127cdddeb969328bd242e5043220916323ddef3e8f09c3519aac68604cb8f6cc8a01aa514bbe678b3c2d57c6a5d69459f3c6adbcd0064007fa97d6dd95157d7281574bb25d82d45f1357627bd5426b55360c9e1c022d671d7c295d43f13f84d21247c8b1ae74a060e60e8aca8c6e9520e0b6defe58", + "public_inputs_hex": "0x15c9b404c344691839abf638a132bd083b09e98c2fadb268923013737d217ad41dfdc7cb6265f100524012f038ee1f205bf8789b70b46c57463dedb4998f7ac800000000000000000000000000000000cb80a0dfbeaeb79af355a70d8f19855c00000000000000000000000000000000915681ec51465d5a3404f61907428b7100000000000000000000000000000000666965e3fee8efc43146d3b5cc157df900000000000000000000000000000000b2039cf00b2cd6017d01a8e38e5d2d102b26ea14534f288a6affb3a0aa4953baf450f69fa48000890435a8ee45258a0a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000214ae98e2b0527277d74d7898e5a1d76041a613ee74f2db890bd6db2fd3a76e9d30329528532c70174aa6a80f652ef9fb86217d722ac4e756dac8ec699873be87009de2f6efc0e2420bebb740d52be9795045be2c08d2f61c665fd8c2946f25a70037344482bad1ff4c46d1ed3887ee2f37d4bc2dd3a1c34c3c6a0b0d75d4fa630000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" } } } diff --git a/circuits/benchmarks/scripts/extract_crisp_verify_gas.sh b/circuits/benchmarks/scripts/extract_crisp_verify_gas.sh index 0b19109d5..ec0b753bb 100755 --- a/circuits/benchmarks/scripts/extract_crisp_verify_gas.sh +++ b/circuits/benchmarks/scripts/extract_crisp_verify_gas.sh @@ -5,7 +5,7 @@ # [--committee minimum|micro|small] [--verbose] [--skip-build] [--force-build] # # Integration test env (also set by run_benchmarks.sh): -# BENCHMARK_PROOF_AGGREGATION=true — always enabled in the benchmark harness +# CIPHERNODE_SKIP_PROOF_AGGREGATION=false — aggregation stays enabled in the benchmark harness # BENCHMARK_MULTITHREAD_JOBS=N — Rayon concurrent ZK jobs (default: 1) # BENCHMARK_DKG_FOLD_ATTESTATION_VERIFIER — EIP-712 verifying contract for fold attestations @@ -165,12 +165,12 @@ echo " [gas] Running CRISP verifier test for Pi_user gas..." CRISP_TEST_EXIT_CODE=${PIPESTATUS[0]} echo " [gas] CRISP test completed (exit=${CRISP_TEST_EXIT_CODE})." require_preset_artifacts -BENCHMARK_PROOF_AGGREGATION=true +CIPHERNODE_SKIP_PROOF_AGGREGATION=false echo " [gas] Running integration test (test_trbfv_actor); proof_aggregation=true, multithread_jobs=${BENCHMARK_MULTITHREAD_JOBS:-1}, profile=release..." ( cd "$REPO_ROOT" && \ BENCHMARK_MODE="$MODE" \ - BENCHMARK_PROOF_AGGREGATION=true \ + CIPHERNODE_SKIP_PROOF_AGGREGATION=false \ BENCHMARK_FOLDED_OUTPUT="$TMP_JSON_FOLDED" \ BENCHMARK_SUMMARY_OUTPUT="$TMP_JSON_SUMMARY" \ cargo test --release -p e3-tests test_trbfv_actor -- --nocapture diff --git a/circuits/benchmarks/scripts/run_benchmarks.sh b/circuits/benchmarks/scripts/run_benchmarks.sh index 566ba2993..59f4e5306 100755 --- a/circuits/benchmarks/scripts/run_benchmarks.sh +++ b/circuits/benchmarks/scripts/run_benchmarks.sh @@ -22,7 +22,7 @@ CIRCUIT_FILTER="" VERBOSE=false PRESET_ARTIFACTS_READY=false MULTITHREAD_JOBS="${BENCHMARK_MULTITHREAD_JOBS:-}" -export BENCHMARK_PROOF_AGGREGATION=true +export CIPHERNODE_SKIP_PROOF_AGGREGATION=false # Parse arguments while [[ $# -gt 0 ]]; do diff --git a/circuits/bin/recursive_aggregation/c6_fold/src/main.nr b/circuits/bin/recursive_aggregation/c6_fold/src/main.nr index 932548cd7..e6ff0126d 100644 --- a/circuits/bin/recursive_aggregation/c6_fold/src/main.nr +++ b/circuits/bin/recursive_aggregation/c6_fold/src/main.nr @@ -14,11 +14,13 @@ use bb_proof_verification::{ }; use lib::configs::default::T; -/// One C6 public row: `expected_sk`, `expected_e_sm`, `ct_commitment`, `d_commitment` (see C6 leaf). -pub global C6_PUBLIC_LEN: u32 = 4; +/// One C6 public row: `expected_sk`, `expected_e_sm`, `ct_commitment`, +/// `domain_hi`, `domain_lo`, `d_commitment` (see C6 leaf). +pub global C6_PUBLIC_LEN: u32 = 6; -/// Same shape as `c3_fold`: 4 pub parameters + 4 columns of `T + 1` party slots each. -pub global C6_FOLD_PUBLIC_LEN: u32 = 4 + (4 * (T + 1)); +/// Four pub parameters + the common domain limbs + four columns of +/// `T + 1` party slots each. +pub global C6_FOLD_PUBLIC_LEN: u32 = 6 + (4 * (T + 1)); pub global C6_FOLD_PREFIX_LEN: u32 = C6_FOLD_PUBLIC_LEN - (4 * (T + 1)); @@ -33,7 +35,7 @@ fn main( acc_key_hash: pub Field, is_first_step: pub bool, slot_index: pub u32, -) -> pub ([Field; T + 1], [Field; T + 1], [Field; T + 1], [Field; T + 1]) { +) -> pub (Field, Field, [Field; T + 1], [Field; T + 1], [Field; T + 1], [Field; T + 1]) { assert(slot_index < T + 1); verify_honk_proof(inner_vk, inner_proof, c6_public_inputs, inner_key_hash); @@ -43,7 +45,14 @@ fn main( let sk_new = c6_public_inputs[0]; let esm_new = c6_public_inputs[1]; let ct_new = c6_public_inputs[2]; - let d_new = c6_public_inputs[3]; + let domain_hi = c6_public_inputs[3]; + let domain_lo = c6_public_inputs[4]; + let d_new = c6_public_inputs[5]; + + if !is_first_step { + assert(acc_public_inputs[4] == domain_hi); + assert(acc_public_inputs[5] == domain_lo); + } let mut out_sk: [Field; T + 1] = [0; T + 1]; let mut out_esm: [Field; T + 1] = [0; T + 1]; @@ -88,5 +97,5 @@ fn main( } } - (out_sk, out_esm, out_ct, out_d) + (domain_hi, domain_lo, out_sk, out_esm, out_ct, out_d) } diff --git a/circuits/bin/recursive_aggregation/c6_fold_kernel/src/main.nr b/circuits/bin/recursive_aggregation/c6_fold_kernel/src/main.nr index 6abb49fbe..593c82263 100644 --- a/circuits/bin/recursive_aggregation/c6_fold_kernel/src/main.nr +++ b/circuits/bin/recursive_aggregation/c6_fold_kernel/src/main.nr @@ -14,11 +14,13 @@ use bb_proof_verification::{ }; use lib::configs::default::T; -/// One C6 public row: `expected_sk`, `expected_e_sm`, `ct_commitment`, `d_commitment` (see C6 leaf). -pub global C6_PUBLIC_LEN: u32 = 4; +/// One C6 public row: `expected_sk`, `expected_e_sm`, `ct_commitment`, +/// `domain_hi`, `domain_lo`, `d_commitment` (see C6 leaf). +pub global C6_PUBLIC_LEN: u32 = 6; -/// Same shape as `c6_fold`: 4 pub parameters + 4 columns of `T + 1` party slots each. -pub global C6_FOLD_PUBLIC_LEN: u32 = 4 + (4 * (T + 1)); +/// Same shape as `c6_fold`: 4 pub parameters + the common domain limbs + +/// 4 columns of `T + 1` party slots each. +pub global C6_FOLD_PUBLIC_LEN: u32 = 6 + (4 * (T + 1)); fn main( inner_vk: UltraHonkVerificationKey, @@ -31,7 +33,7 @@ fn main( acc_key_hash: pub Field, is_first_step: pub bool, slot_index: pub u32, -) -> pub ([Field; T + 1], [Field; T + 1], [Field; T + 1], [Field; T + 1]) { +) -> pub (Field, Field, [Field; T + 1], [Field; T + 1], [Field; T + 1], [Field; T + 1]) { assert(slot_index < T + 1); verify_honk_proof(inner_vk, inner_proof, c6_public_inputs, inner_key_hash); @@ -44,7 +46,9 @@ fn main( let sk_new = c6_public_inputs[0]; let esm_new = c6_public_inputs[1]; let ct_new = c6_public_inputs[2]; - let d_new = c6_public_inputs[3]; + let domain_hi = c6_public_inputs[3]; + let domain_lo = c6_public_inputs[4]; + let d_new = c6_public_inputs[5]; let mut out_sk: [Field; T + 1] = [0; T + 1]; let mut out_esm: [Field; T + 1] = [0; T + 1]; @@ -66,5 +70,5 @@ fn main( } } - (out_sk, out_esm, out_ct, out_d) + (domain_hi, domain_lo, out_sk, out_esm, out_ct, out_d) } diff --git a/circuits/bin/recursive_aggregation/decryption_aggregator/src/main.nr b/circuits/bin/recursive_aggregation/decryption_aggregator/src/main.nr index 67f6a513b..fce82e7d1 100644 --- a/circuits/bin/recursive_aggregation/decryption_aggregator/src/main.nr +++ b/circuits/bin/recursive_aggregation/decryption_aggregator/src/main.nr @@ -21,8 +21,9 @@ use lib::configs::default::T; use lib::math::commitments::compute_vk_hash; use lib::math::committee_hash::committee_hash_limbs; -/// Must match `c6_fold::C6_FOLD_PUBLIC_LEN`: pub params + flattened `(out_sk, out_esm, out_ct, out_d)`. -pub global C6_FOLD_PUBLIC_LEN: u32 = 4 + (4 * (T + 1)); +/// Must match `c6_fold::C6_FOLD_PUBLIC_LEN`: pub params, common domain limbs, +/// and flattened `(out_sk, out_esm, out_ct, out_d)`. +pub global C6_FOLD_PUBLIC_LEN: u32 = 6 + (4 * (T + 1)); pub global C6_FOLD_PREFIX_LEN: u32 = C6_FOLD_PUBLIC_LEN - (4 * (T + 1)); @@ -53,6 +54,8 @@ fn main( committee_members: [Field; N_PARTIES], committee_hash_hi: pub Field, committee_hash_lo: pub Field, + domain_hi: pub Field, + domain_lo: pub Field, ) -> pub (Field, [Field; T + 1], [Field; T + 1], [Field; T + 1], [Field; MAX_MSG_NON_ZERO_COEFFS]) { let (expected_hi, expected_lo) = committee_hash_limbs(committee_members); assert(expected_hi == committee_hash_hi); @@ -61,6 +64,11 @@ fn main( verify_honk_proof_non_zk(c6_fold_vk, c6_fold_proof, c6_fold_public, c6_fold_key_hash); verify_honk_proof_non_zk(c7_vk, c7_proof, c7_public, c7_key_hash); + // The C6 fold only accepts secret-bearing leaf proofs carrying one common + // domain. Expose that domain in the final EVM proof for on-chain checking. + assert(c6_fold_public[4] == domain_hi); + assert(c6_fold_public[5] == domain_lo); + // Cross-circuit C6 to C7: each folded `d_commitment` column entry must match `c7_public[i]` // (leading `T + 1` words of `c7_public`). for i in 0..(T + 1) { diff --git a/circuits/bin/threshold/share_decryption/src/main.nr b/circuits/bin/threshold/share_decryption/src/main.nr index da2ad3cac..de7f03c9b 100644 --- a/circuits/bin/threshold/share_decryption/src/main.nr +++ b/circuits/bin/threshold/share_decryption/src/main.nr @@ -18,6 +18,8 @@ fn main( expected_sk_commitment: pub Field, expected_e_sm_commitment: pub Field, ct_commitment: pub Field, + domain_hi: pub Field, + domain_lo: pub Field, ct0: [Polynomial; L], ct1: [Polynomial; L], sk: [Polynomial; L], @@ -27,6 +29,10 @@ fn main( d: [Polynomial; L], d_native_trunc: [Polynomial; L], ) -> pub Field { + // The domain is intentionally not part of the BFV arithmetic. Making it a + // public input of this secret-bearing proof cryptographically authorizes + // this exact E3 context; recursive aggregation must preserve both limbs. + let _ = (domain_hi, domain_lo); let share_decryption: ShareDecryption = ShareDecryption::new( THRESHOLD_SHARE_DECRYPTION_CONFIGS, expected_sk_commitment, diff --git a/crates/aggregator/src/committee.rs b/crates/aggregator/src/committee.rs index ed13bea53..4129f338e 100644 --- a/crates/aggregator/src/committee.rs +++ b/crates/aggregator/src/committee.rs @@ -22,10 +22,10 @@ pub fn committee_addresses_from_nodes(nodes: &OrderedSet) -> Result, diff --git a/crates/aggregator/src/ext.rs b/crates/aggregator/src/ext.rs index 309bb73b4..3149ea5a7 100644 --- a/crates/aggregator/src/ext.rs +++ b/crates/aggregator/src/ext.rs @@ -202,13 +202,19 @@ fn create_publickey_aggregator( pub struct ThresholdPlaintextAggregatorExtension { bus: BusHandle, sortition: Addr, + proof_aggregation_enabled: bool, } impl ThresholdPlaintextAggregatorExtension { - pub fn create(bus: &BusHandle, sortition: &Addr) -> Box { + pub fn create( + bus: &BusHandle, + sortition: &Addr, + proof_aggregation_enabled: bool, + ) -> Box { Box::new(Self { bus: bus.clone(), sortition: sortition.clone(), + proof_aggregation_enabled, }) } @@ -287,7 +293,7 @@ impl ThresholdPlaintextAggregatorExtension { return false; } }, - proof_aggregation_enabled: meta.proof_aggregation_enabled, + proof_aggregation_enabled: self.proof_aggregation_enabled, committee_addresses, honest_committee_addresses, }, @@ -626,7 +632,7 @@ impl E3Extension for ThresholdPlaintextAggregatorExtension { meta.threshold_n ) })?, - proof_aggregation_enabled: meta.proof_aggregation_enabled, + proof_aggregation_enabled: self.proof_aggregation_enabled, committee_addresses, honest_committee_addresses, }, diff --git a/crates/aggregator/src/plaintext_aggregation/effects/prove_plaintext.rs b/crates/aggregator/src/plaintext_aggregation/effects/prove_plaintext.rs index 3b796f072..4a5658c7f 100644 --- a/crates/aggregator/src/plaintext_aggregation/effects/prove_plaintext.rs +++ b/crates/aggregator/src/plaintext_aggregation/effects/prove_plaintext.rs @@ -88,7 +88,10 @@ impl ThresholdPlaintextAggregator { } if !self.proof_aggregation_enabled { if self.pending.decryption_aggregator_proofs.is_none() { - self.pending.decryption_aggregator_proofs = Some(Vec::new()); + // Reuse the already-generated C7 proofs as non-empty test placeholders. Mock + // decryption verifiers accept them; production verifiers reject them because + // they are not DecryptionAggregator proofs. + self.pending.decryption_aggregator_proofs = self.pending.c7_proofs_pending.clone(); } return Ok(()); } @@ -117,7 +120,7 @@ impl ThresholdPlaintextAggregator { return Ok(()); } if !self.proof_aggregation_enabled { - self.pending.decryption_aggregator_proofs = Some(Vec::new()); + self.pending.decryption_aggregator_proofs = self.pending.c7_proofs_pending.clone(); return Ok(()); } let Some(honest_c6) = self.pending.honest_c6_proofs_for_agg.as_ref() else { diff --git a/crates/aggregator/src/plaintext_aggregation/tests/completion.rs b/crates/aggregator/src/plaintext_aggregation/tests/completion.rs index 8b0bb3475..4e58ce95a 100644 --- a/crates/aggregator/src/plaintext_aggregation/tests/completion.rs +++ b/crates/aggregator/src/plaintext_aggregation/tests/completion.rs @@ -43,10 +43,11 @@ async fn missing_c6_inner_proofs_emit_e3_failed() -> Result<()> { } #[actix::test] -async fn proof_aggregation_disabled_marks_decryption_aggregator_ready() -> Result<()> { +async fn test_skip_reuses_c7_as_mock_verifier_placeholder() -> Result<()> { let (mut aggregator, _history, _e3_id) = build_plaintext_aggregator(generating_c7_state(), false).await?; - aggregator.pending.c7_proofs_pending = Some(vec![dummy_proof(CircuitName::PkAggregation)]); + let c7_proof = dummy_proof(CircuitName::PkAggregation); + aggregator.pending.c7_proofs_pending = Some(vec![c7_proof.clone()]); let ec = test_ctx(E3Failed { e3_id: aggregator.e3_id.clone(), failed_at_stage: E3Stage::None, @@ -58,7 +59,7 @@ async fn proof_aggregation_disabled_marks_decryption_aggregator_ready() -> Resul .pending .decryption_aggregator_proofs .as_ref() - .is_some_and(|p| p.is_empty())); + .is_some_and(|proofs| proofs == &[c7_proof])); assert!(aggregator .pending .decryption_aggregation_correlation diff --git a/crates/aggregator/src/public_key_aggregation/effects/aggregate_dkg_proofs.rs b/crates/aggregator/src/public_key_aggregation/effects/aggregate_dkg_proofs.rs index 7980ee512..aa676e7de 100644 --- a/crates/aggregator/src/public_key_aggregation/effects/aggregate_dkg_proofs.rs +++ b/crates/aggregator/src/public_key_aggregation/effects/aggregate_dkg_proofs.rs @@ -44,9 +44,10 @@ impl PublicKeyAggregator { return Ok(()); } - // `proof_aggregation_enabled` is an E3-level flag shared by all nodes, so honest-party - // proofs should be uniformly Some (aggregation on) or uniformly None (aggregation off). - // A mixed bag would silently truncate the dispatched request below; reject it explicitly. + // Proof aggregation is a node-level test/CI setting and must be configured consistently + // across a test swarm. Honest-party proofs should therefore be uniformly Some + // (aggregation on) or uniformly None (aggregation skipped). A mixed bag would silently + // truncate the dispatched request below; reject it explicitly. let some_count = honest_party_ids .iter() .filter(|id| { @@ -140,10 +141,11 @@ impl PublicKeyAggregator { ); if node_fold_proofs.is_empty() { - // Proof aggregation disabled. Do NOT call `try_publish_complete` here — it + // Proof aggregation was skipped by the node's test/CI setting. Do NOT call + // `try_publish_complete` here — it // is the most common entry into this method, so re-entering it would create // unbounded mutual recursion (stack overflow in deployed nodes). - info!("PublicKeyAggregator: proof aggregation disabled — skipping DkgAggregation"); + info!("PublicKeyAggregator: test/CI skip flag active — skipping DkgAggregation"); return Ok(()); } diff --git a/crates/aggregator/src/public_key_aggregation/effects/publish_result.rs b/crates/aggregator/src/public_key_aggregation/effects/publish_result.rs index c4f2ac589..d0fa9411b 100644 --- a/crates/aggregator/src/public_key_aggregation/effects/publish_result.rs +++ b/crates/aggregator/src/public_key_aggregation/effects/publish_result.rs @@ -5,7 +5,8 @@ use super::super::*; impl PublicKeyAggregator { - /// Publish `PublicKeyAggregated` when C5 (non-ZK recursive) and, if applicable, the EVM DkgAggregator proof are ready (or aggregation skipped). + /// Publish `PublicKeyAggregated` when C5 and the final DkgAggregator proof are ready, or when + /// a test/CI node deliberately skips recursive aggregation. pub(in crate::actors::publickey_aggregator) fn try_publish_complete(&mut self) -> Result<()> { if let Some(ec) = self.state.get().and_then(|s| { if let PublicKeyAggregatorState::GeneratingC5Proof { last_ec, .. } = &s { @@ -68,17 +69,23 @@ impl PublicKeyAggregator { .clone() .ok_or_else(|| anyhow::anyhow!("No EventContext for publish"))?; + let pk_commitment = extract_pk_commitment(c5_proof)?; + // Test/CI nodes reuse the already-generated C5 proof as a non-empty placeholder. Mock + // verifiers accept it; production DKG verifiers reject it because it is not a + // DkgAggregator proof. This keeps the testing escape hatch entirely in the ciphernode. + let published_dkg_proof = dkg_aggregated_proof + .clone() + .or_else(|| all_proofs_are_none.then(|| c5_proof.clone())); + info!( "Publishing PublicKeyAggregated (dkg_evm_proof={})", if dkg_aggregated_proof.is_some() { - "present" + "aggregated" } else { - "skipped" + "test-placeholder" } ); - let pk_commitment = extract_pk_commitment(c5_proof)?; - // Full committee (N entries) — used by on-chain `committee_hash` binding. let mut full_committee_party_ids: Vec = party_nodes.keys().copied().collect(); full_committee_party_ids.sort(); @@ -99,7 +106,9 @@ impl PublicKeyAggregator { )?; Some(ArcBytes::from_bytes(&bundle)) } - None => None, + // The mock fold-attestation verifier used by test deployments only requires a + // non-empty payload. Production deployments never take this path. + None => Some(ArcBytes::from_bytes(&[1])), }; let event = PublicKeyAggregated { @@ -109,7 +118,7 @@ impl PublicKeyAggregator { committee_addresses: committee_addresses.clone(), honest_committee_addresses: honest_committee_addresses.clone(), pk_commitment, - dkg_aggregator_proof: dkg_aggregated_proof.clone(), + dkg_aggregator_proof: published_dkg_proof, dkg_attestation_bundle, }; self.bus.publish(event, ec.clone())?; diff --git a/crates/aggregator/src/public_key_aggregation/keyshare_buffer.rs b/crates/aggregator/src/public_key_aggregation/keyshare_buffer.rs index 8af5b9bab..17c97afbd 100644 --- a/crates/aggregator/src/public_key_aggregation/keyshare_buffer.rs +++ b/crates/aggregator/src/public_key_aggregation/keyshare_buffer.rs @@ -124,7 +124,7 @@ impl Handler for KeyshareCreatedFilterBuffer { }, InterfoldEventData::CommitteeFinalized(data) => { let mut data = data.clone(); - data.sort_by_score(); + data.sort_by_address(); self.committee = Some(data.committee); self.process_buffered_events(); } diff --git a/crates/bfv-client/src/client.rs b/crates/bfv-client/src/client.rs index 91cc11e5a..7f26a2037 100644 --- a/crates/bfv-client/src/client.rs +++ b/crates/bfv-client/src/client.rs @@ -185,6 +185,38 @@ pub fn compute_pk_commitment( Ok(commitment) } +/// Validate client-consumed BFV public-key bytes against the on-chain +/// commitment (C5-proven by the mandatory final DKG proof). +/// +/// Validation is semantic rather than byte-for-byte: `fhe.rs` normalizes an +/// internal variable-time flag while decoding threshold-aggregated keys, so a +/// decode/re-encode cycle is not a stable serialization check. The decoded key +/// is safe to consume only when its circuit commitment matches the expected +/// on-chain commitment. +pub fn validate_pk_commitment( + public_key: &[u8], + expected_commitment: [u8; 32], + degree: usize, + plaintext_modulus: u64, + moduli: Vec, +) -> Result<()> { + use e3_zk_helpers::circuits::threshold::user_data_encryption::utils::compute_public_key_commitment; + + let params = build_client_params(degree, plaintext_modulus, &moduli, None)?; + let decoded = PublicKey::from_bytes(public_key, ¶ms) + .map_err(|e| anyhow!("Error deserializing public key: {e}"))?; + + let actual_commitment = compute_public_key_commitment(¶ms, &decoded) + .map_err(|e| anyhow!("Error computing public key commitment: {e}"))?; + if actual_commitment != expected_commitment { + return Err(anyhow!( + "Public key commitment mismatch: event bytes are not the key proven by C5" + )); + } + + Ok(()) +} + pub fn compute_ct_commitment( ct: Vec, degree: usize, @@ -238,6 +270,69 @@ mod tests { assert!(error.to_string().contains("Invalid BFV parameters")); } + #[test] + fn validates_threshold_key_matching_the_proven_commitment() { + use fhe::mbfv::{Aggregate as _, CommonRandomPoly, PublicKeyShare}; + + let param_set: BfvParamSet = DEFAULT_BFV_PRESET.into(); + let params = build_bfv_params_from_set_arc(param_set); + let mut rng = rng(); + let crp = CommonRandomPoly::new(¶ms, &mut rng).unwrap(); + let shares = (0..2) + .map(|_| { + let secret_key = SecretKey::random(¶ms, &mut rng); + PublicKeyShare::new(&secret_key, crp.clone(), &mut rng).unwrap() + }) + .collect::>(); + let expected_key = PublicKey::from_shares(shares).unwrap(); + let substituted_key = PublicKey::new(&SecretKey::random(¶ms, &mut rng), &mut rng); + let expected_bytes = expected_key.to_bytes(); + let normalized_bytes = PublicKey::from_bytes(&expected_bytes, ¶ms) + .unwrap() + .to_bytes(); + assert_ne!( + normalized_bytes, expected_bytes, + "threshold aggregation must exercise fhe.rs variable-time normalization" + ); + let expected_commitment = compute_pk_commitment( + expected_bytes.clone(), + param_set.degree, + param_set.plaintext_modulus, + param_set.moduli.to_vec(), + ) + .unwrap(); + + validate_pk_commitment( + &expected_bytes, + expected_commitment, + param_set.degree, + param_set.plaintext_modulus, + param_set.moduli.to_vec(), + ) + .unwrap(); + + let error = validate_pk_commitment( + &substituted_key.to_bytes(), + expected_commitment, + param_set.degree, + param_set.plaintext_modulus, + param_set.moduli.to_vec(), + ) + .unwrap_err(); + assert!(error.to_string().contains("commitment mismatch")); + + let mut equivalent_encoding = expected_bytes; + equivalent_encoding.extend_from_slice(&[0x78, 0x01]); + validate_pk_commitment( + &equivalent_encoding, + expected_commitment, + param_set.degree, + param_set.plaintext_modulus, + param_set.moduli.to_vec(), + ) + .unwrap(); + } + #[test] fn verifiable_parameter_builder_uses_the_canonical_error_variance() { for preset in BfvPreset::PAIR_PRESETS { diff --git a/crates/bfv-client/src/lib.rs b/crates/bfv-client/src/lib.rs index 0a216f4f1..5f1891f49 100644 --- a/crates/bfv-client/src/lib.rs +++ b/crates/bfv-client/src/lib.rs @@ -13,6 +13,7 @@ use thiserror::Error as ThisError; pub use client::VerifiableEncryptionResult; pub use client::{ bfv_encrypt, bfv_verifiable_encrypt, compute_ct_commitment, compute_pk_commitment, + validate_pk_commitment, }; #[derive(ThisError, Debug)] diff --git a/crates/ciphernode-builder/Cargo.toml b/crates/ciphernode-builder/Cargo.toml index be212b33c..aeb68865f 100644 --- a/crates/ciphernode-builder/Cargo.toml +++ b/crates/ciphernode-builder/Cargo.toml @@ -6,6 +6,9 @@ license.workspace = true description = "E3 - Ciphernode Builder" repository = "https://github.com/theinterfold/interfold/crates/ciphernode-builder" +[features] +test-only-skip-proof-aggregation = [] + [dependencies] actix.workspace = true alloy.workspace = true diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 5e965017a..571fc8856 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -80,6 +80,7 @@ pub struct CiphernodeBuilder { multithread_cache: Option>, multithread_concurrent_jobs: Option, multithread_report: Option>, + proof_aggregation_enabled: bool, pubkey_agg: bool, rng: SharedRng, sortition_backend: SortitionBackend, @@ -148,6 +149,7 @@ impl CiphernodeBuilder { multithread_cache: None, multithread_concurrent_jobs: None, multithread_report: None, + proof_aggregation_enabled: true, pubkey_agg: false, rng, sortition_backend: SortitionBackend::score(), @@ -325,6 +327,17 @@ impl CiphernodeBuilder { self } + /// Skip recursive proof aggregation for tests and CI. + /// + /// This only changes local ciphernode work. Contracts still verify the final DKG and + /// decryption proof payloads, so this setting is only useful with test deployments whose + /// configured mock verifiers accept the non-empty C5/C7 placeholder payloads. + #[cfg(feature = "test-only-skip-proof-aggregation")] + pub fn with_proof_aggregation_disabled_for_testing(mut self) -> Self { + self.proof_aggregation_enabled = false; + self + } + /// Connect rayon work to the given threadpool pub fn with_shared_taskpool(mut self, pool: &TaskPool) -> Self { self.task_pool = Some(pool.clone()); @@ -800,9 +813,26 @@ impl CiphernodeBuilder { backend.ensure_installed().await?; let _signer = provider_cache.ensure_signer().await?; + let mut interfold_addresses = HashMap::new(); + for chain in self.chains.iter().filter(|c| c.enabled.unwrap_or(true)) { + let provider = provider_cache.ensure_read_provider(chain).await?; + let chain_id = provider.chain_id(); + validate_chain_id(chain, chain_id)?; + interfold_addresses.insert(chain_id, chain.contracts.interfold.address()?); + } + for chain in self.chains.iter().filter(|c| !c.enabled.unwrap_or(true)) { + if let Some(chain_id) = chain.chain_id { + interfold_addresses.insert(chain_id, chain.contracts.interfold.address()?); + } + } + info!("Setting up ThresholdKeyshareExtension"); - e3_builder = - e3_builder.with(ThresholdKeyshareExtension::create(bus, &self.cipher, addr)); + e3_builder = e3_builder.with(ThresholdKeyshareExtension::create( + bus, + &self.cipher, + addr, + interfold_addresses, + )); info!("Setting up ZK actors"); setup_zk_actors( @@ -811,6 +841,7 @@ impl CiphernodeBuilder { _signer, dkg_fold_verifier_by_chain.clone(), zk_recovery.clone(), + self.proof_aggregation_enabled, ); } @@ -836,6 +867,7 @@ impl CiphernodeBuilder { signer, dkg_fold_verifier_by_chain.clone(), zk_recovery, + self.proof_aggregation_enabled, ); } } @@ -845,7 +877,9 @@ impl CiphernodeBuilder { info!("Setting up ThresholdPlaintextAggregatorExtension"); let _ = self.ensure_multithread(bus); e3_builder = e3_builder.with(ThresholdPlaintextAggregatorExtension::create( - bus, sortition, + bus, + sortition, + self.proof_aggregation_enabled, )); } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 7239c9df3..b77de1f77 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -7,6 +7,11 @@ description = "E3 - Interfold CLI" repository = "https://github.com/theinterfold/interfold/crates/cli" build = "build.rs" +[features] +test-only-skip-proof-aggregation = [ + "e3-entrypoint/test-only-skip-proof-aggregation", +] + [[bin]] name = "interfold" path = "src/main.rs" diff --git a/crates/committee-hash/Cargo.toml b/crates/committee-hash/Cargo.toml index da2ab4be8..6a2497dc4 100644 --- a/crates/committee-hash/Cargo.toml +++ b/crates/committee-hash/Cargo.toml @@ -3,9 +3,10 @@ name = "e3-committee-hash" version.workspace = true edition.workspace = true license.workspace = true -description = "Canonical committee hash for DKG / decryption aggregator proofs" +description = "Canonical EVM domain hashes for DKG / decryption aggregator proofs" repository = "https://github.com/theinterfold/interfold/crates/committee-hash" [dependencies] alloy = { workspace = true } hex = { workspace = true } +serde = { workspace = true } diff --git a/crates/committee-hash/src/lib.rs b/crates/committee-hash/src/lib.rs index 798ecd244..03b207ba3 100644 --- a/crates/committee-hash/src/lib.rs +++ b/crates/committee-hash/src/lib.rs @@ -4,10 +4,16 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -//! Canonical committee hash for DKG / decryption aggregator proofs. -//! Must match `CommitteeHashLib.sol` (`keccak256(abi.encodePacked(addresses))`). +//! Canonical EVM hashes for DKG / decryption aggregator proofs. +//! Committee hashing must match `CommitteeHashLib.sol` +//! (`keccak256(abi.encodePacked(addresses))`). Decryption-domain hashing must +//! match `InterfoldPricing.decryptionDomain`. -use alloy::primitives::{keccak256, Address, B256}; +use alloy::{ + primitives::{keccak256, Address, B256, U256}, + sol_types::SolValue, +}; +use serde::{Deserialize, Serialize}; /// Hi/lo limbs of `keccak256(abi.encodePacked(addresses))` for Noir public inputs. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -16,6 +22,22 @@ pub struct CommitteeHashLimbs { pub lo: B256, } +/// Stable, non-secret context needed to bind a C6 decryption-share proof to +/// the E3 that authorized it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DecryptionDomainContext { + pub interfold_address: Address, + pub committee_hash: B256, + pub committee_public_key: B256, +} + +/// Hi/lo 128-bit limbs of the decryption-domain hash for Noir public inputs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DecryptionDomainLimbs { + pub hi: u128, + pub lo: u128, +} + /// `keccak256(abi.encodePacked(addresses))` for the ordered on-chain committee. pub fn hash_committee_addresses(addresses: &[Address]) -> B256 { let packed: Vec = addresses @@ -49,6 +71,56 @@ pub fn committee_hash_field_hex(addresses: &[Address]) -> (String, String) { (field_hex_from_b256(limbs.hi), field_hex_from_b256(limbs.lo)) } +/// Compute the E3 decryption domain: +/// +/// `keccak256(abi.encode(chainId, interfold, e3Id, committeeHash, +/// ciphertextOutputHash, committeePublicKey))`. +/// +/// The Interfold address prevents cross-deployment replay. The remaining +/// fields prevent replay across chains, E3s, committees, ciphertexts, or DKG +/// keys within one deployment. +pub fn hash_decryption_domain( + chain_id: u64, + e3_id: U256, + context: DecryptionDomainContext, + ciphertext_output_hash: B256, +) -> B256 { + keccak256( + ( + U256::from(chain_id), + context.interfold_address, + e3_id, + context.committee_hash, + ciphertext_output_hash, + context.committee_public_key, + ) + .abi_encode(), + ) +} + +/// Split a decryption-domain hash into two 128-bit Noir field elements. +pub fn split_decryption_domain(hash: B256) -> DecryptionDomainLimbs { + DecryptionDomainLimbs { + hi: u128::from_be_bytes(hash[..16].try_into().expect("16-byte high limb")), + lo: u128::from_be_bytes(hash[16..].try_into().expect("16-byte low limb")), + } +} + +/// Hash and split the E3 decryption domain in one step. +pub fn decryption_domain_limbs( + chain_id: u64, + e3_id: U256, + context: DecryptionDomainContext, + ciphertext_output_hash: B256, +) -> DecryptionDomainLimbs { + split_decryption_domain(hash_decryption_domain( + chain_id, + e3_id, + context, + ciphertext_output_hash, + )) +} + fn field_hex_from_b256(value: B256) -> String { format!("0x{}", hex::encode(value)) } @@ -89,4 +161,48 @@ mod tests { expected_lo[16..].copy_from_slice(&hash.0[16..]); assert_eq!(limbs.lo.0, expected_lo); } + + #[test] + fn decryption_domain_matches_solidity_abi_layout() { + let context = DecryptionDomainContext { + interfold_address: address!("0x1111111111111111111111111111111111111111"), + committee_hash: B256::repeat_byte(0x22), + committee_public_key: B256::repeat_byte(0x44), + }; + let ciphertext_hash = B256::repeat_byte(0x33); + let chain_id = 31_337u64; + let e3_id = U256::from(7); + + let mut encoded = [0u8; 32 * 6]; + U256::from(chain_id) + .to_be_bytes_vec() + .iter() + .enumerate() + .for_each(|(index, byte)| encoded[index] = *byte); + encoded[32 + 12..64].copy_from_slice(context.interfold_address.as_slice()); + e3_id + .to_be_bytes_vec() + .iter() + .enumerate() + .for_each(|(index, byte)| encoded[64 + index] = *byte); + encoded[96..128].copy_from_slice(context.committee_hash.as_slice()); + encoded[128..160].copy_from_slice(ciphertext_hash.as_slice()); + encoded[160..192].copy_from_slice(context.committee_public_key.as_slice()); + + let expected = keccak256(encoded); + assert_eq!( + hash_decryption_domain(chain_id, e3_id, context, ciphertext_hash), + expected + ); + + let limbs = split_decryption_domain(expected); + assert_eq!( + limbs.hi, + u128::from_be_bytes(expected[..16].try_into().unwrap()) + ); + assert_eq!( + limbs.lo, + u128::from_be_bytes(expected[16..].try_into().unwrap()) + ); + } } diff --git a/crates/config/src/app_config.rs b/crates/config/src/app_config.rs index 821be4cd3..b53d657ba 100644 --- a/crates/config/src/app_config.rs +++ b/crates/config/src/app_config.rs @@ -58,7 +58,9 @@ pub struct NodeDefinition { #[serde(default = "default_multithread_reserve_threads")] pub multithread_reserve_threads: usize, /// Max concurrent CPU-bound jobs (ZK proofs + TrBFV). When unset, defaults to all CPUs minus - /// `multithread_reserve_threads`. Override with env `E3_NODE__MULTITHREAD_CONCURRENT_JOBS`. + /// `multithread_reserve_threads`. Override the default profile with env + /// `E3_NODE__MULTITHREAD_CONCURRENT_JOBS`, or a named profile with + /// `E3_NODES____MULTITHREAD_CONCURRENT_JOBS`. pub multithread_concurrent_jobs: Option, /// Hard deadline for construction and initial synchronization. A node that cannot reach live /// protocol operation before this deadline exits non-zero instead of remaining falsely alive. @@ -74,6 +76,10 @@ pub struct NodeDefinition { /// Maximum estimated bytes retained by the network startup buffer. #[serde(default = "default_max_buffered_net_bytes")] pub max_buffered_net_bytes: usize, + /// Test/CI-only escape hatch that skips recursive DKG and decryption proof aggregation. + /// On-chain verification remains mandatory, so this requires mock verifiers and a binary + /// compiled with the `test-only-skip-proof-aggregation` Cargo feature. + pub skip_proof_aggregation: bool, } fn default_multithread_reserve_threads() -> usize { @@ -118,6 +124,7 @@ impl Default for NodeDefinition { max_buffered_evm_events: default_max_buffered_evm_events(), max_buffered_net_events: default_max_buffered_net_events(), max_buffered_net_bytes: default_max_buffered_net_bytes(), + skip_proof_aggregation: false, } } } @@ -418,6 +425,11 @@ impl AppConfig { pub fn max_buffered_net_bytes(&self) -> usize { self.node_def().max_buffered_net_bytes } + + /// Whether this node requests the compile-time-gated proof aggregation skip for test/CI runs. + pub fn skip_proof_aggregation(&self) -> bool { + self.node_def().skip_proof_aggregation + } } #[derive(Debug, Default, Deserialize, Serialize)] @@ -500,7 +512,7 @@ pub fn load_config( let config: UnscopedAppConfig = Figment::from(Serialized::defaults(&UnscopedAppConfig::default())) .merge(Yaml::string(&loaded_yaml)) - .merge(Env::prefixed("E3_")) + .merge(Env::prefixed("E3_").split("__")) .merge(Serialized::defaults(&CliOverrides { otel, found_config_file: Some(resolved_config_path), @@ -811,6 +823,56 @@ node: Ok(()) } + #[test] + fn test_skip_proof_aggregation_defaults_off_and_can_be_enabled() -> Result<()> { + let configured: UnscopedAppConfig = serde_yaml::from_str( + r#" +node: + skip_proof_aggregation: true +"#, + )?; + let configured = configured.into_scoped_with_defaults( + "_default", + &PathBuf::from("/default/data"), + &PathBuf::from("/default/config"), + &PathBuf::from("/my/cwd"), + )?; + assert!(configured.skip_proof_aggregation()); + + let default = UnscopedAppConfig::default().into_scoped_with_defaults( + "_default", + &PathBuf::from("/default/data"), + &PathBuf::from("/default/config"), + &PathBuf::from("/my/cwd"), + )?; + assert!(!default.skip_proof_aggregation()); + Ok(()) + } + + #[test] + fn test_skip_proof_aggregation_can_be_enabled_for_named_node_via_env() { + Jail::expect_with(|jail| { + jail.set_env("E3_NODES__CN1__SKIP_PROOF_AGGREGATION", "true"); + + let config: UnscopedAppConfig = + Figment::from(Serialized::defaults(&UnscopedAppConfig::default())) + .merge(Env::prefixed("E3_").split("__")) + .extract() + .map_err(|err| err.to_string())?; + let config = config + .into_scoped_with_defaults( + "cn1", + &PathBuf::from("/default/data"), + &PathBuf::from("/default/config"), + &PathBuf::from("/my/cwd"), + ) + .map_err(|err| err.to_string())?; + + assert!(config.skip_proof_aggregation()); + Ok(()) + }); + } + #[test] fn test_startup_timeout_config_and_default() -> Result<()> { let configured: UnscopedAppConfig = serde_yaml::from_str( diff --git a/crates/entrypoint/Cargo.toml b/crates/entrypoint/Cargo.toml index 72f64194e..5762f93a5 100644 --- a/crates/entrypoint/Cargo.toml +++ b/crates/entrypoint/Cargo.toml @@ -6,6 +6,11 @@ license.workspace = true description = "E3 - CLI Entrypoints" repository = "https://github.com/theinterfold/interfold/crates/entrypoint" +[features] +test-only-skip-proof-aggregation = [ + "e3-ciphernode-builder/test-only-skip-proof-aggregation", +] + [dependencies] actix = { workspace = true } e3-aggregator = { workspace = true } diff --git a/crates/entrypoint/src/start/start.rs b/crates/entrypoint/src/start/start.rs index 00bc486af..b0c986104 100644 --- a/crates/entrypoint/src/start/start.rs +++ b/crates/entrypoint/src/start/start.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use e3_ciphernode_builder::{CiphernodeBuilder, CiphernodeHandle}; use e3_config::AppConfig; use e3_crypto::Cipher; @@ -14,6 +14,8 @@ use rand_chacha::ChaCha20Rng; use std::future::Future; use std::sync::{Arc, Mutex}; use std::time::Duration; +#[cfg(feature = "test-only-skip-proof-aggregation")] +use tracing::warn; use tracing::{info, instrument}; async fn await_startup(future: F, timeout: Duration) -> Result @@ -25,8 +27,20 @@ where .with_context(|| format!("ciphernode startup did not complete within {timeout:?}"))? } +fn validate_proof_aggregation_mode(skip_proof_aggregation: bool) -> Result<()> { + if skip_proof_aggregation && !cfg!(feature = "test-only-skip-proof-aggregation") { + bail!( + "`skip_proof_aggregation` is test/CI-only and this binary was built without the \ + `test-only-skip-proof-aggregation` Cargo feature" + ); + } + Ok(()) +} + #[instrument(name = "app", skip_all)] pub async fn execute(config: &AppConfig) -> Result { + validate_proof_aggregation_mode(config.skip_proof_aggregation())?; + let rng = Arc::new(Mutex::new( ChaCha20Rng::try_from_os_rng().context("failed to seed ChaCha20 RNG from OS")?, )); @@ -48,7 +62,7 @@ pub async fn execute(config: &AppConfig) -> Result { "Ciphernode startup deadline configured" ); - let build = CiphernodeBuilder::new(rng.clone(), cipher.clone()) + let builder = CiphernodeBuilder::new(rng.clone(), cipher.clone()) .with_name(&config.name()) .with_logging() .with_persistence(&config.log_file(), &config.db_file()) @@ -70,8 +84,20 @@ pub async fn execute(config: &AppConfig) -> Result { .with_threshold_plaintext_aggregation() .with_net(config.peers(), config.quic_port()) .with_shared_store() - .with_shared_eventstore() - .build(); + .with_shared_eventstore(); + + #[cfg(feature = "test-only-skip-proof-aggregation")] + let builder = if config.skip_proof_aggregation() { + warn!( + "Skipping recursive proof aggregation for this feature-gated test/CI node; \ + on-chain final proof verification remains mandatory" + ); + builder.with_proof_aggregation_disabled_for_testing() + } else { + builder + }; + + let build = builder.build(); let node = await_startup(build, startup_timeout).await?; Ok(node) @@ -79,7 +105,7 @@ pub async fn execute(config: &AppConfig) -> Result { #[cfg(test)] mod tests { - use super::await_startup; + use super::{await_startup, validate_proof_aggregation_mode}; use anyhow::{bail, Result}; use std::time::Duration; @@ -107,4 +133,22 @@ mod tests { } Ok(()) } + + #[cfg(not(feature = "test-only-skip-proof-aggregation"))] + #[test] + fn production_build_rejects_proof_aggregation_skip() { + let error = validate_proof_aggregation_mode(true) + .expect_err("production build must reject proof aggregation skipping"); + assert!(error + .to_string() + .contains("test-only-skip-proof-aggregation")); + assert!(validate_proof_aggregation_mode(false).is_ok()); + } + + #[cfg(feature = "test-only-skip-proof-aggregation")] + #[test] + fn test_feature_allows_proof_aggregation_skip() { + assert!(validate_proof_aggregation_mode(true).is_ok()); + assert!(validate_proof_aggregation_mode(false).is_ok()); + } } diff --git a/crates/events/Cargo.toml b/crates/events/Cargo.toml index 9a8ba5177..9f4b30f51 100644 --- a/crates/events/Cargo.toml +++ b/crates/events/Cargo.toml @@ -27,6 +27,7 @@ thiserror = { workspace = true } tracing = { workspace = true } tokio = { workspace = true } e3-crypto = { workspace = true } +e3-committee-hash = { workspace = true } e3-trbfv = { workspace = true } e3-utils = { workspace = true } e3-fhe-params = { workspace = true } diff --git a/crates/events/src/committee.rs b/crates/events/src/committee.rs index b65a064ec..c2ac8b05f 100644 --- a/crates/events/src/committee.rs +++ b/crates/events/src/committee.rs @@ -143,7 +143,7 @@ mod tests { #[test] fn picks_lowest_non_expelled_party_in_sorted_committee_as_aggregator() { - // Committee order is already score-sorted before it is stored. + // Committee order is canonical address-ascending before it is stored. let committee = Committee::new(vec![ "0xbbb".to_string(), "0xccc".to_string(), diff --git a/crates/events/src/interfold_event/committee_finalized.rs b/crates/events/src/interfold_event/committee_finalized.rs index 01f602952..13156c1b6 100644 --- a/crates/events/src/interfold_event/committee_finalized.rs +++ b/crates/events/src/interfold_event/committee_finalized.rs @@ -24,7 +24,7 @@ impl CommitteeFinalized { /// `topNodes` layout (see `_sortTopNodesByAscendingAddress` in `CiphernodeRegistryOwnable`). /// The node with the numerically lowest address ends up at index 0 (= party 0). /// Address comparison is done in lowercase to be independent of EIP-55 checksumming. - pub fn sort_by_score(&mut self) { + pub fn sort_by_address(&mut self) { let mut indices: Vec = (0..self.committee.len()).collect(); indices.sort_by_key(|&i| self.committee[i].to_lowercase()); diff --git a/crates/events/src/interfold_event/compute_request/zk.rs b/crates/events/src/interfold_event/compute_request/zk.rs index c82263147..cefac7d85 100644 --- a/crates/events/src/interfold_event/compute_request/zk.rs +++ b/crates/events/src/interfold_event/compute_request/zk.rs @@ -7,6 +7,7 @@ use crate::{Proof, SignedProofPayload}; use alloy::primitives::Address; use derivative::Derivative; +use e3_committee_hash::DecryptionDomainContext; use e3_crypto::SensitiveBytes; use e3_fhe_params::BfvPreset; use e3_utils::utility_types::ArcBytes; @@ -368,6 +369,8 @@ pub struct ThresholdShareDecryptionProofRequest { pub es_poly_sum: Vec, /// Computed decryption share polynomials, one per output index. pub d_share_bytes: Vec, + /// Stable E3 context cryptographically bound into every C6 proof. + pub decryption_domain: DecryptionDomainContext, /// BFV preset for parameter resolution. pub params_preset: BfvPreset, /// Committee size for per-committee circuit artifact resolution. diff --git a/crates/events/src/interfold_event/dkg_recursive_aggregation_complete.rs b/crates/events/src/interfold_event/dkg_recursive_aggregation_complete.rs index 6baa81dbe..903aac368 100644 --- a/crates/events/src/interfold_event/dkg_recursive_aggregation_complete.rs +++ b/crates/events/src/interfold_event/dkg_recursive_aggregation_complete.rs @@ -14,7 +14,8 @@ use crate::{E3id, Proof, SignedDkgFoldAttestation}; use serde::{Deserialize, Serialize}; /// NodeProofAggregator -> PublicKeyAggregator: fully aggregated DKG node proof. -/// When proof aggregation is disabled for the E3, `aggregated_proof` is `None`. +/// The test/CI node-level skip path reports `None`; the public-key aggregator +/// converts that internal state into a non-empty mock-verifier placeholder. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct DKGRecursiveAggregationComplete { pub e3_id: E3id, diff --git a/crates/events/src/interfold_event/e3_requested.rs b/crates/events/src/interfold_event/e3_requested.rs index a0f8c6f07..3bbc6f546 100644 --- a/crates/events/src/interfold_event/e3_requested.rs +++ b/crates/events/src/interfold_event/e3_requested.rs @@ -20,7 +20,7 @@ pub struct E3Requested { pub threshold_m: usize, /// The total committee size for the round pub threshold_n: usize, - /// A seed to provide randomness for the round + /// Shared per-E3 seed for deterministic ticket scoring. pub seed: Seed, /// The error size for the FHE computation. This can be calculated for the E3 program based on /// the size of the ciphertext and the depth of the program [tbd add link] @@ -31,10 +31,6 @@ pub struct E3Requested { /// ABI-encoded BFV parameters (derived from `params_preset`). /// Kept for downstream code that needs the raw bytes (e.g. `TrBFVConfig`). pub params: ArcBytes, - /// When true, ciphernodes generate wrapper/fold proofs for DKG proof - /// aggregation (public verifiability). When false, wrapper/fold proofs - /// are skipped to reduce latency. C5 and C7 proofs are always generated. - pub proof_aggregation_enabled: bool, } impl Default for E3Requested { @@ -47,7 +43,6 @@ impl Default for E3Requested { seed: Seed([0u8; 32]), threshold_m: 0, threshold_n: 0, - proof_aggregation_enabled: false, } } } diff --git a/crates/events/src/interfold_event/plaintext_aggregated.rs b/crates/events/src/interfold_event/plaintext_aggregated.rs index 7023b203f..370047061 100644 --- a/crates/events/src/interfold_event/plaintext_aggregated.rs +++ b/crates/events/src/interfold_event/plaintext_aggregated.rs @@ -17,10 +17,10 @@ use std::fmt::{self, Display}; pub struct PlaintextAggregated { pub e3_id: E3id, pub decrypted_output: Vec, - /// Final DecryptionAggregator (EVM) proof(s): one per ciphertext index. Empty when - /// proof aggregation is disabled. On-chain publication currently only supports a - /// single-output plaintext, in which case the first proof is forwarded to - /// `publishPlaintextOutput`. + /// Final DecryptionAggregator (EVM) proof(s): one per ciphertext index. Test/CI nodes that + /// skip recursive aggregation carry the existing C7 proofs here as mock-verifier + /// placeholders. On-chain publication currently only supports a single-output plaintext, in + /// which case the first proof is forwarded to `publishPlaintextOutput`. pub decryption_aggregator_proofs: Vec, } diff --git a/crates/events/src/interfold_event/proof.rs b/crates/events/src/interfold_event/proof.rs index a53d41d3d..c3bfbc4bc 100644 --- a/crates/events/src/interfold_event/proof.rs +++ b/crates/events/src/interfold_event/proof.rs @@ -316,12 +316,14 @@ mod tests { #[test] fn extract_c6_d_commitment_after_pub_inputs() { - // C6: 3 public inputs + 1 output (`d_commitment` at tail). - let mut signals = vec![0u8; 128]; + // C6: 5 public inputs + 1 output (`d_commitment` at tail). + let mut signals = vec![0u8; 192]; signals[0..32].copy_from_slice(&[0x11; 32]); // expected_sk_commitment signals[32..64].copy_from_slice(&[0x22; 32]); // expected_e_sm_commitment signals[64..96].copy_from_slice(&[0x33; 32]); // ct_commitment - signals[96..128].copy_from_slice(&[0x77; 32]); // d_commitment + signals[96..128].copy_from_slice(&[0x44; 32]); // domain_hi + signals[128..160].copy_from_slice(&[0x55; 32]); // domain_lo + signals[160..192].copy_from_slice(&[0x77; 32]); // d_commitment let proof = make_proof(CircuitName::ThresholdShareDecryption, &signals); assert_eq!(&*proof.extract_output("d_commitment").unwrap(), &[0x77; 32]); @@ -329,11 +331,13 @@ mod tests { #[test] fn extract_c6_public_inputs() { - let mut signals = vec![0u8; 128]; + let mut signals = vec![0u8; 192]; signals[0..32].copy_from_slice(&[0x11; 32]); signals[32..64].copy_from_slice(&[0x22; 32]); signals[64..96].copy_from_slice(&[0x33; 32]); - signals[96..128].copy_from_slice(&[0x77; 32]); + signals[96..128].copy_from_slice(&[0x44; 32]); + signals[128..160].copy_from_slice(&[0x55; 32]); + signals[160..192].copy_from_slice(&[0x77; 32]); let proof = make_proof(CircuitName::ThresholdShareDecryption, &signals); assert_eq!( @@ -345,6 +349,8 @@ mod tests { &[0x22; 32] ); assert_eq!(&*proof.extract_input("ct_commitment").unwrap(), &[0x33; 32]); + assert_eq!(&*proof.extract_input("domain_hi").unwrap(), &[0x44; 32]); + assert_eq!(&*proof.extract_input("domain_lo").unwrap(), &[0x55; 32]); } #[test] diff --git a/crates/events/src/interfold_event/publickey_aggregated.rs b/crates/events/src/interfold_event/publickey_aggregated.rs index 8ba4c61dd..bf79afa34 100644 --- a/crates/events/src/interfold_event/publickey_aggregated.rs +++ b/crates/events/src/interfold_event/publickey_aggregated.rs @@ -20,7 +20,7 @@ pub struct PublicKeyAggregated { pub pubkey: ArcBytes, // TODO: ArcBytes ? pub e3_id: E3id, pub nodes: OrderedSet, - /// Full registered committee (`topNodes`) addresses in ascending `party_id` (score) order. + /// Full registered committee (`topNodes`) addresses in ascending `party_id` (address) order. /// Length `N`. Used by `DecryptionAggregator` for `committee_hash_*` public-input binding. pub committee_addresses: Vec
, /// Honest subset of the committee (size `H ≤ N`) in ascending `party_id` order. @@ -31,10 +31,11 @@ pub struct PublicKeyAggregated { /// Passed as `pkCommitment` to `publishCommittee`. pub pk_commitment: [u8; 32], /// EVM DKG recursive proof (`CircuitName::DkgAggregator`) carrying node folds + C5 - /// for on-chain verification. `None` when proof aggregation is disabled. + /// for on-chain verification. Test/CI skip mode carries a non-empty C5 + /// placeholder accepted only by mock verifiers. pub dkg_aggregator_proof: Option, /// ABI-encoded `(Attestation[], PartySlotBinding[])` for on-chain fold attestation verify. - /// Required when `dkg_aggregator_proof` is present; `None` otherwise. + /// Test/CI skip mode carries a non-empty placeholder accepted only by its mock verifier. pub dkg_attestation_bundle: Option, } diff --git a/crates/events/src/interfold_event/threshold_share_pending.rs b/crates/events/src/interfold_event/threshold_share_pending.rs index 459a6a388..a5b024719 100644 --- a/crates/events/src/interfold_event/threshold_share_pending.rs +++ b/crates/events/src/interfold_event/threshold_share_pending.rs @@ -33,9 +33,6 @@ pub struct ThresholdSharePending { /// Required because collected_encryption_keys may be filtered for expulsions, /// making positional indices diverge from actual party IDs. pub recipient_party_ids: Vec, - /// When true, wrapper/fold proofs are generated for DKG proof aggregation. - /// When false, proof aggregation is skipped (only raw C1-C4 proofs generated). - pub proof_aggregation_enabled: bool, } impl Display for ThresholdSharePending { diff --git a/crates/evm-helpers/src/contracts.rs b/crates/evm-helpers/src/contracts.rs index 451583897..4a24987be 100644 --- a/crates/evm-helpers/src/contracts.rs +++ b/crates/evm-helpers/src/contracts.rs @@ -62,7 +62,6 @@ sol! { bytes32 ciphertextOutput; bytes plaintextOutput; address requester; - bool proofAggregationEnabled; } #[derive(Debug)] @@ -73,7 +72,6 @@ sol! { uint8 paramSet; bytes computeProviderParams; bytes customParams; - bool proofAggregationEnabled; } #[derive(Debug, PartialEq)] @@ -161,7 +159,6 @@ pub trait InterfoldRead { e3_program: Address, param_set: u8, compute_provider_params: Bytes, - proof_aggregation_enabled: bool, ) -> Result; async fn get_e3_stage(&self, e3_id: U256) -> Result; @@ -191,7 +188,6 @@ pub trait InterfoldWrite { param_set: u8, compute_provider_params: Bytes, custom_params: Bytes, - proof_aggregation_enabled: bool, ) -> Result<(TransactionReceipt, U256)>; /// Enable an E3 program @@ -377,7 +373,6 @@ where e3_program: Address, param_set: u8, compute_provider_params: Bytes, - proof_aggregation_enabled: bool, ) -> Result { let e3_request = E3RequestParams { committeeSize: committee_size, @@ -386,7 +381,6 @@ where paramSet: param_set, computeProviderParams: compute_provider_params, customParams: Bytes::new(), - proofAggregationEnabled: proof_aggregation_enabled, }; let contract = Interfold::new(self.contract_address, &self.provider); @@ -442,7 +436,6 @@ impl InterfoldWrite for InterfoldContract { param_set: u8, compute_provider_params: Bytes, custom_params: Bytes, - proof_aggregation_enabled: bool, ) -> Result<(TransactionReceipt, U256)> { let _guard = NONCE_LOCK.lock().await; let wallet_addr = self @@ -460,7 +453,6 @@ impl InterfoldWrite for InterfoldContract { paramSet: param_set, computeProviderParams: compute_provider_params.clone(), customParams: custom_params.clone(), - proofAggregationEnabled: proof_aggregation_enabled, }; let builder = contract.request(e3_request).nonce(nonce); diff --git a/crates/evm-helpers/src/events.rs b/crates/evm-helpers/src/events.rs index a67cd40dc..21330e3fe 100644 --- a/crates/evm-helpers/src/events.rs +++ b/crates/evm-helpers/src/events.rs @@ -50,7 +50,6 @@ sol! { bytes32 ciphertextOutput; bytes plaintextOutput; address requester; - bool proofAggregationEnabled; } #[derive(Debug)] diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index 063a22218..8415a992c 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -40,5 +40,6 @@ e3-entrypoint = { workspace = true } e3-events = { workspace = true, features = ["test-helpers"] } e3-evm = { workspace = true } e3-test-helpers = { workspace = true } +serde_json = { workspace = true } tokio = { workspace = true, features = ["test-util"] } tracing-subscriber = { workspace = true } diff --git a/crates/evm/src/ciphernode_registry/effects.rs b/crates/evm/src/ciphernode_registry/effects.rs index 71b8e1589..730e3d64b 100644 --- a/crates/evm/src/ciphernode_registry/effects.rs +++ b/crates/evm/src/ciphernode_registry/effects.rs @@ -149,16 +149,17 @@ pub async fn publish_committee_to_registry encode_zk_proof(p)?, - None => Bytes::new(), - }; - let attestation_bundle: Bytes = match dkg_attestation_bundle { - Some(b) => Bytes::copy_from_slice(b), - None => Bytes::new(), - }; + // Skip mode creates non-empty mock-only placeholders before this boundary. An absent payload + // is therefore always an internal error, while production verifiers still reject placeholders. + let proof = encode_zk_proof( + dkg_aggregator_proof + .ok_or_else(|| anyhow::anyhow!("mandatory DKG aggregator proof payload missing"))?, + )?; + let attestation_bundle = Bytes::copy_from_slice( + dkg_attestation_bundle + .filter(|bundle| !bundle.is_empty()) + .ok_or_else(|| anyhow::anyhow!("mandatory DKG attestation bundle missing"))?, + ); // RPC may not have synced finalization yet send_tx_with_retry("publishCommittee", &["CommitteeNotFinalized"], || { diff --git a/crates/evm/src/ciphernode_registry/events.rs b/crates/evm/src/ciphernode_registry/events.rs index aa49d2e95..c636f3ef7 100644 --- a/crates/evm/src/ciphernode_registry/events.rs +++ b/crates/evm/src/ciphernode_registry/events.rs @@ -115,7 +115,7 @@ impl From for CommitteeFinalized { scores: value.0.scores.iter().map(|s| s.to_string()).collect(), chain_id: value.1, }; - result.sort_by_score(); + result.sort_by_address(); result } } diff --git a/crates/evm/src/contracts.rs b/crates/evm/src/contracts.rs index 5830c3b8e..f9b8395da 100644 --- a/crates/evm/src/contracts.rs +++ b/crates/evm/src/contracts.rs @@ -36,7 +36,6 @@ sol! { bytes32 ciphertextOutput; bytes plaintextOutput; address requester; - bool proofAggregationEnabled; } // ── Write functions ───────────────────────────────────────────────── diff --git a/crates/evm/src/event_decoding/catalog.rs b/crates/evm/src/event_decoding/catalog.rs index 6c8c28473..9dd2af6e7 100644 --- a/crates/evm/src/event_decoding/catalog.rs +++ b/crates/evm/src/event_decoding/catalog.rs @@ -71,7 +71,7 @@ const INTERFOLD: &[EvmEventDefinition] = &[ EvmEventDefinition::new("E3RefundManagerSet", "E3RefundManagerSet(address)", None), EvmEventDefinition::new( "E3Requested", - "E3Requested(uint256,(uint256,uint8,uint256,uint256[2],bytes32,address,uint8,bytes,address,address,bytes32,bytes32,bytes,address,bool),address)", + "E3Requested(uint256,(uint256,uint8,uint256,uint256[2],bytes32,address,uint8,bytes,address,address,bytes32,bytes32,bytes,address),address)", None, ), EvmEventDefinition::new("E3StageChanged", "E3StageChanged(uint256,uint8,uint8)", Some(1)), @@ -139,7 +139,7 @@ const INTERFOLD: &[EvmEventDefinition] = &[ ), EvmEventDefinition::new( "SlashedFundsEscrowed", - "SlashedFundsEscrowed(uint256,uint256)", + "SlashedFundsEscrowed(uint256,address,uint256)", Some(1), ), EvmEventDefinition::new("SlashingManagerSet", "SlashingManagerSet(address)", None), @@ -181,6 +181,11 @@ const BONDING_REGISTRY: &[EvmEventDefinition] = &[ "ConfigurationUpdated(bytes32,uint256,uint256)", None, ), + EvmEventDefinition::new( + "EligibilityConfigurationVersionUpdated", + "EligibilityConfigurationVersionUpdated(uint256)", + None, + ), EvmEventDefinition::new("Initialized", "Initialized(uint64)", None), EvmEventDefinition::new( "LicenseBondUpdated", @@ -188,6 +193,11 @@ const BONDING_REGISTRY: &[EvmEventDefinition] = &[ None, ), EvmEventDefinition::new("LicenseTokenSet", "LicenseTokenSet(address)", None), + EvmEventDefinition::new( + "LicenseSurplusSwept", + "LicenseSurplusSwept(address,address,uint256)", + None, + ), EvmEventDefinition::new( "LicenseTransferShortfall", "LicenseTransferShortfall(address,uint256,uint256)", @@ -229,6 +239,11 @@ const BONDING_REGISTRY: &[EvmEventDefinition] = &[ "SlashedFundsWithdrawn(address,uint256,uint256)", None, ), + EvmEventDefinition::new( + "SlashingManagerAuthorizationUpdated", + "SlashingManagerAuthorizationUpdated(address,bool)", + None, + ), EvmEventDefinition::new( "SlashingManagerUpdated", "SlashingManagerUpdated(address,address)", @@ -439,9 +454,19 @@ const SLASHING_MANAGER: &[EvmEventDefinition] = &[ "SlashProposed(uint256,uint256,address,bytes32,uint256,uint256,uint256,address,uint8)", Some(2), ), + EvmEventDefinition::new( + "SlashRouteCompleted", + "SlashRouteCompleted(uint256,uint256,address,uint256)", + Some(2), + ), + EvmEventDefinition::new( + "SlashRoutePending", + "SlashRoutePending(uint256,uint256,address,uint256)", + Some(2), + ), EvmEventDefinition::new( "SlashedFundsEscrowedToRefund", - "SlashedFundsEscrowedToRefund(uint256,uint256)", + "SlashedFundsEscrowedToRefund(uint256,address,uint256)", Some(1), ), ]; @@ -449,7 +474,85 @@ const SLASHING_MANAGER: &[EvmEventDefinition] = &[ #[cfg(test)] mod tests { use super::*; - use std::collections::HashSet; + use serde_json::Value; + use std::collections::{HashMap, HashSet}; + use std::fs; + use std::path::PathBuf; + + const CONTRACT_ARTIFACTS: &[(&str, &str)] = &[ + ( + "Interfold", + "artifacts/contracts/Interfold.sol/Interfold.json", + ), + ( + "BondingRegistry", + "artifacts/contracts/registry/BondingRegistry.sol/BondingRegistry.json", + ), + ( + "CiphernodeRegistry", + "artifacts/contracts/registry/CiphernodeRegistryOwnable.sol/CiphernodeRegistryOwnable.json", + ), + ( + "SlashingManager", + "artifacts/contracts/slashing/SlashingManager.sol/SlashingManager.json", + ), + ]; + + fn artifact_path(relative: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../packages/interfold-contracts") + .join(relative) + } + + fn canonical_type(input: &Value) -> String { + let abi_type = input["type"].as_str().expect("ABI input type"); + let Some(tuple_suffix) = abi_type.strip_prefix("tuple") else { + return abi_type.to_owned(); + }; + let components = input["components"] + .as_array() + .expect("tuple ABI components") + .iter() + .map(canonical_type) + .collect::>() + .join(","); + format!("({components}){tuple_suffix}") + } + + fn artifact_events(relative: &str) -> HashMap> { + let artifact: Value = + serde_json::from_str(&fs::read_to_string(artifact_path(relative)).unwrap()).unwrap(); + artifact["abi"] + .as_array() + .expect("artifact ABI") + .iter() + .filter(|item| item["type"] == "event") + .map(|event| { + let inputs = event["inputs"].as_array().expect("event inputs"); + let signature = format!( + "{}({})", + event["name"].as_str().expect("event name"), + inputs + .iter() + .map(canonical_type) + .collect::>() + .join(",") + ); + let mut topic_position = 1; + let mut e3_id_topic = None; + for input in inputs { + if !input["indexed"].as_bool().unwrap_or(false) { + continue; + } + if input["name"].as_str() == Some("e3Id") { + e3_id_topic = Some(topic_position); + } + topic_position += 1; + } + (signature, e3_id_topic) + }) + .collect() + } #[test] fn every_contract_catalog_has_unique_topics() { @@ -483,4 +586,19 @@ mod tests { assert_eq!(definition.name, "TreasuryCredited"); assert_eq!(definition.e3_id_topic, Some(1)); } + + #[test] + fn every_watched_contract_catalog_matches_its_current_abi() { + for (contract, artifact) in CONTRACT_ARTIFACTS { + let expected = artifact_events(artifact); + let actual = catalog(contract) + .iter() + .map(|event| (event.signature.to_owned(), event.e3_id_topic)) + .collect::>(); + assert_eq!( + actual, expected, + "{contract} event catalog drifted from {artifact}" + ); + } + } } diff --git a/crates/evm/src/interfold/events.rs b/crates/evm/src/interfold/events.rs index 0a068171b..4f90e1ce8 100644 --- a/crates/evm/src/interfold/events.rs +++ b/crates/evm/src/interfold/events.rs @@ -93,7 +93,6 @@ impl E3RequestedWithChainId { seed: self.0.e3.seed.into(), error_size, e3_id: E3id::new(self.0.e3Id.to_string(), self.1), - proof_aggregation_enabled: self.0.e3.proofAggregationEnabled, }) } } @@ -571,7 +570,6 @@ mod tests { ciphertextOutput: B256::ZERO, plaintextOutput: Bytes::new(), requester: Address::ZERO, - proofAggregationEnabled: false, }, e3Program: Address::ZERO, }; diff --git a/crates/evm/src/interfold_writing/effects.rs b/crates/evm/src/interfold_writing/effects.rs index f4a8d798b..246fbec20 100644 --- a/crates/evm/src/interfold_writing/effects.rs +++ b/crates/evm/src/interfold_writing/effects.rs @@ -15,11 +15,10 @@ pub(in crate::actors::interfold_sol_writer) async fn publish_plaintext_output< ) -> Result { let e3_id: U256 = e3_id.try_into()?; - // `None` => proof aggregation disabled; contract accepts empty bytes in that case. - let proof: Bytes = match decryption_aggregator_proof { - Some(p) => encode_zk_proof(p)?, - None => Bytes::new(), - }; + // Skip mode creates a non-empty mock-only C7 placeholder before this boundary. + let proof = encode_zk_proof(decryption_aggregator_proof.ok_or_else(|| { + anyhow::anyhow!("mandatory decryption aggregator proof payload missing") + })?)?; send_tx_with_retry( "publishPlaintextOutput", diff --git a/crates/evm/src/interfold_writing/workflow.rs b/crates/evm/src/interfold_writing/workflow.rs index abd454ea0..a93da059f 100644 --- a/crates/evm/src/interfold_writing/workflow.rs +++ b/crates/evm/src/interfold_writing/workflow.rs @@ -18,8 +18,8 @@ use e3_events::CircuitName; /// Validate a decrypted result before it is written on-chain. /// -/// Returns `Ok(())` when exactly one decrypted output is present and (when -/// proof aggregation is enabled) the proof count matches the output count. +/// Returns `Ok(())` when exactly one decrypted output is present and the non-empty proof list +/// matches the output count. /// Returns a human-readable error message otherwise. pub(crate) fn validate_plaintext_output( e3_id: &E3id, @@ -38,11 +38,13 @@ pub(crate) fn validate_plaintext_output( decrypted_output.len() )); } - // `decryption_aggregator_proofs` is empty when proof aggregation is disabled. - // When enabled, its length must match `decrypted_output`. - if !decryption_aggregator_proofs.is_empty() - && decrypted_output.len() != decryption_aggregator_proofs.len() - { + if decryption_aggregator_proofs.is_empty() { + return Err(format!( + "E3 {} has no decryption aggregator proof payload", + e3_id + )); + } + if decrypted_output.len() != decryption_aggregator_proofs.len() { return Err(format!( "E3 {} decrypted_output len ({}) != decryption_aggregator_proofs len ({})", e3_id, @@ -86,8 +88,9 @@ mod tests { } #[test] - fn accepts_single_output_no_proofs() { - assert!(validate_plaintext_output(&e3(), &bytes(1), &[]).is_ok()); + fn rejects_single_output_without_proof() { + let err = validate_plaintext_output(&e3(), &bytes(1), &[]).unwrap_err(); + assert!(err.contains("no decryption aggregator proof")); } #[test] diff --git a/crates/indexer/Cargo.toml b/crates/indexer/Cargo.toml index c6c1f5a87..9567a8653 100644 --- a/crates/indexer/Cargo.toml +++ b/crates/indexer/Cargo.toml @@ -10,7 +10,9 @@ repository = "https://github.com/theinterfold/interfold/crates/indexer" alloy.workspace = true async-trait.workspace = true bincode.workspace = true +e3-bfv-client.workspace = true e3-evm-helpers.workspace = true +e3-fhe-params = { workspace = true, features = ["abi-encoding"] } eyre.workspace = true serde.workspace = true thiserror.workspace = true @@ -18,8 +20,6 @@ tokio.workspace = true tracing.workspace = true [dev-dependencies] -e3-bfv-client.workspace = true -e3-fhe-params.workspace = true fhe.workspace = true fhe-traits.workspace = true rand.workspace = true diff --git a/crates/indexer/src/indexer.rs b/crates/indexer/src/indexer.rs index 85eb4f570..e56088901 100644 --- a/crates/indexer/src/indexer.rs +++ b/crates/indexer/src/indexer.rs @@ -9,10 +9,11 @@ use crate::callback_queue::CallbackQueue; use crate::E3Repository; use alloy::consensus::BlockHeader; use alloy::hex; -use alloy::primitives::Uint; +use alloy::primitives::{keccak256, Uint}; use alloy::providers::Provider; use alloy::sol_types::SolEvent; use async_trait::async_trait; +use e3_bfv_client::validate_pk_commitment; use e3_evm_helpers::{ block_listener::BlockListener, contracts::{ @@ -22,6 +23,7 @@ use e3_evm_helpers::{ event_listener::EventListener, events::{CiphertextOutputPublished, CommitteePublished, PlaintextOutputPublished}, }; +use e3_fhe_params::decode_bfv_params; use eyre::eyre; use eyre::Result; use serde::{de::DeserializeOwned, Serialize}; @@ -352,6 +354,20 @@ impl InterfoldIndexer { let e3 = contract.get_e3(e.e3Id).await?; let e3_params = contract.get_param_set_registry(e3.paramSet).await?; + if e3.encryptionSchemeId == keccak256("fhe.rs:BFV") { + let decoded_params = decode_bfv_params(&e3_params) + .map_err(|error| eyre!("invalid BFV parameters for E3 {e3_id}: {error}"))?; + validate_pk_commitment( + &e.publicKey, + e.pkCommitment.0, + decoded_params.degree(), + decoded_params.plaintext(), + decoded_params.moduli().to_vec(), + ) + .map_err(|error| { + eyre!("rejecting unbound CommitteePublished public key for E3 {e3_id}: {error}") + })?; + } let seed = e3.seed.to_be_bytes(); let request_block = u64_try_from(e3.requestBlock)?; let input_window = [ diff --git a/crates/keyshare/Cargo.toml b/crates/keyshare/Cargo.toml index 00ddd8c6d..afc0bfa16 100644 --- a/crates/keyshare/Cargo.toml +++ b/crates/keyshare/Cargo.toml @@ -12,6 +12,7 @@ anyhow = { workspace = true } async-trait = { workspace = true } bincode = { workspace = true } e3-config = { workspace = true } +e3-committee-hash = { workspace = true } e3-data = { workspace = true } e3-crypto = { workspace = true } e3-events = { workspace = true } @@ -22,6 +23,7 @@ e3-request = { workspace = true } e3-trbfv = { workspace = true } e3-utils = { workspace = true } e3-zk-helpers = { workspace = true } +alloy = { workspace = true } fhe = { workspace = true } fhe-traits = { workspace = true } rand = { workspace = true } diff --git a/crates/keyshare/src/ext.rs b/crates/keyshare/src/ext.rs index 82711d5f5..ab03f95d1 100644 --- a/crates/keyshare/src/ext.rs +++ b/crates/keyshare/src/ext.rs @@ -9,6 +9,7 @@ use crate::{ ThresholdKeyshareState, }; use actix::Actor; +use alloy::primitives::Address; use anyhow::{anyhow, Result}; use async_trait::async_trait; use e3_crypto::Cipher; @@ -17,20 +18,27 @@ use e3_events::{prelude::*, BusHandle, EType, InterfoldEvent, InterfoldEventData use e3_request::{E3Context, E3ContextSnapshot, E3Extension, META_KEY}; use crate::KeyshareState; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; pub struct ThresholdKeyshareExtension { bus: BusHandle, cipher: Arc, address: String, + interfold_addresses: HashMap, } impl ThresholdKeyshareExtension { - pub fn create(bus: &BusHandle, cipher: &Arc, address: &str) -> Box { + pub fn create( + bus: &BusHandle, + cipher: &Arc, + address: &str, + interfold_addresses: HashMap, + ) -> Box { Box::new(Self { bus: bus.clone(), cipher: cipher.to_owned(), address: address.to_owned(), + interfold_addresses, }) } } @@ -51,6 +59,17 @@ impl E3Extension for ThresholdKeyshareExtension { } let e3_id = data.clone().e3_id; + let Some(interfold_address) = self.interfold_addresses.get(&e3_id.chain_id()).copied() + else { + self.bus.err( + EType::KeyGeneration, + anyhow!( + "Interfold address not configured for chain {}", + e3_id.chain_id() + ), + ); + return; + }; let party_id = data.clone().party_id; let Some(meta) = ctx.get_dependency(META_KEY) else { self.bus @@ -66,7 +85,6 @@ impl E3Extension for ThresholdKeyshareExtension { meta.threshold_n as u64, meta.params.clone(), self.address.clone(), - meta.proof_aggregation_enabled, ))); // New container with None @@ -81,6 +99,7 @@ impl E3Extension for ThresholdKeyshareExtension { .params_preset .dkg_counterpart() .unwrap_or(meta.params_preset), + interfold_address, }) .start() .into(), @@ -113,6 +132,16 @@ impl E3Extension for ThresholdKeyshareExtension { .params_preset .dkg_counterpart() .unwrap_or(meta.params_preset); + let interfold_address = self + .interfold_addresses + .get(&snapshot.e3_id.chain_id()) + .copied() + .ok_or_else(|| { + anyhow!( + "Interfold address not configured for chain {}", + snapshot.e3_id.chain_id() + ) + })?; // Construct from snapshot let value = ThresholdKeyshare::new(ThresholdKeyshareParams { @@ -120,6 +149,7 @@ impl E3Extension for ThresholdKeyshareExtension { cipher: self.cipher.clone(), state, share_enc_preset, + interfold_address, }) .start() .into(); diff --git a/crates/keyshare/src/threshold_keyshare/actor.rs b/crates/keyshare/src/threshold_keyshare/actor.rs index 4d18c9066..f6c007d06 100644 --- a/crates/keyshare/src/threshold_keyshare/actor.rs +++ b/crates/keyshare/src/threshold_keyshare/actor.rs @@ -5,6 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use actix::prelude::*; +use alloy::primitives::Address; use anyhow::{anyhow, bail, Context, Result}; use e3_crypto::{Cipher, SensitiveBytes}; use e3_data::Persistable; @@ -111,6 +112,7 @@ pub struct ThresholdKeyshareParams { pub cipher: Arc, pub state: Persistable, pub share_enc_preset: BfvPreset, + pub interfold_address: Address, } /// Ephemeral bridge data for operations already represented by the persisted keyshare phase. @@ -143,6 +145,7 @@ pub struct ThresholdKeyshare { decryption_key_shared_collector: Option>, state: Persistable, share_enc_preset: BfvPreset, + interfold_address: Address, pending: PendingKeyshareWork, } @@ -156,6 +159,7 @@ impl ThresholdKeyshare { decryption_key_shared_collector: None, state: params.state, share_enc_preset: params.share_enc_preset, + interfold_address: params.interfold_address, pending: PendingKeyshareWork::default(), } } diff --git a/crates/keyshare/src/threshold_keyshare/effects/create_decryption_share.rs b/crates/keyshare/src/threshold_keyshare/effects/create_decryption_share.rs index 25544eba8..fd2051462 100644 --- a/crates/keyshare/src/threshold_keyshare/effects/create_decryption_share.rs +++ b/crates/keyshare/src/threshold_keyshare/effects/create_decryption_share.rs @@ -189,6 +189,9 @@ impl ThresholdKeyshare { .aggregated_pk .clone() .ok_or_else(|| anyhow!("Aggregated public key not available for C6 proof"))?; + let decryption_domain = state + .decryption_domain + .ok_or_else(|| anyhow!("E3 decryption domain not available for C6 proof"))?; let threshold_preset = self .share_enc_preset @@ -222,6 +225,7 @@ impl ThresholdKeyshare { sk_poly_sum: decrypting.sk_poly_sum, es_poly_sum: decrypting.es_poly_sum, d_share_bytes: d_share_poly.clone(), + decryption_domain, params_preset: threshold_preset, committee_size, }, diff --git a/crates/keyshare/src/threshold_keyshare/effects/generate_threshold_share.rs b/crates/keyshare/src/threshold_keyshare/effects/generate_threshold_share.rs index 3ccbf11dd..afdee9f16 100644 --- a/crates/keyshare/src/threshold_keyshare/effects/generate_threshold_share.rs +++ b/crates/keyshare/src/threshold_keyshare/effects/generate_threshold_share.rs @@ -140,12 +140,6 @@ impl ThresholdKeyshare { // Cache own plaintext share rows for the AggregatingDecryptionKey transition. self.pending.own_dkg_shares = Some((plan.own_sk_share_raw, plan.own_esi_shares_raw)); - let proof_aggregation_enabled = self - .state - .try_get() - .map(|s| s.proof_aggregation_enabled) - .unwrap_or(true); - info!("Publishing ThresholdSharePending for E3 {}", e3_id); // Publish ThresholdSharePending - ProofRequestActor will generate proof, sign, and publish ThresholdShareCreated @@ -159,7 +153,6 @@ impl ThresholdKeyshare { sk_share_encryption_requests: plan.sk_share_encryption_requests, e_sm_share_encryption_requests: plan.e_sm_share_encryption_requests, recipient_party_ids: plan.recipient_party_ids, - proof_aggregation_enabled, }, ec, )?; diff --git a/crates/keyshare/src/threshold_keyshare/effects/route_events.rs b/crates/keyshare/src/threshold_keyshare/effects/route_events.rs index 34a7a383c..9cc7902fe 100644 --- a/crates/keyshare/src/threshold_keyshare/effects/route_events.rs +++ b/crates/keyshare/src/threshold_keyshare/effects/route_events.rs @@ -16,9 +16,16 @@ impl Handler for ThresholdKeyshare { self.notify_sync(ctx, TypedEvent::new(data, ec)) } InterfoldEventData::PublicKeyAggregated(data) => { + let committee_hash = + e3_committee_hash::hash_committee_addresses(&data.committee_addresses); let pk = ArcBytes::from_bytes(&data.pubkey); let _ = self.state.try_mutate(&ec, |mut s| { s.aggregated_pk = Some(pk); + s.decryption_domain = Some(e3_committee_hash::DecryptionDomainContext { + interfold_address: self.interfold_address, + committee_hash, + committee_public_key: data.pk_commitment.into(), + }); Ok(s) }); } diff --git a/crates/keyshare/src/threshold_keyshare/state.rs b/crates/keyshare/src/threshold_keyshare/state.rs index 4d3ea4aa0..3109d8ebf 100644 --- a/crates/keyshare/src/threshold_keyshare/state.rs +++ b/crates/keyshare/src/threshold_keyshare/state.rs @@ -12,6 +12,7 @@ //! synchronous data and transition logic, which makes it directly unit-testable. use anyhow::{anyhow, Result}; +use e3_committee_hash::DecryptionDomainContext; use e3_crypto::SensitiveBytes; use e3_events::{CiphernodeSelected, E3id, EncryptionKey, PartyId, SignedProofPayload}; use e3_trbfv::{ @@ -190,12 +191,14 @@ pub struct ThresholdKeyshareState { pub params: ArcBytes, /// Aggregated public key bytes, captured from PublicKeyAggregated event for C6 proof. pub aggregated_pk: Option, + /// Public E3 context captured with the aggregated key and bound into every + /// C6 proof so the final decryption proof cannot be replayed elsewhere. + pub decryption_domain: Option, pub expelled_parties: HashSet, /// Honest party IDs in deterministic ascending order (`BTreeSet` guarantees this). /// Downstream proof circuits index parties by position in this sorted set. pub honest_parties: Option>, pub dkg_started_at_unix_secs: Option, - pub proof_aggregation_enabled: bool, /// Set once `KeyshareCreated` has actually been published from an authorized /// path (after C4 honest-set verification, the no-C4-proofs path, or the /// sole-honest fast path). `ReadyForDecryption` is entered *before* that @@ -214,7 +217,6 @@ impl ThresholdKeyshareState { threshold_n: u64, params: ArcBytes, address: String, - proof_aggregation_enabled: bool, ) -> Self { Self { e3_id, @@ -225,10 +227,10 @@ impl ThresholdKeyshareState { threshold_n, params, aggregated_pk: None, + decryption_domain: None, expelled_parties: HashSet::new(), honest_parties: None, dkg_started_at_unix_secs: Some(now_unix_secs()), - proof_aggregation_enabled, keyshare_published: false, } } diff --git a/crates/keyshare/src/threshold_keyshare/state_tests.rs b/crates/keyshare/src/threshold_keyshare/state_tests.rs index da18bc72b..1f4b53e21 100644 --- a/crates/keyshare/src/threshold_keyshare/state_tests.rs +++ b/crates/keyshare/src/threshold_keyshare/state_tests.rs @@ -20,7 +20,6 @@ fn base_state(state: KeyshareState) -> ThresholdKeyshareState { 3, arc(b"params"), "0xabc".to_string(), - true, ) } @@ -32,7 +31,6 @@ fn new_initialises_defaults_and_records_dkg_start() { assert!(s.expelled_parties.is_empty()); assert!(s.honest_parties.is_none()); assert!(s.dkg_started_at_unix_secs.is_some()); - assert!(s.proof_aggregation_enabled); assert_eq!(s.get_threshold_m(), 1); assert_eq!(s.get_threshold_n(), 3); assert_eq!(s.get_party_id(), 0); diff --git a/crates/keyshare/src/threshold_keyshare/tests.rs b/crates/keyshare/src/threshold_keyshare/tests.rs index 8cef1ff68..e66470191 100644 --- a/crates/keyshare/src/threshold_keyshare/tests.rs +++ b/crates/keyshare/src/threshold_keyshare/tests.rs @@ -7,6 +7,7 @@ use super::*; use crate::actors::decryption_key_shared_collector::DecryptionKeySharedCollectionFailed; use actix::{Actor, Addr, Handler}; +use alloy::primitives::Address; use anyhow::Result; use e3_crypto::Cipher; use e3_data::{AutoPersist, DataStore, InMemStore, Persistable, Repository}; @@ -64,6 +65,7 @@ async fn start_actor() -> Result<( cipher: Arc::new(Cipher::from_password("test-password").await?), state: test_state(), share_enc_preset: DEFAULT_BFV_PRESET, + interfold_address: Address::ZERO, }) .start(); diff --git a/crates/multithread/Cargo.toml b/crates/multithread/Cargo.toml index 822de62b6..38bcc4eb7 100644 --- a/crates/multithread/Cargo.toml +++ b/crates/multithread/Cargo.toml @@ -8,9 +8,11 @@ repository.workspace = true [dependencies] actix = { workspace = true } +alloy = { workspace = true } anyhow = { workspace = true } bincode = { workspace = true } e3-bfv-client = { workspace = true } +e3-committee-hash = { workspace = true } e3-data = { workspace = true } e3-fhe-params = { workspace = true } e3-trbfv = { workspace = true } diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index 60f9b17bb..394520070 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -17,6 +17,7 @@ use crate::TaskPool; use crate::TaskTimeouts; use actix::prelude::*; use actix::{Actor, Handler}; +use alloy::primitives::keccak256; use anyhow::Result; use e3_crypto::Cipher; use e3_events::trap_fut; @@ -418,6 +419,12 @@ fn handle_threshold_share_decryption_proof( let bb_work_base = zk_bb_work_id(&request); let artifacts_dir = prover.resolve_artifacts_dir(req.params_preset, req.committee_size.as_str()); + let numeric_e3_id = request.e3_id.clone().try_into().map_err(|e| { + make_zk_error( + &request, + format!("invalid numeric E3 id for decryption domain: {e}"), + ) + })?; for i in 0..num_indices { // Deserialize ciphertext @@ -442,6 +449,12 @@ fn handle_threshold_share_decryption_proof( let d_share_poly = try_poly_pb_from_bytes(&req.d_share_bytes[i], &threshold_params) .map_err(|e| make_zk_error(&request, format!("d_share[{}] deserialize: {}", i, e)))?; let d_share = CrtPolynomial::from_fhe_polynomial(&d_share_poly); + let domain = e3_committee_hash::decryption_domain_limbs( + request.e3_id.chain_id(), + numeric_e3_id, + req.decryption_domain, + keccak256(&req.ciphertext_bytes[i][..]), + ); // Build circuit data let circuit_data = e3_zk_helpers::threshold::share_decryption::ShareDecryptionCircuitData { @@ -450,6 +463,8 @@ fn handle_threshold_share_decryption_proof( s: s.clone(), e, d_share, + domain_hi: domain.hi, + domain_lo: domain.lo, }; // Generate proof diff --git a/crates/request/src/meta.rs b/crates/request/src/meta.rs index 707a2586c..bc802f5ca 100644 --- a/crates/request/src/meta.rs +++ b/crates/request/src/meta.rs @@ -22,7 +22,6 @@ pub struct E3Meta { pub params_preset: BfvPreset, pub params: ArcBytes, pub error_size: ArcBytes, - pub proof_aggregation_enabled: bool, } pub struct E3MetaExtension; @@ -47,7 +46,6 @@ impl E3Extension for E3MetaExtension { params_preset, params, error_size, - proof_aggregation_enabled, .. } = data.clone(); @@ -59,7 +57,6 @@ impl E3Extension for E3MetaExtension { params_preset, params, error_size, - proof_aggregation_enabled, }; ctx.repositories().meta(&e3_id).write(&meta); ctx.set_dependency(META_KEY, meta); diff --git a/crates/slashing/src/commitment_consistency_checker_ext.rs b/crates/slashing/src/commitment_consistency_checker_ext.rs index a1cec584b..3b1a4e036 100644 --- a/crates/slashing/src/commitment_consistency_checker_ext.rs +++ b/crates/slashing/src/commitment_consistency_checker_ext.rs @@ -140,7 +140,6 @@ mod tests { params_preset: BfvPreset::InsecureThreshold512, params: ArcBytes::from_bytes(&[]), error_size: ArcBytes::from_bytes(&[]), - proof_aggregation_enabled: false, } } diff --git a/crates/sortition/src/ciphernode_selection/actor.rs b/crates/sortition/src/ciphernode_selection/actor.rs index 818625525..3ca2bb74a 100644 --- a/crates/sortition/src/ciphernode_selection/actor.rs +++ b/crates/sortition/src/ciphernode_selection/actor.rs @@ -37,7 +37,6 @@ fn e3_meta_from(req: &E3Requested) -> E3Meta { params_preset: req.params_preset, params: req.params.clone(), error_size: req.error_size.clone(), - proof_aggregation_enabled: req.proof_aggregation_enabled, } } diff --git a/crates/sortition/src/ciphernode_selection/handlers.rs b/crates/sortition/src/ciphernode_selection/handlers.rs index e2f7479e4..a5f50bc7a 100644 --- a/crates/sortition/src/ciphernode_selection/handlers.rs +++ b/crates/sortition/src/ciphernode_selection/handlers.rs @@ -136,7 +136,7 @@ impl Handler> for CiphernodeSelector { &self.bus.with_ec(msg.get_ctx()), move || { let (mut msg, ec) = msg.into_components(); - msg.sort_by_score(); + msg.sort_by_address(); info!("CiphernodeSelector received CommitteeFinalized."); let bus = self.bus.clone(); info!("Getting selector state..."); diff --git a/crates/sortition/src/sortition/handlers/committee.rs b/crates/sortition/src/sortition/handlers/committee.rs index fb724d2ea..efcd725e0 100644 --- a/crates/sortition/src/sortition/handlers/committee.rs +++ b/crates/sortition/src/sortition/handlers/committee.rs @@ -13,7 +13,7 @@ impl Handler> for Sortition { _ctx: &mut Self::Context, ) -> Self::Result { let (mut msg, ec) = msg.into_components(); - msg.sort_by_score(); + msg.sort_by_address(); trap(EType::Sortition, &self.bus.with_ec(&ec), || { info!( e3_id = %msg.e3_id, diff --git a/crates/sortition/src/sortition/handlers/registry.rs b/crates/sortition/src/sortition/handlers/registry.rs index 2d8c05ec8..f99e6a0ee 100644 --- a/crates/sortition/src/sortition/handlers/registry.rs +++ b/crates/sortition/src/sortition/handlers/registry.rs @@ -96,7 +96,12 @@ impl Handler> for Sortition { let (msg, ec) = msg.into_components(); trap(EType::Sortition, &self.bus.with_ec(&ec), || { self.node_state.try_mutate(&ec, |mut state_map| { - NodeRegistry::set_operator_active(&mut state_map, msg.operator.clone(), msg.active); + NodeRegistry::set_operator_active( + &mut state_map, + msg.chain_id, + msg.operator.clone(), + msg.active, + ); Ok(state_map) }) }) @@ -113,12 +118,22 @@ impl Handler> for Sortition { ) -> Self::Result { let (msg, ec) = msg.into_components(); trap(EType::Sortition, &self.bus.with_ec(&ec), || { - if msg.parameter == "ticketPrice" { - self.node_state.try_mutate(&ec, |mut state_map| { - NodeRegistry::set_ticket_price(&mut state_map, msg.chain_id, msg.new_value); - Ok(state_map) - })?; + let eligibility_parameter = matches!( + msg.parameter.as_str(), + "ticketPrice" | "licenseRequiredBond" | "licenseActiveBps" | "minTicketBalance" + ); + + if !eligibility_parameter { + return Ok(()); } + + self.node_state.try_mutate(&ec, |mut state_map| { + if msg.parameter == "ticketPrice" { + NodeRegistry::set_ticket_price(&mut state_map, msg.chain_id, msg.new_value); + } + NodeRegistry::invalidate_operator_activity(&mut state_map, msg.chain_id); + Ok(state_map) + })?; Ok(()) }) } diff --git a/crates/sortition/src/sortition/node_registry.rs b/crates/sortition/src/sortition/node_registry.rs index 1d4f5cfa0..39c9b9c67 100644 --- a/crates/sortition/src/sortition/node_registry.rs +++ b/crates/sortition/src/sortition/node_registry.rs @@ -136,21 +136,22 @@ impl NodeRegistry { ); } - /// Update an operator's active status across every chain it appears on. + /// Update an operator's active status on the chain that emitted the event. pub fn set_operator_active( store: &mut HashMap, + chain_id: u64, operator: String, active: bool, ) { - for chain_state in store.values_mut() { - let node = chain_state.nodes.entry(operator.clone()).or_default(); - node.active = active; - info!( - operator = %operator, - active = active, - "Updated operator active status" - ); - } + let chain_state = store.entry(chain_id).or_default(); + let node = chain_state.nodes.entry(operator.clone()).or_default(); + node.active = active; + info!( + operator = %operator, + chain_id = chain_id, + active = active, + "Updated operator active status" + ); } /// Set the ticket price for a chain. @@ -168,6 +169,20 @@ impl NodeRegistry { ); } + /// Mark every operator on one chain inactive after an eligibility-policy + /// update. On-chain status also fails closed until each operator refreshes + /// against the new policy version. + pub fn invalidate_operator_activity(store: &mut HashMap, chain_id: u64) { + let chain_state = store.entry(chain_id).or_default(); + for node in chain_state.nodes.values_mut() { + node.active = false; + } + info!( + chain_id = chain_id, + "Invalidated operator activity after eligibility-policy update" + ); + } + /// Record a published committee and increment active-job counters for each /// of its members. pub fn record_committee_published( diff --git a/crates/sortition/src/sortition/node_registry_tests.rs b/crates/sortition/src/sortition/node_registry_tests.rs index fa17617d2..6d92cd855 100644 --- a/crates/sortition/src/sortition/node_registry_tests.rs +++ b/crates/sortition/src/sortition/node_registry_tests.rs @@ -28,7 +28,7 @@ fn available_tickets_accounts_for_price_and_jobs() { let mut store = HashMap::new(); NodeRegistry::set_ticket_price(&mut store, 1, U256::from(10)); NodeRegistry::set_ticket_balance(&mut store, 1, "0xabc".into(), U256::from(55)); - NodeRegistry::set_operator_active(&mut store, "0xabc".into(), true); + NodeRegistry::set_operator_active(&mut store, 1, "0xabc".into(), true); // floor(55 / 10) = 5 tickets, no active jobs yet. assert_eq!(store[&1].available_tickets("0xabc"), 5); @@ -47,12 +47,26 @@ fn zero_price_yields_no_tickets() { } #[test] -fn operator_active_applies_across_chains() { +fn operator_active_applies_only_to_the_event_chain() { let mut store = HashMap::new(); NodeRegistry::add_node(&mut store, 1, "0xabc".into()); NodeRegistry::add_node(&mut store, 2, "0xabc".into()); - NodeRegistry::set_operator_active(&mut store, "0xabc".into(), true); + NodeRegistry::set_operator_active(&mut store, 1, "0xabc".into(), true); assert!(store[&1].nodes["0xabc"].active); + assert!(!store[&2].nodes["0xabc"].active); +} + +#[test] +fn eligibility_update_invalidates_only_the_target_chain() { + let mut store = HashMap::new(); + NodeRegistry::add_node(&mut store, 1, "0xabc".into()); + NodeRegistry::add_node(&mut store, 2, "0xabc".into()); + NodeRegistry::set_operator_active(&mut store, 1, "0xabc".into(), true); + NodeRegistry::set_operator_active(&mut store, 2, "0xabc".into(), true); + + NodeRegistry::invalidate_operator_activity(&mut store, 1); + + assert!(!store[&1].nodes["0xabc"].active); assert!(store[&2].nodes["0xabc"].active); } @@ -79,7 +93,7 @@ fn get_nodes_with_tickets_filters_inactive_and_empty() { let mut store = HashMap::new(); NodeRegistry::set_ticket_price(&mut store, 1, U256::from(10)); NodeRegistry::set_ticket_balance(&mut store, 1, "active".into(), U256::from(30)); - NodeRegistry::set_operator_active(&mut store, "active".into(), true); + NodeRegistry::set_operator_active(&mut store, 1, "active".into(), true); // Inactive node with balance is excluded. NodeRegistry::set_ticket_balance(&mut store, 1, "inactive".into(), U256::from(30)); diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index bbe0f2e52..b0b02c0f6 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -18,7 +18,7 @@ clap = { workspace = true } e3-aggregator = { workspace = true } e3-config = { workspace = true } e3-crypto = { workspace = true } -e3-ciphernode-builder = { workspace = true } +e3-ciphernode-builder = { workspace = true, features = ["test-only-skip-proof-aggregation"] } e3-data = { workspace = true } e3-events = { workspace = true } e3-evm = { workspace = true } @@ -54,4 +54,3 @@ zeroize = { workspace = true } [dev-dependencies] e3-events = { workspace = true, features = ["test-helpers"] } - diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 216b1a6b3..6b6610280 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -130,15 +130,17 @@ fn benchmark_participant_node_count(threshold_m: usize, threshold_n: usize) -> u /// Whether `test_trbfv_actor` runs the full recursive fold + aggregator path (default: on). /// -/// Benchmark harness always enables proof aggregation (`run_benchmarks.sh` exports `true`). +/// CI may set the ciphernode test flag to skip recursive aggregation; benchmark scripts explicitly +/// leave it enabled. fn benchmark_proof_aggregation_enabled() -> bool { - !matches!( - std::env::var("BENCHMARK_PROOF_AGGREGATION") - .unwrap_or_else(|_| "true".into()) - .to_ascii_lowercase() - .as_str(), - "0" | "false" | "no" | "off" - ) + let value = std::env::var("CIPHERNODE_SKIP_PROOF_AGGREGATION") + .unwrap_or_else(|_| "false".into()) + .to_ascii_lowercase(); + match value.as_str() { + "1" | "true" | "yes" | "on" => false, + "0" | "false" | "no" | "off" => true, + _ => panic!("CIPHERNODE_SKIP_PROOF_AGGREGATION must be a boolean, got {value:?}"), + } } /// Rayon multithread pool concurrency for benchmark runs (`BENCHMARK_MULTITHREAD_JOBS`, default 1). @@ -798,7 +800,7 @@ fn compute_committee_scores(committee: &[String], e3_id: &E3id, seed: Seed) -> V /// /// # Arguments /// * `e3_id` - The E3 computation ID -/// * `seed` - The random seed for sortition +/// * `seed` - The shared seed for deterministic sortition /// * `threshold_m` - Minimum nodes required for decryption /// * `threshold_n` - Committee size /// * `registered_addrs` - List of node addresses eligible for selection @@ -875,7 +877,8 @@ fn determine_committee( Ok((committee, committee_scores, buffer_nodes)) } -/// Lowest-address committee member after `CommitteeFinalized::sort_by_score` (party 0 / active aggregator). +/// Lowest-address committee member after `CommitteeFinalized::sort_by_address` +/// (party 0 / active aggregator). fn active_aggregator_address( committee: &[String], scores: &[String], @@ -888,7 +891,7 @@ fn active_aggregator_address( scores: scores.to_vec(), chain_id, }; - finalized.sort_by_score(); + finalized.sort_by_address(); finalized .committee .first() @@ -1424,6 +1427,7 @@ async fn test_trbfv_actor() -> Result<()> { // Setup ZK backend for proof generation/verification let (zk_backend, _zk_temp) = setup_test_zk_backend(benchmark_params.preset_subdir).await?; + let proof_aggregation_enabled = benchmark_proof_aggregation_enabled(); let nodes = CiphernodeSystemBuilder::new() // All nodes run the same binary under the aggregator-committee model. @@ -1434,7 +1438,7 @@ async fn test_trbfv_actor() -> Result<()> { let addr = rand_eth_addr(&node_rng); println!("Building collector {}!", addr); { - let b = CiphernodeBuilder::new(node_rng, cipher.clone()) + let mut b = CiphernodeBuilder::new(node_rng, cipher.clone()) .with_history_collector() .with_shared_taskpool(&task_pool) .with_multithread_concurrent_jobs(concurrent_jobs) @@ -1448,6 +1452,9 @@ async fn test_trbfv_actor() -> Result<()> { .with_forked_bus(bus.event_bus()) .with_chains(std::slice::from_ref(&bench_chain_config)) .with_logging(); + if !proof_aggregation_enabled { + b = b.with_proof_aggregation_disabled_for_testing(); + } b.build().await } }) @@ -1458,7 +1465,7 @@ async fn test_trbfv_actor() -> Result<()> { let addr = rand_eth_addr(&node_rng); println!("Building normal {}", &addr); { - let b = CiphernodeBuilder::new(node_rng, cipher.clone()) + let mut b = CiphernodeBuilder::new(node_rng, cipher.clone()) .with_history_collector() .with_shared_taskpool(&task_pool) .with_multithread_concurrent_jobs(concurrent_jobs) @@ -1472,6 +1479,9 @@ async fn test_trbfv_actor() -> Result<()> { .with_forked_bus(bus.event_bus()) .with_chains(std::slice::from_ref(&bench_chain_config)) .with_logging(); + if !proof_aggregation_enabled { + b = b.with_proof_aggregation_disabled_for_testing(); + } b.build().await } }, @@ -1530,7 +1540,6 @@ async fn test_trbfv_actor() -> Result<()> { // Trigger actor DKG let e3_id = E3id::new("0", 1); - let proof_aggregation_enabled = benchmark_proof_aggregation_enabled(); println!( "Benchmark trbfv: proof_aggregation={proof_aggregation_enabled}, preset={}, pool_threads={pool_threads}, max_concurrent_jobs={concurrent_jobs}", benchmark_params.preset_subdir @@ -1544,7 +1553,6 @@ async fn test_trbfv_actor() -> Result<()> { error_size, params_preset: benchmark_params.bfv_preset, params, - proof_aggregation_enabled, }; bus.publish_without_context(e3_requested)?; @@ -1783,6 +1791,17 @@ async fn test_trbfv_actor() -> Result<()> { let pubkey_bytes = pubkey_event.pubkey.clone(); let dkg_aggregator_proof = pubkey_event.dkg_aggregator_proof.clone(); + assert!( + dkg_aggregator_proof.is_some(), + "PublicKeyAggregated must always carry a final DKG proof payload" + ); + assert!( + pubkey_event + .dkg_attestation_bundle + .as_ref() + .is_some_and(|bundle| !bundle.is_empty()), + "PublicKeyAggregated must always carry a non-empty DKG attestation payload" + ); let pubkey = PublicKey::from_bytes(&pubkey_bytes, ¶ms_raw)?; @@ -2051,12 +2070,10 @@ async fn test_trbfv_actor() -> Result<()> { ) })?; - if proof_aggregation_enabled { - assert!( - !decryption_aggregator_proofs.is_empty(), - "DecryptionAggregator proofs should be present in PlaintextAggregated when proof_aggregation_enabled" - ); - } + assert!( + !decryption_aggregator_proofs.is_empty(), + "PlaintextAggregated must always carry a final decryption proof payload" + ); if let Ok(path) = std::env::var("BENCHMARK_FOLDED_OUTPUT") { if let (Some(dkg_proof), Some(dec_proof)) = ( diff --git a/crates/zk-helpers/src/circuits/output_layout.rs b/crates/zk-helpers/src/circuits/output_layout.rs index 7131792cb..f04833f5f 100644 --- a/crates/zk-helpers/src/circuits/output_layout.rs +++ b/crates/zk-helpers/src/circuits/output_layout.rs @@ -115,6 +115,8 @@ pub const THRESHOLD_SHARE_DECRYPTION_INPUTS: &[OutputField] = &[ f("expected_sk_commitment"), f("expected_e_sm_commitment"), f("ct_commitment"), + f("domain_hi"), + f("domain_lo"), ]; /// C3 — Share encryption public return (`-> pub Field`). @@ -319,12 +321,14 @@ mod tests { let layout = CircuitOutputLayout::Fixed { fields: THRESHOLD_SHARE_DECRYPTION_OUTPUTS, }; - // C6: 3 public inputs + 1 output = 128 bytes - let mut signals = vec![0u8; 128]; + // C6: 5 public inputs + 1 output = 192 bytes + let mut signals = vec![0u8; 192]; signals[0..32].copy_from_slice(&[0x11; 32]); signals[32..64].copy_from_slice(&[0x22; 32]); signals[64..96].copy_from_slice(&[0x33; 32]); - signals[96..128].copy_from_slice(&[0x77; 32]); + signals[96..128].copy_from_slice(&[0x44; 32]); + signals[128..160].copy_from_slice(&[0x55; 32]); + signals[160..192].copy_from_slice(&[0x77; 32]); assert_eq!( layout.extract_field(&signals, "d_commitment").unwrap(), @@ -362,10 +366,12 @@ mod tests { let layout = CircuitInputLayout::Fixed { fields: THRESHOLD_SHARE_DECRYPTION_INPUTS, }; - let mut signals = vec![0u8; 96]; + let mut signals = vec![0u8; 160]; signals[0..32].copy_from_slice(&[0x11; 32]); signals[32..64].copy_from_slice(&[0x22; 32]); signals[64..96].copy_from_slice(&[0x33; 32]); + signals[96..128].copy_from_slice(&[0x44; 32]); + signals[128..160].copy_from_slice(&[0x55; 32]); assert_eq!( layout @@ -383,6 +389,14 @@ mod tests { layout.extract_field(&signals, "ct_commitment").unwrap(), &[0x33; 32] ); + assert_eq!( + layout.extract_field(&signals, "domain_hi").unwrap(), + &[0x44; 32] + ); + assert_eq!( + layout.extract_field(&signals, "domain_lo").unwrap(), + &[0x55; 32] + ); } #[test] diff --git a/crates/zk-helpers/src/circuits/threshold/share_decryption/circuit.rs b/crates/zk-helpers/src/circuits/threshold/share_decryption/circuit.rs index 54a742956..6e6e97244 100644 --- a/crates/zk-helpers/src/circuits/threshold/share_decryption/circuit.rs +++ b/crates/zk-helpers/src/circuits/threshold/share_decryption/circuit.rs @@ -27,6 +27,9 @@ pub struct ShareDecryptionCircuitData { pub s: CrtPolynomial, pub e: CrtPolynomial, pub d_share: CrtPolynomial, + /// High and low 128-bit limbs of the E3 decryption domain. + pub domain_hi: u128, + pub domain_lo: u128, } impl Circuit for ShareDecryptionCircuit { diff --git a/crates/zk-helpers/src/circuits/threshold/share_decryption/computation.rs b/crates/zk-helpers/src/circuits/threshold/share_decryption/computation.rs index b6ed723cb..4313d5226 100644 --- a/crates/zk-helpers/src/circuits/threshold/share_decryption/computation.rs +++ b/crates/zk-helpers/src/circuits/threshold/share_decryption/computation.rs @@ -132,6 +132,8 @@ pub struct Inputs { pub expected_sk_commitment: BigInt, pub expected_e_sm_commitment: BigInt, pub ct_commitment: BigInt, + pub domain_hi: BigInt, + pub domain_lo: BigInt, } impl Computation for Configs { @@ -369,6 +371,8 @@ impl Computation for Inputs { expected_sk_commitment, expected_e_sm_commitment, ct_commitment, + domain_hi: BigInt::from(data.domain_hi), + domain_lo: BigInt::from(data.domain_lo), }) } @@ -384,6 +388,8 @@ impl Computation for Inputs { let expected_sk_commitment = self.expected_sk_commitment.to_string(); let expected_e_sm_commitment = self.expected_e_sm_commitment.to_string(); let ct_commitment = self.ct_commitment.to_string(); + let domain_hi = self.domain_hi.to_string(); + let domain_lo = self.domain_lo.to_string(); let json = serde_json::json!({ "ct0": ct0, @@ -397,6 +403,8 @@ impl Computation for Inputs { "expected_sk_commitment": expected_sk_commitment, "expected_e_sm_commitment": expected_e_sm_commitment, "ct_commitment": ct_commitment, + "domain_hi": domain_hi, + "domain_lo": domain_lo, }); Ok(json) diff --git a/crates/zk-helpers/src/circuits/threshold/share_decryption/sample.rs b/crates/zk-helpers/src/circuits/threshold/share_decryption/sample.rs index 016af8bf2..cea27069a 100644 --- a/crates/zk-helpers/src/circuits/threshold/share_decryption/sample.rs +++ b/crates/zk-helpers/src/circuits/threshold/share_decryption/sample.rs @@ -241,6 +241,8 @@ impl ShareDecryptionCircuitData { s: CrtPolynomial::from_fhe_polynomial(&sk_poly_sum), e: CrtPolynomial::from_fhe_polynomial(&es_poly_sum), d_share: CrtPolynomial::from_fhe_polynomial(&d_share_rns), + domain_hi: 1, + domain_lo: 2, }) } } diff --git a/crates/zk-prover/src/actor_system.rs b/crates/zk-prover/src/actor_system.rs index 03761cf68..7a6340d2d 100644 --- a/crates/zk-prover/src/actor_system.rs +++ b/crates/zk-prover/src/actor_system.rs @@ -51,6 +51,7 @@ pub fn setup_zk_actors( signer: PrivateKeySigner, dkg_fold_attestation_verifiers_by_chain: HashMap>, recovery: ZkActorRecovery, + proof_aggregation_enabled: bool, ) -> ZkActors { let ZkActorRecovery { finalized_committees, @@ -59,12 +60,16 @@ pub fn setup_zk_actors( let zk_actor = ZkActor::new(backend).start(); let verifier = zk_actor.clone().recipient(); - let proof_request = ProofRequestActor::setup(bus, signer.clone()); + let proof_request = ProofRequestActor::setup(bus, signer.clone(), proof_aggregation_enabled); let proof_verification = ProofVerificationActor::setup(bus, verifier, finalized_committees.clone(), e3_metadata); let share_verification = ShareVerificationActor::setup(bus, finalized_committees); - let node_proof_aggregator = - NodeProofAggregator::setup(bus, signer, dkg_fold_attestation_verifiers_by_chain); + let node_proof_aggregator = NodeProofAggregator::setup( + bus, + signer, + dkg_fold_attestation_verifiers_by_chain, + proof_aggregation_enabled, + ); ZkActors { zk_actor, diff --git a/crates/zk-prover/src/circuits/aggregation/c6_accumulator.rs b/crates/zk-prover/src/circuits/aggregation/c6_accumulator.rs index 91a343356..984a65da6 100644 --- a/crates/zk-prover/src/circuits/aggregation/c6_accumulator.rs +++ b/crates/zk-prover/src/circuits/aggregation/c6_accumulator.rs @@ -22,11 +22,12 @@ use serde::Serialize; /// `total_slots` = `T + 1` (one slot per party index in the C6 leaf layout). fn c6_fold_public_input_field_count(total_slots: usize) -> usize { - 4 + 4 * total_slots + 6 + 4 * total_slots } -/// Public-signal layout of `c6_fold`: 4-field prefix, then 4-field-wide per-slot tail. -const C6_FOLD_PREFIX_LEN: usize = 4; +/// Public-signal layout of `c6_fold`: four fold parameters, two common domain +/// limbs, then a four-field-wide per-slot tail. +const C6_FOLD_PREFIX_LEN: usize = 6; const C6_FOLD_SLOT_WIDTH: usize = 4; struct C6FoldVks { @@ -110,7 +111,7 @@ fn generate_c6_fold_kernel_genesis_proof( Ok(proof) } -fn threshold_share_decryption_inner_public_inputs(proof: &Proof) -> Result<[String; 4], ZkError> { +fn threshold_share_decryption_inner_public_inputs(proof: &Proof) -> Result<[String; 6], ZkError> { if proof.circuit != CircuitName::ThresholdShareDecryption { return Err(ZkError::InvalidInput(format!( "expected ThresholdShareDecryption inner proof, got {}", @@ -122,6 +123,8 @@ fn threshold_share_decryption_inner_public_inputs(proof: &Proof) -> Result<[Stri extract_single_field(proof, "input", field_keys::EXPECTED_SK_COMMITMENT, ctx)?, extract_single_field(proof, "input", field_keys::EXPECTED_E_SM_COMMITMENT, ctx)?, extract_single_field(proof, "input", field_keys::CT_COMMITMENT, ctx)?, + extract_single_field(proof, "input", field_keys::DOMAIN_HI, ctx)?, + extract_single_field(proof, "input", field_keys::DOMAIN_LO, ctx)?, extract_single_field(proof, "output", field_keys::D_COMMITMENT, ctx)?, ]) } @@ -130,7 +133,7 @@ fn threshold_share_decryption_inner_public_inputs(proof: &Proof) -> Result<[Stri struct C6FoldStepInput { inner_vk: Vec, inner_proof: Vec, - c6_public_inputs: [String; 4], + c6_public_inputs: [String; 6], acc_vk: Vec, acc_proof: Vec, acc_public_inputs: Vec, @@ -193,7 +196,7 @@ fn generate_c6_fold_step_with_vks( } else { let p = prior_fold.expect("prior_fold required when is_first_step is false"); let acc_pi = parse_c6_fold_public_field_strings(p)?; - let prior_slots = (acc_pi.len() - 4) / 4; + let prior_slots = (acc_pi.len() - C6_FOLD_PREFIX_LEN) / C6_FOLD_SLOT_WIDTH; if prior_slots == 0 { return Err(ZkError::InvalidInput( "c6_fold proof implies zero slots".into(), diff --git a/crates/zk-prover/src/circuits/aggregation/helpers.rs b/crates/zk-prover/src/circuits/aggregation/helpers.rs index 544ff0310..97cfb3c8a 100644 --- a/crates/zk-prover/src/circuits/aggregation/helpers.rs +++ b/crates/zk-prover/src/circuits/aggregation/helpers.rs @@ -26,6 +26,8 @@ pub mod field_keys { pub const EXPECTED_SK_COMMITMENT: &str = "expected_sk_commitment"; pub const EXPECTED_E_SM_COMMITMENT: &str = "expected_e_sm_commitment"; pub const CT_COMMITMENT: &str = "ct_commitment"; + pub const DOMAIN_HI: &str = "domain_hi"; + pub const DOMAIN_LO: &str = "domain_lo"; pub const D_COMMITMENT: &str = "d_commitment"; } diff --git a/crates/zk-prover/src/circuits/aggregation/node_dkg_fold.rs b/crates/zk-prover/src/circuits/aggregation/node_dkg_fold.rs index 9b321a676..0198e4a07 100644 --- a/crates/zk-prover/src/circuits/aggregation/node_dkg_fold.rs +++ b/crates/zk-prover/src/circuits/aggregation/node_dkg_fold.rs @@ -375,7 +375,7 @@ pub struct DkgAggregationInput<'a> { pub c5_proof: &'a Proof, /// Honest party ids in the same order as `node_fold_proofs` (e.g. sorted ascending). pub party_ids: &'a [u64], - /// Ordered committee addresses (`topNodes` / sortition order) for `committee_hash_*` public inputs. + /// Address-ordered committee (`topNodes` / party order) for `committee_hash_*` public inputs. pub committee_addresses: &'a [Address], } @@ -553,6 +553,8 @@ struct DecryptionAggregatorWitness { committee_members: Vec, committee_hash_hi: String, committee_hash_lo: String, + domain_hi: String, + domain_lo: String, } /// Prove [`CircuitName::DecryptionAggregator`] for each job (C6 fold + C7), with @@ -608,11 +610,18 @@ pub fn prove_decryption_aggregation_jobs( &format!("{e3_id}-c6fold-{i}"), artifacts_dir, )?; + let c6_fold_public = proof_public_field_strings(&c6_fold)?; + let domain_hi = c6_fold_public.get(4).cloned().ok_or_else(|| { + ZkError::InvalidInput("C6 fold proof is missing domain_hi at public input 4".into()) + })?; + let domain_lo = c6_fold_public.get(5).cloned().ok_or_else(|| { + ZkError::InvalidInput("C6 fold proof is missing domain_lo at public input 5".into()) + })?; let witness = DecryptionAggregatorWitness { c6_fold_vk: c6_fold_vk.verification_key.clone(), c6_fold_proof: proof_field_strings(&c6_fold)?, - c6_fold_public: proof_public_field_strings(&c6_fold)?, + c6_fold_public, c7_vk: c7_vk.verification_key.clone(), c7_proof: proof_field_strings(job.c7_proof)?, c7_public: proof_public_field_strings(job.c7_proof)?, @@ -621,6 +630,8 @@ pub fn prove_decryption_aggregation_jobs( committee_members: committee_members.clone(), committee_hash_hi: committee_hash_hi.clone(), committee_hash_lo: committee_hash_lo.clone(), + domain_hi, + domain_lo, }; let json = serde_json::to_value(&witness) diff --git a/crates/zk-prover/src/node_proof_aggregation/actor.rs b/crates/zk-prover/src/node_proof_aggregation/actor.rs index f8d857e48..9ef3e75e6 100644 --- a/crates/zk-prover/src/node_proof_aggregation/actor.rs +++ b/crates/zk-prover/src/node_proof_aggregation/actor.rs @@ -29,6 +29,7 @@ use crate::node_fold_public::extract_node_fold_agg_commits; pub struct NodeProofAggregator { bus: BusHandle, signer: PrivateKeySigner, + proof_aggregation_enabled: bool, /// Per-chain `DkgFoldAttestationVerifier` address (EIP-712 `verifyingContract`). /// Looked up by `e3_id.chain_id()` when signing fold attestations. dkg_fold_attestation_verifiers_by_chain: HashMap>, @@ -42,10 +43,12 @@ impl NodeProofAggregator { bus: &BusHandle, signer: PrivateKeySigner, dkg_fold_attestation_verifiers_by_chain: HashMap>, + proof_aggregation_enabled: bool, ) -> Self { Self { bus: bus.clone(), signer, + proof_aggregation_enabled, dkg_fold_attestation_verifiers_by_chain, states: HashMap::new(), fold_correlation: HashMap::new(), @@ -57,8 +60,15 @@ impl NodeProofAggregator { bus: &BusHandle, signer: PrivateKeySigner, dkg_fold_attestation_verifiers_by_chain: HashMap>, + proof_aggregation_enabled: bool, ) -> Addr { - let addr = Self::new(bus, signer, dkg_fold_attestation_verifiers_by_chain).start(); + let addr = Self::new( + bus, + signer, + dkg_fold_attestation_verifiers_by_chain, + proof_aggregation_enabled, + ) + .start(); bus.subscribe(EventType::ThresholdSharePending, addr.clone().into()); bus.subscribe(EventType::DKGInnerProofReady, addr.clone().into()); bus.subscribe(EventType::ComputeResponse, addr.clone().into()); @@ -112,7 +122,7 @@ mod tests { #[actix::test] async fn node_dkg_fold_compute_error_emits_e3_failed() -> Result<()> { let (bus, _rng, _seed, _params, _crp, _errors, history) = get_common_setup(None)?; - let mut aggregator = NodeProofAggregator::new(&bus, test_signer(), HashMap::new()); + let mut aggregator = NodeProofAggregator::new(&bus, test_signer(), HashMap::new(), true); let e3_id = E3id::new("42", 1); let correlation_id = CorrelationId::new(); @@ -197,7 +207,7 @@ mod tests { #[actix::test] async fn early_inner_proof_is_prebuffered_until_collection_starts() -> Result<()> { let (bus, _rng, _seed, _params, _crp, _errors, history) = get_common_setup(None)?; - let mut aggregator = NodeProofAggregator::new(&bus, test_signer(), HashMap::new()); + let mut aggregator = NodeProofAggregator::new(&bus, test_signer(), HashMap::new(), true); let e3_id = E3id::new("43", 1); let early_proof = dummy_proof(10); diff --git a/crates/zk-prover/src/node_proof_aggregation/effects.rs b/crates/zk-prover/src/node_proof_aggregation/effects.rs index 60a991cff..21028edb3 100644 --- a/crates/zk-prover/src/node_proof_aggregation/effects.rs +++ b/crates/zk-prover/src/node_proof_aggregation/effects.rs @@ -12,10 +12,10 @@ impl NodeProofAggregator { let (msg, ec) = msg.into_components(); let e3_id = msg.e3_id.clone(); - if !msg.proof_aggregation_enabled { + if !self.proof_aggregation_enabled { self.pending_inner_proofs.remove(&e3_id); info!( - "NodeProofAggregator: proof aggregation disabled for E3 {} — skipping", + "NodeProofAggregator: test/CI skip flag active for E3 {}", e3_id ); if let Err(err) = self.bus.publish( @@ -28,7 +28,7 @@ impl NodeProofAggregator { ec, ) { error!( - "NodeProofAggregator: failed to publish DKGRecursiveAggregationComplete (skipped) for E3 {}: {err}", + "NodeProofAggregator: failed to publish skipped DKGRecursiveAggregationComplete for E3 {}: {err}", e3_id ); } diff --git a/crates/zk-prover/src/proof_request/actor.rs b/crates/zk-prover/src/proof_request/actor.rs index 6a72f6946..05224cdc9 100644 --- a/crates/zk-prover/src/proof_request/actor.rs +++ b/crates/zk-prover/src/proof_request/actor.rs @@ -39,6 +39,7 @@ use crate::workflow::proof_request::{ pub struct ProofRequestActor { bus: BusHandle, signer: PrivateKeySigner, + proof_aggregation_enabled: bool, pending: HashMap, threshold_correlation: HashMap, pending_threshold: HashMap, @@ -63,10 +64,11 @@ pub struct ProofRequestActor { } impl ProofRequestActor { - pub fn new(bus: &BusHandle, signer: PrivateKeySigner) -> Self { + pub fn new(bus: &BusHandle, signer: PrivateKeySigner, proof_aggregation_enabled: bool) -> Self { Self { bus: bus.clone(), signer, + proof_aggregation_enabled, pending: HashMap::new(), pending_threshold: HashMap::new(), threshold_correlation: HashMap::new(), @@ -82,8 +84,12 @@ impl ProofRequestActor { } } - pub fn setup(bus: &BusHandle, signer: PrivateKeySigner) -> Addr { - let addr = Self::new(bus, signer).start(); + pub fn setup( + bus: &BusHandle, + signer: PrivateKeySigner, + proof_aggregation_enabled: bool, + ) -> Addr { + let addr = Self::new(bus, signer, proof_aggregation_enabled).start(); bus.subscribe(EventType::EncryptionKeyPending, addr.clone().into()); bus.subscribe(EventType::ComputeResponse, addr.clone().into()); bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); diff --git a/crates/zk-prover/src/proof_request/actor_tests.rs b/crates/zk-prover/src/proof_request/actor_tests.rs index c9865f889..12015fd3c 100644 --- a/crates/zk-prover/src/proof_request/actor_tests.rs +++ b/crates/zk-prover/src/proof_request/actor_tests.rs @@ -27,7 +27,7 @@ async fn next_event(history: &Addr>) -> Result< #[actix::test] async fn c0_compute_error_emits_e3_failed() -> Result<()> { let (bus, _rng, _seed, _params, _crp, _errors, history) = get_common_setup(None)?; - let mut actor = ProofRequestActor::new(&bus, PrivateKeySigner::random()); + let mut actor = ProofRequestActor::new(&bus, PrivateKeySigner::random(), true); let e3_id = E3id::new("44", 1); let correlation_id = CorrelationId::new(); @@ -75,7 +75,7 @@ async fn c0_compute_error_emits_e3_failed() -> Result<()> { #[actix::test] async fn decryption_failure_helper_emits_e3_failed() -> Result<()> { let (bus, _rng, _seed, _params, _crp, _errors, history) = get_common_setup(None)?; - let actor = ProofRequestActor::new(&bus, PrivateKeySigner::random()); + let actor = ProofRequestActor::new(&bus, PrivateKeySigner::random(), true); let e3_id = E3id::new("45", 1); actor.fail_decryption_round( diff --git a/crates/zk-prover/src/proof_request/effects/decryption_key_proofs.rs b/crates/zk-prover/src/proof_request/effects/decryption_key_proofs.rs index 1674c3c16..6a203e62b 100644 --- a/crates/zk-prover/src/proof_request/effects/decryption_key_proofs.rs +++ b/crates/zk-prover/src/proof_request/effects/decryption_key_proofs.rs @@ -94,7 +94,7 @@ impl ProofRequestActor { } if let Some(meta) = self.node_agg_meta.get(&e3_id) { - if meta.proof_aggregation_enabled { + if self.proof_aggregation_enabled { if let Err(err) = self.bus.publish( DKGInnerProofReady { e3_id: e3_id.clone(), diff --git a/crates/zk-prover/src/proof_request/effects/dkg_proofs.rs b/crates/zk-prover/src/proof_request/effects/dkg_proofs.rs index 9b33d4030..9352ee10d 100644 --- a/crates/zk-prover/src/proof_request/effects/dkg_proofs.rs +++ b/crates/zk-prover/src/proof_request/effects/dkg_proofs.rs @@ -57,12 +57,11 @@ impl ProofRequestActor { party_id: msg.full_share.party_id, total_expected, pending_c0: None, - proof_aggregation_enabled: msg.proof_aggregation_enabled, }, ); // If C0 proof arrived before meta, emit DKGInnerProofReady now - if let Some(c0_proof) = pending_c0 { - if msg.proof_aggregation_enabled { + if self.proof_aggregation_enabled { + if let Some(c0_proof) = pending_c0 { if let Err(err) = self.bus.publish( DKGInnerProofReady { e3_id: e3_id.clone(), diff --git a/crates/zk-prover/src/proof_request/effects/encryption_key_result.rs b/crates/zk-prover/src/proof_request/effects/encryption_key_result.rs index a821e54a0..6762865b3 100644 --- a/crates/zk-prover/src/proof_request/effects/encryption_key_result.rs +++ b/crates/zk-prover/src/proof_request/effects/encryption_key_result.rs @@ -89,7 +89,7 @@ impl ProofRequestActor { // Emit DKGInnerProofReady for C0, or buffer if meta not yet available if let Some(meta) = self.node_agg_meta.get(&e3_id) { - if meta.proof_aggregation_enabled { + if self.proof_aggregation_enabled { if let Err(err) = self.bus.publish( DKGInnerProofReady { e3_id: e3_id.clone(), @@ -110,7 +110,6 @@ impl ProofRequestActor { party_id: 0, total_expected: 0, pending_c0: Some(proof), - proof_aggregation_enabled: true, // will be overwritten by ThresholdSharePending }, ); } diff --git a/crates/zk-prover/src/proof_request/effects/signing.rs b/crates/zk-prover/src/proof_request/effects/signing.rs index 8f2704677..c7815da67 100644 --- a/crates/zk-prover/src/proof_request/effects/signing.rs +++ b/crates/zk-prover/src/proof_request/effects/signing.rs @@ -34,7 +34,7 @@ impl ProofRequestActor { ); if let Some(meta) = self.node_agg_meta.get(&e3_id) { - if meta.proof_aggregation_enabled { + if self.proof_aggregation_enabled { if let Err(err) = self.bus.publish( DKGInnerProofReady { e3_id: e3_id.clone(), diff --git a/crates/zk-prover/src/proof_request/state.rs b/crates/zk-prover/src/proof_request/state.rs index 54c9253bf..924d6705d 100644 --- a/crates/zk-prover/src/proof_request/state.rs +++ b/crates/zk-prover/src/proof_request/state.rs @@ -35,8 +35,6 @@ pub(crate) struct NodeAggregationMeta { pub(crate) total_expected: usize, /// Buffered C0 proof, if it arrived before meta was stored. pub(crate) pending_c0: Option, - /// When false, skip emitting DKGInnerProofReady (no recursive DKG aggregation). - pub(crate) proof_aggregation_enabled: bool, } impl NodeAggregationMeta { diff --git a/crates/zk-prover/src/proof_request/workflow_tests.rs b/crates/zk-prover/src/proof_request/workflow_tests.rs index b9960f899..0185994cc 100644 --- a/crates/zk-prover/src/proof_request/workflow_tests.rs +++ b/crates/zk-prover/src/proof_request/workflow_tests.rs @@ -191,7 +191,6 @@ fn node_agg_meta_seq_helpers() { party_id: 0, total_expected: NodeAggregationMeta::total_expected_for(2, 1), pending_c0: None, - proof_aggregation_enabled: true, }; // c4_base_seq sits just after C0..C3 = total_expected - 2. assert_eq!(meta.c4_base_seq(), 4 + 2 + 1); diff --git a/crates/zk-prover/src/proof_verification/actor_tests.rs b/crates/zk-prover/src/proof_verification/actor_tests.rs index e57acd8c6..9c63036c1 100644 --- a/crates/zk-prover/src/proof_verification/actor_tests.rs +++ b/crates/zk-prover/src/proof_verification/actor_tests.rs @@ -134,7 +134,6 @@ async fn restored_context_dispatches_c0_without_replayed_lifecycle_events() { params_preset: preset, params: ArcBytes::default(), error_size: ArcBytes::default(), - proof_aggregation_enabled: false, }, )]); let (observed_tx, mut observed_rx) = mpsc::unbounded_channel(); diff --git a/crates/zk-prover/src/proof_verification/handlers.rs b/crates/zk-prover/src/proof_verification/handlers.rs index a95b4eba4..a13b7c253 100644 --- a/crates/zk-prover/src/proof_verification/handlers.rs +++ b/crates/zk-prover/src/proof_verification/handlers.rs @@ -25,7 +25,7 @@ impl Handler for ProofVerificationActor { InterfoldEventData::CommitteeFinalized(mut data) => { // The EVM decoder already emits canonical address order, but sorting again keeps // this trust boundary correct for replayed/test-produced events as well. - data.sort_by_score(); + data.sort_by_address(); self.store_committee(data.e3_id, &data.committee); } InterfoldEventData::EncryptionKeyReceived(data) => { diff --git a/crates/zk-prover/src/share_verification/handlers.rs b/crates/zk-prover/src/share_verification/handlers.rs index 096c868b2..6f9b21d8a 100644 --- a/crates/zk-prover/src/share_verification/handlers.rs +++ b/crates/zk-prover/src/share_verification/handlers.rs @@ -17,7 +17,7 @@ impl Handler for ShareVerificationActor { InterfoldEventData::CommitteeFinalized(mut data) => { // Mirror the C0 verifier's canonical ordering at this trust boundary. Replayed and // test-produced events are not assumed to have passed through the EVM decoder. - data.sort_by_score(); + data.sort_by_address(); self.store_committee(data.e3_id, &data.committee); } InterfoldEventData::ShareVerificationDispatched(data) => { diff --git a/crates/zk-prover/tests/fold_accumulators_e2e_tests.rs b/crates/zk-prover/tests/fold_accumulators_e2e_tests.rs index 32b177328..1ad667007 100644 --- a/crates/zk-prover/tests/fold_accumulators_e2e_tests.rs +++ b/crates/zk-prover/tests/fold_accumulators_e2e_tests.rs @@ -96,7 +96,8 @@ fn c3_fold_total_slots_from_compiled_json() -> usize { (len - 4) / 3 } -/// Reads slot count from the compiled `c6_fold` ABI (`acc_public_inputs` length is `4 + 4 * slots`). +/// Reads slot count from the compiled `c6_fold` ABI +/// (`acc_public_inputs` length is `6 + 4 * slots`: four fold params plus two domain limbs). fn c6_fold_total_slots_from_compiled_json() -> usize { let path = c6_fold_json_path(); let raw = std::fs::read_to_string(&path).unwrap_or_else(|e| { @@ -119,11 +120,11 @@ fn c6_fold_total_slots_from_compiled_json() -> usize { }) .expect("c6_fold.json: abi.parameters.acc_public_inputs.length") as usize; assert!( - len >= 4 && (len - 4).is_multiple_of(4), - "unexpected acc_public_inputs length {} (expected 4 + 4 * slots)", + len >= 6 && (len - 6).is_multiple_of(4), + "unexpected acc_public_inputs length {} (expected 6 + 4 * slots)", len ); - (len - 4) / 4 + (len - 6) / 4 } #[test] diff --git a/docs/pages/building-with-interfold.mdx b/docs/pages/building-with-interfold.mdx index 0af92edb7..30979247c 100644 --- a/docs/pages/building-with-interfold.mdx +++ b/docs/pages/building-with-interfold.mdx @@ -155,8 +155,8 @@ function getTimeoutConfig() external view returns (E3TimeoutConfig memory config ) external returns (uint256 e3Id); ``` -2. Contract validates the parameters, estimates the fee, and stores an `E3` record seeded with a - random value derived from block entropy. +2. Contract validates the parameters, estimates the fee, and stores an `E3` record with the shared + seed used for deterministic ticket scoring. 3. The E3 program's `validate` hook returns the encryption scheme ID, which is persisted on the `E3` struct. 4. Committee selection is delegated to the `ciphernodeRegistry` via `requestCommittee`. diff --git a/docs/pages/ciphernode-operators/tickets-and-sortition.mdx b/docs/pages/ciphernode-operators/tickets-and-sortition.mdx index 9ef9b0fef..faa470b99 100644 --- a/docs/pages/ciphernode-operators/tickets-and-sortition.mdx +++ b/docs/pages/ciphernode-operators/tickets-and-sortition.mdx @@ -76,7 +76,7 @@ Look for the "Ticket balance" and "available" fields in the output. ## Sortition Algorithm -The sortition process selects committees deterministically based on a random seed: +The sortition process selects committees deterministically from a shared per-E3 seed: ```mermaid flowchart TD diff --git a/docs/pages/computation-flow.mdx b/docs/pages/computation-flow.mdx index a80221194..c46ed940d 100644 --- a/docs/pages/computation-flow.mdx +++ b/docs/pages/computation-flow.mdx @@ -36,8 +36,8 @@ server, Compute Providers, or other network participants. ``` The contract validates the parameters, transfers the required fee from the requester, creates an - `E3` record with a random seed derived from `block.prevrandao`, and delegates committee selection - to the `CiphernodeRegistry`. + `E3` record with a per-E3 ticket-scoring seed derived from `block.prevrandao`, and delegates + committee selection to the `CiphernodeRegistry`. You can estimate the fee before submitting by calling `getE3Quote`: diff --git a/docs/pages/cryptography.mdx b/docs/pages/cryptography.mdx index 135dd20d8..3529f0baa 100644 --- a/docs/pages/cryptography.mdx +++ b/docs/pages/cryptography.mdx @@ -37,11 +37,11 @@ rather than on blind trust in operators. Each confidential workload is an FHE program running inside an **E3**. After someone requests such a computation, a committee of **N** [ciphernodes](https://blog.theinterfold.com/ciphernodes/) is drawn -using **sortition**, so membership is tied to verifiable randomness rather than to a central picker. -There is **no trusted dealer** handing out key material: every ciphernode contributes its own share -of the work and takes part in **PVDKG** (publicly verifiable DKG), producing proofs alongside -messages so that deviating parties can be identified and excluded without exposing other nodes’ -secrets. +using **sortition**, so membership follows publicly checkable ticket scoring rather than a central +picker. There is **no trusted dealer** handing out key material: every ciphernode contributes its +own share of the work and takes part in **PVDKG** (publicly verifiable DKG), producing proofs +alongside messages so that deviating parties can be identified and excluded without exposing other +nodes’ secrets. Write **A** for the **authorised** committee for that E3—initially all **N** members, then shrinking as proofs fail and bad actors drop out of the active path. When DKG finishes, the network publishes @@ -208,8 +208,8 @@ expected commitments match outputs) is only checked off-chain, end-to-end correctness still depends on trusting the committee network rather than on verifiable mathematics. -The aggregation design addresses both problems. When an E3 is requested with -`proofAggregationEnabled = true`, only **two recursive proofs** are posted on-chain per E3 session: +The aggregation design addresses both problems. Only **two recursive proofs** are posted on-chain +per E3 session: 1. **DKG recursive proof**: the aggregator recursively verifies every node's C0–C4 fold chain together with **C5**, which aggregates the public key. Verified on-chain by diff --git a/docs/pages/internals/dkg.mdx b/docs/pages/internals/dkg.mdx index dba119332..730281e78 100644 --- a/docs/pages/internals/dkg.mdx +++ b/docs/pages/internals/dkg.mdx @@ -330,10 +330,16 @@ aggregator. ## Proof Aggregation and On-Chain Anchoring -E3 programs can opt into **proof aggregation** by setting `proofAggregationEnabled = true` at -request time. In that mode the network does not post per-node, per-circuit proofs on-chain. Instead -the aggregator submits two recursive proofs — one for DKG, one for decryption — together with -per-node attestations that bind each surfaced commitment to a specific registered operator. +**Proof aggregation is mandatory at the contract boundary.** The network does not post per-node, +per-circuit proofs on-chain. Instead the aggregator submits two recursive proofs — one for DKG, one +for decryption — together with per-node attestations that bind each surfaced commitment to a +specific registered operator. + +Ciphernodes built with the `test-only-skip-proof-aggregation` Cargo feature expose a test/CI-only +`skip_proof_aggregation` node setting. It skips the recursive workers and reuses the +already-generated C5/C7 proofs as non-empty placeholders for mock verifiers. Production builds do +not contain the disabling path and reject configurations that request it. Contracts still execute +their verifier calls. ### Canonical party_id assignment @@ -494,10 +500,10 @@ Production deploys on a non-canonical preset must regenerate locally for that de The repo enforces sync with two modes of `pnpm generate:verifiers`: - **`--check`** — used by `tests/integration/lib/prebuild.sh` (when - `PROOF_AGGREGATION_ENABLED=true`) and by the benchmark gas-extraction scripts. Regenerates each - verifier in memory, runs the same `prettier-plugin-solidity` formatting the committed files use, - and diffs against the committed `.sol`. Any drift fails the run with a clear fix recipe instead of - silently rewriting committed contracts mid-test. + `CIPHERNODE_SKIP_PROOF_AGGREGATION=false`) and by the benchmark gas-extraction scripts. + Regenerates each verifier in memory, runs the same `prettier-plugin-solidity` formatting the + committed files use, and diffs against the committed `.sol`. Any drift fails the run with a clear + fix recipe instead of silently rewriting committed contracts mid-test. - **`--write`** (the default for manual invocation) — overwrites the committed files. Run this when you intentionally bump the canonical-preset circuits or the Noir/bb toolchain, then commit the diff. diff --git a/docs/pages/internals/sortition.mdx b/docs/pages/internals/sortition.mdx index f11a74b35..316ae955d 100644 --- a/docs/pages/internals/sortition.mdx +++ b/docs/pages/internals/sortition.mdx @@ -18,7 +18,7 @@ each E3 request. The design goals are: --- -## Randomness Source +## Sortition Seed The seed for every sortition round is derived from the EVM's `block.prevrandao` (EIP-4399) combined with the E3 identifier: @@ -27,13 +27,10 @@ with the E3 identifier: seed = uint256(keccak256(abi.encode(block.prevrandao, e3Id))); ``` -`prevrandao` is the RANDAO reveal from the beacon chain. It is unpredictable before the block is -proposed but publicly verifiable afterwards. Combining it with `e3Id` ensures different rounds -produce independent seeds even within the same block. - -> **Limitation:** `prevrandao` is weak against a block proposer who can selectively withhold their -> block. This is an accepted trade-off for now; future versions may use a commit-reveal scheme or an -> external VRF. +This is a convenient shared per-E3 value for deterministic ticket scoring. It is not fed into BFV +key generation, and the protocol does not treat it as cryptographically unbiasable randomness. +`prevrandao` can be influenced by block production; a future design can use a stronger randomness +source if the sortition threat model comes to require one. The seed is emitted with `E3Requested` and used by all nodes to deterministically compute the same committee. @@ -151,23 +148,24 @@ After the submission window closes, anyone can call `finalizeCommittee(e3Id)`: emitted - If fewer → `CommitteeFormationFailed` is emitted and the E3 fails with a refund -`CommitteeFinalized` carries the committee as an ordered list of addresses. The Rust runtime then -**normalizes** this list by **sorting it by ascending score** to produce a canonical party ordering: +Before emitting `CommitteeFinalized`, the registry **normalizes** the selected committee by +**sorting it by ascending address**. The Rust runtime repeats that normalization at its trust +boundary: ``` -party_id = index in score-sorted committee (lowest score = 0) +party_id = index in address-sorted committee (lowest address = 0) ``` -This normalization is done in `CommitteeFinalized::sort_by_score()` and ensures every node -independently derives the same `party_id` for each member, regardless of the order the contract -happened to emit them. +The runtime normalization is done in `CommitteeFinalized::sort_by_address()` and ensures every node +independently derives the same `party_id` for each member, including replayed or test-produced +events that did not pass through the EVM decoder. --- ## Aggregator Assignment -The **active aggregator** is the node with `party_id = 0` — the node that won the lowest overall -score. It is responsible for: +The **active aggregator** is the node with `party_id = 0` — the selected committee member with the +lowest address. It is responsible for: - Collecting and verifying ZK proofs from all other committee members - Aggregating decryption shares @@ -221,7 +219,7 @@ sequenceDiagram Chain->>NodeA: CommitteeFinalized([NodeA, NodeB], [scoreA, scoreB]) Chain->>NodeB: CommitteeFinalized([NodeA, NodeB], [scoreA, scoreB]) - Note over NodeA,NodeB: Both sort by score → same party_ids
Lowest score = party_id 0 (aggregator) + Note over NodeA,NodeB: Both sort by address → same party_ids
Lowest address = party_id 0 (aggregator) ``` --- @@ -230,7 +228,7 @@ sequenceDiagram | Property | Mechanism | | ----------------------- | ------------------------------------------------------------------- | -| **Unpredictability** | `seed` derived from `prevrandao` — unknown until block proposal | +| **Shared seed** | `prevrandao` and `e3Id` provide a common per-E3 scoring input | | **Proportionality** | Expected min-score over N tickets scales with N | | **Sybil-resistance** | Splitting tickets across wallets doesn't improve expected min-score | | **Determinism** | Hash function is identical on-chain (Solidity) and off-chain (Rust) | diff --git a/examples/CRISP/Cargo.lock b/examples/CRISP/Cargo.lock index 6fff6991d..64dd74392 100644 --- a/examples/CRISP/Cargo.lock +++ b/examples/CRISP/Cargo.lock @@ -2494,7 +2494,9 @@ dependencies = [ "alloy", "async-trait", "bincode", + "e3-bfv-client", "e3-evm-helpers", + "e3-fhe-params", "eyre", "serde", "thiserror 1.0.69", diff --git a/examples/CRISP/Readme.md b/examples/CRISP/Readme.md index ec0622da8..85bf88dd6 100644 --- a/examples/CRISP/Readme.md +++ b/examples/CRISP/Readme.md @@ -51,7 +51,7 @@ The simplest way to run CRISP is: ```bash # Optional: choose local profile (copied to crisp.dev.env on first setup) cp crisp.dev.env.example crisp.dev.env -# Edit CRISP_PROOF_AGGREGATION_ENABLED and CRISP_BFV_PRESET (see docs/PROOF_AGGREGATION_AND_ZK.md) +# Edit CRISP_SKIP_PROOF_AGGREGATION and CRISP_BFV_PRESET (see docs/PROOF_AGGREGATION_AND_ZK.md) # Install dependencies and build everything (applies crisp.dev.env → server/.env) pnpm dev:setup @@ -184,13 +184,13 @@ After `pnpm dev:up`, contract addresses are written automatically to `interfold. Edit **`crisp.dev.env`** (created from `crisp.dev.env.example` on first `pnpm dev:setup`): -| Variable | Default | Effect | -| --------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------- | -| `CRISP_BFV_PRESET` | `insecure-512` | DKG circuit build preset when aggregation is on | -| `CRISP_PROOF_AGGREGATION_ENABLED` | `false` | Synced to `server/.env`; controls DKG circuit build, deploy (`ENABLE_ZK_VERIFICATION`), and runtime aggregation | +| Variable | Default | Effect | +| ------------------------------ | -------------- | --------------------------------------------------------------------- | +| `CRISP_BFV_PRESET` | `insecure-512` | DKG circuit build preset for full aggregation | +| `CRISP_SKIP_PROOF_AGGREGATION` | `true` | Ciphernode-only local-dev skip; also selects mock verifier deployment | -`pnpm dev:setup` applies this profile (build DKG circuits when needed, sync `server/.env`). -`pnpm dev:up` deploys contracts using the same flags. +`pnpm dev:setup` applies this profile and builds recursive circuits only when needed. `pnpm dev:up` +deploys contracts using the same flags. See **[docs/PROOF_AGGREGATION_AND_ZK.md](./docs/PROOF_AGGREGATION_AND_ZK.md)** for modes, address sync, and troubleshooting (`VkHashMismatch`, etc.). diff --git a/examples/CRISP/crisp.dev.env.example b/examples/CRISP/crisp.dev.env.example index 7ffe9a963..9c89740a6 100644 --- a/examples/CRISP/crisp.dev.env.example +++ b/examples/CRISP/crisp.dev.env.example @@ -9,9 +9,9 @@ # secure-8192 -> param_set 1 (production-grade; slower proving) CRISP_BFV_PRESET=insecure-512 -# When true: +# When false: # - dev:setup runs `pnpm build:circuits --preset ` # - dev:up deploy sets ENABLE_ZK_VERIFICATION=true (BfvPkVerifier, fold attestations) -# - server/.env gets E3_PROOF_AGGREGATION_ENABLED=true -# When false (default): mock verifiers, no DKG circuit build, faster DKG. -CRISP_PROOF_AGGREGATION_ENABLED=false +# - ciphernodes run recursive proof aggregation +# When true (default): mock verifiers, no recursive aggregation build, faster DKG. +CRISP_SKIP_PROOF_AGGREGATION=true diff --git a/examples/CRISP/docs/PROOF_AGGREGATION_AND_ZK.md b/examples/CRISP/docs/PROOF_AGGREGATION_AND_ZK.md index 571f59beb..730ffbe4c 100644 --- a/examples/CRISP/docs/PROOF_AGGREGATION_AND_ZK.md +++ b/examples/CRISP/docs/PROOF_AGGREGATION_AND_ZK.md @@ -4,26 +4,28 @@ **Source of truth for local dev:** `examples/CRISP/crisp.dev.env` (from `crisp.dev.env.example`). -| Variable | Default | Effect | -| --------------------------------- | -------------- | ----------------------------------------------------- | -| `CRISP_BFV_PRESET` | `insecure-512` | `pnpm build:circuits --preset` when aggregation is on | -| `CRISP_PROOF_AGGREGATION_ENABLED` | `false` | Drives setup, deploy, and `server/.env` | +| Variable | Default | Effect | +| ------------------------------ | -------------- | ---------------------------------------------------- | +| `CRISP_BFV_PRESET` | `insecure-512` | `pnpm build:circuits --preset` for full aggregation | +| `CRISP_SKIP_PROOF_AGGREGATION` | `true` | Enables the ciphernode-only local-dev/test skip flag | -`pnpm dev:setup` copies the example if missing, syncs `E3_PROOF_AGGREGATION_ENABLED` into -`server/.env`, and builds DKG circuits only when aggregation is `true`. `pnpm dev:up` → -`crisp_deploy.sh` sets `ENABLE_ZK_VERIFICATION` from the same file. +`pnpm dev:setup` copies the example if missing and builds recursive circuits only when the skip flag +is `false`. `pnpm dev:up` → `crisp_deploy.sh` deploys real verifiers for full aggregation and mock +verifiers for skipped local-dev runs. After changing `crisp.dev.env`, re-run `pnpm dev:setup` and a fresh `pnpm dev:up` (wipe `.interfold/data` when switching modes). Lower-level switches (kept in sync by the scripts): -| Switch | Where | Effect | -| ------------------------------ | ---------------------------------------------------- | ------------------------------- | -| `E3_PROOF_AGGREGATION_ENABLED` | `server/.env` (managed by setup) | Passed to `Interfold.requestE3` | -| `ENABLE_ZK_VERIFICATION` | Set at deploy from `CRISP_PROOF_AGGREGATION_ENABLED` | Real vs mock BFV verifiers | +| Switch | Where | Effect | +| --------------------------------------- | ---------------------- | --------------------------------------- | +| `E3_NODES__CN*__SKIP_PROOF_AGGREGATION` | Ciphernode environment | Skips recursive aggregation workers | +| `ENABLE_ZK_VERIFICATION` | Deploy environment | Selects real vs mock on-chain verifiers | -Misalignment causes `publishCommittee` to revert with **`VkHashMismatch()`** (`0x0c260259`). +The contracts always require and verify final proof payloads. Skip mode works only because the local +deployment uses mock verifiers; enabling the skip flag against production verifiers causes +publication to fail. --- @@ -37,14 +39,14 @@ verifier checks. ```bash # crisp.dev.env CRISP_BFV_PRESET=insecure-512 -CRISP_PROOF_AGGREGATION_ENABLED=false +CRISP_SKIP_PROOF_AGGREGATION=true ``` ### Steps ```bash # From examples/CRISP -pnpm dev:setup # once — skips DKG circuit build, syncs server/.env +pnpm dev:setup # once — skips recursive aggregation circuit build pnpm dev:up ``` @@ -59,8 +61,8 @@ pnpm cli init ### What you should see - Ciphernodes skip long `NodeDkgFold` / `zk_dkg_aggregation` runs -- `publishCommittee` succeeds without a DKG Honk proof (empty `proof` bytes are allowed when - aggregation is disabled on the E3) +- `publishCommittee` still verifies a non-empty ciphernode placeholder through the configured mock + PK and fold-attestation verifiers - `POST /rounds/current` returns 200 once the indexer has recorded the round --- @@ -75,7 +77,7 @@ Honk proof, and `BfvPkVerifier` checks at `publishCommittee`. ```bash # crisp.dev.env CRISP_BFV_PRESET=insecure-512 -CRISP_PROOF_AGGREGATION_ENABLED=true +CRISP_SKIP_PROOF_AGGREGATION=false ``` CRISP `requestE3` still uses on-chain `param_set = 0` (`InsecureThreshold512`) unless you change the @@ -86,7 +88,7 @@ server — keep `CRISP_BFV_PRESET=insecure-512` for the default Minimum committe ```bash cd examples/CRISP # Edit crisp.dev.env (or crisp.dev.env.example → crisp.dev.env) as above -pnpm dev:setup # builds DKG circuits + syncs server/.env +pnpm dev:setup # builds DKG circuits rm -rf .interfold/data # required when switching from Mode A pnpm dev:up # deploy with ENABLE_ZK_VERIFICATION=true pnpm cli init @@ -112,11 +114,11 @@ proving). ## Invalid combinations -| Deploy | `E3_PROOF_AGGREGATION_ENABLED` | Result | -| ---------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Mock (`ENABLE_ZK_VERIFICATION` unset) | `true` | Ciphernodes generate real aggregation proofs, but `Interfold.pkVerifier` is `MockPkVerifier`. Attestation path may still run if a previous ZK deploy left `DkgFoldAttestationVerifier` on the registry from stale `deployed_contracts.json` on a **fresh** Anvil — always use `clean:deployments` + fresh chain. Prefer wiping `.interfold/data` and setting aggregation `false` for mock deploy. | -| ZK (`ENABLE_ZK_VERIFICATION=true`) | `false` | Valid but skips on-chain DKG proof verification; committee publication uses empty proof bytes. | -| ZK, circuits recompiled **after** deploy | `true` | **`VkHashMismatch()`** at `publishCommittee` — redeploy `BfvPkVerifier` (full ZK deploy) after `pnpm compile:circuits`. | +| Deploy | Ciphernode skip flag | Result | +| ---------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------- | +| Mock verifiers | `false` | Valid, but unnecessarily performs the expensive recursive workflow. | +| Production verifiers | `true` | Final C5/C7 placeholders are rejected; no contract-side bypass exists. | +| ZK, circuits recompiled **after** deploy | `false` | **`VkHashMismatch()`**; redeploy the production verifier wrappers after rebuilding circuits. | --- @@ -126,7 +128,7 @@ proving). - `interfold.config.yaml` (ciphernode contract watches) - `server/.env` (`INTERFOLD_ADDRESS`, `E3_PROGRAM_ADDRESS`, `CRISP_VOTING_TOKEN`, registry, fee - token, mock refs, `E3_PROOF_AGGREGATION_ENABLED` from `crisp.dev.env`) + token, and mock references) - `client/.env` (`VITE_CRISP_TOKEN`) No manual copy from `deployed_contracts.json` is required. Stale addresses only happen if you skip @@ -144,7 +146,7 @@ deploy). **Fix:** -1. Set `CRISP_PROOF_AGGREGATION_ENABLED=true` in `crisp.dev.env` (and matching `CRISP_BFV_PRESET`) +1. Set `CRISP_SKIP_PROOF_AGGREGATION=false` in `crisp.dev.env` (and matching `CRISP_BFV_PRESET`) 2. `pnpm dev:setup` then `rm -rf .interfold/data && pnpm dev:up` 3. `pnpm cli init` @@ -170,10 +172,10 @@ RPC is running. Harmless for CRISP-on-Anvil. ## Reference: what the scripts do -| Step | Mode A (`CRISP_PROOF_AGGREGATION_ENABLED=false`) | Mode B (`=true`) | -| ---------------- | ----------------------------------------------------------------- | --------------------------------------------------------- | -| `pnpm dev:setup` | Skips `build:circuits`; sets aggregation `false` in `server/.env` | `pnpm build:circuits --preset …`; sets aggregation `true` | -| `pnpm dev:up` | Mock BFV verifiers | `ENABLE_ZK_VERIFICATION=true` + prints env vars | +| Step | Mode A (`CRISP_SKIP_PROOF_AGGREGATION=true`) | Mode B (`=false`) | +| ---------------- | -------------------------------------------- | --------------------------------------------- | +| `pnpm dev:setup` | Skips recursive circuit builds | `pnpm build:circuits --preset …` | +| `pnpm dev:up` | Mock PK/decryption/fold verifiers | Production verifiers + full recursive proving | See also: `packages/interfold-contracts/scripts/deployInterfold.ts`, `packages/interfold-contracts/contracts/verifiers/bfv/BfvPkVerifier.sol`, and diff --git a/examples/CRISP/interfold.config.yaml b/examples/CRISP/interfold.config.yaml index 160028964..5c584033e 100644 --- a/examples/CRISP/interfold.config.yaml +++ b/examples/CRISP/interfold.config.yaml @@ -45,6 +45,9 @@ program: # node: # multithread_reserve_threads: 1 # multithread_concurrent_jobs: 8 + # # Test/CI only. Requires mock on-chain proof verifiers and an Interfold binary built with + # # the `test-only-skip-proof-aggregation` Cargo feature. + # skip_proof_aggregation: false nodes: cn1: address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' diff --git a/examples/CRISP/packages/crisp-contracts/README.md b/examples/CRISP/packages/crisp-contracts/README.md index 10cb09549..5a67cb52b 100644 --- a/examples/CRISP/packages/crisp-contracts/README.md +++ b/examples/CRISP/packages/crisp-contracts/README.md @@ -25,8 +25,7 @@ pnpm test Local deploy is driven by **`../../crisp.dev.env`** (see **[../../docs/PROOF_AGGREGATION_AND_ZK.md](../../docs/PROOF_AGGREGATION_AND_ZK.md)**): -- `pnpm dev:setup` — applies profile, builds DKG circuits when - `CRISP_PROOF_AGGREGATION_ENABLED=true` +- `pnpm dev:setup` — applies profile, builds DKG circuits when `CRISP_SKIP_PROOF_AGGREGATION=false` - `pnpm dev:up` → `scripts/crisp_deploy.sh` — sets `ENABLE_ZK_VERIFICATION` from the same file ### CRISP-only deploy (Interfold already deployed) diff --git a/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockInterfold.sol b/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockInterfold.sol index 12300af7f..c8ce8b962 100644 --- a/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockInterfold.sol +++ b/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockInterfold.sol @@ -34,8 +34,7 @@ contract MockInterfold { committeePublicKey: committeePublicKey, ciphertextOutput: bytes32(0), plaintextOutput: plaintextOutput, - requester: address(0), - proofAggregationEnabled: false + requester: address(0) }); IE3Program(program).validate(nextE3Id, 0, bytes(""), bytes(""), abi.encode(address(0), nextE3Id, 2, 0, 0)); @@ -71,8 +70,7 @@ contract MockInterfold { committeePublicKey: committeePublicKey, ciphertextOutput: bytes32(0), plaintextOutput: plaintextOutput, - requester: address(0), - proofAggregationEnabled: false + requester: address(0) }); } } diff --git a/examples/CRISP/packages/crisp-contracts/deploy/syncCrispEnv.ts b/examples/CRISP/packages/crisp-contracts/deploy/syncCrispEnv.ts index 73e62e26e..ae676923a 100644 --- a/examples/CRISP/packages/crisp-contracts/deploy/syncCrispEnv.ts +++ b/examples/CRISP/packages/crisp-contracts/deploy/syncCrispEnv.ts @@ -16,25 +16,6 @@ const __dirname = path.dirname(__filename) /** examples/CRISP */ const CRISP_ROOT = path.join(__dirname, '..', '..', '..') -function parseSimpleEnvFile(filePath: string): Record { - if (!fs.existsSync(filePath)) { - return {} - } - const out: Record = {} - for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { - const trimmed = line.trim() - if (!trimmed || trimmed.startsWith('#')) { - continue - } - const eq = trimmed.indexOf('=') - if (eq === -1) { - continue - } - out[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim() - } - return out -} - function ensureEnvFile(envPath: string, examplePath: string): void { if (!fs.existsSync(envPath)) { if (!fs.existsSync(examplePath)) { @@ -66,10 +47,7 @@ function deploymentAddress(contractName: string, chain: string): string | undefi return readDeploymentArgs(contractName, chain)?.address } -/** - * Writes localhost deployment addresses into server/.env and client/.env, and - * syncs E3_PROOF_AGGREGATION_ENABLED from crisp.dev.env. - */ +/** Writes localhost deployment addresses into server/.env and client/.env. */ export function syncCrispEnvFromDeployments(chain: string): void { const interfoldAddress = deploymentAddress('Interfold', chain) const feeTokenAddress = deploymentAddress('MockUSDC', chain) @@ -88,12 +66,6 @@ export function syncCrispEnvFromDeployments(chain: string): void { throw new Error(`Cannot sync CRISP .env files: missing deployments for ${missing.join(', ')} on chain "${chain}"`) } - const crispDev = parseSimpleEnvFile(path.join(CRISP_ROOT, 'crisp.dev.env')) - const proofAggregation = - crispDev.CRISP_PROOF_AGGREGATION_ENABLED ?? - parseSimpleEnvFile(path.join(CRISP_ROOT, 'crisp.dev.env.example')).CRISP_PROOF_AGGREGATION_ENABLED ?? - 'false' - const serverEnv = path.join(CRISP_ROOT, 'server', '.env') const clientEnv = path.join(CRISP_ROOT, 'client', '.env') @@ -106,7 +78,6 @@ export function syncCrispEnvFromDeployments(chain: string): void { E3_PROGRAM_ADDRESS: programAddress!, CIPHERNODE_REGISTRY_ADDRESS: registryAddress!, CRISP_VOTING_TOKEN: votingTokenAddress!, - E3_PROOF_AGGREGATION_ENABLED: proofAggregation, } const mockMappings: Array<[string, string]> = [ @@ -129,5 +100,4 @@ export function syncCrispEnvFromDeployments(chain: string): void { console.log(`Synced deployment addresses → ${path.relative(CRISP_ROOT, serverEnv)}`) console.log(`Synced VITE_CRISP_TOKEN → ${path.relative(CRISP_ROOT, clientEnv)}`) - console.log(` E3_PROOF_AGGREGATION_ENABLED=${proofAggregation} (from crisp.dev.env)`) } diff --git a/examples/CRISP/packages/crisp-contracts/hardhat.config.ts b/examples/CRISP/packages/crisp-contracts/hardhat.config.ts index aa9d956c4..b14e8acec 100644 --- a/examples/CRISP/packages/crisp-contracts/hardhat.config.ts +++ b/examples/CRISP/packages/crisp-contracts/hardhat.config.ts @@ -135,6 +135,7 @@ const config: HardhatUserConfig = { '@interfold/contracts/contracts/test/MockCiphernodeRegistry.sol', '@interfold/contracts/contracts/test/MockComputeProvider.sol', '@interfold/contracts/contracts/test/MockDecryptionVerifier.sol', + '@interfold/contracts/contracts/test/MockDkgFoldAttestationVerifier.sol', '@interfold/contracts/contracts/test/MockPkVerifier.sol', '@interfold/contracts/contracts/test/MockE3Program.sol', '@interfold/contracts/contracts/test/MockSlashingVerifier.sol', diff --git a/examples/CRISP/scripts/crisp_deploy.sh b/examples/CRISP/scripts/crisp_deploy.sh index 8c0846fa5..5b3c8ba5b 100755 --- a/examples/CRISP/scripts/crisp_deploy.sh +++ b/examples/CRISP/scripts/crisp_deploy.sh @@ -19,7 +19,7 @@ cd "${CRISP_ROOT}/packages/crisp-contracts" pnpm clean:deployments --network localhost -if [[ "$CRISP_PROOF_AGGREGATION_ENABLED" == "true" ]]; then +if [[ "$CRISP_SKIP_PROOF_AGGREGATION" == "false" ]]; then export ENABLE_ZK_VERIFICATION=true echo "Deploy: ENABLE_ZK_VERIFICATION=true (BfvPkVerifier + fold attestation)" else diff --git a/examples/CRISP/scripts/lib/dev_config.sh b/examples/CRISP/scripts/lib/dev_config.sh index 19a003b5a..6d2ada07a 100644 --- a/examples/CRISP/scripts/lib/dev_config.sh +++ b/examples/CRISP/scripts/lib/dev_config.sh @@ -21,7 +21,7 @@ load_crisp_dev_config() { set +a CRISP_BFV_PRESET="${CRISP_BFV_PRESET:-insecure-512}" - CRISP_PROOF_AGGREGATION_ENABLED="${CRISP_PROOF_AGGREGATION_ENABLED:-false}" + CRISP_SKIP_PROOF_AGGREGATION="${CRISP_SKIP_PROOF_AGGREGATION:-true}" case "$CRISP_BFV_PRESET" in insecure-512 | secure-8192) ;; @@ -31,33 +31,26 @@ load_crisp_dev_config() { ;; esac - case "$CRISP_PROOF_AGGREGATION_ENABLED" in + case "$CRISP_SKIP_PROOF_AGGREGATION" in true | false) ;; *) - echo "Invalid CRISP_PROOF_AGGREGATION_ENABLED='${CRISP_PROOF_AGGREGATION_ENABLED}' (use true or false)" >&2 + echo "Invalid CRISP_SKIP_PROOF_AGGREGATION='${CRISP_SKIP_PROOF_AGGREGATION}' (use true or false)" >&2 exit 1 ;; esac - if [[ "$CRISP_PROOF_AGGREGATION_ENABLED" == "true" ]]; then - export ENABLE_ZK_VERIFICATION=true - else + if [[ "$CRISP_SKIP_PROOF_AGGREGATION" == "true" ]]; then unset ENABLE_ZK_VERIFICATION - fi - - export CRISP_BFV_PRESET CRISP_PROOF_AGGREGATION_ENABLED CRISP_ROOT REPO_ROOT -} - -_set_env_kv() { - local file=$1 key=$2 value=$3 - if [[ -f "$file" ]] && grep -q "^${key}=" "$file"; then - local tmp - tmp="$(mktemp)" - sed "s|^${key}=.*|${key}=${value}|" "$file" >"$tmp" - mv "$tmp" "$file" else - echo "${key}=${value}" >>"$file" + export ENABLE_ZK_VERIFICATION=true fi + export E3_NODES__CN1__SKIP_PROOF_AGGREGATION="$CRISP_SKIP_PROOF_AGGREGATION" + export E3_NODES__CN2__SKIP_PROOF_AGGREGATION="$CRISP_SKIP_PROOF_AGGREGATION" + export E3_NODES__CN3__SKIP_PROOF_AGGREGATION="$CRISP_SKIP_PROOF_AGGREGATION" + export E3_NODES__CN4__SKIP_PROOF_AGGREGATION="$CRISP_SKIP_PROOF_AGGREGATION" + export E3_NODES__CN5__SKIP_PROOF_AGGREGATION="$CRISP_SKIP_PROOF_AGGREGATION" + + export CRISP_BFV_PRESET CRISP_SKIP_PROOF_AGGREGATION CRISP_ROOT REPO_ROOT } apply_crisp_dev_config_to_server_env() { @@ -65,10 +58,13 @@ apply_crisp_dev_config_to_server_env() { if [[ ! -f "$server_env" ]]; then cp "${CRISP_ROOT}/server/.env.example" "$server_env" fi - _set_env_kv "$server_env" "E3_PROOF_AGGREGATION_ENABLED" "$CRISP_PROOF_AGGREGATION_ENABLED" } build_interfold_circuits_at_setup() { + if [[ "$CRISP_SKIP_PROOF_AGGREGATION" == "true" ]]; then + echo "Skipping recursive proof-aggregation circuit build for the CRISP dev profile." + return 0 + fi local committee="${CRISP_COMMITTEE:-minimum}" echo "Building interfold circuits (preset=${CRISP_BFV_PRESET}, committee=${committee})..." ( @@ -101,9 +97,9 @@ print_crisp_dev_config_summary() { CRISP dev profile (${CRISP_ROOT}/crisp.dev.env): CRISP_BFV_PRESET=${CRISP_BFV_PRESET} - CRISP_PROOF_AGGREGATION_ENABLED=${CRISP_PROOF_AGGREGATION_ENABLED} + CRISP_SKIP_PROOF_AGGREGATION=${CRISP_SKIP_PROOF_AGGREGATION} ENABLE_ZK_VERIFICATION=${ENABLE_ZK_VERIFICATION:-false} (used at deploy via dev:up) - server/.env E3_PROOF_AGGREGATION_ENABLED synced by dev:setup + ciphernode skip flag=${CRISP_SKIP_PROOF_AGGREGATION} Contract addresses synced by dev:up (deploy → server/.env, client/.env, interfold.config.yaml) EOF diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index cb1b3fe2a..cf3da9a18 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -45,4 +45,3 @@ E3_COMPUTE_PROVIDER_BATCH_SIZE=4 # Must be a power of 2 # Whether to enable proof aggregation for E3 programs. # Managed by pnpm dev:setup from ../crisp.dev.env — edit that file, then re-run dev:setup. # See ../docs/PROOF_AGGREGATION_AND_ZK.md. -E3_PROOF_AGGREGATION_ENABLED=false diff --git a/examples/CRISP/server/src/cli/commands.rs b/examples/CRISP/server/src/cli/commands.rs index b789aa670..03c2b9d71 100644 --- a/examples/CRISP/server/src/cli/commands.rs +++ b/examples/CRISP/server/src/cli/commands.rs @@ -270,8 +270,6 @@ pub async fn initialize_crisp_round( U256::from(window_start + CONFIG.e3_duration), ]; - let proof_aggregation_enabled = CONFIG.e3_proof_aggregation_enabled; - let fee_amount = contract .get_e3_quote( committee_size, @@ -279,7 +277,6 @@ pub async fn initialize_crisp_round( e3_program, param_set, compute_provider_params_bytes.clone(), - proof_aggregation_enabled, ) .await?; info!("Fee required: {} tokens", fee_amount); @@ -333,7 +330,6 @@ pub async fn initialize_crisp_round( param_set, compute_provider_params_bytes, custom_params_bytes, - proof_aggregation_enabled, ) .await .map_err(format_request_e3_revert)?; diff --git a/examples/CRISP/server/src/config.rs b/examples/CRISP/server/src/config.rs index 56c421bfe..948fffffd 100644 --- a/examples/CRISP/server/src/config.rs +++ b/examples/CRISP/server/src/config.rs @@ -28,8 +28,6 @@ pub struct Config { pub chain_id: u64, pub cron_api_key: String, // E3 parameters - #[serde(default)] - pub e3_proof_aggregation_enabled: bool, pub e3_committee_size: u8, // 0=Minimum, 1=Micro, 2=Small pub e3_duration: u64, pub e3_compute_provider_name: String, diff --git a/examples/CRISP/server/src/server/routes/rounds.rs b/examples/CRISP/server/src/server/routes/rounds.rs index 0949dae3c..240b67e49 100644 --- a/examples/CRISP/server/src/server/routes/rounds.rs +++ b/examples/CRISP/server/src/server/routes/rounds.rs @@ -227,8 +227,6 @@ pub async fn initialize_crisp_round( batch_size: CONFIG.e3_compute_provider_batch_size, }; - let proof_aggregation_enabled = CONFIG.e3_proof_aggregation_enabled; - let compute_provider_params = Bytes::from(bincode::serialize(&compute_provider_params)?); let (receipt, e3_id) = contract .request_e3( @@ -238,7 +236,6 @@ pub async fn initialize_crisp_round( param_set, compute_provider_params, custom_params_bytes, - proof_aggregation_enabled, ) .await?; info!( diff --git a/packages/interfold-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/interfold-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index 31715c871..d17ca01a7 100644 --- a/packages/interfold-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/interfold-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -49,6 +49,17 @@ "name": "InvalidAmount", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + } + ], + "name": "InvalidBondingAsset", + "type": "error" + }, { "inputs": [], "name": "InvalidConfiguration", @@ -84,11 +95,32 @@ "name": "OperatorUnderSlash", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "OutstandingAssetLiabilities", + "type": "error" + }, { "inputs": [], "name": "RenounceOwnershipDisabled", "type": "error" }, + { + "inputs": [], + "name": "ReservedSlashedFunds", + "type": "error" + }, { "inputs": [], "name": "Unauthorized", @@ -148,6 +180,19 @@ "name": "ConfigurationUpdated", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "version", + "type": "uint256" + } + ], + "name": "EligibilityConfigurationVersionUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -179,6 +224,31 @@ "name": "LicenseBondUpdated", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LicenseSurplusSwept", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -306,6 +376,25 @@ "name": "SlashedFundsWithdrawn", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "slashingManager", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "authorized", + "type": "bool" + } + ], + "name": "SlashingManagerAuthorizationUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -382,6 +471,38 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "authorizedSlashingManagerAt", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "authorizedSlashingManagerCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -462,6 +583,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "eligibilityConfigurationVersion", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "exitDelay", @@ -601,6 +735,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "candidate", + "type": "address" + } + ], + "name": "isAuthorizedSlashingManager", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -739,7 +892,33 @@ "type": "uint256" } ], - "name": "redirectSlashedTicketFunds", + "name": "redirectReservedSlashedTicketFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "refreshOperatorStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "operators", + "type": "address[]" + } + ], + "name": "refreshOperatorStatuses", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -764,6 +943,32 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "reserveSlashedTicketFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "reservedSlashedTicketBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -777,6 +982,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "oldSlashingManager", + "type": "address" + } + ], + "name": "revokeSlashingManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1011,6 +1229,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "sweepLicenseSurplus", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "ticketPrice", @@ -1024,6 +1255,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "ticketToken", + "outputs": [ + { + "internalType": "contract InterfoldTicketToken", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -1043,6 +1287,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "totalLicenseLiability", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -1081,5 +1338,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-6d77a1fac0033f87a36ca50619587087010b9b66" + "buildInfoId": "solc-0_8_28-b08e2b9fb42fa5cb0f49eb46a9e6e77e295b46b0" } \ No newline at end of file diff --git a/packages/interfold-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/interfold-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 810d6fa89..05db5169d 100644 --- a/packages/interfold-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/interfold-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -1109,6 +1109,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "numCiphernodes", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -1332,5 +1345,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-6d77a1fac0033f87a36ca50619587087010b9b66" + "buildInfoId": "solc-0_8_28-b08e2b9fb42fa5cb0f49eb46a9e6e77e295b46b0" } \ No newline at end of file diff --git a/packages/interfold-contracts/artifacts/contracts/interfaces/IInterfold.sol/IInterfold.json b/packages/interfold-contracts/artifacts/contracts/interfaces/IInterfold.sol/IInterfold.json index 1c0c00c4c..84e5fc500 100644 --- a/packages/interfold-contracts/artifacts/contracts/interfaces/IInterfold.sol/IInterfold.json +++ b/packages/interfold-contracts/artifacts/contracts/interfaces/IInterfold.sol/IInterfold.json @@ -697,11 +697,6 @@ "internalType": "address", "name": "requester", "type": "address" - }, - { - "internalType": "bool", - "name": "proofAggregationEnabled", - "type": "bool" } ], "indexed": false, @@ -1120,6 +1115,12 @@ "name": "e3Id", "type": "uint256" }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, { "indexed": false, "internalType": "uint256", @@ -1318,6 +1319,11 @@ "name": "e3Id", "type": "uint256" }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, { "internalType": "uint256", "name": "amount", @@ -1478,11 +1484,6 @@ "internalType": "address", "name": "requester", "type": "address" - }, - { - "internalType": "bool", - "name": "proofAggregationEnabled", - "type": "bool" } ], "internalType": "struct E3", @@ -1526,11 +1527,6 @@ "internalType": "bytes", "name": "customParams", "type": "bytes" - }, - { - "internalType": "bool", - "name": "proofAggregationEnabled", - "type": "bool" } ], "internalType": "struct IInterfold.E3RequestParams", @@ -1997,11 +1993,6 @@ "internalType": "bytes", "name": "customParams", "type": "bytes" - }, - { - "internalType": "bool", - "name": "proofAggregationEnabled", - "type": "bool" } ], "internalType": "struct IInterfold.E3RequestParams", @@ -2087,11 +2078,6 @@ "internalType": "address", "name": "requester", "type": "address" - }, - { - "internalType": "bool", - "name": "proofAggregationEnabled", - "type": "bool" } ], "internalType": "struct E3", @@ -2390,5 +2376,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IInterfold.sol", - "buildInfoId": "solc-0_8_28-6d77a1fac0033f87a36ca50619587087010b9b66" + "buildInfoId": "solc-0_8_28-cba8eb9c8c72239d405341adafff7527f0fb462b" } \ No newline at end of file diff --git a/packages/interfold-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json b/packages/interfold-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json index b9fa2e50e..2c825db95 100644 --- a/packages/interfold-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json +++ b/packages/interfold-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json @@ -23,6 +23,11 @@ "name": "AppealPending", "type": "error" }, + { + "inputs": [], + "name": "AppealResolutionWindowActive", + "type": "error" + }, { "inputs": [], "name": "AppealUpheld", @@ -73,6 +78,11 @@ "name": "InsufficientAttestations", "type": "error" }, + { + "inputs": [], + "name": "InsufficientRoutingGas", + "type": "error" + }, { "inputs": [], "name": "InvalidPolicy", @@ -574,6 +584,68 @@ "name": "SlashProposed", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "SlashRouteCompleted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "SlashRoutePending", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -583,6 +655,12 @@ "name": "e3Id", "type": "uint256" }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, { "indexed": false, "internalType": "uint256", @@ -680,16 +758,11 @@ "inputs": [ { "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "amount", + "name": "proposalId", "type": "uint256" } ], - "name": "escrowSlashedFundsToRefund", + "name": "executeSlash", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -702,7 +775,7 @@ "type": "uint256" } ], - "name": "executeSlash", + "name": "expireAppeal", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -725,6 +798,81 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getE3Dependencies", + "outputs": [ + { + "internalType": "address", + "name": "bonding", + "type": "address" + }, + { + "internalType": "address", + "name": "registry", + "type": "address" + }, + { + "internalType": "address", + "name": "interfoldContract", + "type": "address" + }, + { + "internalType": "address", + "name": "refundManager", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "name": "getPendingSlashRoute", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "pending", + "type": "bool" + } + ], + "internalType": "struct ISlashingManager.PendingSlashRoute", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -905,7 +1053,7 @@ "type": "address" } ], - "name": "hasOpenLaneBProposal", + "name": "hasOpenSlashProposal", "outputs": [ { "internalType": "bool", @@ -1081,6 +1229,44 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "name": "retrySlashRoute", + "outputs": [ + { + "internalType": "bool", + "name": "routed", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "name": "routePendingSlashFunds", + "outputs": [ + { + "internalType": "bool", + "name": "routed", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1172,6 +1358,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "snapshotE3Dependencies", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "totalProposals", @@ -1233,5 +1432,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ISlashingManager.sol", - "buildInfoId": "solc-0_8_28-6d77a1fac0033f87a36ca50619587087010b9b66" + "buildInfoId": "solc-0_8_28-a061e0a52134a96e9721aff61615698a1eae6886" } \ No newline at end of file diff --git a/packages/interfold-contracts/artifacts/contracts/token/InterfoldTicketToken.sol/InterfoldTicketToken.json b/packages/interfold-contracts/artifacts/contracts/token/InterfoldTicketToken.sol/InterfoldTicketToken.json index c83934fff..a82e9edd6 100644 --- a/packages/interfold-contracts/artifacts/contracts/token/InterfoldTicketToken.sol/InterfoldTicketToken.json +++ b/packages/interfold-contracts/artifacts/contracts/token/InterfoldTicketToken.sol/InterfoldTicketToken.json @@ -1384,5 +1384,5 @@ ] }, "inputSourceName": "project/contracts/token/InterfoldTicketToken.sol", - "buildInfoId": "solc-0_8_28-4eebd49e068b5e860a0155b6fc1dddcc50e5763e" + "buildInfoId": "solc-0_8_28-cbb48f089c866ef144e6f8c2927a419cbfdb1af2" } \ No newline at end of file diff --git a/packages/interfold-contracts/audits/storage-layouts/BondingRegistry.json b/packages/interfold-contracts/audits/storage-layouts/BondingRegistry.json new file mode 100644 index 000000000..1c30ae515 --- /dev/null +++ b/packages/interfold-contracts/audits/storage-layouts/BondingRegistry.json @@ -0,0 +1,445 @@ +{ + "_format": "interfold-storage-layout-v1", + "contract": "BondingRegistry", + "source": "contracts/registry/BondingRegistry.sol", + "baseline": { + "buildInfoId": "solc-0_8_28-cbb48f089c866ef144e6f8c2927a419cbfdb1af2", + "compiler": "0.8.28+commit.7893614a", + "evmVersion": "paris", + "optimizerRuns": 1, + "sourceCommit": "f4c9ba955f437ae8c607b464c859b7809718be86", + "sourceSha256": "4d982b7b2b4ef8ea7d7db4b75891007f774afe8e4ec6815b20abd12fd998c5d9" + }, + "storage": [ + { + "astId": 26785, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "ticketToken", + "offset": 0, + "slot": "0", + "type": "t_contract(InterfoldTicketToken)38979" + }, + { + "astId": 26789, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "licenseToken", + "offset": 0, + "slot": "1", + "type": "t_contract(IERC20)4293" + }, + { + "astId": 26793, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "registry", + "offset": 0, + "slot": "2", + "type": "t_contract(ICiphernodeRegistry)21841" + }, + { + "astId": 26796, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "slashingManager", + "offset": 0, + "slot": "3", + "type": "t_address" + }, + { + "astId": 26801, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "authorizedDistributors", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_bool)" + }, + { + "astId": 26804, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "authorizedDistributorCount", + "offset": 0, + "slot": "5", + "type": "t_uint256" + }, + { + "astId": 26823, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "slashedFundsTreasury", + "offset": 0, + "slot": "6", + "type": "t_address" + }, + { + "astId": 26826, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "ticketPrice", + "offset": 0, + "slot": "7", + "type": "t_uint256" + }, + { + "astId": 26829, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "licenseRequiredBond", + "offset": 0, + "slot": "8", + "type": "t_uint256" + }, + { + "astId": 26832, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "minTicketBalance", + "offset": 0, + "slot": "9", + "type": "t_uint256" + }, + { + "astId": 26835, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "exitDelay", + "offset": 0, + "slot": "10", + "type": "t_uint64" + }, + { + "astId": 26838, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "licenseActiveBps", + "offset": 0, + "slot": "11", + "type": "t_uint256" + }, + { + "astId": 26841, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "numActiveOperators", + "offset": 0, + "slot": "12", + "type": "t_uint256" + }, + { + "astId": 26861, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "operators", + "offset": 0, + "slot": "13", + "type": "t_mapping(t_address,t_struct(Operator)26855_storage)" + }, + { + "astId": 26864, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "slashedTicketBalance", + "offset": 0, + "slot": "14", + "type": "t_uint256" + }, + { + "astId": 26867, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "slashedLicenseBond", + "offset": 0, + "slot": "15", + "type": "t_uint256" + }, + { + "astId": 26871, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "_exits", + "offset": 0, + "slot": "16", + "type": "t_struct(ExitQueueState)24674_storage" + }, + { + "astId": 26874, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "eligibilityConfigurationVersion", + "offset": 0, + "slot": "21", + "type": "t_uint256" + }, + { + "astId": 26877, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "reservedSlashedTicketBalance", + "offset": 0, + "slot": "22", + "type": "t_uint256" + }, + { + "astId": 26881, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "_authorizedSlashingManagers", + "offset": 0, + "slot": "23", + "type": "t_array(t_address)dyn_storage" + }, + { + "astId": 26886, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "_authorizedSlashingManagerIndex", + "offset": 0, + "slot": "24", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 26893, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "totalLicenseLiability", + "offset": 0, + "slot": "25", + "type": "t_uint256" + }, + { + "astId": 29436, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "__gap", + "offset": 0, + "slot": "26", + "type": "t_array(t_uint256)47_storage" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "base": "t_address", + "encoding": "dynamic_array", + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_struct(ExitTranche)24643_storage)dyn_storage": { + "base": "t_struct(ExitTranche)24643_storage", + "encoding": "dynamic_array", + "label": "struct ExitQueueLib.ExitTranche[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)47_storage": { + "base": "t_uint256", + "encoding": "inplace", + "label": "uint256[47]", + "numberOfBytes": "1504" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(ICiphernodeRegistry)21841": { + "encoding": "inplace", + "label": "contract ICiphernodeRegistry", + "numberOfBytes": "20" + }, + "t_contract(IERC20)4293": { + "encoding": "inplace", + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(InterfoldTicketToken)38979": { + "encoding": "inplace", + "label": "contract InterfoldTicketToken", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_array(t_struct(ExitTranche)24643_storage)dyn_storage)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => struct ExitQueueLib.ExitTranche[])", + "numberOfBytes": "32", + "value": "t_array(t_struct(ExitTranche)24643_storage)dyn_storage" + }, + "t_mapping(t_address,t_bool)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_address,t_struct(Operator)26855_storage)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => struct BondingRegistry.Operator)", + "numberOfBytes": "32", + "value": "t_struct(Operator)26855_storage" + }, + "t_mapping(t_address,t_struct(PendingAmounts)24649_storage)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => struct ExitQueueLib.PendingAmounts)", + "numberOfBytes": "32", + "value": "t_struct(PendingAmounts)24649_storage" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_struct(ExitQueueState)24674_storage": { + "encoding": "inplace", + "label": "struct ExitQueueLib.ExitQueueState", + "members": [ + { + "astId": 24656, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "operatorQueues", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_address,t_array(t_struct(ExitTranche)24643_storage)dyn_storage)" + }, + { + "astId": 24660, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "queueHeadIndexTicket", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 24664, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "queueHeadIndexLicense", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 24669, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "pendingTotals", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_address,t_struct(PendingAmounts)24649_storage)" + }, + { + "astId": 24673, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "liveTrancheCount", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_uint256)" + } + ], + "numberOfBytes": "160" + }, + "t_struct(ExitTranche)24643_storage": { + "encoding": "inplace", + "label": "struct ExitQueueLib.ExitTranche", + "members": [ + { + "astId": 24638, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "unlockTimestamp", + "offset": 0, + "slot": "0", + "type": "t_uint64" + }, + { + "astId": 24640, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "ticketAmount", + "offset": 0, + "slot": "1", + "type": "t_uint256" + }, + { + "astId": 24642, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "licenseAmount", + "offset": 0, + "slot": "2", + "type": "t_uint256" + } + ], + "numberOfBytes": "96" + }, + "t_struct(Operator)26855_storage": { + "encoding": "inplace", + "label": "struct BondingRegistry.Operator", + "members": [ + { + "astId": 26844, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "licenseBond", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 26846, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "exitUnlocksAt", + "offset": 0, + "slot": "1", + "type": "t_uint64" + }, + { + "astId": 26848, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "registered", + "offset": 8, + "slot": "1", + "type": "t_bool" + }, + { + "astId": 26850, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "exitRequested", + "offset": 9, + "slot": "1", + "type": "t_bool" + }, + { + "astId": 26852, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "active", + "offset": 10, + "slot": "1", + "type": "t_bool" + }, + { + "astId": 26854, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "eligibilityVersion", + "offset": 0, + "slot": "2", + "type": "t_uint256" + } + ], + "numberOfBytes": "96" + }, + "t_struct(PendingAmounts)24649_storage": { + "encoding": "inplace", + "label": "struct ExitQueueLib.PendingAmounts", + "members": [ + { + "astId": 24646, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "ticketAmount", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 24648, + "contract": "project/contracts/registry/BondingRegistry.sol:BondingRegistry", + "label": "licenseAmount", + "offset": 0, + "slot": "1", + "type": "t_uint256" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "encoding": "inplace", + "label": "uint64", + "numberOfBytes": "8" + } + } +} diff --git a/packages/interfold-contracts/audits/storage-layouts/CiphernodeRegistryOwnable.json b/packages/interfold-contracts/audits/storage-layouts/CiphernodeRegistryOwnable.json new file mode 100644 index 000000000..f9492f062 --- /dev/null +++ b/packages/interfold-contracts/audits/storage-layouts/CiphernodeRegistryOwnable.json @@ -0,0 +1,518 @@ +{ + "_format": "interfold-storage-layout-v1", + "contract": "CiphernodeRegistryOwnable", + "source": "contracts/registry/CiphernodeRegistryOwnable.sol", + "baseline": { + "buildInfoId": "solc-0_8_28-cbb48f089c866ef144e6f8c2927a419cbfdb1af2", + "compiler": "0.8.28+commit.7893614a", + "evmVersion": "paris", + "optimizerRuns": 1, + "sourceCommit": "f4c9ba955f437ae8c607b464c859b7809718be86", + "sourceSha256": "033ae30ce7a728081ca6cf34e050c970a5ed23fbab21ca81b66f4c8bd759af5e" + }, + "storage": [ + { + "astId": 29504, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "interfold", + "offset": 0, + "slot": "0", + "type": "t_contract(IInterfold)23381" + }, + { + "astId": 29508, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "bondingRegistry", + "offset": 0, + "slot": "1", + "type": "t_contract(IBondingRegistry)21192" + }, + { + "astId": 29512, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "numCiphernodes", + "offset": 0, + "slot": "2", + "type": "t_uint256" + }, + { + "astId": 29515, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "sortitionSubmissionWindow", + "offset": 0, + "slot": "3", + "type": "t_uint256" + }, + { + "astId": 29535, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "ciphernodes", + "offset": 0, + "slot": "4", + "type": "t_struct(LazyIMTData)14046_storage" + }, + { + "astId": 29540, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "ciphernodeEnabled", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_address,t_bool)" + }, + { + "astId": 29545, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "ciphernodeTreeIndex", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint40)" + }, + { + "astId": 29550, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "roots", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_uint256,t_uint256)" + }, + { + "astId": 29555, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "publicKeyHashes", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_uint256,t_bytes32)" + }, + { + "astId": 29561, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "committees", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_uint256,t_struct(Committee)21253_storage)" + }, + { + "astId": 29565, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "slashingManager", + "offset": 0, + "slot": "11", + "type": "t_contract(ISlashingManager)24020" + }, + { + "astId": 29569, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "dkgFoldAttestationVerifier", + "offset": 0, + "slot": "12", + "type": "t_contract(IDkgFoldAttestationVerifier)21935" + }, + { + "astId": 29576, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "pendingDkgFoldAttestationVerifier", + "offset": 0, + "slot": "13", + "type": "t_address" + }, + { + "astId": 29578, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "pendingDkgFoldAttestationVerifierAt", + "offset": 0, + "slot": "14", + "type": "t_uint256" + }, + { + "astId": 29581, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "accusationVoteValidity", + "offset": 0, + "slot": "15", + "type": "t_uint256" + }, + { + "astId": 29592, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "pendingAccusationVoteValidity", + "offset": 0, + "slot": "16", + "type": "t_uint256" + }, + { + "astId": 29594, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "pendingAccusationVoteValidityAt", + "offset": 0, + "slot": "17", + "type": "t_uint256" + }, + { + "astId": 29600, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "dkgPartyIds", + "offset": 0, + "slot": "18", + "type": "t_mapping(t_uint256,t_array(t_uint256)dyn_storage)" + }, + { + "astId": 29605, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "dkgSkAggCommits", + "offset": 0, + "slot": "19", + "type": "t_mapping(t_uint256,t_array(t_bytes32)dyn_storage)" + }, + { + "astId": 29610, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "dkgEsmAggCommits", + "offset": 0, + "slot": "20", + "type": "t_mapping(t_uint256,t_array(t_bytes32)dyn_storage)" + }, + { + "astId": 29626, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "_committeeDependencies", + "offset": 0, + "slot": "21", + "type": "t_mapping(t_uint256,t_struct(CommitteeDependencies)29620_storage)" + }, + { + "astId": 32150, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "__gap", + "offset": 0, + "slot": "22", + "type": "t_array(t_uint256)50_storage" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "base": "t_address", + "encoding": "dynamic_array", + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_bytes32)dyn_storage": { + "base": "t_bytes32", + "encoding": "dynamic_array", + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)50_storage": { + "base": "t_uint256", + "encoding": "inplace", + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_array(t_uint256)dyn_storage": { + "base": "t_uint256", + "encoding": "dynamic_array", + "label": "uint256[]", + "numberOfBytes": "32" + }, + "t_array(t_uint32)2_storage": { + "base": "t_uint32", + "encoding": "inplace", + "label": "uint32[2]", + "numberOfBytes": "32" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "encoding": "inplace", + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IBondingRegistry)21192": { + "encoding": "inplace", + "label": "contract IBondingRegistry", + "numberOfBytes": "20" + }, + "t_contract(IDkgFoldAttestationVerifier)21935": { + "encoding": "inplace", + "label": "contract IDkgFoldAttestationVerifier", + "numberOfBytes": "20" + }, + "t_contract(IInterfold)23381": { + "encoding": "inplace", + "label": "contract IInterfold", + "numberOfBytes": "20" + }, + "t_contract(ISlashingManager)24020": { + "encoding": "inplace", + "label": "contract ISlashingManager", + "numberOfBytes": "20" + }, + "t_enum(CommitteeStage)21216": { + "encoding": "inplace", + "label": "enum ICiphernodeRegistry.CommitteeStage", + "numberOfBytes": "1" + }, + "t_enum(MemberStatus)21210": { + "encoding": "inplace", + "label": "enum ICiphernodeRegistry.MemberStatus", + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_address,t_enum(MemberStatus)21210)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => enum ICiphernodeRegistry.MemberStatus)", + "numberOfBytes": "32", + "value": "t_enum(MemberStatus)21210" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_address,t_uint40)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint40)", + "numberOfBytes": "32", + "value": "t_uint40" + }, + "t_mapping(t_uint256,t_array(t_bytes32)dyn_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => bytes32[])", + "numberOfBytes": "32", + "value": "t_array(t_bytes32)dyn_storage" + }, + "t_mapping(t_uint256,t_array(t_uint256)dyn_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => uint256[])", + "numberOfBytes": "32", + "value": "t_array(t_uint256)dyn_storage" + }, + "t_mapping(t_uint256,t_bytes32)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => bytes32)", + "numberOfBytes": "32", + "value": "t_bytes32" + }, + "t_mapping(t_uint256,t_struct(Committee)21253_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct ICiphernodeRegistry.Committee)", + "numberOfBytes": "32", + "value": "t_struct(Committee)21253_storage" + }, + "t_mapping(t_uint256,t_struct(CommitteeDependencies)29620_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct CiphernodeRegistryOwnable.CommitteeDependencies)", + "numberOfBytes": "32", + "value": "t_struct(CommitteeDependencies)29620_storage" + }, + "t_mapping(t_uint256,t_uint256)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_struct(Committee)21253_storage": { + "encoding": "inplace", + "label": "struct ICiphernodeRegistry.Committee", + "members": [ + { + "astId": 21220, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "stage", + "offset": 0, + "slot": "0", + "type": "t_enum(CommitteeStage)21216" + }, + { + "astId": 21222, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "seed", + "offset": 0, + "slot": "1", + "type": "t_uint256" + }, + { + "astId": 21224, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "requestBlock", + "offset": 0, + "slot": "2", + "type": "t_uint256" + }, + { + "astId": 21226, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "committeeDeadline", + "offset": 0, + "slot": "3", + "type": "t_uint256" + }, + { + "astId": 21228, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "publicKey", + "offset": 0, + "slot": "4", + "type": "t_bytes32" + }, + { + "astId": 21232, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "threshold", + "offset": 0, + "slot": "5", + "type": "t_array(t_uint32)2_storage" + }, + { + "astId": 21235, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "topNodes", + "offset": 0, + "slot": "6", + "type": "t_array(t_address)dyn_storage" + }, + { + "astId": 21237, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "committeeHash", + "offset": 0, + "slot": "7", + "type": "t_bytes32" + }, + { + "astId": 21241, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "submitted", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_address,t_bool)" + }, + { + "astId": 21245, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "scoreOf", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 21250, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "memberStatus", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_address,t_enum(MemberStatus)21210)" + }, + { + "astId": 21252, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "activeCount", + "offset": 0, + "slot": "11", + "type": "t_uint256" + } + ], + "numberOfBytes": "384" + }, + "t_struct(CommitteeDependencies)29620_storage": { + "encoding": "inplace", + "label": "struct CiphernodeRegistryOwnable.CommitteeDependencies", + "members": [ + { + "astId": 29613, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "interfoldContract", + "offset": 0, + "slot": "0", + "type": "t_contract(IInterfold)23381" + }, + { + "astId": 29616, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "bonding", + "offset": 0, + "slot": "1", + "type": "t_contract(IBondingRegistry)21192" + }, + { + "astId": 29619, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "slashManager", + "offset": 0, + "slot": "2", + "type": "t_contract(ISlashingManager)24020" + } + ], + "numberOfBytes": "96" + }, + "t_struct(LazyIMTData)14046_storage": { + "encoding": "inplace", + "label": "struct LazyIMTData", + "members": [ + { + "astId": 14039, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "maxIndex", + "offset": 0, + "slot": "0", + "type": "t_uint40" + }, + { + "astId": 14041, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "numberOfLeaves", + "offset": 5, + "slot": "0", + "type": "t_uint40" + }, + { + "astId": 14045, + "contract": "project/contracts/registry/CiphernodeRegistryOwnable.sol:CiphernodeRegistryOwnable", + "label": "elements", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_uint256,t_uint256)" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "encoding": "inplace", + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint40": { + "encoding": "inplace", + "label": "uint40", + "numberOfBytes": "5" + } + } +} diff --git a/packages/interfold-contracts/audits/storage-layouts/E3RefundManager.json b/packages/interfold-contracts/audits/storage-layouts/E3RefundManager.json new file mode 100644 index 000000000..7e6a110b0 --- /dev/null +++ b/packages/interfold-contracts/audits/storage-layouts/E3RefundManager.json @@ -0,0 +1,501 @@ +{ + "_format": "interfold-storage-layout-v1", + "contract": "E3RefundManager", + "source": "contracts/E3RefundManager.sol", + "baseline": { + "buildInfoId": "solc-0_8_28-cbb48f089c866ef144e6f8c2927a419cbfdb1af2", + "compiler": "0.8.28+commit.7893614a", + "evmVersion": "paris", + "optimizerRuns": 1, + "sourceCommit": "f4c9ba955f437ae8c607b464c859b7809718be86", + "sourceSha256": "b53052d82326cff5eb5d756a27de0d34598237d109f1bac18ba6208ca2fc11e8" + }, + "storage": [ + { + "astId": 15242, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "interfold", + "offset": 0, + "slot": "0", + "type": "t_contract(IInterfold)23381" + }, + { + "astId": 15246, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "bondingRegistry", + "offset": 0, + "slot": "1", + "type": "t_contract(IBondingRegistry)21192" + }, + { + "astId": 15249, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "treasury", + "offset": 0, + "slot": "2", + "type": "t_address" + }, + { + "astId": 15253, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_workAllocation", + "offset": 0, + "slot": "3", + "type": "t_struct(WorkValueAllocation)22040_storage" + }, + { + "astId": 15259, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_distributions", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_struct(RefundDistribution)22076_storage)" + }, + { + "astId": 15266, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_requesterClaimed", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))" + }, + { + "astId": 15271, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_claimCount", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_uint256,t_uint256)" + }, + { + "astId": 15276, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_honestNodeClaimCount", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_uint256,t_uint256)" + }, + { + "astId": 15281, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_totalHonestNodePaid", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_uint256,t_uint256)" + }, + { + "astId": 15287, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_honestNodes", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_uint256,t_array(t_address)dyn_storage)" + }, + { + "astId": 15295, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_pendingTreasury", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_address,t_mapping(t_contract(IERC20)4293,t_uint256))" + }, + { + "astId": 15302, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_honestNodeClaimed", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))" + }, + { + "astId": 15308, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_e3PolicySnapshots", + "offset": 0, + "slot": "12", + "type": "t_mapping(t_uint256,t_struct(E3PolicySnapshot)22055_storage)" + }, + { + "astId": 15311, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "policyVersion", + "offset": 0, + "slot": "13", + "type": "t_uint64" + }, + { + "astId": 15319, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_pendingSlashedByToken", + "offset": 0, + "slot": "14", + "type": "t_mapping(t_uint256,t_mapping(t_contract(IERC20)4293,t_uint256))" + }, + { + "astId": 15329, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_pendingSlashedClaims", + "offset": 0, + "slot": "15", + "type": "t_mapping(t_uint256,t_mapping(t_contract(IERC20)4293,t_mapping(t_address,t_uint256)))" + }, + { + "astId": 15337, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_pendingTrackedTreasury", + "offset": 0, + "slot": "16", + "type": "t_mapping(t_address,t_mapping(t_contract(IERC20)4293,t_uint256))" + }, + { + "astId": 15343, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_tokenLiability", + "offset": 0, + "slot": "17", + "type": "t_mapping(t_contract(IERC20)4293,t_uint256)" + }, + { + "astId": 15348, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "_successSettlementReady", + "offset": 0, + "slot": "18", + "type": "t_mapping(t_uint256,t_bool)" + }, + { + "astId": 17656, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "__gap", + "offset": 0, + "slot": "19", + "type": "t_array(t_uint256)50_storage" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "base": "t_address", + "encoding": "dynamic_array", + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)50_storage": { + "base": "t_uint256", + "encoding": "inplace", + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IBondingRegistry)21192": { + "encoding": "inplace", + "label": "contract IBondingRegistry", + "numberOfBytes": "20" + }, + "t_contract(IERC20)4293": { + "encoding": "inplace", + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IInterfold)23381": { + "encoding": "inplace", + "label": "contract IInterfold", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_address,t_mapping(t_contract(IERC20)4293,t_uint256))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(contract IERC20 => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_contract(IERC20)4293,t_uint256)" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_contract(IERC20)4293,t_mapping(t_address,t_uint256))": { + "encoding": "mapping", + "key": "t_contract(IERC20)4293", + "label": "mapping(contract IERC20 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_uint256)" + }, + "t_mapping(t_contract(IERC20)4293,t_uint256)": { + "encoding": "mapping", + "key": "t_contract(IERC20)4293", + "label": "mapping(contract IERC20 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_uint256,t_array(t_address)dyn_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => address[])", + "numberOfBytes": "32", + "value": "t_array(t_address)dyn_storage" + }, + "t_mapping(t_uint256,t_bool)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_bool))": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => mapping(address => bool))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_bool)" + }, + "t_mapping(t_uint256,t_mapping(t_contract(IERC20)4293,t_mapping(t_address,t_uint256)))": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => mapping(contract IERC20 => mapping(address => uint256)))", + "numberOfBytes": "32", + "value": "t_mapping(t_contract(IERC20)4293,t_mapping(t_address,t_uint256))" + }, + "t_mapping(t_uint256,t_mapping(t_contract(IERC20)4293,t_uint256))": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => mapping(contract IERC20 => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_contract(IERC20)4293,t_uint256)" + }, + "t_mapping(t_uint256,t_struct(E3PolicySnapshot)22055_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct IE3RefundManager.E3PolicySnapshot)", + "numberOfBytes": "32", + "value": "t_struct(E3PolicySnapshot)22055_storage" + }, + "t_mapping(t_uint256,t_struct(RefundDistribution)22076_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct IE3RefundManager.RefundDistribution)", + "numberOfBytes": "32", + "value": "t_struct(RefundDistribution)22076_storage" + }, + "t_mapping(t_uint256,t_uint256)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_struct(E3PolicySnapshot)22055_storage": { + "encoding": "inplace", + "label": "struct IE3RefundManager.E3PolicySnapshot", + "members": [ + { + "astId": 22044, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "allocation", + "offset": 0, + "slot": "0", + "type": "t_struct(WorkValueAllocation)22040_storage" + }, + { + "astId": 22046, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "treasury", + "offset": 0, + "slot": "1", + "type": "t_address" + }, + { + "astId": 22048, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "interfold", + "offset": 0, + "slot": "2", + "type": "t_address" + }, + { + "astId": 22050, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "registry", + "offset": 0, + "slot": "3", + "type": "t_address" + }, + { + "astId": 22052, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "version", + "offset": 20, + "slot": "3", + "type": "t_uint64" + }, + { + "astId": 22054, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "initialized", + "offset": 28, + "slot": "3", + "type": "t_bool" + } + ], + "numberOfBytes": "128" + }, + "t_struct(RefundDistribution)22076_storage": { + "encoding": "inplace", + "label": "struct IE3RefundManager.RefundDistribution", + "members": [ + { + "astId": 22058, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "requesterAmount", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 22060, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "honestNodeAmount", + "offset": 0, + "slot": "1", + "type": "t_uint256" + }, + { + "astId": 22062, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "protocolAmount", + "offset": 0, + "slot": "2", + "type": "t_uint256" + }, + { + "astId": 22064, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "totalSlashed", + "offset": 0, + "slot": "3", + "type": "t_uint256" + }, + { + "astId": 22066, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "honestNodeCount", + "offset": 0, + "slot": "4", + "type": "t_uint256" + }, + { + "astId": 22068, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "calculated", + "offset": 0, + "slot": "5", + "type": "t_bool" + }, + { + "astId": 22071, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "feeToken", + "offset": 1, + "slot": "5", + "type": "t_contract(IERC20)4293" + }, + { + "astId": 22073, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "originalPayment", + "offset": 0, + "slot": "6", + "type": "t_uint256" + }, + { + "astId": 22075, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "perNodeAmount", + "offset": 0, + "slot": "7", + "type": "t_uint256" + } + ], + "numberOfBytes": "256" + }, + "t_struct(WorkValueAllocation)22040_storage": { + "encoding": "inplace", + "label": "struct IE3RefundManager.WorkValueAllocation", + "members": [ + { + "astId": 22031, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "committeeFormationBps", + "offset": 0, + "slot": "0", + "type": "t_uint16" + }, + { + "astId": 22033, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "dkgBps", + "offset": 2, + "slot": "0", + "type": "t_uint16" + }, + { + "astId": 22035, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "decryptionBps", + "offset": 4, + "slot": "0", + "type": "t_uint16" + }, + { + "astId": 22037, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "protocolBps", + "offset": 6, + "slot": "0", + "type": "t_uint16" + }, + { + "astId": 22039, + "contract": "project/contracts/E3RefundManager.sol:E3RefundManager", + "label": "successSlashedNodeBps", + "offset": 8, + "slot": "0", + "type": "t_uint16" + } + ], + "numberOfBytes": "32" + }, + "t_uint16": { + "encoding": "inplace", + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "encoding": "inplace", + "label": "uint64", + "numberOfBytes": "8" + } + } +} diff --git a/packages/interfold-contracts/audits/storage-layouts/Interfold.json b/packages/interfold-contracts/audits/storage-layouts/Interfold.json new file mode 100644 index 000000000..4e517b848 --- /dev/null +++ b/packages/interfold-contracts/audits/storage-layouts/Interfold.json @@ -0,0 +1,834 @@ +{ + "_format": "interfold-storage-layout-v1", + "contract": "Interfold", + "source": "contracts/Interfold.sol", + "baseline": { + "buildInfoId": "solc-0_8_28-cbb48f089c866ef144e6f8c2927a419cbfdb1af2", + "compiler": "0.8.28+commit.7893614a", + "evmVersion": "paris", + "optimizerRuns": 1, + "sourceCommit": "f4c9ba955f437ae8c607b464c859b7809718be86", + "sourceSha256": "c47078c161d1661c35246109c4d67e43c9b23bba2df96a81f0bb126afa76ec1a" + }, + "storage": [ + { + "astId": 17719, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "ciphernodeRegistry", + "offset": 0, + "slot": "0", + "type": "t_contract(ICiphernodeRegistry)21841" + }, + { + "astId": 17723, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "bondingRegistry", + "offset": 0, + "slot": "1", + "type": "t_contract(IBondingRegistry)21192" + }, + { + "astId": 17727, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "e3RefundManager", + "offset": 0, + "slot": "2", + "type": "t_contract(IE3RefundManager)22457" + }, + { + "astId": 17731, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "slashingManager", + "offset": 0, + "slot": "3", + "type": "t_contract(ISlashingManager)24020" + }, + { + "astId": 17735, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "feeToken", + "offset": 0, + "slot": "4", + "type": "t_contract(IERC20)4293" + }, + { + "astId": 17738, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "maxDuration", + "offset": 0, + "slot": "5", + "type": "t_uint256" + }, + { + "astId": 17741, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "nexte3Id", + "offset": 0, + "slot": "6", + "type": "t_uint256" + }, + { + "astId": 17747, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "e3Programs", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_contract(IE3Program)22021,t_bool)" + }, + { + "astId": 17753, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "e3s", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_uint256,t_struct(E3)21981_storage)" + }, + { + "astId": 17759, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "decryptionVerifiers", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_bytes32,t_contract(IDecryptionVerifier)21908)" + }, + { + "astId": 17765, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "pkVerifiers", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_bytes32,t_contract(IPkVerifier)23419)" + }, + { + "astId": 17770, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "paramSetRegistry", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint8,t_bytes_storage)" + }, + { + "astId": 17775, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "e3Payments", + "offset": 0, + "slot": "12", + "type": "t_mapping(t_uint256,t_uint256)" + }, + { + "astId": 17781, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_e3Stages", + "offset": 0, + "slot": "13", + "type": "t_mapping(t_uint256,t_enum(E3Stage)22486)" + }, + { + "astId": 17787, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_e3Deadlines", + "offset": 0, + "slot": "14", + "type": "t_mapping(t_uint256,t_struct(E3Deadlines)22518_storage)" + }, + { + "astId": 17793, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_e3FailureReasons", + "offset": 0, + "slot": "15", + "type": "t_mapping(t_uint256,t_enum(FailureReason)22502)" + }, + { + "astId": 17798, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_e3Requesters", + "offset": 0, + "slot": "16", + "type": "t_mapping(t_uint256,t_address)" + }, + { + "astId": 17804, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_e3FeeTokens", + "offset": 0, + "slot": "17", + "type": "t_mapping(t_uint256,t_contract(IERC20)4293)" + }, + { + "astId": 17812, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "committeeThresholds", + "offset": 0, + "slot": "18", + "type": "t_mapping(t_enum(CommitteeSize)22477,t_array(t_uint32)2_storage)" + }, + { + "astId": 17817, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_e3ProtocolShareBps", + "offset": 0, + "slot": "19", + "type": "t_mapping(t_uint256,t_uint16)" + }, + { + "astId": 17822, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_e3ProtocolTreasury", + "offset": 0, + "slot": "20", + "type": "t_mapping(t_uint256,t_address)" + }, + { + "astId": 17826, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_timeoutConfig", + "offset": 0, + "slot": "21", + "type": "t_struct(E3TimeoutConfig)22510_storage" + }, + { + "astId": 17830, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_pricingConfig", + "offset": 0, + "slot": "24", + "type": "t_struct(PricingConfig)22550_storage" + }, + { + "astId": 17840, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_feeTokenAllowed", + "offset": 0, + "slot": "33", + "type": "t_mapping(t_contract(IERC20)4293,t_bool)" + }, + { + "astId": 17847, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_pendingRewards", + "offset": 0, + "slot": "34", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_uint256))" + }, + { + "astId": 17855, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_pendingTreasury", + "offset": 0, + "slot": "35", + "type": "t_mapping(t_address,t_mapping(t_contract(IERC20)4293,t_uint256))" + }, + { + "astId": 17868, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "markFailedGracePeriod", + "offset": 0, + "slot": "36", + "type": "t_uint256" + }, + { + "astId": 17874, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "_e3Dependencies", + "offset": 0, + "slot": "37", + "type": "t_mapping(t_uint256,t_struct(E3Dependencies)17865_storage)" + }, + { + "astId": 20606, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "__gap", + "offset": 0, + "slot": "38", + "type": "t_array(t_uint256)49_storage" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)2_storage": { + "base": "t_uint256", + "encoding": "inplace", + "label": "uint256[2]", + "numberOfBytes": "64" + }, + "t_array(t_uint256)49_storage": { + "base": "t_uint256", + "encoding": "inplace", + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint32)2_storage": { + "base": "t_uint32", + "encoding": "inplace", + "label": "uint32[2]", + "numberOfBytes": "32" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "encoding": "inplace", + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "encoding": "bytes", + "label": "bytes", + "numberOfBytes": "32" + }, + "t_contract(IBondingRegistry)21192": { + "encoding": "inplace", + "label": "contract IBondingRegistry", + "numberOfBytes": "20" + }, + "t_contract(ICiphernodeRegistry)21841": { + "encoding": "inplace", + "label": "contract ICiphernodeRegistry", + "numberOfBytes": "20" + }, + "t_contract(IDecryptionVerifier)21908": { + "encoding": "inplace", + "label": "contract IDecryptionVerifier", + "numberOfBytes": "20" + }, + "t_contract(IE3Program)22021": { + "encoding": "inplace", + "label": "contract IE3Program", + "numberOfBytes": "20" + }, + "t_contract(IE3RefundManager)22457": { + "encoding": "inplace", + "label": "contract IE3RefundManager", + "numberOfBytes": "20" + }, + "t_contract(IERC20)4293": { + "encoding": "inplace", + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IPkVerifier)23419": { + "encoding": "inplace", + "label": "contract IPkVerifier", + "numberOfBytes": "20" + }, + "t_contract(ISlashingManager)24020": { + "encoding": "inplace", + "label": "contract ISlashingManager", + "numberOfBytes": "20" + }, + "t_enum(CommitteeSize)22477": { + "encoding": "inplace", + "label": "enum IInterfold.CommitteeSize", + "numberOfBytes": "1" + }, + "t_enum(E3Stage)22486": { + "encoding": "inplace", + "label": "enum IInterfold.E3Stage", + "numberOfBytes": "1" + }, + "t_enum(FailureReason)22502": { + "encoding": "inplace", + "label": "enum IInterfold.FailureReason", + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_mapping(t_contract(IERC20)4293,t_uint256))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(contract IERC20 => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_contract(IERC20)4293,t_uint256)" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_bytes32,t_contract(IDecryptionVerifier)21908)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => contract IDecryptionVerifier)", + "numberOfBytes": "32", + "value": "t_contract(IDecryptionVerifier)21908" + }, + "t_mapping(t_bytes32,t_contract(IPkVerifier)23419)": { + "encoding": "mapping", + "key": "t_bytes32", + "label": "mapping(bytes32 => contract IPkVerifier)", + "numberOfBytes": "32", + "value": "t_contract(IPkVerifier)23419" + }, + "t_mapping(t_contract(IE3Program)22021,t_bool)": { + "encoding": "mapping", + "key": "t_contract(IE3Program)22021", + "label": "mapping(contract IE3Program => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_contract(IERC20)4293,t_bool)": { + "encoding": "mapping", + "key": "t_contract(IERC20)4293", + "label": "mapping(contract IERC20 => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_contract(IERC20)4293,t_uint256)": { + "encoding": "mapping", + "key": "t_contract(IERC20)4293", + "label": "mapping(contract IERC20 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_enum(CommitteeSize)22477,t_array(t_uint32)2_storage)": { + "encoding": "mapping", + "key": "t_enum(CommitteeSize)22477", + "label": "mapping(enum IInterfold.CommitteeSize => uint32[2])", + "numberOfBytes": "32", + "value": "t_array(t_uint32)2_storage" + }, + "t_mapping(t_uint256,t_address)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => address)", + "numberOfBytes": "32", + "value": "t_address" + }, + "t_mapping(t_uint256,t_contract(IERC20)4293)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => contract IERC20)", + "numberOfBytes": "32", + "value": "t_contract(IERC20)4293" + }, + "t_mapping(t_uint256,t_enum(E3Stage)22486)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => enum IInterfold.E3Stage)", + "numberOfBytes": "32", + "value": "t_enum(E3Stage)22486" + }, + "t_mapping(t_uint256,t_enum(FailureReason)22502)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => enum IInterfold.FailureReason)", + "numberOfBytes": "32", + "value": "t_enum(FailureReason)22502" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_uint256))": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_uint256)" + }, + "t_mapping(t_uint256,t_struct(E3)21981_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct E3)", + "numberOfBytes": "32", + "value": "t_struct(E3)21981_storage" + }, + "t_mapping(t_uint256,t_struct(E3Deadlines)22518_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct IInterfold.E3Deadlines)", + "numberOfBytes": "32", + "value": "t_struct(E3Deadlines)22518_storage" + }, + "t_mapping(t_uint256,t_struct(E3Dependencies)17865_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct Interfold.E3Dependencies)", + "numberOfBytes": "32", + "value": "t_struct(E3Dependencies)17865_storage" + }, + "t_mapping(t_uint256,t_uint16)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => uint16)", + "numberOfBytes": "32", + "value": "t_uint16" + }, + "t_mapping(t_uint256,t_uint256)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_uint8,t_bytes_storage)": { + "encoding": "mapping", + "key": "t_uint8", + "label": "mapping(uint8 => bytes)", + "numberOfBytes": "32", + "value": "t_bytes_storage" + }, + "t_struct(E3)21981_storage": { + "encoding": "inplace", + "label": "struct E3", + "members": [ + { + "astId": 21948, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "seed", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 21951, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "committeeSize", + "offset": 0, + "slot": "1", + "type": "t_enum(CommitteeSize)22477" + }, + { + "astId": 21953, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "requestBlock", + "offset": 0, + "slot": "2", + "type": "t_uint256" + }, + { + "astId": 21957, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "inputWindow", + "offset": 0, + "slot": "3", + "type": "t_array(t_uint256)2_storage" + }, + { + "astId": 21959, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "encryptionSchemeId", + "offset": 0, + "slot": "5", + "type": "t_bytes32" + }, + { + "astId": 21962, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "e3Program", + "offset": 0, + "slot": "6", + "type": "t_contract(IE3Program)22021" + }, + { + "astId": 21964, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "paramSet", + "offset": 20, + "slot": "6", + "type": "t_uint8" + }, + { + "astId": 21966, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "customParams", + "offset": 0, + "slot": "7", + "type": "t_bytes_storage" + }, + { + "astId": 21969, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "decryptionVerifier", + "offset": 0, + "slot": "8", + "type": "t_contract(IDecryptionVerifier)21908" + }, + { + "astId": 21972, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "pkVerifier", + "offset": 0, + "slot": "9", + "type": "t_contract(IPkVerifier)23419" + }, + { + "astId": 21974, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "committeePublicKey", + "offset": 0, + "slot": "10", + "type": "t_bytes32" + }, + { + "astId": 21976, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "ciphertextOutput", + "offset": 0, + "slot": "11", + "type": "t_bytes32" + }, + { + "astId": 21978, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "plaintextOutput", + "offset": 0, + "slot": "12", + "type": "t_bytes_storage" + }, + { + "astId": 21980, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "requester", + "offset": 0, + "slot": "13", + "type": "t_address" + } + ], + "numberOfBytes": "448" + }, + "t_struct(E3Deadlines)22518_storage": { + "encoding": "inplace", + "label": "struct IInterfold.E3Deadlines", + "members": [ + { + "astId": 22513, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "dkgDeadline", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 22515, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "computeDeadline", + "offset": 0, + "slot": "1", + "type": "t_uint256" + }, + { + "astId": 22517, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "decryptionDeadline", + "offset": 0, + "slot": "2", + "type": "t_uint256" + } + ], + "numberOfBytes": "96" + }, + "t_struct(E3Dependencies)17865_storage": { + "encoding": "inplace", + "label": "struct Interfold.E3Dependencies", + "members": [ + { + "astId": 17858, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "registry", + "offset": 0, + "slot": "0", + "type": "t_contract(ICiphernodeRegistry)21841" + }, + { + "astId": 17861, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "refundManager", + "offset": 0, + "slot": "1", + "type": "t_contract(IE3RefundManager)22457" + }, + { + "astId": 17864, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "slashManager", + "offset": 0, + "slot": "2", + "type": "t_contract(ISlashingManager)24020" + } + ], + "numberOfBytes": "96" + }, + "t_struct(E3TimeoutConfig)22510_storage": { + "encoding": "inplace", + "label": "struct IInterfold.E3TimeoutConfig", + "members": [ + { + "astId": 22505, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "dkgWindow", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 22507, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "computeWindow", + "offset": 0, + "slot": "1", + "type": "t_uint256" + }, + { + "astId": 22509, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "decryptionWindow", + "offset": 0, + "slot": "2", + "type": "t_uint256" + } + ], + "numberOfBytes": "96" + }, + "t_struct(PricingConfig)22550_storage": { + "encoding": "inplace", + "label": "struct IInterfold.PricingConfig", + "members": [ + { + "astId": 22521, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "keyGenFixedPerNode", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 22523, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "keyGenPerEncryptionProof", + "offset": 0, + "slot": "1", + "type": "t_uint256" + }, + { + "astId": 22525, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "coordinationPerPair", + "offset": 0, + "slot": "2", + "type": "t_uint256" + }, + { + "astId": 22527, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "availabilityPerNodePerSec", + "offset": 0, + "slot": "3", + "type": "t_uint256" + }, + { + "astId": 22529, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "decryptionPerNode", + "offset": 0, + "slot": "4", + "type": "t_uint256" + }, + { + "astId": 22531, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "publicationBase", + "offset": 0, + "slot": "5", + "type": "t_uint256" + }, + { + "astId": 22533, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "verificationPerProof", + "offset": 0, + "slot": "6", + "type": "t_uint256" + }, + { + "astId": 22535, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "protocolTreasury", + "offset": 0, + "slot": "7", + "type": "t_address" + }, + { + "astId": 22537, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "marginBps", + "offset": 20, + "slot": "7", + "type": "t_uint16" + }, + { + "astId": 22539, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "protocolShareBps", + "offset": 22, + "slot": "7", + "type": "t_uint16" + }, + { + "astId": 22541, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "dkgUtilizationBps", + "offset": 24, + "slot": "7", + "type": "t_uint16" + }, + { + "astId": 22543, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "computeUtilizationBps", + "offset": 26, + "slot": "7", + "type": "t_uint16" + }, + { + "astId": 22545, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "decryptUtilizationBps", + "offset": 28, + "slot": "7", + "type": "t_uint16" + }, + { + "astId": 22547, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "minCommitteeSize", + "offset": 0, + "slot": "8", + "type": "t_uint32" + }, + { + "astId": 22549, + "contract": "project/contracts/Interfold.sol:Interfold", + "label": "minThreshold", + "offset": 4, + "slot": "8", + "type": "t_uint32" + } + ], + "numberOfBytes": "288" + }, + "t_uint16": { + "encoding": "inplace", + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "encoding": "inplace", + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "encoding": "inplace", + "label": "uint8", + "numberOfBytes": "1" + } + } +} diff --git a/packages/interfold-contracts/contracts/E3RefundManager.sol b/packages/interfold-contracts/contracts/E3RefundManager.sol index cf9ef486b..d7cf82753 100644 --- a/packages/interfold-contracts/contracts/E3RefundManager.sol +++ b/packages/interfold-contracts/contracts/E3RefundManager.sol @@ -17,6 +17,7 @@ import { import { IE3RefundManager } from "./interfaces/IE3RefundManager.sol"; import { IInterfold } from "./interfaces/IInterfold.sol"; import { IBondingRegistry } from "./interfaces/IBondingRegistry.sol"; +import { ICiphernodeRegistry } from "./interfaces/ICiphernodeRegistry.sol"; /** * @title E3RefundManager @@ -24,6 +25,9 @@ import { IBondingRegistry } from "./interfaces/IBondingRegistry.sol"; * @dev Implements fault-attribution based refund system * */ +// Per-E3 token, claim, routing, and immutable policy ledgers are intentionally +// isolated rather than packed into an opaque monolithic mapping. +// solhint-disable-next-line max-states-count contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { using SafeERC20 for IERC20; //////////////////////////////////////////////////////////// @@ -42,9 +46,9 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { /// @notice Maps E3 ID to refund distribution mapping(uint256 e3Id => RefundDistribution distribution) internal _distributions; - /// @notice Tracks claims per E3 per address + /// @notice Tracks requester-refund claims per E3 per address mapping(uint256 e3Id => mapping(address claimer => bool hasClaimed)) - internal _claimed; + internal _requesterClaimed; /// @notice Tracks number of claims made per E3 mapping(uint256 e3Id => uint256 count) internal _claimCount; /// @notice Tracks number of honest node claims made per E3 @@ -53,18 +57,31 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { mapping(uint256 e3Id => uint256 amount) internal _totalHonestNodePaid; /// @notice Maps E3 ID to honest node addresses mapping(uint256 e3Id => address[] nodes) internal _honestNodes; - /// @notice Pending slashed funds awaiting E3 terminal state - mapping(uint256 e3Id => uint256 amount) internal _pendingSlashedFunds; - - /// @notice Pull-payment ledger for success-path slashed-fund credits (e3Id => node => amount) - mapping(uint256 e3Id => mapping(address account => uint256 amount)) - internal _pendingSlashedSuccess; - /// @notice Snapshotted payment token for success-path slashed-fund credits - mapping(uint256 e3Id => IERC20 token) internal _slashedSuccessToken; /// @notice Treasury pull-payment ledger for protocol slashed-fund share and dust. /// @dev Per-treasury so historical treasuries can drain even after rotation. mapping(address treasury => mapping(IERC20 token => uint256 amount)) internal _pendingTreasury; + /// @notice Tracks honest-node reward claims independently from requester claims + mapping(uint256 e3Id => mapping(address claimer => bool hasClaimed)) + internal _honestNodeClaimed; + /// @notice Per-E3 settlement economics frozen at request time. + mapping(uint256 e3Id => E3PolicySnapshot snapshot) + internal _e3PolicySnapshots; + /// @notice Monotonic version for allocation or treasury changes. + uint64 public policyVersion; + /// @notice Unsettled slash escrow keyed by its actual underlying asset. + mapping(uint256 e3Id => mapping(IERC20 token => uint256 amount)) + internal _pendingSlashedByToken; + /// @notice Pull-payment slash entitlements, preserving token denomination. + mapping(uint256 e3Id => mapping(IERC20 token => mapping(address account => uint256 amount))) + internal _pendingSlashedClaims; + /// @notice Slashed portion of the shared treasury pull ledger. + mapping(address account => mapping(IERC20 token => uint256 amount)) + internal _pendingTrackedTreasury; + /// @notice Protected slashed-fund liabilities for each token. + mapping(IERC20 token => uint256 amount) internal _tokenLiability; + /// @notice Whether successful completion enabled slash settlement for this E3. + mapping(uint256 e3Id => bool ready) internal _successSettlementReady; //////////////////////////////////////////////////////////// // // // Modifiers // @@ -76,6 +93,15 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { _; } + /// @dev Authorizes the Interfold contract frozen for an existing E3. + modifier onlyE3Interfold(uint256 e3Id) { + E3PolicySnapshot storage policy = _e3PolicySnapshots[e3Id]; + if (!policy.initialized || msg.sender != policy.interfold) { + revert Unauthorized(); + } + _; + } + //////////////////////////////////////////////////////////// // // // Initialization // @@ -112,6 +138,7 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { protocolBps: 500, successSlashedNodeBps: 5000 }); + policyVersion = 1; if (_owner != owner()) _transferOwnership(_owner); } @@ -147,30 +174,25 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { uint256 originalPayment, address[] calldata honestNodes, IERC20 paymentToken - ) external onlyInterfold { + ) external onlyE3Interfold(e3Id) { require(!_distributions[e3Id].calculated, "Already calculated"); require(originalPayment > 0, "No payment"); require(address(paymentToken) != address(0), "Invalid fee token"); - // Calculate work value based on stage - IInterfold.E3Stage failedAt = _getFailedAtStage(e3Id); - (uint16 workCompletedBps, uint16 workRemainingBps) = calculateWorkValue( - failedAt + // Attribute the failure before touching requester escrow. Supplier-side + // failures return the entire fee payment; requester-side failures pay + // completed work according to the request-time policy. + IInterfold.FailureReason reason = _interfoldFor(e3Id).getFailureReason( + e3Id ); - - // Calculate base distribution - uint256 honestNodeAmount = (originalPayment * workCompletedBps) / - BPS_BASE; - uint256 requesterAmount = (originalPayment * workRemainingBps) / - BPS_BASE; - uint256 protocolAmount = originalPayment - - honestNodeAmount - - requesterAmount; + ( + uint256 honestNodeAmount, + uint256 requesterAmount, + uint256 protocolAmount + ) = _baseDistribution(e3Id, originalPayment, reason); // No honest nodes: fold work share into the requester refund (mirrors - // {Interfold._distributeRewards} success-path). Avoids per-failure dust - // stranded outside `_pendingSlashedFunds` (which `withdrawOrphanedSlashedFunds` - // cannot reach). + // {Interfold._distributeRewards} success-path). if (honestNodes.length == 0 && honestNodeAmount > 0) { requesterAmount += honestNodeAmount; honestNodeAmount = 0; @@ -198,24 +220,17 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { // Credit protocol fee via pull-payment so a malicious/reverting/blacklisted // treasury cannot brick failed-E3 processing. if (protocolAmount > 0) { - _pendingTreasury[treasury][paymentToken] += protocolAmount; + address policyTreasury = _treasuryFor(e3Id); + _pendingTreasury[policyTreasury][paymentToken] += protocolAmount; emit TreasurySlashedCredited( - treasury, + policyTreasury, paymentToken, protocolAmount ); } - // Apply any slashed funds that arrived before the distribution was calculated - uint256 pending = _pendingSlashedFunds[e3Id]; - if (pending > 0) { - _pendingSlashedFunds[e3Id] = 0; - _applySlashedFunds(e3Id, pending); - } - - // Snapshot per-honest-node payout AFTER folding in pre-distribution slashed - // funds. `claimHonestNodeReward` reads this directly so the per-node payout - // is immutable for the distribution's lifetime. + // Snapshot the base fee-token payout. Slashed assets are deliberately + // separate token-specific pull claims and never mutate these amounts. RefundDistribution storage finalDist = _distributions[e3Id]; if (honestNodes.length > 0) { finalDist.perNodeAmount = @@ -223,6 +238,10 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { honestNodes.length; } + if (_pendingSlashedByToken[e3Id][paymentToken] > 0) { + _settleSlashedFunds(e3Id, paymentToken); + } + emit RefundDistributionCalculated( e3Id, finalDist.requesterAmount, @@ -232,12 +251,66 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { ); } - /// @notice Get the stage at which E3 failed (for work calculation) - function _getFailedAtStage( - uint256 e3Id - ) internal view returns (IInterfold.E3Stage) { - IInterfold.FailureReason reason = interfold.getFailureReason(e3Id); + function _baseDistribution( + uint256 e3Id, + uint256 originalPayment, + IInterfold.FailureReason reason + ) + internal + view + returns ( + uint256 honestNodeAmount, + uint256 requesterAmount, + uint256 protocolAmount + ) + { + if (getFailurePayer(reason) == FailurePayer.Ciphernodes) { + return (0, originalPayment, 0); + } + + IInterfold.E3Stage failedAt = _getFailedAtStage(reason); + ( + uint16 workCompletedBps, + uint16 workRemainingBps + ) = _calculateWorkValue(failedAt, _allocationFor(e3Id)); + honestNodeAmount = (originalPayment * workCompletedBps) / BPS_BASE; + requesterAmount = (originalPayment * workRemainingBps) / BPS_BASE; + protocolAmount = originalPayment - honestNodeAmount - requesterAmount; + } + + /// @inheritdoc IE3RefundManager + function getFailurePayer( + IInterfold.FailureReason reason + ) public pure returns (FailurePayer payer) { + if ( + reason == IInterfold.FailureReason.NoInputsReceived || + reason == IInterfold.FailureReason.ComputeTimeout || + reason == IInterfold.FailureReason.ComputeProviderExpired || + reason == IInterfold.FailureReason.ComputeProviderFailed || + reason == IInterfold.FailureReason.RequesterCancelled + ) { + return FailurePayer.Requester; + } + if ( + reason == IInterfold.FailureReason.CommitteeFormationTimeout || + reason == IInterfold.FailureReason.InsufficientCommitteeMembers || + reason == IInterfold.FailureReason.DKGTimeout || + reason == IInterfold.FailureReason.DKGInvalidShares || + reason == IInterfold.FailureReason.DecryptionTimeout || + reason == IInterfold.FailureReason.DecryptionInvalidShares || + reason == IInterfold.FailureReason.VerificationFailed + ) { + return FailurePayer.Ciphernodes; + } + + revert InvalidFailureReason(reason); + } + + /// @notice Get the stage at which a requester-attributable E3 failed. + function _getFailedAtStage( + IInterfold.FailureReason reason + ) internal pure returns (IInterfold.E3Stage) { // Map failure reason to stage if ( reason == IInterfold.FailureReason.CommitteeFormationTimeout || @@ -277,8 +350,13 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { function calculateWorkValue( IInterfold.E3Stage stage ) public view returns (uint16 workCompletedBps, uint16 workRemainingBps) { - WorkValueAllocation memory alloc = _workAllocation; + return _calculateWorkValue(stage, _workAllocation); + } + function _calculateWorkValue( + IInterfold.E3Stage stage, + WorkValueAllocation memory alloc + ) internal pure returns (uint16 workCompletedBps, uint16 workRemainingBps) { if ( stage == IInterfold.E3Stage.Requested || stage == IInterfold.E3Stage.None @@ -311,25 +389,27 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { RefundDistribution storage dist = _distributions[e3Id]; if (!dist.calculated) revert RefundNotCalculated(e3Id); - // Guard against pre-upgrade records where feeToken was not yet stored + // A calculated distribution must always retain its settlement token. require( address(dist.feeToken) != address(0), "feeToken not initialized" ); - address requester = interfold.getRequester(e3Id); + address requester = _interfoldFor(e3Id).getRequester(e3Id); if (msg.sender != requester) revert NotRequester(e3Id, msg.sender); - if (_claimed[e3Id][msg.sender]) revert AlreadyClaimed(e3Id, msg.sender); + if (_requesterClaimed[e3Id][msg.sender]) { + revert AlreadyClaimed(e3Id, msg.sender); + } amount = dist.requesterAmount; if (amount == 0) revert NoRefundAvailable(e3Id); - _claimed[e3Id][msg.sender] = true; + _requesterClaimed[e3Id][msg.sender] = true; _claimCount[e3Id]++; // Use the per-E3 fee token (not the global one, which may have been rotated) - dist.feeToken.safeTransfer(msg.sender, amount); + _transferPreservingSlashedLiability(dist.feeToken, msg.sender, amount); emit RefundClaimed(e3Id, msg.sender, amount, "REQUESTER"); } @@ -341,13 +421,16 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { RefundDistribution storage dist = _distributions[e3Id]; require(dist.calculated, RefundNotCalculated(e3Id)); - // Guard against pre-upgrade records where feeToken was not yet stored + // A calculated distribution must always retain its settlement token. require( address(dist.feeToken) != address(0), "feeToken not initialized" ); - require(!_claimed[e3Id][msg.sender], AlreadyClaimed(e3Id, msg.sender)); + require( + !_honestNodeClaimed[e3Id][msg.sender], + AlreadyClaimed(e3Id, msg.sender) + ); // Check if caller is honest node address[] memory nodes = _honestNodes[e3Id]; @@ -359,8 +442,8 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { require(dist.honestNodeCount > 0, NoRefundAvailable(e3Id)); // Read the snapshot taken at `calculateRefund` time — immutable for the - // distribution's lifetime; post-claim slashed funds are routed to - // `_pendingSlashedFunds` and never mutate `dist.honestNodeAmount`. + // distribution's lifetime; post-claim slashed funds remain in the + // token-specific slash ledger and never mutate `dist.honestNodeAmount`. uint256 perNodeAmount = dist.perNodeAmount; require(perNodeAmount > 0, NoRefundAvailable(e3Id)); @@ -376,19 +459,24 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { ? dist.honestNodeAmount - paidIncludingThis : 0; if (dust > 0) { - _pendingTreasury[treasury][dist.feeToken] += dust; - emit TreasurySlashedCredited(treasury, dist.feeToken, dust); + address policyTreasury = _treasuryFor(e3Id); + _pendingTreasury[policyTreasury][dist.feeToken] += dust; + emit TreasurySlashedCredited( + policyTreasury, + dist.feeToken, + dust + ); } } _totalHonestNodePaid[e3Id] += amount; - _claimed[e3Id][msg.sender] = true; + _honestNodeClaimed[e3Id][msg.sender] = true; _claimCount[e3Id]++; // Direct transfer to the honest node (refund path; bypasses BondingRegistry // distributor authorization and operator-registered checks). IERC20 token = dist.feeToken; - token.safeTransfer(msg.sender, amount); + _transferPreservingSlashedLiability(token, msg.sender, amount); emit RefundClaimed(e3Id, msg.sender, amount, "HONEST_NODE"); } @@ -396,150 +484,138 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { /// @inheritdoc IE3RefundManager function escrowSlashedFunds( uint256 e3Id, + IERC20 token, uint256 amount - ) external onlyInterfold { + ) external onlyE3Interfold(e3Id) { require(amount > 0, "Zero amount"); + require(address(token) != address(0), "Invalid slash token"); - RefundDistribution storage dist = _distributions[e3Id]; - if (dist.calculated) { - if (_claimCount[e3Id] == 0) { - _applySlashedFunds(e3Id, amount); - } else if (dist.honestNodeCount > 0) { - // Distribution calculated and a claim has landed, but honest nodes existed. - // Credit the latecomer slash to the honest committee (pull via - // `claimSlashedFundsOnSuccess`) instead of orphaning to the treasury. - // No double-pay risk: requester + unclaimed honest portions were already - // settled by the initial `_applySlashedFunds`. - IERC20 token = dist.feeToken; - if (address(_slashedSuccessToken[e3Id]) == address(0)) { - _slashedSuccessToken[e3Id] = token; - } - address[] storage nodes = _honestNodes[e3Id]; - uint256 n = nodes.length; - uint256 perNode = amount / n; - uint256 distributed = 0; - for (uint256 i = 0; i < n; i++) { - uint256 nodeAmount = perNode; - if (i == n - 1) { - nodeAmount = amount - distributed; - } - if (nodeAmount > 0) { - _pendingSlashedSuccess[e3Id][nodes[i]] += nodeAmount; - emit SlashedFundsCredited( - e3Id, - nodes[i], - token, - nodeAmount - ); - } - distributed += nodeAmount; - } - } else { - _pendingSlashedFunds[e3Id] += amount; - } - } else { - _pendingSlashedFunds[e3Id] += amount; + _pendingSlashedByToken[e3Id][token] += amount; + _increaseTokenLiability(token, amount); + + if (_distributions[e3Id].calculated || _successSettlementReady[e3Id]) { + _settleSlashedFunds(e3Id, token); } - emit SlashedFundsEscrowed(e3Id, amount); + emit SlashedFundsEscrowed(e3Id, token, amount); + } + + /// @inheritdoc IE3RefundManager + function settleSlashedFunds(uint256 e3Id, IERC20 token) external { + require(_pendingSlashedByToken[e3Id][token] > 0, NothingToClaim()); + require( + _distributions[e3Id].calculated || _successSettlementReady[e3Id], + "E3 not settled" + ); + _settleSlashedFunds(e3Id, token); } /// @inheritdoc IE3RefundManager function distributeSlashedFundsOnSuccess( uint256 e3Id, - address[] calldata honestNodes, IERC20 paymentToken - ) external onlyInterfold { - uint256 escrowed = _pendingSlashedFunds[e3Id]; - if (escrowed == 0) return; - _pendingSlashedFunds[e3Id] = 0; - + ) external onlyE3Interfold(e3Id) { require(address(paymentToken) != address(0), "Invalid fee token"); - _slashedSuccessToken[e3Id] = paymentToken; + require(!_successSettlementReady[e3Id], "Already initialized"); + _successSettlementReady[e3Id] = true; - uint256 toNodes = (escrowed * _workAllocation.successSlashedNodeBps) / - BPS_BASE; - uint256 toProtocol = escrowed - toNodes; - - // Credit treasury share — pull only. - if (toProtocol > 0) { - _pendingTreasury[treasury][paymentToken] += toProtocol; - emit TreasurySlashedCredited(treasury, paymentToken, toProtocol); + if (_pendingSlashedByToken[e3Id][paymentToken] > 0) { + _settleSlashedFunds(e3Id, paymentToken); } + } - if (toNodes > 0 && honestNodes.length > 0) { - uint256 perNode = toNodes / honestNodes.length; - uint256 distributed = 0; - for (uint256 i = 0; i < honestNodes.length; i++) { - uint256 nodeAmount = perNode; - if (i == honestNodes.length - 1) { - nodeAmount = toNodes - distributed; - } - if (nodeAmount > 0) { - // credit per-node so one blacklisted/reverting recipient - // does not brick payouts for the rest of the committee. - _pendingSlashedSuccess[e3Id][honestNodes[i]] += nodeAmount; - emit SlashedFundsCredited( - e3Id, - honestNodes[i], - paymentToken, - nodeAmount - ); - } - distributed += nodeAmount; - } - } else if (toNodes > 0) { - // No honest nodes — funnel the node share to treasury for governance triage. - _pendingTreasury[treasury][paymentToken] += toNodes; - emit TreasurySlashedCredited(treasury, paymentToken, toNodes); - } + function _settleSlashedFunds(uint256 e3Id, IERC20 token) internal { + uint256 amount = _pendingSlashedByToken[e3Id][token]; + _pendingSlashedByToken[e3Id][token] = 0; + (address[] memory nodes, ) = ICiphernodeRegistry( + _e3PolicySnapshots[e3Id].registry + ).getActiveCommitteeNodes(e3Id); - emit SlashedFundsDistributedOnSuccess(e3Id, toNodes, toProtocol); + if (_distributions[e3Id].calculated) { + _settleFailedE3Slash(e3Id, token, amount, nodes); + } else { + _settleSuccessfulE3Slash(e3Id, token, amount, nodes); + } } - /// @notice Apply slashed funds to an E3's refund distribution - /// @dev This function is ONLY called on the failure path.Priority: make requester whole first, - /// then distribute remainder to honest nodes. - /// The requester is filled up to their original E3 payment before honest nodes receive - /// any portion, ensuring the party who paid for the computation is compensated first. - /// @param e3Id The E3 ID - /// @param amount The slashed amount to apply - function _applySlashedFunds(uint256 e3Id, uint256 amount) internal { + function _settleFailedE3Slash( + uint256 e3Id, + IERC20 token, + uint256 amount, + address[] memory nodes + ) internal { RefundDistribution storage dist = _distributions[e3Id]; - - // Priority: make requester whole first - // requesterGap = how much more the requester needs to reach their original payment - uint256 requesterGap = dist.originalPayment > dist.requesterAmount - ? dist.originalPayment - dist.requesterAmount - : 0; - uint256 toRequester = amount >= requesterGap ? requesterGap : amount; - uint256 toHonestNodes = amount - toRequester; - - // No honest nodes: residual would be unclaimable (`claimHonestNodeReward` - // reverts and `withdrawOrphanedSlashedFunds` only drains `_pendingSlashedFunds`). - // Route to treasury pull-pool; requester cap (originalPayment) is preserved. - if (dist.honestNodeCount == 0 && toHonestNodes > 0) { - IERC20 token = dist.feeToken; - if (address(token) != address(0)) { - _pendingTreasury[treasury][token] += toHonestNodes; - emit TreasurySlashedCredited(treasury, token, toHonestNodes); - } + uint256 toHonestNodes = amount; + uint256 toProtocol; + if (nodes.length == 0) { + // There is no honest service-provider recipient. Do not turn a + // punitive slash into requester over-compensation. + toProtocol = amount; toHonestNodes = 0; } + if (token == dist.feeToken) dist.totalSlashed += amount; + + _creditNodeSlashedClaims(e3Id, token, nodes, toHonestNodes); + _creditTrackedTreasury(_treasuryFor(e3Id), token, toProtocol); + emit SlashedFundsApplied(e3Id, token, 0, toHonestNodes); + } + + function _settleSuccessfulE3Slash( + uint256 e3Id, + IERC20 token, + uint256 amount, + address[] memory nodes + ) internal { + uint256 toNodes = (amount * _successSlashedNodeBpsFor(e3Id)) / BPS_BASE; + uint256 toProtocol = amount - toNodes; + if (nodes.length == 0) { + toProtocol += toNodes; + toNodes = 0; + } - dist.requesterAmount += toRequester; - dist.honestNodeAmount += toHonestNodes; - dist.totalSlashed += amount; + _creditNodeSlashedClaims(e3Id, token, nodes, toNodes); + _creditTrackedTreasury(_treasuryFor(e3Id), token, toProtocol); + emit SlashedFundsDistributedOnSuccess(e3Id, token, toNodes, toProtocol); + } - // Re-snapshot perNodeAmount for the pre-first-claim path (gated by - // `_claimCount==0` in `escrowSlashedFunds`). Post-first-claim funds bypass - // this code via `_pendingSlashedFunds`, so the snapshot stays immutable - // across the claim window. - if (dist.honestNodeCount > 0) { - dist.perNodeAmount = dist.honestNodeAmount / dist.honestNodeCount; + function _creditNodeSlashedClaims( + uint256 e3Id, + IERC20 token, + address[] memory nodes, + uint256 amount + ) internal { + if (amount == 0) return; + uint256 perNode = amount / nodes.length; + uint256 distributed; + for (uint256 i = 0; i < nodes.length; i++) { + uint256 nodeAmount = i == nodes.length - 1 + ? amount - distributed + : perNode; + _creditSlashedClaim(e3Id, token, nodes[i], nodeAmount); + distributed += nodeAmount; } + } + + function _creditSlashedClaim( + uint256 e3Id, + IERC20 token, + address account, + uint256 amount + ) internal { + if (amount == 0) return; + _pendingSlashedClaims[e3Id][token][account] += amount; + emit SlashedFundsCredited(e3Id, account, token, amount); + } - emit SlashedFundsApplied(e3Id, toRequester, toHonestNodes); + function _creditTrackedTreasury( + address account, + IERC20 token, + uint256 amount + ) internal { + if (amount == 0) return; + _pendingTreasury[account][token] += amount; + _pendingTrackedTreasury[account][token] += amount; + emit TreasurySlashedCredited(account, token, amount); } //////////////////////////////////////////////////////////// @@ -555,11 +631,41 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { } /// @inheritdoc IE3RefundManager - function hasClaimed( + function pendingSlashedFunds( + uint256 e3Id, + IERC20 token + ) external view returns (uint256) { + return _pendingSlashedByToken[e3Id][token]; + } + + /// @inheritdoc IE3RefundManager + function pendingSlashedClaim( + uint256 e3Id, + IERC20 token, + address account + ) external view returns (uint256) { + return _pendingSlashedClaims[e3Id][token][account]; + } + + /// @inheritdoc IE3RefundManager + function tokenLiability(IERC20 token) external view returns (uint256) { + return _tokenLiability[token]; + } + + /// @inheritdoc IE3RefundManager + function hasRequesterClaimed( + uint256 e3Id, + address claimant + ) external view returns (bool) { + return _requesterClaimed[e3Id][claimant]; + } + + /// @inheritdoc IE3RefundManager + function hasHonestNodeClaimed( uint256 e3Id, address claimant ) external view returns (bool) { - return _claimed[e3Id][claimant]; + return _honestNodeClaimed[e3Id][claimant]; } /// @inheritdoc IE3RefundManager @@ -571,6 +677,62 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { return _workAllocation; } + /// @inheritdoc IE3RefundManager + function getE3PolicySnapshot( + uint256 e3Id + ) external view returns (E3PolicySnapshot memory snapshot) { + return _e3PolicySnapshots[e3Id]; + } + + /// @inheritdoc IE3RefundManager + function snapshotE3Policy( + uint256 e3Id, + address registry + ) external onlyInterfold { + E3PolicySnapshot storage snapshot = _e3PolicySnapshots[e3Id]; + require(!snapshot.initialized, "Policy already snapshotted"); + require(registry != address(0), "Invalid registry"); + if (policyVersion == 0) policyVersion = 1; + snapshot.allocation = _workAllocation; + snapshot.treasury = treasury; + snapshot.interfold = msg.sender; + snapshot.registry = registry; + snapshot.version = policyVersion; + snapshot.initialized = true; + emit E3PolicySnapshotted( + e3Id, + policyVersion, + treasury, + msg.sender, + registry, + _workAllocation + ); + } + + function _treasuryFor(uint256 e3Id) internal view returns (address) { + E3PolicySnapshot storage policy = _e3PolicySnapshots[e3Id]; + return policy.treasury; + } + + function _interfoldFor(uint256 e3Id) internal view returns (IInterfold) { + E3PolicySnapshot storage policy = _e3PolicySnapshots[e3Id]; + return IInterfold(policy.interfold); + } + + function _allocationFor( + uint256 e3Id + ) internal view returns (WorkValueAllocation memory allocation) { + E3PolicySnapshot storage policy = _e3PolicySnapshots[e3Id]; + return policy.allocation; + } + + function _successSlashedNodeBpsFor( + uint256 e3Id + ) internal view returns (uint16) { + E3PolicySnapshot storage policy = _e3PolicySnapshots[e3Id]; + return policy.allocation.successSlashedNodeBps; + } + //////////////////////////////////////////////////////////// // // // Admin Functions // @@ -594,6 +756,7 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { require(allocation.successSlashedNodeBps <= BPS_BASE, "Invalid BPS"); _workAllocation = allocation; + policyVersion = policyVersion == 0 ? 1 : policyVersion + 1; emit WorkAllocationUpdated(allocation); } @@ -613,55 +776,10 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { require(_treasury != address(0), "Invalid treasury"); address oldValue = treasury; treasury = _treasury; + policyVersion = policyVersion == 0 ? 1 : policyVersion + 1; emit TreasuryUpdated(oldValue, _treasury); } - /// @notice Recover orphaned slashed funds for an E3 that has already completed - /// or whose failure was already fully processed. - /// @dev When a slash executes after an E3 has completed (or after failure claims - /// have started), funds accumulate in `_pendingSlashedFunds` with no drain - /// path. This function allows the owner to redirect them to the treasury. - /// Only callable when the E3 is in a terminal state (Complete or Failed) - /// and the funds cannot be distributed through normal channels. - /// @param e3Id The E3 ID - /// @param paymentToken The token the slashed funds are denominated in - function withdrawOrphanedSlashedFunds( - uint256 e3Id, - IERC20 paymentToken - ) external onlyOwner { - uint256 amount = _pendingSlashedFunds[e3Id]; - require(amount > 0, "No orphaned funds"); - - // Only allow withdrawal when E3 is in a terminal state - IInterfold.E3Stage stage = interfold.getE3Stage(e3Id); - require( - stage == IInterfold.E3Stage.Complete || - stage == IInterfold.E3Stage.Failed, - "E3 not in terminal state" - ); - - // If E3 is Failed and distribution hasn't been calculated yet, - // funds should flow through the normal processE3Failure path - if (stage == IInterfold.E3Stage.Failed) { - RefundDistribution storage dist = _distributions[e3Id]; - require(dist.calculated, "Use processE3Failure first"); - // refuse to redirect to treasury when honest nodes existed — the - // latecomer crediting in `escrowSlashedFunds` should have routed funds - // to them. Reaching this branch with honest nodes implies an invariant - // violation that governance must triage off-chain. - require( - dist.honestNodeCount == 0, - "Honest nodes present; use slash crediting" - ); - } - - _pendingSlashedFunds[e3Id] = 0; - require(address(paymentToken) != address(0), "Invalid fee token"); - paymentToken.safeTransfer(treasury, amount); - - emit OrphanedSlashedFundsWithdrawn(e3Id, amount); - } - //////////////////////////////////////////////////////////// // // // Pull-Payment Claim Functions // @@ -669,43 +787,16 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { //////////////////////////////////////////////////////////// /// @inheritdoc IE3RefundManager - function claimSlashedFundsOnSuccess( - uint256 e3Id + function claimSlashedFunds( + uint256 e3Id, + IERC20 token ) external returns (uint256 amount) { - amount = _claimSlashedFundsOnSuccess(e3Id, msg.sender); + amount = _pendingSlashedClaims[e3Id][token][msg.sender]; require(amount > 0, NothingToClaim()); - } - - /// @inheritdoc IE3RefundManager - function claimSlashedFundsOnSuccessBatch( - uint256[] calldata e3Ids - ) external { - uint256 len = e3Ids.length; - uint256 totalClaimed; - for (uint256 i = 0; i < len; i++) { - totalClaimed += _claimSlashedFundsOnSuccess(e3Ids[i], msg.sender); - } - require(totalClaimed > 0, NothingToClaim()); - } - - function _claimSlashedFundsOnSuccess( - uint256 e3Id, - address account - ) internal returns (uint256 amount) { - amount = _pendingSlashedSuccess[e3Id][account]; - if (amount == 0) return 0; - _pendingSlashedSuccess[e3Id][account] = 0; - IERC20 token = _slashedSuccessToken[e3Id]; - token.safeTransfer(account, amount); - emit SlashedFundsClaimed(e3Id, account, token, amount); - } - - /// @inheritdoc IE3RefundManager - function pendingSlashedFundsOnSuccess( - uint256 e3Id, - address account - ) external view returns (uint256) { - return _pendingSlashedSuccess[e3Id][account]; + _pendingSlashedClaims[e3Id][token][msg.sender] = 0; + _decreaseTokenLiability(token, amount); + _transferPreservingSlashedLiability(token, msg.sender, amount); + emit SlashedFundsClaimed(e3Id, msg.sender, token, amount); } /// @inheritdoc IE3RefundManager @@ -713,7 +804,12 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { amount = _pendingTreasury[msg.sender][token]; require(amount > 0, NothingToClaim()); _pendingTreasury[msg.sender][token] = 0; - token.safeTransfer(msg.sender, amount); + uint256 tracked = _pendingTrackedTreasury[msg.sender][token]; + if (tracked > 0) { + _pendingTrackedTreasury[msg.sender][token] = 0; + _decreaseTokenLiability(token, tracked); + } + _transferPreservingSlashedLiability(token, msg.sender, amount); emit TreasurySlashedClaimed(msg.sender, token, amount); } @@ -725,6 +821,32 @@ contract E3RefundManager is IE3RefundManager, Ownable2StepUpgradeable { return _pendingTreasury[treasuryAddr][token]; } + function _increaseTokenLiability(IERC20 token, uint256 amount) internal { + _tokenLiability[token] += amount; + _assertTokenSolvent(token); + } + + function _decreaseTokenLiability(IERC20 token, uint256 amount) internal { + _tokenLiability[token] -= amount; + } + + function _transferPreservingSlashedLiability( + IERC20 token, + address recipient, + uint256 amount + ) internal { + token.safeTransfer(recipient, amount); + _assertTokenSolvent(token); + } + + function _assertTokenSolvent(IERC20 token) internal view { + uint256 liability = _tokenLiability[token]; + uint256 balance = token.balanceOf(address(this)); + if (balance < liability) { + revert InsolventToken(token, liability, balance); + } + } + //////////////////////////////////////////////////////////// // // // ERC-165 Interface Detection // diff --git a/packages/interfold-contracts/contracts/Interfold.sol b/packages/interfold-contracts/contracts/Interfold.sol index b0961a56d..d714936c6 100644 --- a/packages/interfold-contracts/contracts/Interfold.sol +++ b/packages/interfold-contracts/contracts/Interfold.sol @@ -51,10 +51,6 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { /// operator margin cannot be configured to make requests unaffordable. uint16 public constant MAX_MARGIN_BPS = 5_000; - /// @notice Thrown when the quoted fee exceeds the requester-supplied bound. - /// Declared in {IInterfold} so {InterfoldPricing} can revert with the - /// same selector when validating a quote via DELEGATECALL. - //////////////////////////////////////////////////////////// // // // Storage Variables // @@ -163,6 +159,12 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { mapping(address treasury => mapping(IERC20 token => uint256 amount)) internal _pendingTreasury; + struct E3Dependencies { + ICiphernodeRegistry registry; + IE3RefundManager refundManager; + ISlashingManager slashManager; + } + /// @notice Grace window (seconds) after a stage deadline during which only /// the original requester, owner, or an active committee member /// can call {markE3Failed}. After the grace window, anyone @@ -171,6 +173,10 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { /// restriction is undesired. uint256 public markFailedGracePeriod; + /// @notice Lifecycle contracts frozen when each E3 is requested. + mapping(uint256 e3Id => E3Dependencies dependencies) + internal _e3Dependencies; + /// @notice Emitted when the {markFailedGracePeriod} value is updated. event MarkFailedGracePeriodSet(uint256 gracePeriod); @@ -180,31 +186,6 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { // // //////////////////////////////////////////////////////////// - /// @notice Restricts function to CiphernodeRegistry contract only - modifier onlyCiphernodeRegistry() { - require( - msg.sender == address(ciphernodeRegistry), - OnlyCiphernodeRegistry() - ); - _; - } - - /// @notice Restricts function to CiphernodeRegistry or SlashingManager - modifier onlyCiphernodeRegistryOrSlashingManager() { - require( - msg.sender == address(ciphernodeRegistry) || - msg.sender == address(slashingManager), - OnlyCiphernodeRegistryOrSlashingManager() - ); - _; - } - - /// @notice Restricts function to SlashingManager contract only - modifier onlySlashingManager() { - require(msg.sender == address(slashingManager), OnlySlashingManager()); - _; - } - //////////////////////////////////////////////////////////// // // // Initialization // @@ -230,7 +211,8 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { IE3RefundManager _e3RefundManager, IERC20 _feeToken, uint256 _maxDuration, - E3TimeoutConfig calldata config + E3TimeoutConfig calldata config, + PricingConfig calldata pricingConfig ) public initializer { require(_owner != address(0), "Invalid owner"); __Ownable_init(msg.sender); @@ -241,11 +223,7 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { setFeeToken(_feeToken); _setTimeoutConfig(config); - // Default pricing parameters applied via the linked InterfoldPricing - // library (assembly SSTOREs against the caller's _pricingConfig - // slots) so the 15-field literal stays out of Interfold's runtime - // bytecode (EIP-170 24,576-byte cap). - InterfoldPricing.applyDefaultPricingConfig(); + _setPricingConfig(pricingConfig); if (_owner != owner()) _transferOwnership(_owner); } @@ -289,16 +267,22 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { block.timestamp, _timeoutConfig.computeWindow, _timeoutConfig.decryptionWindow, - maxDuration, - e3Fee + maxDuration ); e3Id = nexte3Id; nexte3Id++; - // Seed uses block.prevrandao combined with e3Id as additional entropy. - // While prevrandao is not cryptographically unpredictable (validator-controlled), - // the combination with the unique, incrementing e3Id mitigates manipulation. - // The seed is used solely for weighted sortition, not for cryptographic key generation. + E3Dependencies storage dependencies = _e3Dependencies[e3Id]; + dependencies.registry = ciphernodeRegistry; + dependencies.refundManager = e3RefundManager; + dependencies.slashManager = slashingManager; + dependencies.refundManager.snapshotE3Policy( + e3Id, + address(dependencies.registry) + ); + // The seed is only a shared per-E3 input to deterministic ticket + // scoring; it is not BFV key material and the protocol does not rely + // on it for cryptographic unpredictability. uint256 seed = uint256(keccak256(abi.encode(block.prevrandao, e3Id))); e3Payments[e3Id] = e3Fee; @@ -327,7 +311,6 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { e3.e3Program = requestParams.e3Program; e3.paramSet = requestParams.paramSet; e3.customParams = requestParams.customParams; - e3.proofAggregationEnabled = requestParams.proofAggregationEnabled; e3.requester = msg.sender; bytes memory e3ProgramParams = paramSetRegistry[requestParams.paramSet]; @@ -364,7 +347,7 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { feeToken.safeTransferFrom(msg.sender, address(this), e3Fee); require( - ciphernodeRegistry.requestCommittee(e3Id, seed, threshold), + dependencies.registry.requestCommittee(e3Id, seed, threshold), CommitteeSelectionFailed() ); @@ -419,7 +402,10 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { bytes calldata plaintextOutput, bytes calldata proof ) external returns (bool success) { - E3 memory e3 = getE3(e3Id); + require( + e3s[e3Id].e3Program != IE3Program(address(0)), + E3DoesNotExist(e3Id) + ); // Check we are in the right stage // no need to check if there's a ciphertext as we would not @@ -440,27 +426,10 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { e3s[e3Id].plaintextOutput = plaintextOutput; _e3Stages[e3Id] = E3Stage.Complete; - if (e3.proofAggregationEnabled) { - require(proof.length > 0, ProofRequired()); - // Reaching `CiphertextReady` implies the committee was published, so - // `getCommitteeHash` is guaranteed non-zero here; the registry still - // reverts with `CommitteeNotPublished` if that invariant ever breaks. - bytes32 committeeHash = ciphernodeRegistry.getCommitteeHash(e3Id); - // Wrapper reverts on any failure with a typed error (no `bool false`). - e3.decryptionVerifier.verify( - e3Id, - ciphernodeRegistry.rootAt(e3Id), - ciphernodeRegistry.getCommitteeNodes(e3Id), - e3.ciphertextOutput, - e3.committeePublicKey, - keccak256(plaintextOutput), - committeeHash, - proof - ); - success = true; - } else { - success = true; - } + require(proof.length > 0, ProofRequired()); + + _verifyPlaintext(e3Id, keccak256(plaintextOutput), proof); + success = true; _distributeRewards(e3Id); @@ -468,6 +437,23 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { emit E3StageChanged(e3Id, E3Stage.CiphertextReady, E3Stage.Complete); } + function _verifyPlaintext( + uint256 e3Id, + bytes32 plaintextHash, + bytes calldata proof + ) internal view { + E3 storage e3 = e3s[e3Id]; + InterfoldPricing.verifyPlaintext( + address(e3.decryptionVerifier), + address(_registryFor(e3Id)), + e3Id, + e3.ciphertextOutput, + e3.committeePublicKey, + plaintextHash, + proof + ); + } + //////////////////////////////////////////////////////////// // // // Internal Functions // @@ -480,8 +466,9 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { /// transfer — single recipient, no other party harmed. /// @param e3Id The ID of the E3 for which to distribute rewards. function _distributeRewards(uint256 e3Id) internal { - (address[] memory activeNodes, ) = ciphernodeRegistry + (address[] memory activeNodes, ) = _registryFor(e3Id) .getActiveCommitteeNodes(e3Id); + IE3RefundManager refundManager = _refundManagerFor(e3Id); uint256 activeLength = activeNodes.length; uint256 totalAmount = e3Payments[e3Id]; @@ -491,11 +478,7 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { IERC20 paymentToken = _e3FeeTokens[e3Id]; if (totalAmount == 0) { - e3RefundManager.distributeSlashedFundsOnSuccess( - e3Id, - activeNodes, - paymentToken - ); + refundManager.distributeSlashedFundsOnSuccess(e3Id, paymentToken); return; } @@ -507,11 +490,7 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { if (requester != address(0)) { paymentToken.safeTransfer(requester, totalAmount); } - e3RefundManager.distributeSlashedFundsOnSuccess( - e3Id, - activeNodes, - paymentToken - ); + refundManager.distributeSlashedFundsOnSuccess(e3Id, paymentToken); return; } @@ -549,11 +528,7 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { emit RewardsDistributed(e3Id, activeNodes, amounts); - e3RefundManager.distributeSlashedFundsOnSuccess( - e3Id, - activeNodes, - paymentToken - ); + refundManager.distributeSlashedFundsOnSuccess(e3Id, paymentToken); } /// @notice Credits per-node reward balances and emits `RewardCredited`. @@ -576,30 +551,6 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { /// @dev Uses active committee view from the registry (which excludes expelled/slashed members). /// @param e3Id The ID of the E3. /// @return honestNodes An array of addresses of honest committee nodes. - function _getHonestNodes( - uint256 e3Id - ) private view returns (address[] memory) { - FailureReason reason = _e3FailureReasons[e3Id]; - - // Early failures have no committee - if ( - reason == FailureReason.CommitteeFormationTimeout || - reason == FailureReason.InsufficientCommitteeMembers - ) { - return new address[](0); - } - - // Use active committee nodes (already filtered by expulsion) - try ciphernodeRegistry.getActiveCommitteeNodes(e3Id) returns ( - address[] memory nodes, - uint256[] memory - ) { - return nodes; - } catch { - return new address[](0); // Committee not published (DKG failed) - } - } - //////////////////////////////////////////////////////////// // // // Set Functions // @@ -650,11 +601,9 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { ); feeToken = _feeToken; // Auto allow-list the active fee token so `request()` keeps working - // after a rotation. Owner can still explicitly toggle later. - if (!_feeTokenAllowed[_feeToken]) { - _feeTokenAllowed[_feeToken] = true; - emit FeeTokenAllowed(_feeToken, true); - } + // after a rotation. FeeTokenSet provides the rotation receipt; explicit + // allow-list changes emit FeeTokenAllowed via setFeeTokenAllowed. + _feeTokenAllowed[_feeToken] = true; emit FeeTokenSet(address(_feeToken)); } @@ -746,11 +695,7 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { require(encodedParams.length > 0, "Empty params"); bytes memory previous = paramSetRegistry[paramSet]; paramSetRegistry[paramSet] = encodedParams; - if (previous.length == 0) { - emit ParamSetRegistered(paramSet, encodedParams); - } else { - emit ParamSetUpdated(paramSet, previous, encodedParams); - } + InterfoldPricing.emitParamSetChange(paramSet, previous, encodedParams); } /// @notice Sets the E3 Refund Manager contract address @@ -758,10 +703,7 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { function setE3RefundManager( IE3RefundManager _e3RefundManager ) public onlyOwner { - require( - address(_e3RefundManager) != address(0), - "Invalid E3RefundManager address" - ); + require(address(_e3RefundManager) != address(0)); e3RefundManager = _e3RefundManager; emit E3RefundManagerSet(address(_e3RefundManager)); } @@ -771,10 +713,7 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { function setSlashingManager( ISlashingManager _slashingManager ) external onlyOwner { - require( - address(_slashingManager) != address(0), - "Invalid SlashingManager address" - ); + require(address(_slashingManager) != address(0)); slashingManager = _slashingManager; emit SlashingManagerSet(address(_slashingManager)); } @@ -791,17 +730,17 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { require(payment > 0, NoPaymentToRefund(e3Id)); e3Payments[e3Id] = 0; // Prevent double processing - address[] memory honestNodes = _getHonestNodes(e3Id); + address[] memory honestNodes = InterfoldPricing.honestNodes( + address(_registryFor(e3Id)), + e3Id, + uint8(_e3FailureReasons[e3Id]) + ); IERC20 paymentToken = _e3FeeTokens[e3Id]; - paymentToken.safeTransfer(address(e3RefundManager), payment); - e3RefundManager.calculateRefund( - e3Id, - payment, - honestNodes, - paymentToken - ); + IE3RefundManager refundManager = _refundManagerFor(e3Id); + paymentToken.safeTransfer(address(refundManager), payment); + refundManager.calculateRefund(e3Id, payment, honestNodes, paymentToken); emit E3FailureProcessed(e3Id, payment, honestNodes.length); } @@ -809,16 +748,23 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { /// @inheritdoc IInterfold function escrowSlashedFunds( uint256 e3Id, + IERC20 token, uint256 amount - ) external onlySlashingManager { - e3RefundManager.escrowSlashedFunds(e3Id, amount); - emit SlashedFundsEscrowed(e3Id, amount); + ) external { + InterfoldPricing.validateSlashCaller( + msg.sender, + address(_slashingManagerFor(e3Id)) + ); + _refundManagerFor(e3Id).escrowSlashedFunds(e3Id, token, amount); + emit SlashedFundsEscrowed(e3Id, token, amount); } /// @inheritdoc IInterfold - function onCommitteeFinalized( - uint256 e3Id - ) external onlyCiphernodeRegistry { + function onCommitteeFinalized(uint256 e3Id) external { + InterfoldPricing.validateRegistryCaller( + msg.sender, + address(_registryFor(e3Id)) + ); // Update E3 lifecycle stage - committee finalized, DKG starting E3Stage current = _e3Stages[e3Id]; if (current != E3Stage.Requested) { @@ -841,7 +787,11 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { function onCommitteePublished( uint256 e3Id, bytes32 committeePublicKey - ) external onlyCiphernodeRegistry { + ) external { + InterfoldPricing.validateRegistryCaller( + msg.sender, + address(_registryFor(e3Id)) + ); E3 storage e3 = e3s[e3Id]; E3Stage current = _e3Stages[e3Id]; if (current != E3Stage.CommitteeFinalized) { @@ -860,10 +810,12 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { } /// @inheritdoc IInterfold - function onE3Failed( - uint256 e3Id, - uint8 reason - ) external onlyCiphernodeRegistryOrSlashingManager { + function onE3Failed(uint256 e3Id, uint8 reason) external { + InterfoldPricing.validateRegistryOrSlashCaller( + msg.sender, + address(_registryFor(e3Id)), + address(_slashingManagerFor(e3Id)) + ); require( reason > 0 && reason <= uint8(FailureReason._MAX_FAILURE_REASON), "Invalid failure reason" @@ -906,7 +858,7 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { block.timestamp < graceEnds && msg.sender != _e3Requesters[e3Id] && msg.sender != owner() && - !ciphernodeRegistry.isCommitteeMember(e3Id, msg.sender) + !_registryFor(e3Id).isCommitteeMember(e3Id, msg.sender) ) { revert MarkE3FailedInGracePeriod(e3Id, graceEnds); } @@ -968,7 +920,7 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { ) private view returns (uint256 deadline, FailureReason reason) { if (stage == E3Stage.Requested) return ( - ciphernodeRegistry.getCommitteeDeadline(e3Id), + _registryFor(e3Id).getCommitteeDeadline(e3Id), FailureReason.CommitteeFormationTimeout ); E3Deadlines memory d = _e3Deadlines[e3Id]; @@ -1059,6 +1011,10 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { function setPricingConfig( PricingConfig calldata config ) external onlyOwner { + _setPricingConfig(config); + } + + function _setPricingConfig(PricingConfig calldata config) internal { // Validation is delegated to {InterfoldPricing.validatePricingConfig} // (external library link) to keep the deployed Interfold runtime // bytecode under the EIP-170 24,576-byte cap. Revert selectors are @@ -1195,6 +1151,24 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { return _pendingTreasury[treasury][token]; } + function _registryFor( + uint256 e3Id + ) private view returns (ICiphernodeRegistry) { + return _e3Dependencies[e3Id].registry; + } + + function _refundManagerFor( + uint256 e3Id + ) private view returns (IE3RefundManager) { + return _e3Dependencies[e3Id].refundManager; + } + + function _slashingManagerFor( + uint256 e3Id + ) private view returns (ISlashingManager) { + return _e3Dependencies[e3Id].slashManager; + } + //////////////////////////////////////////////////////////// // // // ERC-165 Interface Detection // @@ -1218,5 +1192,5 @@ contract Interfold is IInterfold, Ownable2StepUpgradeable { /// array's length accordingly to preserve storage layout compatibility /// across upgrades. // solhint-disable-next-line var-name-mixedcase - uint256[50] private __gap; + uint256[49] private __gap; } diff --git a/packages/interfold-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/interfold-contracts/contracts/interfaces/IBondingRegistry.sol index 7f1b6c47e..af5e2f14f 100644 --- a/packages/interfold-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/interfold-contracts/contracts/interfaces/IBondingRegistry.sol @@ -31,14 +31,26 @@ interface IBondingRegistry { error ExitInProgress(); error ExitNotReady(); error InvalidAmount(); + + /// @notice A bonding asset must be a deployed contract (except the one-time + /// zero license-token placeholder used during circular deployment). + error InvalidBondingAsset(address asset); + + /// @notice A bonding asset cannot rotate while balances remain denominated in it. + error OutstandingAssetLiabilities(address asset, uint256 amount); + error InvalidConfiguration(); error NoPendingDeregistration(); error OnlyRewardDistributor(); error ArrayLengthMismatch(); - /// @notice Thrown when an operator attempts to deregister while at least one Lane B - /// slash proposal against them is still pending execution. + /// @notice Thrown when an operator attempts to withdraw collateral while any + /// financial slash proposal against them remains unresolved. error OperatorUnderSlash(); + /// @notice Treasury withdrawal or generic routing attempted to consume funds + /// reserved for a pending E3 slash route. + error ReservedSlashedFunds(); + /// @notice Thrown when {setExitDelay} input is outside the permitted range. error ExitDelayOutOfBounds(uint64 exitDelay); @@ -110,6 +122,12 @@ interface IBondingRegistry { uint256 newValue ); + /** + * @notice Emitted when an eligibility setting invalidates cached operator status. + * @param version New eligibility-policy version. + */ + event EligibilityConfigurationVersionUpdated(uint256 indexed version); + /** * @notice Emitted when a reward distributor is authorized or revoked * @param distributor Address of the distributor @@ -150,6 +168,19 @@ interface IBondingRegistry { */ event LicenseTokenSet(address indexed licenseToken); + /** + * @notice Emitted when governance removes license tokens that are not + * backing any active bond, pending exit, or slashed-fund claim. + * @param token License token whose surplus was swept + * @param to Slashed-funds treasury that received the surplus + * @param amount Amount requested for transfer + */ + event LicenseSurplusSwept( + address indexed token, + address indexed to, + uint256 amount + ); + /** * @notice Emitted when the registry is set * @param registry Address of the registry @@ -162,6 +193,14 @@ interface IBondingRegistry { address indexed next ); + /// @notice Emitted whenever a slashing manager gains or loses authority. + /// @dev Replaced managers remain authorized until governance explicitly + /// revokes them so snapshotted E3s and open proposals can finish. + event SlashingManagerAuthorizationUpdated( + address indexed slashingManager, + bool authorized + ); + /** * @notice Emitted whenever a `licenseToken.safeTransfer` performed by the * registry sends FEWER tokens than requested (typical of @@ -171,8 +210,8 @@ interface IBondingRegistry { * in the registry as an unaccounted-for surplus. Operators and * monitoring infrastructure should treat any emission of this * event as evidence that the configured `licenseToken` is not a - * well-behaved ERC-20 and should be replaced via - * `setLicenseToken`. + * well-behaved ERC-20. Rotation is permitted only after every + * balance denominated in the old token has been drained. * @param recipient The address that received the (short) transfer * @param expectedAmount The amount the registry intended to send * @param actualAmount The actual delta in registry-held balance @@ -193,6 +232,13 @@ interface IBondingRegistry { */ function getLicenseToken() external view returns (address); + /** + * @notice Total license-token obligations held by the registry. + * @dev Covers active bonds, queued exits, and slashed funds awaiting + * treasury withdrawal. + */ + function totalLicenseLiability() external view returns (uint256); + /** * @notice Get ticket token address * @return Ticket token address @@ -261,6 +307,22 @@ interface IBondingRegistry { */ function numActiveOperators() external view returns (uint256); + /// @notice Current eligibility-policy version. + function eligibilityConfigurationVersion() external view returns (uint256); + + /** + * @notice Re-evaluate one registered operator under the current eligibility policy. + * @dev Permissionless so operators or governance can restore current status after + * an eligibility configuration update. + */ + function refreshOperatorStatus(address operator) external; + + /** + * @notice Re-evaluate a batch of registered operators under the current policy. + * @dev Intended for governance/operator automation after a policy update. + */ + function refreshOperatorStatuses(address[] calldata operators) external; + /** * @notice Check if operator has deregistration in progress * @param operator Address of the operator @@ -330,6 +392,12 @@ interface IBondingRegistry { */ function slashedTicketBalance() external view returns (uint256); + /// @notice Get slashed ticket funds reserved for retryable E3 routing. + function reservedSlashedTicketBalance() external view returns (uint256); + + /// @notice Get the ticket wrapper whose underlying asset backs ticket slashes. + function ticketToken() external view returns (InterfoldTicketToken); + /** * @notice Get total slashed license bond * @return Amount of license bond slashed and available for treasury withdrawal @@ -423,14 +491,16 @@ interface IBondingRegistry { bytes32 reason ) external; - /** - * @notice Redirect slashed ticket funds to a specified address - * @param to Address to receive the slashed funds (underlying stablecoin) - * @param amount Amount of slashed ticket balance to redirect - * @dev Only callable by authorized slashing manager. Pays out underlying stablecoin - * from burned ticket tokens. Assumes underlying stablecoin matches the E3 fee token. - */ - function redirectSlashedTicketFunds(address to, uint256 amount) external; + /// @notice Reserve slashed ticket funds so treasury cannot withdraw them. + /// @dev Only callable by the configured slashing manager. + function reserveSlashedTicketFunds(uint256 amount) external; + + /// @notice Route and consume previously reserved slashed ticket funds. + /// @dev Only callable by the configured slashing manager. + function redirectReservedSlashedTicketFunds( + address to, + uint256 amount + ) external; // ====================== // Reward Distribution Functions @@ -455,28 +525,28 @@ interface IBondingRegistry { /** * @notice Set ticket price * @param newTicketPrice New price per ticket - * @dev Only callable by contract owner + * @dev Only callable by contract owner. Invalidates cached operator status. */ function setTicketPrice(uint256 newTicketPrice) external; /** * @notice Set license bond price required * @param newLicenseRequiredBond New license bond price - * @dev Only callable by contract owner + * @dev Only callable by contract owner. Invalidates cached operator status. */ function setLicenseRequiredBond(uint256 newLicenseRequiredBond) external; /** * @notice Set license active BPS * @param newBps New license active BPS - * @dev Only callable by contract owner + * @dev Only callable by contract owner. Invalidates cached operator status. */ function setLicenseActiveBps(uint256 newBps) external; /** * @notice Set minimum ticket balance required for activation * @param newMinTicketBalance New minimum ticket balance - * @dev Only callable by contract owner + * @dev Only callable by contract owner. Invalidates cached operator status. */ function setMinTicketBalance(uint256 newMinTicketBalance) external; @@ -501,6 +571,14 @@ interface IBondingRegistry { */ function setLicenseToken(IERC20 newLicenseToken) external; + /** + * @notice Send unaccounted license-token surplus to the slashed-funds treasury. + * @dev Never transfers active bonds, queued exits, or slashed-fund liabilities. + * This is the governance path for clearing donated dust before rotation. + * @return amount Amount requested for transfer + */ + function sweepLicenseSurplus() external returns (uint256 amount); + /** * @notice Set slashed funds treasury address * @param newSlashedFundsTreasury New slashed funds treasury address @@ -522,6 +600,32 @@ interface IBondingRegistry { */ function setSlashingManager(address newSlashingManager) external; + /** + * @notice Revoke a non-current slashing manager after every E3 and proposal + * that depends on it has reached a terminal state. + * @param oldSlashingManager Manager whose authority should be removed + */ + function revokeSlashingManager(address oldSlashingManager) external; + + /** + * @notice Whether a manager may slash collateral or route reserved slash funds. + */ + function isAuthorizedSlashingManager( + address candidate + ) external view returns (bool); + + /** + * @notice Number of currently authorized slashing managers. + */ + function authorizedSlashingManagerCount() external view returns (uint256); + + /** + * @notice Authorized slashing manager at `index`. + */ + function authorizedSlashingManagerAt( + uint256 index + ) external view returns (address); + /** * @notice Set reward distributor address * @param newRewardDistributor New reward distributor address diff --git a/packages/interfold-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/interfold-contracts/contracts/interfaces/ICiphernodeRegistry.sol index a505c4189..5310c85c7 100644 --- a/packages/interfold-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/interfold-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -15,6 +15,9 @@ import { IBondingRegistry } from "./IBondingRegistry.sol"; * and coordinates committee selection for E3 computations */ interface ICiphernodeRegistry { + /// @notice Current number of registered ciphernodes. + function numCiphernodes() external view returns (uint256); + /// @notice Tracks a committee member's lifecycle state for a given E3. enum MemberStatus { None, @@ -111,10 +114,12 @@ interface ICiphernodeRegistry { /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. - /// @param publicKey Public key of the committee. + /// @param publicKey Serialized public-key transport hint. Consumers MUST + /// deserialize it and recompute `pkCommitment` before using it for + /// encryption. The commitment is proven by the mandatory final DKG + /// proof; test-only mock verifiers may accept placeholder payloads. /// @param pkCommitment Hash-based aggregated PK commitment for the committee. - /// @param proof DkgAggregator (EVM) proof bytes verified prior to publication, - /// or empty bytes when proof aggregation is disabled for the E3. + /// @param proof Non-empty DkgAggregator (EVM) proof bytes verified prior to publication. event CommitteePublished( uint256 indexed e3Id, address[] nodes, @@ -395,17 +400,15 @@ interface ICiphernodeRegistry { /// @notice Publishes the public key resulting from the committee selection process. /// @dev Permissionless once the committee is finalized. - /// When `e3.proofAggregationEnabled` is true, the `proof` is verified against - /// `pkCommitment` via the E3's pk verifier. When disabled, `proof` may be empty - /// and `pkCommitment` is trusted from the (signed) aggregator. + /// The `proof` is verified against `pkCommitment` via the E3's pk verifier. /// @param e3Id ID of the E3 for which to select the committee. /// @param publicKey The public key generated by the given committee. /// @param pkCommitment Hash-based aggregated PK commitment for the committee. /// @param proof DkgAggregator (EVM) proof ABI-encoded `(bytes rawProof, bytes32[] publicInputs)`, - /// or empty bytes when proof aggregation is disabled. + /// as accepted by the configured public-key verifier. /// @param dkgAttestationBundle ABI-encoded /// `(DkgFoldAttestationLib.Attestation[] attestations, DkgFoldAttestationLib.PartySlotBinding[] bindings)`. - /// Required (non-empty) when proof aggregation is enabled; ignored otherwise. + /// Required and non-empty for every committee publication. function publishCommittee( uint256 e3Id, bytes calldata publicKey, diff --git a/packages/interfold-contracts/contracts/interfaces/IDecryptionVerifier.sol b/packages/interfold-contracts/contracts/interfaces/IDecryptionVerifier.sol index 06d13aa32..2588b5dc2 100644 --- a/packages/interfold-contracts/contracts/interfaces/IDecryptionVerifier.sol +++ b/packages/interfold-contracts/contracts/interfaces/IDecryptionVerifier.sol @@ -13,9 +13,9 @@ pragma solidity 0.8.28; * the final EVM proof and enforces: * - the immutable recursive sub-circuit VK hashes * - the plaintext slot matches the caller-supplied hash - * - a domain-binding slot binding the proof to - * (chainId, this, e3Id, committeeRoot, sortedNodes, ciphertextOutputHash, - * committeePublicKey, plaintextOutputHash) + * - a domain-binding slot supplied by Interfold and derived from + * (chainId, Interfold address, e3Id, committeeHash, + * ciphertextOutputHash, committeePublicKey) * and reverts on any mismatch. */ interface IDecryptionVerifier { @@ -37,27 +37,16 @@ interface IDecryptionVerifier { /// recomputed on-chain from the call context. error DomainBindingMismatch(); - /// @notice Verify a DecryptionAggregator EVM proof and bind it to the full - /// on-chain call context. - /// @param e3Id Identifier of the E3 the plaintext was decrypted for. - /// @param committeeRoot Ciphernode IMT root snapshotted at committee request time - /// (`CiphernodeRegistry.rootAt(e3Id)`). - /// @param sortedNodes The on-chain-selected committee (`c.topNodes`), bound into - /// the domain-binding hash. - /// @param ciphertextOutputHash The previously-published ciphertext hash - /// (`e3.ciphertextOutput`). - /// @param committeePublicKey The committee's aggregated PK commitment - /// (`e3.committeePublicKey`). + /// @notice Verify a DecryptionAggregator EVM proof and bind it to the E3 + /// domain recomputed by Interfold. + /// @param decryptionDomain `keccak256(abi.encode(chainId, interfold, e3Id, + /// committeeHash, ciphertextOutputHash, committeePublicKey))`. /// @param plaintextOutputHash `keccak256(plaintextOutput)` expected by the Interfold. /// @param committeeHash `keccak256(abi.encodePacked(topNodes))` for the on-chain committee. /// @param proof ABI-encoded `(bytes rawProof, bytes32[] publicInputs)`. /// @return success Always `true` on success; the wrapper reverts on any failure. function verify( - uint256 e3Id, - uint256 committeeRoot, - address[] calldata sortedNodes, - bytes32 ciphertextOutputHash, - bytes32 committeePublicKey, + bytes32 decryptionDomain, bytes32 plaintextOutputHash, bytes32 committeeHash, bytes calldata proof diff --git a/packages/interfold-contracts/contracts/interfaces/IE3.sol b/packages/interfold-contracts/contracts/interfaces/IE3.sol index fcdd0d950..1f53b57c8 100644 --- a/packages/interfold-contracts/contracts/interfaces/IE3.sol +++ b/packages/interfold-contracts/contracts/interfaces/IE3.sol @@ -44,5 +44,4 @@ struct E3 { bytes32 ciphertextOutput; bytes plaintextOutput; address requester; - bool proofAggregationEnabled; } diff --git a/packages/interfold-contracts/contracts/interfaces/IE3RefundManager.sol b/packages/interfold-contracts/contracts/interfaces/IE3RefundManager.sol index b20b56178..f859f3177 100644 --- a/packages/interfold-contracts/contracts/interfaces/IE3RefundManager.sol +++ b/packages/interfold-contracts/contracts/interfaces/IE3RefundManager.sol @@ -13,6 +13,13 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; * @dev Handles refund calculation and claiming for failed E3s */ interface IE3RefundManager { + /// @notice Identifies which collateral pool bears a failed E3's completed-work cost. + enum FailurePayer { + None, + Requester, + Ciphernodes + } + //////////////////////////////////////////////////////////// // // // Structs // @@ -26,16 +33,25 @@ interface IE3RefundManager { uint16 protocolBps; uint16 successSlashedNodeBps; } + /// @notice Immutable settlement policy selected when an E3 is requested. + struct E3PolicySnapshot { + WorkValueAllocation allocation; + address treasury; + address interfold; + address registry; + uint64 version; + bool initialized; + } /// @notice Refund distribution for a failed E3 struct RefundDistribution { uint256 requesterAmount; // Amount for requester uint256 honestNodeAmount; // Total amount for honest nodes uint256 protocolAmount; // Amount for protocol treasury - uint256 totalSlashed; // Slashed funds added + uint256 totalSlashed; // Cumulative settled slashes denominated in the E3 fee token uint256 honestNodeCount; // Number of honest nodes bool calculated; // Whether distribution is calculated IERC20 feeToken; // The fee token used for this E3's payment (stored per-E3 to survive token rotations) - uint256 originalPayment; // Original E3 payment amount (for making requester whole) + uint256 originalPayment; // Original E3 payment amount retained for settlement auditability uint256 perNodeAmount; // Snapshotted per-honest-node payout; 0 when honestNodeCount==0 } //////////////////////////////////////////////////////////// @@ -59,10 +75,15 @@ interface IE3RefundManager { bytes32 claimType ); /// @notice Emitted when slashed funds are escrowed for an E3 - event SlashedFundsEscrowed(uint256 indexed e3Id, uint256 amount); + event SlashedFundsEscrowed( + uint256 indexed e3Id, + IERC20 indexed token, + uint256 amount + ); /// @notice Emitted when slashed funds are applied to a failed E3's refund distribution event SlashedFundsApplied( uint256 indexed e3Id, + IERC20 indexed token, uint256 toRequester, uint256 toHonestNodes ); @@ -71,6 +92,7 @@ interface IE3RefundManager { /// `SlashedFundsCredited` / `TreasurySlashedCredited` for per-recipient detail. event SlashedFundsDistributedOnSuccess( uint256 indexed e3Id, + IERC20 indexed token, uint256 toNodes, uint256 toProtocol ); @@ -102,8 +124,15 @@ interface IE3RefundManager { ); /// @notice Emitted when work allocation is updated event WorkAllocationUpdated(WorkValueAllocation allocation); - /// @notice Emitted when orphaned slashed funds are withdrawn to treasury - event OrphanedSlashedFundsWithdrawn(uint256 indexed e3Id, uint256 amount); + /// @notice Emitted when an E3 freezes its settlement policy. + event E3PolicySnapshotted( + uint256 indexed e3Id, + uint64 indexed version, + address indexed treasury, + address interfold, + address registry, + WorkValueAllocation allocation + ); /// @notice Emitted when the Interfold address is set event InterfoldSet(address indexed interfold); /// @notice Emitted when the treasury address is set @@ -129,6 +158,10 @@ interface IE3RefundManager { error Unauthorized(); /// @notice Caller has no pending balance to claim error NothingToClaim(); + /// @notice Recorded liabilities exceed the manager's balance of a token. + error InsolventToken(IERC20 token, uint256 liability, uint256 balance); + /// @notice Failure reason has no configured economic responsibility. + error InvalidFailureReason(IInterfold.FailureReason reason); //////////////////////////////////////////////////////////// // // @@ -147,6 +180,17 @@ interface IE3RefundManager { IERC20 paymentToken ) external; + /// @notice Return the party whose collateral pays completed work for a failure reason. + /// @dev Requester failures pay completed work from fee escrow. Ciphernode/supply + /// failures return all fee escrow and use actual ticket slashes to pay honest nodes. + function getFailurePayer( + IInterfold.FailureReason reason + ) external pure returns (FailurePayer payer); + + /// @notice Freeze the current allocation, treasury, and committee registry for an E3. + /// @dev Only Interfold may call this, exactly once, during request creation. + function snapshotE3Policy(uint256 e3Id, address registry) external; + /// @notice Requester claims their refund /// @param e3Id The failed E3 ID /// @return amount The amount claimed @@ -163,16 +207,44 @@ interface IE3RefundManager { /// @notice Escrow slashed funds — destination decided at terminal state /// @param e3Id The E3 ID + /// @param token The actual ticket-underlying token transferred into escrow /// @param amount The slashed amount - function escrowSlashedFunds(uint256 e3Id, uint256 amount) external; + function escrowSlashedFunds( + uint256 e3Id, + IERC20 token, + uint256 amount + ) external; + + /// @notice Settle one recorded token after the E3 reaches a terminal state. + function settleSlashedFunds(uint256 e3Id, IERC20 token) external; + + /// @notice Pull a token-specific slashed-fund entitlement. + function claimSlashedFunds( + uint256 e3Id, + IERC20 token + ) external returns (uint256 amount); + + /// @notice Pending, unsettled slash escrow for an E3 and token. + function pendingSlashedFunds( + uint256 e3Id, + IERC20 token + ) external view returns (uint256 amount); + + /// @notice Token-specific slash entitlement for an account. + function pendingSlashedClaim( + uint256 e3Id, + IERC20 token, + address account + ) external view returns (uint256 amount); + + /// @notice Total protected liabilities recorded for a token. + function tokenLiability(IERC20 token) external view returns (uint256); /// @notice Distribute escrowed slashed funds on success /// @param e3Id The E3 ID - /// @param honestNodes Honest node addresses /// @param paymentToken The fee token for this E3 function distributeSlashedFundsOnSuccess( uint256 e3Id, - address[] calldata honestNodes, IERC20 paymentToken ) external; @@ -183,11 +255,14 @@ interface IE3RefundManager { uint256 e3Id ) external view returns (RefundDistribution memory distribution); - /// @notice Check if address has claimed refund - /// @param e3Id The E3 ID - /// @param claimant The address to check - /// @return claimed Whether the address has claimed - function hasClaimed( + /// @notice Check whether an address claimed the requester-refund role + function hasRequesterClaimed( + uint256 e3Id, + address claimant + ) external view returns (bool claimed); + + /// @notice Check whether an address claimed the honest-node reward role + function hasHonestNodeClaimed( uint256 e3Id, address claimant ) external view returns (bool claimed); @@ -213,30 +288,10 @@ interface IE3RefundManager { view returns (WorkValueAllocation memory allocation); - //////////////////////////////////////////////////////////// - // // - // Success-Path Slashed-Funds Pull Payments // - // // - //////////////////////////////////////////////////////////// - - /// @notice Honest node pulls credited success-path slashed funds. - /// @param e3Id The successful E3 ID. - /// @return amount Amount transferred. - function claimSlashedFundsOnSuccess( + /// @notice Return the settlement policy frozen for an E3. + function getE3PolicySnapshot( uint256 e3Id - ) external returns (uint256 amount); - - /// @notice Batch pull credited success-path slashed funds across multiple E3s. - /// @dev Each e3Id may use a different reward token (recorded at request time); - /// events carry the per-E3 token address. A mixed-token sum return would be - /// meaningless, so the function is intentionally void. - function claimSlashedFundsOnSuccessBatch(uint256[] calldata e3Ids) external; - - /// @notice Get pending success-path slashed-funds credit for (e3Id, account). - function pendingSlashedFundsOnSuccess( - uint256 e3Id, - address account - ) external view returns (uint256); + ) external view returns (E3PolicySnapshot memory snapshot); /// @notice Treasury pulls accrued credits (protocol slashed-fund share + dust). /// @dev Caller must be the treasury that was credited. diff --git a/packages/interfold-contracts/contracts/interfaces/IInterfold.sol b/packages/interfold-contracts/contracts/interfaces/IInterfold.sol index 3fd2214d6..acf74791f 100644 --- a/packages/interfold-contracts/contracts/interfaces/IInterfold.sol +++ b/packages/interfold-contracts/contracts/interfaces/IInterfold.sol @@ -258,7 +258,11 @@ interface IInterfold { /// @notice Emitted when slashed funds are escrowed for an E3 /// @param e3Id The E3 ID. /// @param amount The amount of slashed funds escrowed. - event SlashedFundsEscrowed(uint256 indexed e3Id, uint256 amount); + event SlashedFundsEscrowed( + uint256 indexed e3Id, + IERC20 indexed token, + uint256 amount + ); /// @notice Emitted when a failed E3 is processed for refunds. /// @param e3Id The ID of the failed E3. @@ -350,7 +354,7 @@ interface IInterfold { /// @param output The invalid output data. error InvalidOutput(bytes output); - /// @notice Thrown when proof aggregation is enabled but no proof was supplied. + /// @notice Thrown when a mandatory final proof was not supplied. error ProofRequired(); /// @notice Thrown when the committee size has not been configured with thresholds. @@ -496,12 +500,6 @@ interface IInterfold { uint8 paramSet; bytes computeProviderParams; bytes customParams; - /// @notice When true, ciphernodes generate and fold wrapper proofs - /// for DKG proof aggregation (public verifiability). When - /// false, wrapper/fold proofs are skipped to reduce latency. - /// C5 and C7 proofs are always generated and verified on-chain - /// regardless of this flag. - bool proofAggregationEnabled; } //////////////////////////////////////////////////////////// @@ -536,9 +534,8 @@ interface IInterfold { /// @dev This function MUST emit the PlaintextOutputPublished event. /// @param e3Id ID of the E3. /// @param plaintextOutput ABI encoded plaintext output. - /// @param proof DecryptionAggregator (EVM) proof ABI-encoded - /// `(bytes rawProof, bytes32[] publicInputs)`, or empty bytes when proof - /// aggregation is disabled for the E3. + /// @param proof Required DecryptionAggregator (EVM) proof ABI-encoded + /// `(bytes rawProof, bytes32[] publicInputs)`. function publishPlaintextOutput( uint256 e3Id, bytes calldata plaintextOutput, @@ -683,8 +680,13 @@ interface IInterfold { /// @notice Escrow slashed funds for deferred distribution /// @dev Called by SlashingManager. Proxies to E3RefundManager. /// @param e3Id The E3 ID. + /// @param token Actual ticket-underlying token transferred into escrow. /// @param amount Amount of slashed funds to escrow. - function escrowSlashedFunds(uint256 e3Id, uint256 amount) external; + function escrowSlashedFunds( + uint256 e3Id, + IERC20 token, + uint256 amount + ) external; //////////////////////////////////////////////////////////// // // diff --git a/packages/interfold-contracts/contracts/interfaces/ISlashingManager.sol b/packages/interfold-contracts/contracts/interfaces/ISlashingManager.sol index f96aacdda..eb6ccdd63 100644 --- a/packages/interfold-contracts/contracts/interfaces/ISlashingManager.sol +++ b/packages/interfold-contracts/contracts/interfaces/ISlashingManager.sol @@ -66,7 +66,7 @@ interface ISlashingManager { /** * @notice Slash proposal details tracking the full lifecycle of a slash * @dev Stores all state needed for proposal, appeal, and execution workflows - * @param e3Id ID of the E3 computation this slash relates to (0 for non-E3 slashes) + * @param e3Id ID of the E3 computation this slash relates to * @param operator Address of the ciphernode operator being slashed * @param reason Hash of the slash reason (maps to SlashPolicy configuration) * @param ticketAmount Amount of ticket collateral to slash (copied from policy at proposal time) @@ -104,6 +104,14 @@ interface ISlashingManager { uint8 failureReason; } + /// @notice Durable routing record for slashed ticket funds awaiting escrow. + struct PendingSlashRoute { + uint256 e3Id; + address token; + uint256 amount; + bool pending; + } + // ====================== // Errors // ====================== @@ -156,6 +164,9 @@ interface ISlashingManager { /// @notice Thrown when attempting to execute a slash before the appeal window has closed error AppealWindowActive(); + /// @notice Thrown when an unresolved appeal has not reached its terminal expiry. + error AppealResolutionWindowActive(); + /// @notice Thrown when attempting to file a second appeal for the same proposal error AlreadyAppealed(); @@ -204,7 +215,7 @@ interface ISlashingManager { /// @notice Thrown when the attestation `deadline` has passed at the time of submission error SignatureExpired(); - /// @notice Thrown when an operator action is gated by an unresolved Lane B slash proposal + /// @notice Thrown when an operator action is gated by any unresolved slash proposal error OperatorUnderSlash(); /// @notice Thrown when a ban operation requires a distinct governance confirmer @@ -213,6 +224,10 @@ interface ISlashingManager { /// @notice Thrown when no pending ban proposal exists for the target node error NoPendingBan(); + /// @notice The slash transaction did not leave enough gas for its bounded + /// initial routing attempt. + error InsufficientRoutingGas(); + // ====================== // Events // ====================== @@ -336,7 +351,11 @@ interface ISlashingManager { * @param e3Id ID of the E3 computation * @param amount Amount of slashed funds escrowed (underlying stablecoin) */ - event SlashedFundsEscrowedToRefund(uint256 indexed e3Id, uint256 amount); + event SlashedFundsEscrowedToRefund( + uint256 indexed e3Id, + address indexed token, + uint256 amount + ); /** * @notice Emitted when routing slashed funds fails (funds remain in BondingRegistry) @@ -345,6 +364,22 @@ interface ISlashingManager { */ event RoutingFailed(uint256 indexed e3Id, uint256 amount); + /// @notice Emitted before a slash route is attempted and durably reserved. + event SlashRoutePending( + uint256 indexed proposalId, + uint256 indexed e3Id, + address indexed token, + uint256 amount + ); + + /// @notice Emitted after a pending slash route reaches E3 escrow. + event SlashRouteCompleted( + uint256 indexed proposalId, + uint256 indexed e3Id, + address indexed token, + uint256 amount + ); + /** * @notice Emitted when the bonding registry is set * @param bondingRegistry Address of the bonding registry @@ -406,14 +441,48 @@ interface ISlashingManager { function isBanned(address node) external view returns (bool isBanned); /** - * @notice Returns true if the operator has at least one unresolved Lane B slash proposal - * @dev Used by BondingRegistry to block `deregisterOperator` while a slash is pending. + * @notice Returns true if the operator has at least one unresolved financial slash proposal. + * @dev Used by BondingRegistry to block collateral withdrawals and exit claims. * @param operator Operator address to check */ - function hasOpenLaneBProposal( + function hasOpenSlashProposal( address operator ) external view returns (bool); + /// @notice Freeze all contracts used to validate and execute slashes for an E3. + /// @dev Called exactly once by the configured Interfold during E3 creation. + function snapshotE3Dependencies(uint256 e3Id) external; + + /// @notice Return the contracts frozen for an E3's slashing lifecycle. + function getE3Dependencies( + uint256 e3Id + ) + external + view + returns ( + address bonding, + address registry, + address interfoldContract, + address refundManager + ); + + /// @notice Return a slash route that remains pending after an initial failure. + function getPendingSlashRoute( + uint256 proposalId + ) external view returns (PendingSlashRoute memory); + + /// @notice Permissionlessly retry a pending slash route. + /// @return routed True when this call completed the route; false when the + /// proposal had already been routed or never created a ticket route. + function retrySlashRoute(uint256 proposalId) external returns (bool routed); + + /// @notice Atomically consume a proposal's reserved funds and account them + /// in the snapshotted E3 refund manager. + /// @dev Self-call only; exposed for try/catch transaction atomicity. + function routePendingSlashFunds( + uint256 proposalId + ) external returns (bool routed); + /** * @notice Returns the bonding registry contract used for executing slashes * @return registry The IBondingRegistry contract instance @@ -558,16 +627,6 @@ interface ISlashingManager { */ function executeSlash(uint256 proposalId) external; - /** - * @notice Atomically redirects slashed ticket funds to E3RefundManager escrow - * @dev Only callable by this contract (self-call pattern for try/catch atomicity). - * Transfers underlying stablecoin from BondingRegistry to E3RefundManager - * and calls Interfold.escrowSlashedFunds to update the escrow balance. - * @param e3Id ID of the E3 computation - * @param amount Amount of slashed ticket balance to escrow - */ - function escrowSlashedFundsToRefund(uint256 e3Id, uint256 amount) external; - /** * @notice Returns the EIP-712 domain separator used to authenticate attestation votes */ @@ -598,6 +657,13 @@ interface ISlashingManager { string calldata resolution ) external; + /** + * @notice Conclusively upholds an appeal if governance misses its resolution grace period. + * @dev Permissionless terminal path that releases the operator's collateral gate. + * @param proposalId ID of the unresolved appealed proposal + */ + function expireAppeal(uint256 proposalId) external; + // ====================== // Ban Management // ====================== diff --git a/packages/interfold-contracts/contracts/lib/ExitQueueLib.sol b/packages/interfold-contracts/contracts/lib/ExitQueueLib.sol index 516d4b545..ebe55b546 100644 --- a/packages/interfold-contracts/contracts/lib/ExitQueueLib.sol +++ b/packages/interfold-contracts/contracts/lib/ExitQueueLib.sol @@ -46,12 +46,14 @@ library ExitQueueLib { * @param queueHeadIndexTicket Maps operator addresses to the head index for tickets * @param queueHeadIndexLicense Maps operator addresses to the head index for licenses * @param pendingTotals Maps operator addresses to their total pending amounts + * @param liveTrancheCount Maps operators to the number of non-empty tranches. */ struct ExitQueueState { mapping(address operator => ExitTranche[] operatorQueues) operatorQueues; mapping(address operator => uint256 queueHeadIndexTicket) queueHeadIndexTicket; mapping(address operator => uint256 queueHeadIndexLicense) queueHeadIndexLicense; mapping(address operator => PendingAmounts operatorPendings) pendingTotals; + mapping(address operator => uint256 count) liveTrancheCount; } /** @@ -157,29 +159,30 @@ library ExitQueueLib { ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + // Keep both asset heads canonical before enforcing the scan-span cap. + // An asset-specific head may otherwise lag behind ticket-only or + // license-only tranches that the other asset's operation drained. + _advanceEmptyHeads(state, operator); + _pruneEmptyTail(state, operator); + uint256 len = operatorQueue.length; - bool merged; - if (len != 0) { - ExitTranche storage lastTranche = operatorQueue[len - 1]; - if (lastTranche.unlockTimestamp == unlockTimestamp) { - if (ticketAmount != 0) lastTranche.ticketAmount += ticketAmount; - if (licenseAmount != 0) { - lastTranche.licenseAmount += licenseAmount; - } - merged = true; - } - } + bool merged = _mergeIntoTail( + state, + operator, + unlockTimestamp, + ticketAmount, + licenseAmount + ); if (!merged) { - // Enforce a hard cap on the number of LIVE tranches an operator - // can hold simultaneously. "Live" = tranches at or after the - // earliest per-asset head (the lower of the two head indices). - // See `MAX_ACTIVE_TRANCHES`. - uint256 headT = state.queueHeadIndexTicket[operator]; - uint256 headL = state.queueHeadIndexLicense[operator]; - uint256 earliestHead = headT < headL ? headT : headL; + uint256 ticketHead = state.queueHeadIndexTicket[operator]; + uint256 licenseHead = state.queueHeadIndexLicense[operator]; + uint256 earliestHead = ticketHead < licenseHead + ? ticketHead + : licenseHead; require( - len - earliestHead < MAX_ACTIVE_TRANCHES, + state.liveTrancheCount[operator] < MAX_ACTIVE_TRANCHES && + len - earliestHead < MAX_ACTIVE_TRANCHES, TooManyTranches() ); @@ -187,6 +190,7 @@ library ExitQueueLib { t.unlockTimestamp = unlockTimestamp; t.ticketAmount = ticketAmount; t.licenseAmount = licenseAmount; + state.liveTrancheCount[operator]++; } _updatePendingTotals( @@ -205,6 +209,45 @@ library ExitQueueLib { ); } + /** + * @dev Merges assets into the live tail when its timestamp matches. + * Revives an asset-specific head if that asset had previously been + * drained from a tranche still kept alive by the other asset. + */ + function _mergeIntoTail( + ExitQueueState storage state, + address operator, + uint64 unlockTimestamp, + uint256 ticketAmount, + uint256 licenseAmount + ) private returns (bool merged) { + ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + uint256 len = operatorQueue.length; + if (len == 0) return false; + + uint256 lastIndex = len - 1; + ExitTranche storage lastTranche = operatorQueue[lastIndex]; + bool lastTrancheIsLive = lastTranche.ticketAmount != 0 || + lastTranche.licenseAmount != 0; + if ( + !lastTrancheIsLive || lastTranche.unlockTimestamp != unlockTimestamp + ) return false; + + if (ticketAmount != 0) { + if (state.queueHeadIndexTicket[operator] > lastIndex) { + state.queueHeadIndexTicket[operator] = lastIndex; + } + lastTranche.ticketAmount += ticketAmount; + } + if (licenseAmount != 0) { + if (state.queueHeadIndexLicense[operator] > lastIndex) { + state.queueHeadIndexLicense[operator] = lastIndex; + } + lastTranche.licenseAmount += licenseAmount; + } + return true; + } + /** * @notice Queues only tickets for exit with a time delay * @dev Convenience function that calls queueAssetsForExit with licenseAmount = 0 @@ -504,6 +547,10 @@ library ExitQueueLib { tranche.licenseAmount -= amountToTake; } + if (tranche.ticketAmount == 0 && tranche.licenseAmount == 0) { + state.liveTrancheCount[operator]--; + } + remainingWanted -= amountToTake; takenAmount += amountToTake; @@ -520,5 +567,58 @@ library ExitQueueLib { } else { state.queueHeadIndexLicense[operator] = head; } + _advanceEmptyHeads(state, operator); + _pruneEmptyTail(state, operator); + } + + /// @dev Advance both asset heads across tranches that are empty for that + /// asset. This keeps the physical scan span aligned with live work, + /// including when only one asset class is claimed or slashed. + function _advanceEmptyHeads( + ExitQueueState storage state, + address operator + ) private { + ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + uint256 len = operatorQueue.length; + uint256 ticketHead = state.queueHeadIndexTicket[operator]; + while ( + ticketHead < len && operatorQueue[ticketHead].ticketAmount == 0 + ) { + ticketHead++; + } + state.queueHeadIndexTicket[operator] = ticketHead; + + uint256 licenseHead = state.queueHeadIndexLicense[operator]; + while ( + licenseHead < len && operatorQueue[licenseHead].licenseAmount == 0 + ) { + licenseHead++; + } + state.queueHeadIndexLicense[operator] = licenseHead; + } + + /// @dev Remove fully drained tail entries so repeated queue/claim cycles + /// cannot grow the scanned history behind an earlier locked tranche. + /// Both per-asset heads are clamped because either may have advanced + /// past an entry that is now removed. + function _pruneEmptyTail( + ExitQueueState storage state, + address operator + ) private { + ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + uint256 len = operatorQueue.length; + while (len != 0) { + ExitTranche storage tail = operatorQueue[len - 1]; + if (tail.ticketAmount != 0 || tail.licenseAmount != 0) break; + operatorQueue.pop(); + len--; + } + + if (state.queueHeadIndexTicket[operator] > len) { + state.queueHeadIndexTicket[operator] = len; + } + if (state.queueHeadIndexLicense[operator] > len) { + state.queueHeadIndexLicense[operator] = len; + } } } diff --git a/packages/interfold-contracts/contracts/lib/InterfoldPricing.sol b/packages/interfold-contracts/contracts/lib/InterfoldPricing.sol index 74e50c05a..e72ebd868 100644 --- a/packages/interfold-contracts/contracts/lib/InterfoldPricing.sol +++ b/packages/interfold-contracts/contracts/lib/InterfoldPricing.sol @@ -6,6 +6,8 @@ pragma solidity >=0.8.27; import { IInterfold } from "../interfaces/IInterfold.sol"; +import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; +import { IDecryptionVerifier } from "../interfaces/IDecryptionVerifier.sol"; /** * @title InterfoldPricing @@ -27,77 +29,100 @@ library InterfoldPricing { uint16 internal constant MAX_MARGIN_BPS = 5_000; uint32 internal constant MAX_COMMITTEE_SIZE = 256; - /// @notice Writes the default {IInterfold.PricingConfig} directly to - /// the linked {Interfold} storage starting at slot 24. Called - /// via DELEGATECALL from {Interfold.initialize}, so SSTORE - /// targets the caller's storage. Hosted in the library so - /// the 15-field literal stays out of Interfold runtime bytecode - /// (EIP-170 24,576-byte cap). - /// @dev Slot map for `_pricingConfig` (struct field order in - /// {IInterfold.PricingConfig}): - /// 24: keyGenFixedPerNode - /// 25: keyGenPerEncryptionProof - /// 26: coordinationPerPair - /// 27: availabilityPerNodePerSec - /// 28: decryptionPerNode - /// 29: publicationBase - /// 30: verificationPerProof - /// 31: packed { protocolTreasury(20) | marginBps(2) | - /// protocolShareBps(2) | dkgUtilizationBps(2) | - /// computeUtilizationBps(2) | decryptUtilizationBps(2) } - /// 32: packed { minCommitteeSize(4) | minThreshold(4) } - /// The contract storage layout snapshot in - /// `audits/storage-layouts/Interfold-v1.json` MUST keep these - /// slots stable; any storage reordering requires updating the - /// constants below. - function applyDefaultPricingConfig() external { - // Packed slot 31: - // marginBps = 1000 << 160 - // protocolShareBps = 0 << 176 - // dkgUtilizationBps = 2500 << 192 - // computeUtilizationBps = 5000 << 208 - // decryptUtilizationBps = 2500 << 224 - // protocolTreasury (low 160 bits) and trailing padding are zero. - uint256 slot31 = (uint256(1000) << 160) | - (uint256(2500) << 192) | - (uint256(5000) << 208) | - (uint256(2500) << 224); - // solhint-disable-next-line no-inline-assembly - assembly { - sstore(24, 100000) // keyGenFixedPerNode = 0.10 USDC - sstore(25, 50000) // keyGenPerEncryptionProof = 0.05 USDC - sstore(26, 10000) // coordinationPerPair = 0.01 USDC - sstore(27, 50) // availabilityPerNodePerSec = 0.00005 USDC - sstore(28, 300000) // decryptionPerNode = 0.30 USDC - sstore(29, 1000000) // publicationBase = 1.00 USDC - sstore(30, 5000) // verificationPerProof = 0.005 USDC - sstore(31, slot31) - // slot 32 (minCommitteeSize | minThreshold) stays zero. + event ParamSetRegistered(uint8 paramSet, bytes encodedParams); + event ParamSetUpdated( + uint8 paramSet, + bytes previousParams, + bytes newParams + ); + + function emitParamSetChange( + uint8 paramSet, + bytes calldata previous, + bytes calldata current + ) external { + if (previous.length == 0) { + emit ParamSetRegistered(paramSet, current); + } else { + emit ParamSetUpdated(paramSet, previous, current); } } - /// @notice Returns the default {IInterfold.PricingConfig} applied by - /// {Interfold.initialize}. Hosted in the external library so the - /// 15-field literal stays out of the Interfold runtime bytecode - /// (EIP-170 24,576-byte cap). - function defaultPricingConfig() - external - pure - returns (IInterfold.PricingConfig memory cfg) - { - cfg.keyGenFixedPerNode = 100000; // 0.10 USDC - cfg.keyGenPerEncryptionProof = 50000; // 0.05 USDC - cfg.coordinationPerPair = 10000; // 0.01 USDC - cfg.availabilityPerNodePerSec = 50; // 0.00005 USDC - cfg.decryptionPerNode = 300000; // 0.30 USDC - cfg.publicationBase = 1000000; // 1.00 USDC - cfg.verificationPerProof = 5000; // 0.005 USDC - cfg.marginBps = 1000; // 10% - cfg.dkgUtilizationBps = 2500; // 25% - cfg.computeUtilizationBps = 5000; // 50% - cfg.decryptUtilizationBps = 2500; // 25% - // protocolTreasury, protocolShareBps, minCommitteeSize, minThreshold - // remain zero by default and use the struct zero-initialization. + function validateRegistryCaller( + address caller, + address registry + ) external pure { + require(caller == registry, IInterfold.OnlyCiphernodeRegistry()); + } + + function validateSlashCaller( + address caller, + address slashManager + ) external pure { + require(caller == slashManager, IInterfold.OnlySlashingManager()); + } + + function validateRegistryOrSlashCaller( + address caller, + address registry, + address slashManager + ) external pure { + require( + caller == registry || caller == slashManager, + IInterfold.OnlyCiphernodeRegistryOrSlashingManager() + ); + } + + function verifyPlaintext( + address verifierAddress, + address registryAddress, + uint256 e3Id, + bytes32 ciphertextHash, + bytes32 committeePublicKey, + bytes32 plaintextHash, + bytes calldata proof + ) external view { + bytes32 committeeHash = ICiphernodeRegistry(registryAddress) + .getCommitteeHash(e3Id); + bytes32 decryptionDomain = keccak256( + abi.encode( + block.chainid, + address(this), + e3Id, + committeeHash, + ciphertextHash, + committeePublicKey + ) + ); + IDecryptionVerifier(verifierAddress).verify( + decryptionDomain, + plaintextHash, + committeeHash, + proof + ); + } + + function honestNodes( + address registryAddress, + uint256 e3Id, + uint8 reason + ) external view returns (address[] memory) { + ICiphernodeRegistry registry = ICiphernodeRegistry(registryAddress); + if ( + reason == + uint8(IInterfold.FailureReason.CommitteeFormationTimeout) || + reason == + uint8(IInterfold.FailureReason.InsufficientCommitteeMembers) + ) return new address[](0); + + try registry.getActiveCommitteeNodes(e3Id) returns ( + address[] memory nodes, + uint256[] memory + ) { + return nodes; + } catch { + return new address[](0); + } } /// @notice Mirrors the four validation gates at the top of @@ -206,7 +231,7 @@ library InterfoldPricing { revert IInterfold.BelowMinThreshold(threshold[0], minThreshold); } - /// @notice Mirrors the input-window / duration gates at the top + /// @notice Mirrors the input-window and duration gates /// of {Interfold.request}. Reverts with the same selectors so off- /// chain `revertedWithCustomError(interfold, ...)` lookups keep /// working. @@ -215,14 +240,12 @@ library InterfoldPricing { /// @param computeWindow `_timeoutConfig.computeWindow`. /// @param decryptionWindow `_timeoutConfig.decryptionWindow`. /// @param maxDuration The Interfold-wide upper bound. - /// @param quotedFee Fee returned by {InterfoldPricing.quote}. function validateRequest( uint256[2] calldata inputWindow, uint256 nowTs, uint256 computeWindow, uint256 decryptionWindow, - uint256 maxDuration, - uint256 quotedFee // solhint-disable-line no-unused-vars + uint256 maxDuration ) external pure { if (inputWindow[0] < nowTs) revert IInterfold.InvalidInputDeadlineStart(inputWindow[0]); diff --git a/packages/interfold-contracts/contracts/registry/BondingRegistry.sol b/packages/interfold-contracts/contracts/registry/BondingRegistry.sol index be413234c..ec6004809 100644 --- a/packages/interfold-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/interfold-contracts/contracts/registry/BondingRegistry.sol @@ -132,6 +132,7 @@ contract BondingRegistry is bool registered; bool exitRequested; bool active; + uint256 eligibilityVersion; } /// @notice Maps operator address to their state data @@ -150,13 +151,37 @@ contract BondingRegistry is /// @dev Internal state for managing exit queue of tickets and licenses ExitQueueLib.ExitQueueState private _exits; + /// @notice Version of the current operator-eligibility policy. + /// @dev Every eligibility update advances the version and resets + /// {numActiveOperators}. Operators fail closed until refreshed. + uint256 public eligibilityConfigurationVersion; + + /// @notice Slashed tickets committed to retryable E3 refund routes. + uint256 public reservedSlashedTicketBalance; + + /// @notice Slashing managers that may finish snapshotted E3 lifecycles. + /// @dev Rotating the current manager does not revoke its predecessor. + address[] internal _authorizedSlashingManagers; + + /// @dev One-based index into {_authorizedSlashingManagers}; zero means unauthorized. + mapping(address manager => uint256 indexPlusOne) + internal _authorizedSlashingManagerIndex; + + /// @notice Maximum number of concurrently authorized slashing managers. + uint256 public constant MAX_AUTHORIZED_SLASHING_MANAGERS = 32; + + /// @inheritdoc IBondingRegistry + uint256 public totalLicenseLiability; + // ====================== // Modifiers // ====================== - /// @dev Restricts function access to only the slashing manager - modifier onlySlashingManager() { - if (msg.sender != slashingManager) revert Unauthorized(); + /// @dev Restricts function access to current or retained historical managers. + modifier onlyAuthorizedSlashingManager() { + if (_authorizedSlashingManagerIndex[msg.sender] == 0) { + revert Unauthorized(); + } _; } @@ -176,6 +201,21 @@ contract BondingRegistry is _; } + /// @dev Keeps active and already-queued collateral available while any + /// financial slash proposal against the operator is unresolved. + modifier noOpenSlashProposal(address operator) { + uint256 len = _authorizedSlashingManagers.length; + for (uint256 i = 0; i < len; i++) { + if ( + ISlashingManager(_authorizedSlashingManagers[i]) + .hasOpenSlashProposal(operator) + ) { + revert OperatorUnderSlash(); + } + } + _; + } + //////////////////////////////////////////////////////////// // // // Initialization // @@ -307,7 +347,24 @@ contract BondingRegistry is /// @inheritdoc IBondingRegistry function isActive(address operator) external view returns (bool) { - return operators[operator].active; + Operator storage op = operators[operator]; + return + op.eligibilityVersion == eligibilityConfigurationVersion && + op.active; + } + + /// @inheritdoc IBondingRegistry + function refreshOperatorStatus(address operator) public { + require(operators[operator].registered, NotRegistered()); + _updateOperatorStatus(operator); + } + + /// @inheritdoc IBondingRegistry + function refreshOperatorStatuses(address[] calldata operatorList) external { + uint256 len = operatorList.length; + for (uint256 i = 0; i < len; i++) { + refreshOperatorStatus(operatorList[i]); + } } /// @inheritdoc IBondingRegistry @@ -316,6 +373,25 @@ contract BondingRegistry is return op.exitRequested && block.timestamp < op.exitUnlocksAt; } + /// @inheritdoc IBondingRegistry + function isAuthorizedSlashingManager( + address candidate + ) external view returns (bool) { + return _authorizedSlashingManagerIndex[candidate] != 0; + } + + /// @inheritdoc IBondingRegistry + function authorizedSlashingManagerCount() external view returns (uint256) { + return _authorizedSlashingManagers.length; + } + + /// @inheritdoc IBondingRegistry + function authorizedSlashingManagerAt( + uint256 index + ) external view returns (address) { + return _authorizedSlashingManagers[index]; + } + // ====================== // Operator Functions // ====================== @@ -328,10 +404,16 @@ contract BondingRegistry is operators[msg.sender].exitUnlocksAt = 0; } - require( - !ISlashingManager(slashingManager).isBanned(msg.sender), - CiphernodeBanned() - ); + require(slashingManager != address(0), ZeroAddress()); + uint256 managerCount = _authorizedSlashingManagers.length; + for (uint256 i = 0; i < managerCount; i++) { + require( + !ISlashingManager(_authorizedSlashingManagers[i]).isBanned( + msg.sender + ), + CiphernodeBanned() + ); + } require(!operators[msg.sender].registered, AlreadyRegistered()); require( operators[msg.sender].licenseBond >= licenseRequiredBond, @@ -347,21 +429,14 @@ contract BondingRegistry is } /// @inheritdoc IBondingRegistry - function deregisterOperator() external noExitInProgress(msg.sender) { + function deregisterOperator() + external + noExitInProgress(msg.sender) + noOpenSlashProposal(msg.sender) + { Operator storage op = operators[msg.sender]; require(op.registered, NotRegistered()); - // block deregistration while an unresolved Lane B slash proposal exists. - // An operator could otherwise drain ticket / license collateral during the appeal - // window and leave the slasher with nothing to slash. - address sm = slashingManager; - if (sm != address(0)) { - require( - !ISlashingManager(sm).hasOpenLaneBProposal(msg.sender), - OperatorUnderSlash() - ); - } - op.registered = false; op.exitRequested = true; op.exitUnlocksAt = uint64(block.timestamp) + exitDelay; @@ -425,7 +500,7 @@ contract BondingRegistry is /// @inheritdoc IBondingRegistry function removeTicketBalance( uint256 amount - ) external noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) noOpenSlashProposal(msg.sender) { require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); require( @@ -456,7 +531,12 @@ contract BondingRegistry is /// @inheritdoc IBondingRegistry function unbondLicense( uint256 amount - ) external nonReentrant noExitInProgress(msg.sender) { + ) + external + nonReentrant + noExitInProgress(msg.sender) + noOpenSlashProposal(msg.sender) + { require(amount != 0, ZeroAmount()); require( operators[msg.sender].licenseBond >= amount, @@ -484,7 +564,7 @@ contract BondingRegistry is function claimExits( uint256 maxTicketAmount, uint256 maxLicenseAmount - ) external nonReentrant { + ) external nonReentrant noOpenSlashProposal(msg.sender) { (uint256 ticketClaim, uint256 licenseClaim) = _exits.claimAssets( msg.sender, maxTicketAmount, @@ -494,6 +574,7 @@ contract BondingRegistry is if (ticketClaim > 0) ticketToken.payout(msg.sender, ticketClaim); if (licenseClaim > 0) { + totalLicenseLiability -= licenseClaim; _safeTransferLicenseWithDeltaCheck(msg.sender, licenseClaim); } } @@ -507,7 +588,7 @@ contract BondingRegistry is address operator, uint256 requestedSlashAmount, bytes32 slashReason - ) external onlySlashingManager returns (uint256) { + ) external onlyAuthorizedSlashingManager returns (uint256) { require(requestedSlashAmount != 0, ZeroAmount()); (uint256 pendingTicketBalance, ) = _exits.getPendingAmounts(operator); @@ -561,7 +642,7 @@ contract BondingRegistry is address operator, uint256 requestedSlashAmount, bytes32 slashReason - ) external onlySlashingManager nonReentrant { + ) external onlyAuthorizedSlashingManager nonReentrant { require(requestedSlashAmount != 0, ZeroAmount()); Operator storage operatorData = operators[operator]; @@ -609,14 +690,27 @@ contract BondingRegistry is } /// @inheritdoc IBondingRegistry - function redirectSlashedTicketFunds( + function reserveSlashedTicketFunds( + uint256 amount + ) external onlyAuthorizedSlashingManager { + require(amount > 0, ZeroAmount()); + require( + amount <= slashedTicketBalance - reservedSlashedTicketBalance, + InsufficientBalance() + ); + reservedSlashedTicketBalance += amount; + } + + /// @inheritdoc IBondingRegistry + function redirectReservedSlashedTicketFunds( address to, uint256 amount - ) external onlySlashingManager { + ) external onlyAuthorizedSlashingManager { require(to != address(0), ZeroAddress()); require(amount > 0, ZeroAmount()); - require(amount <= slashedTicketBalance, InsufficientBalance()); + require(amount <= reservedSlashedTicketBalance, InsufficientBalance()); + reservedSlashedTicketBalance -= amount; slashedTicketBalance -= amount; ticketToken.payout(to, amount); } @@ -654,7 +748,9 @@ contract BondingRegistry is require(newTicketPrice != 0, InvalidConfiguration()); uint256 oldValue = ticketPrice; + if (oldValue == newTicketPrice) return; ticketPrice = newTicketPrice; + _invalidateEligibilityStatuses(); emit ConfigurationUpdated("ticketPrice", oldValue, newTicketPrice); } @@ -666,7 +762,9 @@ contract BondingRegistry is require(newLicenseRequiredBond != 0, InvalidConfiguration()); uint256 oldValue = licenseRequiredBond; + if (oldValue == newLicenseRequiredBond) return; licenseRequiredBond = newLicenseRequiredBond; + _invalidateEligibilityStatuses(); emit ConfigurationUpdated( "licenseRequiredBond", @@ -680,15 +778,20 @@ contract BondingRegistry is require(newBps > 0 && newBps <= BPS_BASE, InvalidConfiguration()); uint256 oldValue = licenseActiveBps; + if (oldValue == newBps) return; licenseActiveBps = newBps; + _invalidateEligibilityStatuses(); emit ConfigurationUpdated("licenseActiveBps", oldValue, newBps); } /// @inheritdoc IBondingRegistry function setMinTicketBalance(uint256 newMinTicketBalance) public onlyOwner { + require(newMinTicketBalance != 0, InvalidConfiguration()); uint256 oldValue = minTicketBalance; + if (oldValue == newMinTicketBalance) return; minTicketBalance = newMinTicketBalance; + _invalidateEligibilityStatuses(); emit ConfigurationUpdated( "minTicketBalance", @@ -725,14 +828,63 @@ contract BondingRegistry is function setTicketToken( InterfoldTicketToken newTicketToken ) public onlyOwner { + address next = address(newTicketToken); + if (next.code.length == 0) revert InvalidBondingAsset(next); + + InterfoldTicketToken current = ticketToken; + if (address(current) != address(0) && current != newTicketToken) { + uint256 liabilities = current.totalSupply() + + current.payableBalance(); + if (liabilities != 0) { + revert OutstandingAssetLiabilities( + address(current), + liabilities + ); + } + } ticketToken = newTicketToken; - emit TicketTokenSet(address(newTicketToken)); + emit TicketTokenSet(next); } /// @inheritdoc IBondingRegistry function setLicenseToken(IERC20 newLicenseToken) public onlyOwner { + address next = address(newLicenseToken); + IERC20 current = licenseToken; + + // BondingRegistry is deployed before FOLD because FOLD stores the + // registry address immutably. Permit only that initial zero placeholder. + if (next == address(0)) { + if (address(current) != address(0)) { + revert InvalidBondingAsset(next); + } + } else if (next.code.length == 0) { + revert InvalidBondingAsset(next); + } + + if (address(current) != address(0) && current != newLicenseToken) { + uint256 liabilities = current.balanceOf(address(this)); + if (liabilities != 0) { + revert OutstandingAssetLiabilities( + address(current), + liabilities + ); + } + } licenseToken = newLicenseToken; - emit LicenseTokenSet(address(newLicenseToken)); + emit LicenseTokenSet(next); + } + + /// @inheritdoc IBondingRegistry + function sweepLicenseSurplus() external onlyOwner returns (uint256 amount) { + IERC20 current = licenseToken; + uint256 balance = current.balanceOf(address(this)); + uint256 liabilities = totalLicenseLiability; + if (balance <= liabilities) return 0; + + amount = balance - liabilities; + address treasury = slashedFundsTreasury; + _safeTransferLicenseWithDeltaCheck(treasury, amount); + emit LicenseSurplusSwept(address(current), treasury, amount); } /// @inheritdoc IBondingRegistry @@ -743,14 +895,46 @@ contract BondingRegistry is /// @inheritdoc IBondingRegistry function setSlashingManager(address newSlashingManager) public onlyOwner { - // zero-address protection and explicit event so a missed setter - // call is observable off-chain. require(newSlashingManager != address(0), ZeroAddress()); + if (_authorizedSlashingManagerIndex[newSlashingManager] == 0) { + require( + _authorizedSlashingManagers.length < + MAX_AUTHORIZED_SLASHING_MANAGERS, + InvalidConfiguration() + ); + _authorizedSlashingManagers.push(newSlashingManager); + _authorizedSlashingManagerIndex[ + newSlashingManager + ] = _authorizedSlashingManagers.length; + emit SlashingManagerAuthorizationUpdated(newSlashingManager, true); + } address oldValue = slashingManager; slashingManager = newSlashingManager; emit SlashingManagerUpdated(oldValue, newSlashingManager); } + /// @inheritdoc IBondingRegistry + function revokeSlashingManager( + address oldSlashingManager + ) external onlyOwner { + require(oldSlashingManager != slashingManager, InvalidConfiguration()); + uint256 indexPlusOne = _authorizedSlashingManagerIndex[ + oldSlashingManager + ]; + if (indexPlusOne == 0) revert Unauthorized(); + + uint256 index = indexPlusOne - 1; + uint256 lastIndex = _authorizedSlashingManagers.length - 1; + if (index != lastIndex) { + address moved = _authorizedSlashingManagers[lastIndex]; + _authorizedSlashingManagers[index] = moved; + _authorizedSlashingManagerIndex[moved] = index + 1; + } + _authorizedSlashingManagers.pop(); + delete _authorizedSlashingManagerIndex[oldSlashingManager]; + emit SlashingManagerAuthorizationUpdated(oldSlashingManager, false); + } + /// @notice Disabled. Reverts unconditionally. function renounceOwnership() public view override onlyOwner { revert RenounceOwnershipDisabled(); @@ -792,7 +976,10 @@ contract BondingRegistry is uint256 ticketAmount, uint256 licenseAmount ) public onlyOwner { - require(ticketAmount <= slashedTicketBalance, InsufficientBalance()); + require( + ticketAmount <= slashedTicketBalance - reservedSlashedTicketBalance, + ReservedSlashedFunds() + ); require(licenseAmount <= slashedLicenseBond, InsufficientBalance()); if (ticketAmount > 0) { @@ -802,6 +989,7 @@ contract BondingRegistry is if (licenseAmount > 0) { slashedLicenseBond -= licenseAmount; + totalLicenseLiability -= licenseAmount; _safeTransferLicenseWithDeltaCheck( slashedFundsTreasury, licenseAmount @@ -830,6 +1018,7 @@ contract BondingRegistry is uint256 actualReceived = licenseToken.balanceOf(address(this)) - balanceBefore; require(actualReceived == amount, InvalidAmount()); + totalLicenseLiability += amount; emit LicenseBondUpdated( operator, @@ -846,12 +1035,17 @@ contract BondingRegistry is /// @param operator Address of the operator to update function _updateOperatorStatus(address operator) internal { Operator storage op = operators[operator]; + uint256 currentVersion = eligibilityConfigurationVersion; + bool oldActiveStatus = op.eligibilityVersion == currentVersion && + op.active; bool newActiveStatus = op.registered && op.licenseBond >= _minLicenseBond() && (ticketToken.balanceOf(operator) / ticketPrice >= minTicketBalance); - if (op.active != newActiveStatus) { - op.active = newActiveStatus; + op.eligibilityVersion = currentVersion; + op.active = newActiveStatus; + + if (oldActiveStatus != newActiveStatus) { if (newActiveStatus) { numActiveOperators++; } else { @@ -868,12 +1062,23 @@ contract BondingRegistry is return (licenseRequiredBond * licenseActiveBps) / BPS_BASE; } + /// @dev Invalidates every cached active status in O(1). Operators are + /// considered inactive until they refresh under the new version. + function _invalidateEligibilityStatuses() internal { + eligibilityConfigurationVersion++; + numActiveOperators = 0; + emit EligibilityConfigurationVersionUpdated( + eligibilityConfigurationVersion + ); + } + /// @dev `safeTransfer` of the license token, measuring the RECIPIENT-side delta /// to detect fee-on-transfer / rebasing behavior (sender-side delta misses /// fees that burn or reroute). Internal accounting is already decremented at /// the call site, so a shortfall emits {LicenseTransferShortfall} rather than /// reverting (a revert would brick claims if the token starts taking fees); - /// the owner can swap the token via {setLicenseToken}. + /// governance must pause new bonding and drain every liability before + /// rotating the token via {setLicenseToken}. function _safeTransferLicenseWithDeltaCheck( address recipient, uint256 expectedAmount @@ -909,5 +1114,5 @@ contract BondingRegistry is /// @dev Reserved storage slots for future upgrades. // solhint-disable-next-line var-name-mixedcase - uint256[50] private __gap; + uint256[47] private __gap; } diff --git a/packages/interfold-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/interfold-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index bf5faa96c..6a1c35279 100644 --- a/packages/interfold-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/interfold-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -80,7 +80,7 @@ contract CiphernodeRegistryOwnable is IBondingRegistry public bondingRegistry; /// @notice Current number of registered ciphernodes - uint256 public numCiphernodes; + uint256 public override numCiphernodes; /// @notice Submission Window for an E3 Sortition. /// @dev The submission window is the time period during which the ciphernodes can submit @@ -168,6 +168,16 @@ contract CiphernodeRegistryOwnable is mapping(uint256 e3Id => bytes32[] skAggCommits) internal dkgSkAggCommits; mapping(uint256 e3Id => bytes32[] esmAggCommits) internal dkgEsmAggCommits; + struct CommitteeDependencies { + IInterfold interfoldContract; + IBondingRegistry bonding; + ISlashingManager slashManager; + } + + /// @notice External contracts frozen for each committee lifecycle. + mapping(uint256 e3Id => CommitteeDependencies dependencies) + internal _committeeDependencies; + //////////////////////////////////////////////////////////// // // // Modifiers // @@ -195,12 +205,6 @@ contract CiphernodeRegistryOwnable is _; } - /// @dev Restricts function access to only the slashing manager - modifier onlySlashingManager() { - require(msg.sender == address(slashingManager), NotSlashingManager()); - _; - } - //////////////////////////////////////////////////////////// // // // Initialization // @@ -258,6 +262,14 @@ contract CiphernodeRegistryOwnable is CommitteeAlreadyRequested() ); + CommitteeDependencies storage dependencies = _committeeDependencies[ + e3Id + ]; + dependencies.interfoldContract = IInterfold(msg.sender); + dependencies.bonding = bondingRegistry; + dependencies.slashManager = slashingManager; + dependencies.slashManager.snapshotE3Dependencies(e3Id); + uint256 activeCount = bondingRegistry.numActiveOperators(); require( threshold[1] <= activeCount, @@ -307,24 +319,22 @@ contract CiphernodeRegistryOwnable is c.publicKey = pkCommitment; publicKeyHashes[e3Id] = pkCommitment; - E3 memory e3 = interfold.getE3(e3Id); - if (e3.proofAggregationEnabled) { - // Bind to the on-chain committee (c.topNodes), not caller-supplied - // nodes, so a wrong `nodes` input cannot pre-commit the prover to - // an attacker's set (C-08). - _verifyAndStoreDkgAnchors( - e3Id, - e3, - roots[e3Id], - c.topNodes, - pkCommitment, - committeeHash, - proof, - dkgAttestationBundle - ); - } + E3 memory e3 = _interfoldFor(e3Id).getE3(e3Id); + // Bind to the on-chain committee (c.topNodes), not caller-supplied + // nodes, so a wrong `nodes` input cannot pre-commit the prover to + // an attacker's set (C-08). + _verifyAndStoreDkgAnchors( + e3Id, + e3, + roots[e3Id], + c.topNodes, + pkCommitment, + committeeHash, + proof, + dkgAttestationBundle + ); - interfold.onCommitteePublished(e3Id, pkCommitment); + _interfoldFor(e3Id).onCommitteePublished(e3Id, pkCommitment); emit CommitteePublished( e3Id, @@ -545,7 +555,10 @@ contract CiphernodeRegistryOwnable is CommitteeDeadlineReached() ); require(!c.submitted[msg.sender], NodeAlreadySubmitted()); - require(isCiphernodeEligible(msg.sender), NodeNotEligible()); + require( + isEnabled(msg.sender) && _bondingFor(e3Id).isActive(msg.sender), + NodeNotEligible() + ); // Validate node eligibility and ticket number _validateNodeEligibility(msg.sender, ticketNumber, e3Id); @@ -596,7 +609,7 @@ contract CiphernodeRegistryOwnable is c.topNodes.length, c.threshold[1] ); - interfold.onE3Failed( + _interfoldFor(e3Id).onE3Failed( e3Id, uint8(IInterfold.FailureReason.InsufficientCommitteeMembers) ); @@ -614,7 +627,7 @@ contract CiphernodeRegistryOwnable is scores[i] = c.scoreOf[c.topNodes[i]]; } - interfold.onCommitteeFinalized(e3Id); + _interfoldFor(e3Id).onCommitteeFinalized(e3Id); emit SortitionCommitteeFinalized(e3Id, c.topNodes, scores); return true; } @@ -867,11 +880,11 @@ contract CiphernodeRegistryOwnable is uint256 e3Id, address node, bytes32 reason - ) - external - onlySlashingManager - returns (uint256 activeCount, uint32 thresholdM) - { + ) external returns (uint256 activeCount, uint32 thresholdM) { + require( + msg.sender == address(_slashingManagerFor(e3Id)), + NotSlashingManager() + ); Committee storage c = committees[e3Id]; require( c.stage == ICiphernodeRegistry.CommitteeStage.Finalized, @@ -1018,10 +1031,8 @@ contract CiphernodeRegistryOwnable is uint256 e3Id ) internal view { require(ticketNumber > 0, InvalidTicketNumber()); - require( - address(bondingRegistry) != address(0), - BondingRegistryNotSet() - ); + IBondingRegistry e3Bonding = _bondingFor(e3Id); + require(address(e3Bonding) != address(0), BondingRegistryNotSet()); Committee storage c = committees[e3Id]; @@ -1032,11 +1043,11 @@ contract CiphernodeRegistryOwnable is // and selection weight below derive purely from the historical // ticket balance at `c.requestBlock - 1`, so churn between request // time and the ticket submission window cannot inflate weights. - uint256 ticketBalance = bondingRegistry.getTicketBalanceAtBlock( + uint256 ticketBalance = e3Bonding.getTicketBalanceAtBlock( node, c.requestBlock - 1 ); - uint256 ticketPrice = bondingRegistry.ticketPrice(); + uint256 ticketPrice = e3Bonding.ticketPrice(); require(ticketPrice > 0, InvalidTicketNumber()); uint256 availableTickets = ticketBalance / ticketPrice; @@ -1045,6 +1056,24 @@ contract CiphernodeRegistryOwnable is require(ticketNumber <= availableTickets, InvalidTicketNumber()); } + function _bondingFor( + uint256 e3Id + ) internal view returns (IBondingRegistry e3Bonding) { + return _committeeDependencies[e3Id].bonding; + } + + function _interfoldFor( + uint256 e3Id + ) internal view returns (IInterfold e3Interfold) { + return _committeeDependencies[e3Id].interfoldContract; + } + + function _slashingManagerFor( + uint256 e3Id + ) internal view returns (ISlashingManager e3SlashingManager) { + return _committeeDependencies[e3Id].slashManager; + } + /// @notice Sort `topNodes` by ascending address before committee finalization. /// @dev Canonical address-ascending order so `CommitteeHashLib.hash(topNodes)` /// matches what off-chain aggregators independently compute over the same diff --git a/packages/interfold-contracts/contracts/slashing/SlashingManager.sol b/packages/interfold-contracts/contracts/slashing/SlashingManager.sol index 4db5a7e57..be1972fb9 100644 --- a/packages/interfold-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/interfold-contracts/contracts/slashing/SlashingManager.sol @@ -11,6 +11,7 @@ import { } from "@openzeppelin/contracts/access/extensions/AccessControlDefaultAdminRules.sol"; import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; @@ -45,6 +46,10 @@ contract SlashingManager is /// period during which governance can delay slash execution. uint64 public constant MAX_APPEAL_WINDOW = 30 days; + /// @notice Maximum time governance has after the appeal window closes to + /// resolve a filed appeal. Expiry is fail-safe in the operator's favour. + uint64 public constant APPEAL_RESOLUTION_GRACE = 7 days; + /// @notice Emitted when {bondingRegistry} is updated. event BondingRegistryUpdated( address indexed previous, @@ -82,6 +87,25 @@ contract SlashingManager is /// @notice Reference to the E3 Refund Manager for routing slashed funds IE3RefundManager public e3RefundManager; + struct E3Dependencies { + IBondingRegistry bonding; + ICiphernodeRegistry registry; + IInterfold interfoldContract; + IE3RefundManager refundManager; + bool initialized; + } + + /// @notice Contracts frozen for each E3's complete slashing lifecycle. + mapping(uint256 e3Id => E3Dependencies dependencies) + internal _e3Dependencies; + + /// @notice Slash routes retained until their reserved ticket funds reach E3 escrow. + mapping(uint256 proposalId => PendingSlashRoute route) + internal _pendingSlashRoutes; + + uint256 internal constant INITIAL_ROUTE_GAS = 400_000; + uint256 internal constant MIN_INITIAL_ROUTE_GAS = 450_000; + /// @notice Mapping from slash reason hash to its configured policy mapping(bytes32 reason => SlashPolicy policy) public slashPolicies; @@ -100,12 +124,10 @@ contract SlashingManager is /// Lane B key is keccak256(abi.encode(e3Id, operator, keccak256(evidence))) — exact evidence bytes. mapping(bytes32 evidenceKey => bool consumed) public evidenceConsumed; - /// @notice Number of unexecuted Lane B proposals per operator. - /// @dev Incremented in `proposeSlashEvidence`, decremented in `executeSlash` and on - /// `resolveAppeal(upheld=true)`. `BondingRegistry.deregisterOperator` blocks while - /// this is > 0 so an operator cannot evade an inbound Lane B slash by exiting - /// during the appeal window. - mapping(address operator => uint256 openCount) internal _openLaneBCount; + /// @notice Number of unresolved financial proposals per operator, across both lanes. + /// @dev Incremented for every proposal and decremented only at successful execution or terminal + /// appeal resolution, so collateral cannot leave during a deferred slash. + mapping(address operator => uint256 openCount) internal _openProposalCount; /// @notice Pending two-step manual ban proposals. /// @dev `unbanNode` is single-step because it is strictly less dangerous than ban. @@ -236,10 +258,10 @@ contract SlashingManager is } /// @inheritdoc ISlashingManager - function hasOpenLaneBProposal( + function hasOpenSlashProposal( address operator ) external view returns (bool) { - return _openLaneBCount[operator] > 0; + return _openProposalCount[operator] > 0; } /// @inheritdoc ISlashingManager @@ -247,6 +269,59 @@ contract SlashingManager is return _domainSeparatorV4(); } + /// @inheritdoc ISlashingManager + function getE3Dependencies( + uint256 e3Id + ) + external + view + returns ( + address bonding, + address registry, + address interfoldContract, + address refundManager + ) + { + E3Dependencies memory dependencies = _dependenciesFor(e3Id); + return ( + address(dependencies.bonding), + address(dependencies.registry), + address(dependencies.interfoldContract), + address(dependencies.refundManager) + ); + } + + /// @inheritdoc ISlashingManager + function getPendingSlashRoute( + uint256 proposalId + ) external view returns (PendingSlashRoute memory) { + return _pendingSlashRoutes[proposalId]; + } + + /// @inheritdoc ISlashingManager + function retrySlashRoute( + uint256 proposalId + ) external returns (bool routed) { + if (!_pendingSlashRoutes[proposalId].pending) return false; + return this.routePendingSlashFunds(proposalId); + } + + /// @inheritdoc ISlashingManager + function snapshotE3Dependencies(uint256 e3Id) external { + require( + msg.sender == address(interfold) || + msg.sender == address(ciphernodeRegistry), + Unauthorized() + ); + E3Dependencies storage dependencies = _e3Dependencies[e3Id]; + require(!dependencies.initialized, InvalidProposal()); + dependencies.bonding = bondingRegistry; + dependencies.registry = ciphernodeRegistry; + dependencies.interfoldContract = interfold; + dependencies.refundManager = e3RefundManager; + dependencies.initialized = true; + } + // ====================== // Admin Functions // ====================== @@ -271,6 +346,16 @@ contract SlashingManager is } // Cap the appeal window so governance cannot indefinitely delay slashing. require(policy.appealWindow <= MAX_APPEAL_WINDOW, InvalidPolicy()); + if (policy.failureReason > 0) { + // A policy that can fail an E3 must also remove the proven-faulty + // operator before the refund manager snapshots honest recipients. + require(policy.affectsCommittee, InvalidPolicy()); + require( + policy.failureReason < + uint8(IInterfold.FailureReason._MAX_FAILURE_REASON), + InvalidPolicy() + ); + } slashPolicies[reason] = policy; emit SlashPolicyUpdated(reason, policy); @@ -384,7 +469,7 @@ contract SlashingManager is require(policy.requiresProof, InvalidPolicy()); require( - ciphernodeRegistry.isCommitteeMember(e3Id, operator), + _dependenciesFor(e3Id).registry.isCommitteeMember(e3Id, operator), OperatorNotInCommittee() ); @@ -418,6 +503,8 @@ contract SlashingManager is p.affectsCommittee = policy.affectsCommittee; p.failureReason = policy.failureReason; + _openProposalCount[operator] += 1; + emit SlashProposed( proposalId, e3Id, @@ -433,6 +520,7 @@ contract SlashingManager is // Legacy atomic path: when no challenge window is configured, execute now. // Otherwise defer to `executeSlash` after `executableAt`. if (policy.appealWindow == 0) { + _openProposalCount[operator] -= 1; _executeSlash(proposalId, Lane.LaneA); } } @@ -444,9 +532,8 @@ contract SlashingManager is uint256 e3Id, uint256 partyId ) internal view returns (address operator) { - (uint256[] memory partyIds, , ) = ciphernodeRegistry.getDkgAnchors( - e3Id - ); + ICiphernodeRegistry registry = _dependenciesFor(e3Id).registry; + (uint256[] memory partyIds, , ) = registry.getDkgAnchors(e3Id); bool found = false; for (uint256 i = 0; i < partyIds.length; i++) { if (partyIds[i] == partyId) { @@ -455,7 +542,7 @@ contract SlashingManager is } } require(found, PartyIdNotInDkgAnchors()); - return ciphernodeRegistry.canonicalCommitteeNodeAt(e3Id, partyId); + return registry.canonicalCommitteeNodeAt(e3Id, partyId); } /// @inheritdoc ISlashingManager @@ -471,6 +558,10 @@ contract SlashingManager is SlashPolicy memory policy = slashPolicies[reason]; require(policy.enabled, SlashReasonDisabled()); require(!policy.requiresProof, InvalidPolicy()); + require( + _dependenciesFor(e3Id).registry.isCommitteeMember(e3Id, operator), + OperatorNotInCommittee() + ); // Evidence replay protection — reason-independent to prevent cross-reason replay bytes32 evidenceKey = keccak256( @@ -482,9 +573,9 @@ contract SlashingManager is proposalId = totalProposals; totalProposals = proposalId + 1; - // Track unresolved Lane B proposals per operator so BondingRegistry blocks - // `deregisterOperator` until they execute, expire, or are upheld on appeal. - _openLaneBCount[operator] += 1; + // Track the unresolved proposal so collateral remains slashable until + // successful execution or terminal appeal resolution. + _openProposalCount[operator] += 1; uint256 executableAt = block.timestamp + policy.appealWindow; SlashProposal storage p = _proposals[proposalId]; @@ -531,12 +622,10 @@ contract SlashingManager is } Lane lane = p.proofVerified ? Lane.LaneA : Lane.LaneB; - if (lane == Lane.LaneB) { - // Decrement BEFORE `_executeSlash` so a reentrant deregister triggered by - // slash side-effects (e.g. `expelCommitteeMember`) is not gated on this - // proposal. Other open Lane B proposals keep the gate raised. - _openLaneBCount[p.operator] -= 1; - } + // Decrement BEFORE `_executeSlash` so a reentrant operator action triggered + // by slash side-effects is not gated on this proposal. Other open proposals + // keep the gate raised. A downstream revert restores the count atomically. + _openProposalCount[p.operator] -= 1; _executeSlash(proposalId, lane); } @@ -581,8 +670,7 @@ contract SlashingManager is // Get committee threshold — need at least M agreeing votes { - (, uint32 thresholdM, , ) = ciphernodeRegistry - .getCommitteeViability(e3Id); + uint32 thresholdM = _committeeThresholdM(e3Id); require(thresholdM > 0, InvalidProposal()); require(numVotes >= thresholdM, InsufficientAttestations()); } @@ -611,7 +699,7 @@ contract SlashingManager is // Verify voter is an active committee member for this E3 require( - ciphernodeRegistry.isCommitteeMemberActive(e3Id, voter), + _isCommitteeMemberActive(e3Id, voter), VoterNotInCommittee() ); @@ -638,24 +726,43 @@ contract SlashingManager is } } + function _committeeThresholdM( + uint256 e3Id + ) internal view returns (uint32 thresholdM) { + (, thresholdM, , ) = _dependenciesFor(e3Id) + .registry + .getCommitteeViability(e3Id); + } + + function _isCommitteeMemberActive( + uint256 e3Id, + address voter + ) internal view returns (bool) { + return + _dependenciesFor(e3Id).registry.isCommitteeMemberActive( + e3Id, + voter + ); + } + /// @dev Executes a slash: applies financial penalties, optional ban, and committee expulsion. - /// Lane B: if the operator deregistered or exited during the appeal window, penalties - /// gracefully become 0 (BondingRegistry uses min(requested, available)). Accepted tradeoff. + /// BondingRegistry keeps active and queued collateral locked while any + /// proposal from a retained slashing manager remains unresolved. /// @dev `p.executed = true` is deferred until AFTER the two `bondingRegistry.slash*` /// calls succeed but BEFORE any other external interaction. This protects the /// proposal from being permanently marked as executed when the financial leg /// reverts (e.g. an attacker griefs the operator's exit queue with enough - /// tranches to OOG `_takeAssetsFromQueue` — a Lane B operator could otherwise - /// lose all retry attempts). The `MAX_ACTIVE_TRANCHES` cap in ExitQueueLib is + /// tranches to OOG `_takeAssetsFromQueue`). The `MAX_ACTIVE_TRANCHES` cap is /// the primary defence; this ordering provides defence-in-depth. function _executeSlash(uint256 proposalId, Lane lane) internal { SlashProposal storage p = _proposals[proposalId]; + E3Dependencies memory dependencies = _dependenciesFor(p.e3Id); uint256 actualTicketSlashed = 0; // Execute financial penalties if (p.ticketAmount > 0) { - actualTicketSlashed = bondingRegistry.slashTicketBalance( + actualTicketSlashed = dependencies.bonding.slashTicketBalance( p.operator, p.ticketAmount, p.reason @@ -663,7 +770,7 @@ contract SlashingManager is } if (p.licenseAmount > 0) { - bondingRegistry.slashLicenseBond( + dependencies.bonding.slashLicenseBond( p.operator, p.licenseAmount, p.reason @@ -673,8 +780,8 @@ contract SlashingManager is // Financial penalties succeeded — commit `executed` before any further // external interaction (committee expulsion, refund escrow self-call, // interfold routing) so that reentrancy via those paths cannot re-enter - // _executeSlash for the same proposal, while still allowing Lane B - // to retry if either bondingRegistry.slash* leg above reverts. + // _executeSlash for the same proposal, while still allowing a deferred + // proposal to retry if either bondingRegistry.slash* leg above reverts. p.executed = true; // Ban node if snapshotted policy requires it @@ -686,14 +793,20 @@ contract SlashingManager is // Committee expulsion for E3-scoped slashes (uses snapshotted behavioral flags) // expelCommitteeMember returns (activeCount, thresholdM) — one call instead of three if (p.affectsCommittee) { - (uint256 activeCount, uint32 thresholdM) = ciphernodeRegistry + (uint256 activeCount, uint32 thresholdM) = dependencies + .registry .expelCommitteeMember(p.e3Id, p.operator, p.reason); // If active count drops below M, fail the E3 if (activeCount < thresholdM && p.failureReason > 0) { // NOTE: catch block must not be empty (solc optimizer bug, see below) // solhint-disable-next-line no-empty-blocks - try interfold.onE3Failed(p.e3Id, p.failureReason) { + try + dependencies.interfoldContract.onE3Failed( + p.e3Id, + p.failureReason + ) + { // Side effects occur in the external call } catch { // E3 already failed or other error — slash still proceeds @@ -702,15 +815,35 @@ contract SlashingManager is } } - // Escrow slashed ticket funds for deferred distribution. - // Self-call for try/catch atomicity — on failure, funds stay in BondingRegistry. + // Reserve and attempt escrow. Failure leaves both a durable proposal + // route and a matching BondingRegistry reservation for permissionless retry. if (actualTicketSlashed > 0) { - // NOTE: catch must not be empty — solc >=0.8.28 optimizer bug. - // solhint-disable no-empty-blocks + dependencies.bonding.reserveSlashedTicketFunds(actualTicketSlashed); + PendingSlashRoute storage route = _pendingSlashRoutes[proposalId]; + route.e3Id = p.e3Id; + route.token = address( + dependencies.bonding.ticketToken().underlying() + ); + route.amount = actualTicketSlashed; + route.pending = true; + emit SlashRoutePending( + proposalId, + p.e3Id, + route.token, + actualTicketSlashed + ); + + require( + gasleft() >= MIN_INITIAL_ROUTE_GAS, + InsufficientRoutingGas() + ); try - this.escrowSlashedFundsToRefund(p.e3Id, actualTicketSlashed) - {} catch { - // solhint-enable no-empty-blocks + this.routePendingSlashFunds{ gas: INITIAL_ROUTE_GAS }( + proposalId + ) + returns (bool routed) { + require(routed, InvalidProposal()); + } catch { emit RoutingFailed(p.e3Id, actualTicketSlashed); } } @@ -728,15 +861,46 @@ contract SlashingManager is } /// @inheritdoc ISlashingManager - /// @dev Atomically redirects slashed funds to E3RefundManager escrow. - /// External with self-only access for try/catch atomicity. - function escrowSlashedFundsToRefund(uint256 e3Id, uint256 amount) external { + function routePendingSlashFunds( + uint256 proposalId + ) external returns (bool routed) { require(msg.sender == address(this), Unauthorized()); - address refundManager = address(e3RefundManager); - require(refundManager != address(0), ZeroAddress()); - bondingRegistry.redirectSlashedTicketFunds(refundManager, amount); - interfold.escrowSlashedFunds(e3Id, amount); - emit SlashedFundsEscrowedToRefund(e3Id, amount); + PendingSlashRoute storage route = _pendingSlashRoutes[proposalId]; + require(route.pending, InvalidProposal()); + + E3Dependencies memory dependencies = _dependenciesFor(route.e3Id); + // Clear before interacting so a callback cannot route the same reserve + // twice. Any downstream failure reverts this write together with the + // transfer and accounting, leaving the route pending for another retry. + route.pending = false; + dependencies.bonding.redirectReservedSlashedTicketFunds( + address(dependencies.refundManager), + route.amount + ); + dependencies.interfoldContract.escrowSlashedFunds( + route.e3Id, + IERC20(route.token), + route.amount + ); + emit SlashedFundsEscrowedToRefund( + route.e3Id, + route.token, + route.amount + ); + emit SlashRouteCompleted( + proposalId, + route.e3Id, + route.token, + route.amount + ); + return true; + } + + function _dependenciesFor( + uint256 e3Id + ) internal view returns (E3Dependencies memory dependencies) { + dependencies = _e3Dependencies[e3Id]; + require(dependencies.initialized, InvalidProposal()); } // ====================== @@ -781,10 +945,9 @@ contract SlashingManager is p.resolved = true; p.appealUpheld = appealUpheld; - // an upheld appeal terminates the proposal — it can never `executeSlash`, - // so the open Lane B gate must drop here. Lane A proposals are not counted. - if (appealUpheld && !p.proofVerified) { - _openLaneBCount[p.operator] -= 1; + // An upheld appeal terminates the proposal, so its collateral gate ends. + if (appealUpheld) { + _openProposalCount[p.operator] -= 1; } emit AppealResolved( @@ -796,6 +959,31 @@ contract SlashingManager is ); } + /// @inheritdoc ISlashingManager + function expireAppeal(uint256 proposalId) external { + require(proposalId < totalProposals, InvalidProposal()); + SlashProposal storage p = _proposals[proposalId]; + require(p.appealed, InvalidProposal()); + require(!p.resolved, AlreadyResolved()); + require(!p.executed, AlreadyExecuted()); + require( + block.timestamp >= p.executableAt + APPEAL_RESOLUTION_GRACE, + AppealResolutionWindowActive() + ); + + p.resolved = true; + p.appealUpheld = true; + _openProposalCount[p.operator] -= 1; + + emit AppealResolved( + proposalId, + p.operator, + true, + msg.sender, + "governance resolution window expired" + ); + } + // ====================== // Ban Management // ====================== diff --git a/packages/interfold-contracts/contracts/test/ExitQueueHarness.sol b/packages/interfold-contracts/contracts/test/ExitQueueHarness.sol new file mode 100644 index 000000000..1af73b03f --- /dev/null +++ b/packages/interfold-contracts/contracts/test/ExitQueueHarness.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +pragma solidity 0.8.28; + +import { ExitQueueLib } from "../lib/ExitQueueLib.sol"; + +contract ExitQueueHarness { + using ExitQueueLib for ExitQueueLib.ExitQueueState; + + ExitQueueLib.ExitQueueState private _state; + + function queue( + address operator, + uint64 delay, + uint256 ticketAmount, + uint256 licenseAmount + ) external { + _state.queueAssetsForExit(operator, delay, ticketAmount, licenseAmount); + } + + function slash( + address operator, + uint256 ticketAmount, + uint256 licenseAmount + ) external returns (uint256 ticketsSlashed, uint256 licensesSlashed) { + return + _state.slashPendingAssets( + operator, + ticketAmount, + licenseAmount, + true + ); + } + + function claim( + address operator, + uint256 ticketAmount, + uint256 licenseAmount + ) external returns (uint256 ticketsClaimed, uint256 licensesClaimed) { + return _state.claimAssets(operator, ticketAmount, licenseAmount); + } + + function queueSlashQueue( + address operator, + uint64 delay, + uint256 firstTicketAmount, + uint256 secondTicketAmount + ) external { + _state.queueTicketsForExit(operator, delay, firstTicketAmount); + _state.slashPendingAssets(operator, firstTicketAmount, 0, true); + _state.queueTicketsForExit(operator, delay, secondTicketAmount); + } + + function queueTicketThenLicense( + address operator, + uint64 delay, + uint256 ticketAmount, + uint256 licenseAmount + ) external { + _state.queueTicketsForExit(operator, delay, ticketAmount); + _state.queueLicensesForExit(operator, delay, licenseAmount); + } + + function liveTrancheCount( + address operator + ) external view returns (uint256) { + return _state.liveTrancheCount[operator]; + } + + function tranche( + address operator, + uint256 index + ) external view returns (ExitQueueLib.ExitTranche memory) { + return _state.operatorQueues[operator][index]; + } + + function queueLength(address operator) external view returns (uint256) { + return _state.operatorQueues[operator].length; + } +} diff --git a/packages/interfold-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/interfold-contracts/contracts/test/MockCiphernodeRegistry.sol index d1bbf2017..d7e6b5e4f 100644 --- a/packages/interfold-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/interfold-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -10,6 +10,8 @@ import { IInterfold } from "../interfaces/IInterfold.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; contract MockCiphernodeRegistry is ICiphernodeRegistry { + uint256 public override numCiphernodes; + /// @notice Configurable committee members per E3 for testing mapping(uint256 e3Id => address[] nodes) private _committeeNodes; @@ -85,11 +87,13 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { return false; } - // solhint-disable-next-line no-empty-blocks - function addCiphernode(address) external pure {} + function addCiphernode(address) external { + numCiphernodes++; + } - // solhint-disable-next-line no-empty-blocks - function removeCiphernode(address) external pure {} + function removeCiphernode(address) external { + numCiphernodes--; + } function publishCommittee( uint256, @@ -253,6 +257,10 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { } contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { + function numCiphernodes() external pure returns (uint256) { + return 0; + } + function requestCommittee( uint256, uint256, diff --git a/packages/interfold-contracts/contracts/test/MockDecryptionVerifier.sol b/packages/interfold-contracts/contracts/test/MockDecryptionVerifier.sol index 0c70c941a..8b5b21314 100644 --- a/packages/interfold-contracts/contracts/test/MockDecryptionVerifier.sol +++ b/packages/interfold-contracts/contracts/test/MockDecryptionVerifier.sol @@ -14,10 +14,6 @@ contract MockDecryptionVerifier is IDecryptionVerifier { bytes4 private constant _FAIL_MAGIC = 0xdeadbeef; function verify( - uint256, - uint256, - address[] calldata, - bytes32, bytes32, bytes32, bytes32, diff --git a/packages/interfold-contracts/contracts/test/MockDkgFoldAttestationVerifier.sol b/packages/interfold-contracts/contracts/test/MockDkgFoldAttestationVerifier.sol new file mode 100644 index 000000000..ca0defbe0 --- /dev/null +++ b/packages/interfold-contracts/contracts/test/MockDkgFoldAttestationVerifier.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +pragma solidity 0.8.28; + +import { + IDkgFoldAttestationVerifier +} from "../interfaces/IDkgFoldAttestationVerifier.sol"; + +/// @notice Test-only verifier used when ciphernodes skip recursive proof aggregation. +/// @dev Production deployments must use DkgFoldAttestationVerifier. +contract MockDkgFoldAttestationVerifier is IDkgFoldAttestationVerifier { + function verify( + address, + uint256, + uint256, + bytes calldata, + bytes calldata + ) + external + pure + returns ( + uint256[] memory partyIds, + bytes32[] memory skAggCommits, + bytes32[] memory esmAggCommits + ) + { + return (new uint256[](0), new bytes32[](0), new bytes32[](0)); + } +} diff --git a/packages/interfold-contracts/contracts/token/sale/InterfoldTokenSaleDeployer.sol b/packages/interfold-contracts/contracts/token/sale/InterfoldTokenSaleDeployer.sol index 07ca3382c..43ceb0fb8 100644 --- a/packages/interfold-contracts/contracts/token/sale/InterfoldTokenSaleDeployer.sol +++ b/packages/interfold-contracts/contracts/token/sale/InterfoldTokenSaleDeployer.sol @@ -22,8 +22,8 @@ interface IFoldToken { /** * @title InterfoldTokenSaleDeployer - * @notice Operator-callable sale deployment factory. The Safe passed as - * `protocolAdmin` becomes the FOLD owner; the caller only pays gas. + * @notice Designated-operator sale deployment factory. The Safe passed as + * `protocolAdmin` becomes the FOLD owner. * @dev The CCA auction address is discovered by the script from LBPStrategy * logs after deployment. The Safe then accepts FOLD ownership and sets * FOLD's claim source once. @@ -77,11 +77,16 @@ contract InterfoldTokenSaleDeployer { error SaleAmountTooLarge(); error FoldOwnershipNotRetained(address owner); error ArithmeticOverflow(); + error UnauthorizedOperator(address caller); /// @notice The Safe that becomes FOLD owner/admin. // solhint-disable-next-line immutable-vars-naming address public immutable protocolAdmin; + /// @notice The deployment operator fixed when this factory is created. + // solhint-disable-next-line immutable-vars-naming + address public immutable deploymentOperator; + /// @notice Replay guard: each config can be deployed exactly once. mapping(bytes32 configHash => bool used) public usedConfigHashes; @@ -107,18 +112,23 @@ contract InterfoldTokenSaleDeployer { constructor(address protocolAdmin_) { if (protocolAdmin_ == address(0)) revert ZeroAddress(); protocolAdmin = protocolAdmin_; + deploymentOperator = msg.sender; } /** * @notice Deploys FOLD and starts a Uniswap LiquidityLauncher/LBPStrategy * distribution in one transaction. - * @dev Callable by the operator wallet. FOLD ownership is transferred to + * @dev Callable only by the operator wallet that deployed this factory. + * FOLD ownership is transferred to * the Safe at the end, but remains pending until the Safe accepts it. */ function deploySaleWithLiquidityLauncher( LbpSaleConfig calldata config, bytes calldata foldInitCode ) external returns (address fold) { + if (msg.sender != deploymentOperator) { + revert UnauthorizedOperator(msg.sender); + } bytes32 configHash = _checkLbpConfig(config, foldInitCode); fold = _create(foldInitCode); diff --git a/packages/interfold-contracts/contracts/verifiers/bfv/BfvDecryptionVerifier.sol b/packages/interfold-contracts/contracts/verifiers/bfv/BfvDecryptionVerifier.sol index 84b26f5cd..77c9305ba 100644 --- a/packages/interfold-contracts/contracts/verifiers/bfv/BfvDecryptionVerifier.sol +++ b/packages/interfold-contracts/contracts/verifiers/bfv/BfvDecryptionVerifier.sol @@ -23,22 +23,24 @@ import { CommitteeHashLib } from "../../lib/CommitteeHashLib.sol"; * [1] = expectedC7KeyHash (VK anchor) * [2] = committee_hash_hi * [3] = committee_hash_lo - * [4 .. 4+1+(3*(T+1))) = circuit-internal (sk, esm, ct columns) + * [4] = decryption_domain_hi + * [5] = decryption_domain_lo + * [6 .. 6+1+(3*(T+1))) = circuit-internal (sk, esm, ct columns) * [last 100] = plaintext message coefficients (100 u64 LE) - * Total: expectedPublicInputsLen = 4 + 1 + 3*(T+1) + 100. + * Total: expectedPublicInputsLen = 6 + 1 + 3*(T+1) + 100. * * The two VK-hash slots are checked against contract immutables set at * construction; this anchors the recursive aggregation trust and * prevents a malicious aggregator from substituting a forged sub-VK. * - * NOTE -- domain binding relaxation: wrapper-level chainId/deployment/e3Id - * binding requires a dedicated circuit public input. The current circuits - * do not expose such a slot. Full cryptographic enforcement tracked as - * future work. The caller-supplied `e3Id`, `committeeRoot`, `sortedNodes`, - * `ciphertextOutputHash`, and `committeePublicKey` are preserved in the - * interface for forward compatibility. + * Each secret-bearing C6 proof commits to the same domain limbs. C6Fold + * preserves them, and DecryptionAggregator exposes them here, so an + * aggregator cannot re-label an existing proof for another E3. */ contract BfvDecryptionVerifier is IDecryptionVerifier { + error InvalidCircuitVerifier(address verifier); + error InvalidVerificationKeyHash(); + /// @dev Message is always the last 100 public inputs (100 uint64 coeffs = 800 bytes plaintext). uint256 internal constant MESSAGE_COEFFS_COUNT = 100; @@ -54,10 +56,14 @@ contract BfvDecryptionVerifier is IDecryptionVerifier { /// @dev `publicInputs` index for `committee_hash_lo`. uint256 internal constant COMMITTEE_HASH_LO_IDX = 3; + /// @dev Public input indices for the E3 decryption-domain limbs. + uint256 internal constant DECRYPTION_DOMAIN_HI_IDX = 4; + uint256 internal constant DECRYPTION_DOMAIN_LO_IDX = 5; + /// @notice BFV threshold `T`; must match the compiled DecryptionAggregator circuit. uint256 public immutable threshold; - /// @dev `4 + DEC_RETURN_PREFIX_LEN + DEC_RETURN_COLUMN_COUNT*(T+1) + MESSAGE_COEFFS_COUNT`. + /// @dev `6 + DEC_RETURN_PREFIX_LEN + DEC_RETURN_COLUMN_COUNT*(T+1) + MESSAGE_COEFFS_COUNT`. uint256 internal immutable expectedPublicInputsLen; /// @notice Underlying Honk verifier for the DecryptionAggregator circuit. @@ -80,9 +86,16 @@ contract BfvDecryptionVerifier is IDecryptionVerifier { uint256 _threshold ) { require(_threshold > 0, "BfvDecryptionVerifier: threshold=0"); + if (_circuitVerifier.code.length == 0) { + revert InvalidCircuitVerifier(_circuitVerifier); + } + if ( + _expectedC6FoldKeyHash == bytes32(0) || + _expectedC7KeyHash == bytes32(0) + ) revert InvalidVerificationKeyHash(); threshold = _threshold; expectedPublicInputsLen = - 4 + + 6 + DEC_RETURN_PREFIX_LEN + (DEC_RETURN_COLUMN_COUNT * (_threshold + 1)) + MESSAGE_COEFFS_COUNT; @@ -94,11 +107,7 @@ contract BfvDecryptionVerifier is IDecryptionVerifier { /// @inheritdoc IDecryptionVerifier function verify( - uint256 e3Id, - uint256 committeeRoot, - address[] calldata sortedNodes, - bytes32 ciphertextOutputHash, - bytes32 committeePublicKey, + bytes32 decryptionDomain, bytes32 plaintextOutputHash, bytes32 committeeHash, bytes calldata proof @@ -133,20 +142,24 @@ contract BfvDecryptionVerifier is IDecryptionVerifier { ) { revert DomainBindingMismatch(); } + if ( + publicInputs[DECRYPTION_DOMAIN_HI_IDX] != + CommitteeHashLib.hi(decryptionDomain) + ) { + revert DomainBindingMismatch(); + } + if ( + publicInputs[DECRYPTION_DOMAIN_LO_IDX] != + CommitteeHashLib.lo(decryptionDomain) + ) { + revert DomainBindingMismatch(); + } // Plaintext hash check: 100-coefficient plaintext must hash to the claimed value. if (!_verifyPlaintextHash(publicInputs, plaintextOutputHash)) { revert PlaintextHashMismatch(); } - // Suppress unused-variable warnings for forward-compatibility params. - // These will be used for circuit-level domain binding in a future circuit update. - e3Id; - committeeRoot; - sortedNodes; - ciphertextOutputHash; - committeePublicKey; - // Bubble up as a revert instead of a silent `false`. if (!circuitVerifier.verify(rawProof, publicInputs)) { revert InvalidProof(); diff --git a/packages/interfold-contracts/contracts/verifiers/bfv/BfvPkVerifier.sol b/packages/interfold-contracts/contracts/verifiers/bfv/BfvPkVerifier.sol index ffd6f049c..7e5d853f1 100644 --- a/packages/interfold-contracts/contracts/verifiers/bfv/BfvPkVerifier.sol +++ b/packages/interfold-contracts/contracts/verifiers/bfv/BfvPkVerifier.sol @@ -39,6 +39,9 @@ import { CommitteeHashLib } from "../../lib/CommitteeHashLib.sol"; * `sortedNodes` are preserved in the interface for forward compatibility. */ contract BfvPkVerifier is IPkVerifier { + error InvalidCircuitVerifier(address verifier); + error InvalidVerificationKeyHash(); + /// @dev `dkg_aggregator` return field count. uint256 internal constant DKG_RETURN_TAIL_LEN = 2; @@ -78,6 +81,13 @@ contract BfvPkVerifier is IPkVerifier { uint256 _h ) { require(_h > 0, "BfvPkVerifier: h=0"); + if (_circuitVerifier.code.length == 0) { + revert InvalidCircuitVerifier(_circuitVerifier); + } + if ( + _expectedNodesFoldKeyHash == bytes32(0) || + _expectedC5KeyHash == bytes32(0) + ) revert InvalidVerificationKeyHash(); h = _h; committeeHashHiIdx = 2 + _h; committeeHashLoIdx = 3 + _h; diff --git a/packages/interfold-contracts/contracts/verifiers/bfv/honk/DecryptionAggregatorVerifier.sol b/packages/interfold-contracts/contracts/verifiers/bfv/honk/DecryptionAggregatorVerifier.sol index 110829355..02b055088 100644 --- a/packages/interfold-contracts/contracts/verifiers/bfv/honk/DecryptionAggregatorVerifier.sol +++ b/packages/interfold-contracts/contracts/verifiers/bfv/honk/DecryptionAggregatorVerifier.sol @@ -7,8 +7,8 @@ pragma solidity >=0.8.21; uint256 constant N = 2097152; uint256 constant LOG_N = 21; -uint256 constant NUMBER_OF_PUBLIC_INPUTS = 127; -uint256 constant VK_HASH = 0x179aeedaf3c48066180561e127d73c1ffbabf175e47589b309ddec6b1cd679d3; +uint256 constant NUMBER_OF_PUBLIC_INPUTS = 129; +uint256 constant VK_HASH = 0x194ab481ffef18a45daf58632a2a4f33c0a18e1aa9f86c6c36a4110060062ccc; library HonkVerificationKey { function loadVerificationKey() internal @@ -18,149 +18,149 @@ library HonkVerificationKey { Honk.VerificationKey memory vk = Honk.VerificationKey({ circuitSize: uint256(2097152), logCircuitSize: uint256(21), - publicInputsSize: uint256(127), + publicInputsSize: uint256(129), ql: Honk.G1Point({ x: uint256( - 0x21b909972bfe373b93f74e8d1b23d4f9da2d359033f9bea3bfd0dff4b14583b7 + 0x010521afb34a08599b6050718fc9b8c87a4d2c07eb2dd5bb601b0a5f1c7e184a ), y: uint256( - 0x00cca3f61b1c83f29f138790d10246931d7ac90c67f0868928a8f87f115bae3f + 0x2d2da06c4f068f98957930d8e3e6620ec8c4ac6b2efd070e453ea994503eeacb ) }), qr: Honk.G1Point({ x: uint256( - 0x282acc4814ca5bff02f14c4a58b751ecc70c1c01494610385bff82d9072bf548 + 0x1152e6db8ad8a05dbb30c8168db63ca5d9f3949ab9bf441afe3ff7e242d7157f ), y: uint256( - 0x1d4bd882f21883ba61543964458224dbd4a53595d002e18c0db812390c45416c + 0x18ef95f1bcaf881852b1c3a393cf4d1d1f306cc9cb6837a3e35a251f903e982c ) }), qo: Honk.G1Point({ x: uint256( - 0x1dd7761ff2c72c1af984fb53d1b3b42460c3323214a2d7dde42c62973d34198e + 0x110388762f39683a2464c340dfb43812e108e539c39b112196cbf77e5a48bf8b ), y: uint256( - 0x13040272a4c0bedcfdec2b313f9efe3ee9a0bbd5ca58d9432dc8d832127e99b0 + 0x1477d531425197458cee05db775238ccbf8146c6e1cc5aabdcafe6dae4b790c1 ) }), q4: Honk.G1Point({ x: uint256( - 0x174f2955dfa54bf68658a287d4658af6994ecd888993452c4c14f1c60871ce56 + 0x1882a8a60fd8e59740e19b600c5459a4b642c7e9b089f8926b88b5a66cb3e904 ), y: uint256( - 0x0cda4ce133f1492a2f13a0e29c6df44b8e697f84e481f1b2dbf2d8d741be5e04 + 0x20c2cea5d12751d28f53d5be4854f392d68c8ca45c74c3fc4f5d1aa6ee01688a ) }), qm: Honk.G1Point({ x: uint256( - 0x22ac07bc4c7d102054ba8dc18954f43d66ed6c57ede3a78e5fe44e80ab26daff + 0x23adba2e6e3307f724078a2ecffb74fd97b3f4c728d09ef210dd4206bdff8a4b ), y: uint256( - 0x01a5cce0a2e3607ae4fc406e7379aed53d7cd2cdb0d3a14e759531cee30cb9ea + 0x05b21af164d64b64d280ed384c88a8c3934ed2667164579ed8708f4877d3830f ) }), qc: Honk.G1Point({ x: uint256( - 0x11fee8c098df12a40892852407a771a7d280dbfbab5eeb06b23896095ca7a290 + 0x0162dc5fec3fbc821088520d018a189bfba2d0694703a50e2ff68e1746d4ddfa ), y: uint256( - 0x17ec33cd33eacb4335ba2e0b3baffe2b0bd0f8371c7cf7213447d3ba6dd4ba6b + 0x0d3bf9f887cc9f913494d7b19fc9d2f584956209134f92cfcc492d251fcb495f ) }), qLookup: Honk.G1Point({ x: uint256( - 0x13143d24a192079453fc93ca72b6be61609f4d042621b3d3973bfa341bb8a424 + 0x0e01e2471d2cc58c36a2af88d7c457a0c643082e0ea549b50e64780288cddb33 ), y: uint256( - 0x2dee6429bc80fc94550da46393279fc6e08def2542d28bbc312a082f31e56409 + 0x1bdf27437b6f422c586cd3ccef4cb4d8bb8b1cf1db74d99a6f85215f93af99f3 ) }), qArith: Honk.G1Point({ x: uint256( - 0x2624d4d9d7eac2515cb4b322afc263ddc87c535791e2206eef64bc24024968e1 + 0x22a366917320f410156b25de971a1e9d15251295e2cc326cb5b8c7f1801a2ce1 ), y: uint256( - 0x105a6a9c813245babf469aebeafca60e878d41b05f79125dedf362bee561b5ec + 0x18b3f08d795debf73da2daa73f1d567f7d38e94e3aa40f637a365fb0e5a76821 ) }), qDeltaRange: Honk.G1Point({ x: uint256( - 0x1cec49a84cd964f7dccf24f37f746eb4660ffa446ba4e79d04582d86fc5fb2be + 0x194e200afec2b4572c3206296df1b4c1122b2634aebf9a1ec5369d39035482ba ), y: uint256( - 0x16acc276874333a56f75e2c79d9e723e9ac1bb18d1ab5bd579a3ab1702464ed0 + 0x06a89d0def7080d25a384a7a4af8930b5c0f8796439b8eeab6a1d1bc6f44f5fe ) }), qElliptic: Honk.G1Point({ x: uint256( - 0x006554df9837516dfb90ce208134e4b81d29ebf81032b08330501733f5f20d6a + 0x1cb1882e6203b03ff642b4b149440c663a805f7cab669675dd3956891fa7996c ), y: uint256( - 0x0ff31f52484554b3123ffc5c911d928e91ee373db03b305bd1350ae27aba4169 + 0x301b524d91f9ef744a9120228b2434d6fcf625ea55aa95e2f4521adbef1c9a11 ) }), qMemory: Honk.G1Point({ x: uint256( - 0x28fad415a8ba66a6c2d15321977f696a033b56580937a63c0be78be9ccdbf00a + 0x234f73f3382bba3e07b886671eacdbb5cd23d3edecadccd5ae41e644543e0bc3 ), y: uint256( - 0x229fa12d35300e25b3095908acfed5751d51e93cd6ecf4af6757ba5a4c540c18 + 0x041c69a96e8c443ef178f6dda788f59d4577eced661436c793d46ec99b6ff316 ) }), qNnf: Honk.G1Point({ x: uint256( - 0x27769c90ab027f74a7f86fbe3a1832e41518cd4975e8ba110311664df43f0ce3 + 0x0e47e62e6db09aa6cbdefbc3136d417c81bdc3993795fb6081760bbc1bf9ebd7 ), y: uint256( - 0x24a8f977133bcb034382e4cbcd3e335373ae5aac0e67824a2647554a52536b87 + 0x287df15d22e3ed8af0a13608fa903f9cf2ae3e93d47368189f36d1df673fec2e ) }), qPoseidon2External: Honk.G1Point({ x: uint256( - 0x2eb3443efed96b06718b28d1bfbbc35a407b6af60f720ba5a9d0ac78501f0ed2 + 0x1c85b9febc61f29919ebeffd0e83fd1af35c73c7612123d55047696f768920e1 ), y: uint256( - 0x17022aa4435561f83bdddcaa9174723a1e31c11d128a3455edc0b21bf22d334b + 0x2e180d1bcd14356e561423bf126837d55f33666c3525d15c4a38ec65c50e1348 ) }), qPoseidon2Internal: Honk.G1Point({ x: uint256( - 0x2ef9e66a814fe6821f53a2a2e1e93ac8630a347d7c9fee2afd2edcdc13bc0548 + 0x07b50c401476d1319de4be38bb5ac8712e4df5f889c4f332e8e0f1f0b2afb2e5 ), y: uint256( - 0x0ebdcee17969483e898170e905ff58418ad7e99173fa87c028966bd8c040c923 + 0x0895e445eb250bcb0753bb742ab19332740313526bb5734e0397fc89726323e4 ) }), s1: Honk.G1Point({ x: uint256( - 0x235a96328f656f5e8e3935de342e7ffb06d3400d2e11e03ff3f5e9729dec07de + 0x22c639fd089056e756d0dd5f49b1f4bc6dc774ec4b1ed8d7546479af37c7a3f4 ), y: uint256( - 0x23b38403e02d9a93b48b7e21c10d7360fd7a39299f6aae2174d47e2df318775f + 0x0aa616c5cb54e9d85733de81170d6384579ba7e72a375e69ff2722ca3af4cac2 ) }), s2: Honk.G1Point({ x: uint256( - 0x2c15698c01375d97f94676f1bbdb3ba5b157bacaadec11b12cf074cf212f6e12 + 0x07e8fe3a63ece67d929f1ae2e111188d86ef3485e5755cccf7069a09e8f5392c ), y: uint256( - 0x141ec5c8ba7190c9cb1fab4f19e817d9d8f9cb2c0cedd614d4f7820a2f7c4abb + 0x1283fc4212300f568be4d9bf1df7e10eba2c0a522a460afd8610a271df3c21d4 ) }), s3: Honk.G1Point({ x: uint256( - 0x22e7871e851cfc6514318d6f16d1c34305dd0e3c0dbe39df3527feda3b0d1eb0 + 0x18a7cd98bdfa528a77c6cd37da0efe90bd61368e8d26c4fed7e8669e461529e9 ), y: uint256( - 0x078c546da57b7d1340a5a5b11922ab15592a2c3d32553532c318f0a238768a01 + 0x2313045014b131da2894b943835443bab338f9da58eea9b00f1f508b351c4ba2 ) }), s4: Honk.G1Point({ x: uint256( - 0x17e47cca2b9876b87b90039176b89b889b2e6f6ab55bf5b6ade7026c1886a55b + 0x0d90d96b1aaf3b28a8a4a478450f36dc0543f181e6aad3c5bb1f6d49de24000c ), y: uint256( - 0x1277395e4b6af40bd3099eedef9f6f1f4a3f6e95a1c0540bc521df5df391fdce + 0x020f9e28c5643771079a8c2cee87806e122bca98e2db61c540ce6e5621da1435 ) }), t1: Honk.G1Point({ @@ -197,34 +197,34 @@ library HonkVerificationKey { }), id1: Honk.G1Point({ x: uint256( - 0x162e6ffc2acbbe037aa8301684ed9e2d850a2c83a3c1a3164453b5c2187c8c75 + 0x1d2c7ad198ae54fb6a7fc6fc40f17c013d4fdfaf7443766f69c9d2327c01d30f ), y: uint256( - 0x22f0647302fbfc4d83670140b7ec0cd606fd991bd3e7cebeee96ee3b6169e538 + 0x156a0ae17a38cc19f98d28ba414ae4e6b6a5bddd07e960a9064b779e6c3d0cf3 ) }), id2: Honk.G1Point({ x: uint256( - 0x1fc1d8dce21a638cd9695d5ed2d796b7b1423fe391391cbf0076dabcd5b1229c + 0x1e00feff267b2444858036a89fc5d53c6a3fcfb6094eb652d46edf6796874d11 ), y: uint256( - 0x2e4d338298032c5426ca47e6970b8ef0b055728771a8ad6b455f4d3001abd402 + 0x3045db4643b53c1783fc74d6c8bfa440fd28d010c3069f843375ad65fc3a4811 ) }), id3: Honk.G1Point({ x: uint256( - 0x011c7ccc37d9abaf9dd6ffb88f045f8f6adb02dde453b8645b7a5461356255f8 + 0x09fc12b4fa13d47337ccab21e46377db14ff27de07660f77ff9eecccd5e1f113 ), y: uint256( - 0x0186d6fa335ec0a6179c9edeb2cfca478103eb4989218cd11ddeb6a4762ff294 + 0x02cd05f2f8779395bb30f6efe75c0cfacd0c54daa0c93d6e6fddf72e9591503e ) }), id4: Honk.G1Point({ x: uint256( - 0x1be8e47ef6bff9941f3febe177d14f28448a16fe9dd81b1c9cfd05bd9136c02c + 0x1c4df7145e8eb4fe3b49165e87646fbfad9afef3f5980c08712fad3bb9fe8ff5 ), y: uint256( - 0x1d6ea8c9b1f0fd0d27694dee140ef177141fc8e1d240e5715834070a82a9d7e0 + 0x0dc75037ff2094d4a3fa0e7f689905ec8b6ef1cf087a47d36daceca7564c9912 ) }), lagrangeFirst: Honk.G1Point({ @@ -237,10 +237,10 @@ library HonkVerificationKey { }), lagrangeLast: Honk.G1Point({ x: uint256( - 0x201feccb28b5ddf7440c37e1a8d5676a8f9d7feb0e373436b3413fa9f775fd6b + 0x162e289cadfb8e38f36041f2d12ef29d09757e625ed43ee4211cfd4d54a3efea ), y: uint256( - 0x03f87d81d9e68bc20ce687e8a53620c9947d06fdc887f89e9fc6a023c8880e74 + 0x2d6edc8136af3ddcb87da500d93e149310d086b3af87031bb6e34ff231bfa101 ) }) }); diff --git a/packages/interfold-contracts/ignition/modules/interfold.ts b/packages/interfold-contracts/ignition/modules/interfold.ts index b67421608..b82e01f9f 100644 --- a/packages/interfold-contracts/ignition/modules/interfold.ts +++ b/packages/interfold-contracts/ignition/modules/interfold.ts @@ -17,6 +17,23 @@ export default buildModule("Interfold", (m) => { computeWindow: 86400, decryptionWindow: 3600, }); + const pricingConfig = m.getParameter("pricingConfig", { + keyGenFixedPerNode: 100000, + keyGenPerEncryptionProof: 50000, + coordinationPerPair: 10000, + availabilityPerNodePerSec: 50, + decryptionPerNode: 300000, + publicationBase: 1000000, + verificationPerProof: 5000, + protocolTreasury: "0x0000000000000000000000000000000000000000", + marginBps: 1000, + protocolShareBps: 0, + dkgUtilizationBps: 2500, + computeUtilizationBps: 5000, + decryptUtilizationBps: 2500, + minCommitteeSize: 0, + minThreshold: 0, + }); // Pure pricing math is delegated to the InterfoldPricing external library // (DELEGATECALL link) so the deployed Interfold runtime stays under the @@ -34,6 +51,7 @@ export default buildModule("Interfold", (m) => { feeToken, maxDuration, timeoutConfig, + pricingConfig, ]); const interfold = m.contract("TransparentUpgradeableProxy", [ diff --git a/packages/interfold-contracts/package.json b/packages/interfold-contracts/package.json index 597119cc9..e53a4da2c 100644 --- a/packages/interfold-contracts/package.json +++ b/packages/interfold-contracts/package.json @@ -162,11 +162,12 @@ "configure:slashing-policies": "hardhat run scripts/runConfigureSlashingPolicies.ts --network localhost", "deploy:verifiers": "hardhat run scripts/deployVerifiers.ts", "deploy:faucet": "hardhat run scripts/deployFaucet.ts --network sepolia", - "upgrade:interfold": "hardhat run scripts/upgrade/interfold.ts", - "upgrade:bondingRegistry": "hardhat run scripts/upgrade/bondingRegistry.ts", + "upgrade:interfold": "pnpm validate:upgrade && hardhat run scripts/upgrade/interfold.ts", + "upgrade:bondingRegistry": "pnpm validate:upgrade && hardhat run scripts/upgrade/bondingRegistry.ts", "validate:upgrade": "hardhat run scripts/validateUpgrade.ts", - "upgrade:ciphernodeRegistryOwnable": "hardhat run scripts/upgrade/ciphernodeRegistryOwnable.ts", - "upgrade:e3RefundManager": "hardhat run scripts/upgrade/e3RefundManager.ts", + "snapshot:storage-layouts": "tsx scripts/snapshotStorageLayouts.ts", + "upgrade:ciphernodeRegistryOwnable": "pnpm validate:upgrade && hardhat run scripts/upgrade/ciphernodeRegistryOwnable.ts", + "upgrade:e3RefundManager": "pnpm validate:upgrade && hardhat run scripts/upgrade/e3RefundManager.ts", "e3:activate": "hardhat e3:activate", "e3:enable": "hardhat interfold:enableE3", "ciphernode:add": "hardhat ciphernode:add", @@ -198,6 +199,7 @@ "sale:validate": "tsx scripts/sale.ts --action validate", "sale:bid-claim": "tsx scripts/sale.ts --action bid-claim", "sale:accept-ownership": "tsx scripts/sale.ts --action accept-ownership", + "size:check": "hardhat run scripts/checkContractSize.ts", "cca:schedule": "tsx scripts/ccaSchedule.ts", "protocol": "tsx scripts/protocol.ts", "protocol:deploy": "tsx scripts/protocol.ts --action deploy", diff --git a/packages/interfold-contracts/scripts/checkContractSize.ts b/packages/interfold-contracts/scripts/checkContractSize.ts new file mode 100644 index 000000000..6e9754464 --- /dev/null +++ b/packages/interfold-contracts/scripts/checkContractSize.ts @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: LGPL-3.0-only +import hre from "hardhat"; + +const EIP170_LIMIT_BYTES = 24_576; +const REQUIRED_HEADROOM_BYTES = 256; +const MAX_RUNTIME_BYTES = EIP170_LIMIT_BYTES - REQUIRED_HEADROOM_BYTES; + +const RELEASE_CONTRACTS = [ + "Interfold", + "DkgAggregatorVerifier", + "DecryptionAggregatorVerifier", +] as const; + +let failed = false; +for (const contractName of RELEASE_CONTRACTS) { + const artifact = await hre.artifacts.readArtifact(contractName); + const runtimeBytes = (artifact.deployedBytecode.length - 2) / 2; + const headroom = EIP170_LIMIT_BYTES - runtimeBytes; + + console.log( + `${contractName}: ${runtimeBytes} runtime bytes (${headroom} bytes EIP-170 headroom)`, + ); + + if (runtimeBytes > MAX_RUNTIME_BYTES) { + failed = true; + console.error( + `${contractName} exceeds the ${MAX_RUNTIME_BYTES}-byte release budget ` + + `(EIP-170 minus ${REQUIRED_HEADROOM_BYTES} bytes).`, + ); + } +} + +if (failed) process.exitCode = 1; diff --git a/packages/interfold-contracts/scripts/deployAndSave/interfold.ts b/packages/interfold-contracts/scripts/deployAndSave/interfold.ts index 36a954158..391e045d4 100644 --- a/packages/interfold-contracts/scripts/deployAndSave/interfold.ts +++ b/packages/interfold-contracts/scripts/deployAndSave/interfold.ts @@ -106,6 +106,23 @@ export const deployAndSaveInterfold = async ({ feeToken, maxDuration, timeoutConfig, + { + keyGenFixedPerNode: 100000, + keyGenPerEncryptionProof: 50000, + coordinationPerPair: 10000, + availabilityPerNodePerSec: 50, + decryptionPerNode: 300000, + publicationBase: 1000000, + verificationPerProof: 5000, + protocolTreasury: "0x0000000000000000000000000000000000000000", + marginBps: 1000, + protocolShareBps: 0, + dkgUtilizationBps: 2500, + computeUtilizationBps: 5000, + decryptUtilizationBps: 2500, + minCommitteeSize: 0, + minThreshold: 0, + }, ]); const ProxyCF = await ethers.getContractFactory( diff --git a/packages/interfold-contracts/scripts/deployInterfold.ts b/packages/interfold-contracts/scripts/deployInterfold.ts index 9309f7e6c..5aac39576 100644 --- a/packages/interfold-contracts/scripts/deployInterfold.ts +++ b/packages/interfold-contracts/scripts/deployInterfold.ts @@ -604,6 +604,21 @@ export const deployInterfold = async ( "Successfully set DkgFoldAttestationVerifier on CiphernodeRegistry", ); } + } else if (shouldDeployMocks) { + console.log("Deploying MockDkgFoldAttestationVerifier for test/CI..."); + const mockDkgFoldAttestationVerifier = await ethers.deployContract( + "MockDkgFoldAttestationVerifier", + ); + await mockDkgFoldAttestationVerifier.waitForDeployment(); + dkgFoldAttestationVerifierAddress = + await mockDkgFoldAttestationVerifier.getAddress(); + const tx = await ciphernodeRegistry.setInitialDkgFoldAttestationVerifier( + dkgFoldAttestationVerifierAddress, + ); + await tx.wait(); + console.log( + "Successfully set MockDkgFoldAttestationVerifier on CiphernodeRegistry", + ); } const verifierLines = diff --git a/packages/interfold-contracts/scripts/protocol/deployContracts.ts b/packages/interfold-contracts/scripts/protocol/deployContracts.ts index 575caed0c..6bca3d5ca 100644 --- a/packages/interfold-contracts/scripts/protocol/deployContracts.ts +++ b/packages/interfold-contracts/scripts/protocol/deployContracts.ts @@ -4,7 +4,7 @@ import { ADDRESS_ONE } from "./constants"; import { ensurePoseidonT3 } from "./poseidon"; import { deployProxy } from "./proxies"; import type { ProtocolConfigFile, ProtocolDeployResult } from "./types"; -import { deployedAddress, timeoutConfig } from "./values"; +import { deployedAddress, pricingConfig, timeoutConfig } from "./values"; export async function deployProtocolContracts( ethers: any, @@ -73,6 +73,7 @@ export async function deployProtocolContracts( config.feeToken, BigInt(config.interfold.maxDuration), timeoutConfig(config.interfold.timeoutConfig), + pricingConfig(config.interfold.pricing), ]), ); diff --git a/packages/interfold-contracts/scripts/snapshotStorageLayouts.ts b/packages/interfold-contracts/scripts/snapshotStorageLayouts.ts new file mode 100644 index 000000000..480a19f27 --- /dev/null +++ b/packages/interfold-contracts/scripts/snapshotStorageLayouts.ts @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// Explicit maintainer-only baseline generator. The caller must identify the +// reviewed production build-info and the exact source commit it represents. +import { execFileSync } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; + +import { + PACKAGE_DIR, + SNAPSHOT_DIR, + type StorageSnapshot, + UPGRADEABLE_CONTRACTS, + loadLayoutFromBuildInfo, + sha256, +} from "./storageLayouts"; + +function argument(name: string): string { + const index = process.argv.indexOf(name); + const value = index === -1 ? undefined : process.argv[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`Missing required ${name} argument.`); + } + return value; +} + +async function main(): Promise { + const outputPath = path.resolve(argument("--build-info")); + const sourceCommit = argument("--source-commit"); + if (!/^[0-9a-f]{40}$/i.test(sourceCommit)) { + throw new Error("--source-commit must be a full 40-character Git SHA."); + } + + fs.mkdirSync(SNAPSHOT_DIR, { recursive: true }); + for (const { source, contract } of UPGRADEABLE_CONTRACTS) { + const located = loadLayoutFromBuildInfo(outputPath, source, contract); + const committedSource = execFileSync( + "git", + ["show", `${sourceCommit}:packages/interfold-contracts/${source}`], + { cwd: PACKAGE_DIR, encoding: "utf8" }, + ); + if (sha256(committedSource) !== sha256(located.sourceContent)) { + throw new Error( + `${source}:${contract} in build-info does not match ${sourceCommit}.`, + ); + } + const snapshot: StorageSnapshot = { + _format: "interfold-storage-layout-v1", + contract, + source, + baseline: { + buildInfoId: located.buildInfoId, + compiler: located.compiler, + evmVersion: located.evmVersion, + optimizerRuns: located.optimizerRuns, + sourceCommit, + sourceSha256: sha256(located.sourceContent), + }, + storage: located.layout.storage, + types: located.layout.types, + }; + const snapshotPath = path.join(SNAPSHOT_DIR, `${contract}.json`); + fs.writeFileSync(snapshotPath, `${JSON.stringify(snapshot, null, 2)}\n`); + console.log(` * wrote ${snapshotPath}`); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/packages/interfold-contracts/scripts/storageLayouts.ts b/packages/interfold-contracts/scripts/storageLayouts.ts new file mode 100644 index 000000000..162d3bf35 --- /dev/null +++ b/packages/interfold-contracts/scripts/storageLayouts.ts @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: LGPL-3.0-only +import { createHash } from "crypto"; +import * as fs from "fs"; +import * as path from "path"; +import { fileURLToPath } from "url"; + +export interface StorageVar { + astId: number; + contract: string; + label: string; + offset: number; + slot: string; + type: string; +} + +export interface StorageType { + base?: string; + encoding: string; + key?: string; + label: string; + members?: StorageVar[]; + numberOfBytes: string; + value?: string; +} + +export interface StorageLayout { + storage: StorageVar[]; + types: Record; +} + +export interface StorageSnapshot extends StorageLayout { + _format: "interfold-storage-layout-v1"; + baseline: { + buildInfoId: string; + compiler: string; + evmVersion: string; + optimizerRuns: number; + sourceCommit: string; + sourceSha256: string; + }; + contract: string; + source: string; +} + +export const UPGRADEABLE_CONTRACTS = [ + { source: "contracts/Interfold.sol", contract: "Interfold" }, + { + source: "contracts/registry/CiphernodeRegistryOwnable.sol", + contract: "CiphernodeRegistryOwnable", + }, + { + source: "contracts/registry/BondingRegistry.sol", + contract: "BondingRegistry", + }, + { + source: "contracts/E3RefundManager.sol", + contract: "E3RefundManager", + }, +] as const; + +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +export const PACKAGE_DIR = path.resolve(SCRIPT_DIR, ".."); +export const SNAPSHOT_DIR = path.join(PACKAGE_DIR, "audits/storage-layouts"); +export const BUILD_INFO_DIR = path.join(PACKAGE_DIR, "artifacts/build-info"); + +interface BuildInfoInput { + id: string; + input: { + settings: { + evmVersion?: string; + optimizer?: { runs?: number }; + }; + sources: Record; + }; + solcLongVersion?: string; + solcVersion: string; +} + +interface BuildInfoOutput { + output: { + contracts: Record< + string, + Record + >; + }; +} + +export interface LocatedLayout { + buildInfoId: string; + compiler: string; + evmVersion: string; + layout: StorageLayout; + optimizerRuns: number; + sourceContent: string; + sourceKey: string; +} + +function pairedInputPath(outputPath: string): string { + if (!outputPath.endsWith(".output.json")) { + throw new Error(`Expected a *.output.json build-info path: ${outputPath}`); + } + return outputPath.replace(/\.output\.json$/, ".json"); +} + +function sourceKeys(source: string): string[] { + return [source, `project/${source}`]; +} + +export function loadLayoutFromBuildInfo( + outputPath: string, + source: string, + contract: string, +): LocatedLayout { + const inputPath = pairedInputPath(outputPath); + if (!fs.existsSync(inputPath)) { + throw new Error(`Paired build-info input is missing: ${inputPath}`); + } + + const input = JSON.parse( + fs.readFileSync(inputPath, "utf8"), + ) as BuildInfoInput; + const output = JSON.parse( + fs.readFileSync(outputPath, "utf8"), + ) as BuildInfoOutput; + + for (const sourceKey of sourceKeys(source)) { + const layout = + output.output.contracts?.[sourceKey]?.[contract]?.storageLayout; + const sourceContent = input.input.sources?.[sourceKey]?.content; + if (layout && sourceContent !== undefined) { + return { + buildInfoId: input.id, + compiler: input.solcLongVersion ?? input.solcVersion, + evmVersion: input.input.settings.evmVersion ?? "unknown", + layout, + optimizerRuns: input.input.settings.optimizer?.runs ?? 0, + sourceContent, + sourceKey, + }; + } + } + + throw new Error(`${source}:${contract} is not present in ${outputPath}.`); +} + +export function findCurrentLayout( + source: string, + contract: string, +): LocatedLayout { + if (!fs.existsSync(BUILD_INFO_DIR)) { + throw new Error(`No build-info directory. Run Hardhat compile first.`); + } + + const currentSource = fs.readFileSync(path.join(PACKAGE_DIR, source), "utf8"); + const outputs = fs + .readdirSync(BUILD_INFO_DIR) + .filter((name) => name.endsWith(".output.json")) + .map((name) => path.join(BUILD_INFO_DIR, name)) + .sort( + (left, right) => fs.statSync(right).mtimeMs - fs.statSync(left).mtimeMs, + ); + + for (const outputPath of outputs) { + try { + const located = loadLayoutFromBuildInfo(outputPath, source, contract); + if (located.sourceContent === currentSource) return located; + } catch { + // Incremental Hardhat build-info files contain only their compilation job. + } + } + + throw new Error( + `No build-info storage layout matches the current ${source}:${contract}. ` + + `Run \`pnpm compile:contracts --force\` and retry.`, + ); +} + +export function sha256(content: string): string { + return createHash("sha256").update(content).digest("hex"); +} + +function compareType( + contract: string, + pathLabel: string, + previous: StorageLayout, + previousTypeId: string, + current: StorageLayout, + currentTypeId: string, + errors: string[], + visited: Set, +): void { + const visitKey = `${previousTypeId}:${currentTypeId}`; + if (visited.has(visitKey)) return; + visited.add(visitKey); + + const before = previous.types[previousTypeId]; + const after = current.types[currentTypeId]; + if (!before || !after) { + errors.push( + `${contract}: ${pathLabel} has unresolved storage type metadata.`, + ); + return; + } + if (before.encoding !== after.encoding || before.label !== after.label) { + errors.push( + `${contract}: ${pathLabel} type changed from ${before.label}/${before.encoding} ` + + `to ${after.label}/${after.encoding}.`, + ); + return; + } + + for (const key of ["base", "key", "value"] as const) { + const beforeChild = before[key]; + const afterChild = after[key]; + if ((beforeChild === undefined) !== (afterChild === undefined)) { + errors.push(`${contract}: ${pathLabel} ${key} type changed.`); + } else if (beforeChild && afterChild) { + compareType( + contract, + `${pathLabel}.${key}`, + previous, + beforeChild, + current, + afterChild, + errors, + visited, + ); + } + } + + if (before.members) { + if (!after.members) { + errors.push( + `${contract}: ${pathLabel} no longer exposes struct members.`, + ); + return; + } + for (const oldMember of before.members) { + const newMember = after.members.find( + (candidate) => candidate.label === oldMember.label, + ); + if (!newMember) { + errors.push( + `${contract}: ${pathLabel}.${oldMember.label} was removed or renamed.`, + ); + continue; + } + if ( + oldMember.slot !== newMember.slot || + oldMember.offset !== newMember.offset + ) { + errors.push( + `${contract}: ${pathLabel}.${oldMember.label} moved from ` + + `${oldMember.slot}+${oldMember.offset} to ` + + `${newMember.slot}+${newMember.offset}.`, + ); + } + compareType( + contract, + `${pathLabel}.${oldMember.label}`, + previous, + oldMember.type, + current, + newMember.type, + errors, + visited, + ); + } + } else if (before.numberOfBytes !== after.numberOfBytes) { + errors.push( + `${contract}: ${pathLabel} size changed from ${before.numberOfBytes} ` + + `to ${after.numberOfBytes} bytes.`, + ); + } +} + +function gapEnd(layout: StorageLayout, gap: StorageVar): bigint { + const bytes = BigInt(layout.types[gap.type].numberOfBytes); + return BigInt(gap.slot) + bytes / 32n; +} + +export function diffLayouts( + contract: string, + previous: StorageLayout, + current: StorageLayout, +): string[] { + const errors: string[] = []; + const previousGap = previous.storage.find((entry) => entry.label === "__gap"); + const currentGap = current.storage.find((entry) => entry.label === "__gap"); + + for (const oldEntry of previous.storage) { + if (oldEntry.label === "__gap") continue; + const newEntry = current.storage.find( + (candidate) => candidate.label === oldEntry.label, + ); + if (!newEntry) { + errors.push( + `${contract}: state variable \`${oldEntry.label}\` was removed or renamed.`, + ); + continue; + } + if ( + oldEntry.slot !== newEntry.slot || + oldEntry.offset !== newEntry.offset + ) { + errors.push( + `${contract}: \`${oldEntry.label}\` moved from ${oldEntry.slot}+${oldEntry.offset} ` + + `to ${newEntry.slot}+${newEntry.offset}.`, + ); + } + compareType( + contract, + oldEntry.label, + previous, + oldEntry.type, + current, + newEntry.type, + errors, + new Set(), + ); + } + + if (previousGap) { + if (!currentGap) { + errors.push(`${contract}: reserved __gap was removed.`); + return errors; + } + if (gapEnd(previous, previousGap) !== gapEnd(current, currentGap)) { + errors.push( + `${contract}: reserved __gap must shrink from the front without changing its end slot.`, + ); + } + const oldGapStart = BigInt(previousGap.slot); + const newGapStart = BigInt(currentGap.slot); + if (newGapStart < oldGapStart) { + errors.push(`${contract}: reserved __gap moved backward.`); + } + const previousLabels = new Set( + previous.storage.map((entry) => entry.label), + ); + for (const entry of current.storage) { + if (previousLabels.has(entry.label) || entry.label === "__gap") continue; + const slot = BigInt(entry.slot); + if (slot < oldGapStart || slot >= newGapStart) { + errors.push( + `${contract}: new variable \`${entry.label}\` at slot ${entry.slot} ` + + `does not consume the front of the reserved gap.`, + ); + } + } + } + + return errors; +} diff --git a/packages/interfold-contracts/scripts/utils.ts b/packages/interfold-contracts/scripts/utils.ts index add04d1f0..8506c4070 100644 --- a/packages/interfold-contracts/scripts/utils.ts +++ b/packages/interfold-contracts/scripts/utils.ts @@ -110,7 +110,7 @@ export function bfvDkgCommitteeHashIndices(h: number): { /** `decryption_aggregator` EVM public-input count for BFV threshold `t`. */ export function bfvDecExpectedPublicInputsLen(threshold: number): number { - return 108 + 3 * threshold; + return 110 + 3 * threshold; } /** `publicInputs` indices for decryption-aggregator committee hash limbs. */ @@ -118,6 +118,11 @@ export function bfvDecCommitteeHashIndices(): { hi: number; lo: number } { return { hi: 2, lo: 3 }; } +/** `publicInputs` indices for decryption-aggregator E3 domain limbs. */ +export function bfvDecDomainIndices(): { hi: number; lo: number } { + return { hi: 4, lo: 5 }; +} + /** Recursive VK hashes for `BfvPkVerifier` sub-circuits (from `pnpm compile:circuits`). */ export function getBfvPkSubCircuitVkHashPaths() { const root = getRepoRoot(); diff --git a/packages/interfold-contracts/scripts/validateUpgrade.ts b/packages/interfold-contracts/scripts/validateUpgrade.ts index 6fd419368..b6f760459 100644 --- a/packages/interfold-contracts/scripts/validateUpgrade.ts +++ b/packages/interfold-contracts/scripts/validateUpgrade.ts @@ -1,197 +1,78 @@ // SPDX-License-Identifier: LGPL-3.0-only -// -// H-22: storage-layout snapshot + diff for upgradeable contracts. -// -// This script does NOT depend on `@openzeppelin/hardhat-upgrades` (the OZ -// upgrades plugin is currently not compatible with Hardhat 3 — once it ships -// Hardhat 3 support, prefer it). Instead it reads the `storageLayout` solc -// output produced by `hardhat.config.ts` (see the `outputSelection` block) -// from the latest build-info file, then for each upgradeable contract: -// -// * If `audits/storage-layouts/.json` exists, diff the live -// layout against the committed snapshot and FAIL on any of: -// - a slot whose `type` or `label` changed, -// - a slot whose `offset` or `slot` index moved, -// - a state variable that was removed. -// Appending new variables at the END is allowed (this is what `__gap` -// reservations are for). -// * If the snapshot is missing, write it (first run / new contract). -// -// Run with: `pnpm hardhat compile && pnpm exec ts-node scripts/validateUpgrade.ts` -// -// Marked as a best-effort guard; CI should call this on every PR that -// touches an upgradeable contract. +// Read-only storage-layout compatibility gate for upgradeable contracts. +// Missing baselines are fatal; maintainers create them explicitly with +// `pnpm snapshot:storage-layouts` from reviewed production build-info. import * as fs from "fs"; import * as path from "path"; -import { fileURLToPath } from "url"; -interface StorageVar { - astId: number; - contract: string; - label: string; - offset: number; - slot: string; - type: string; -} - -interface StorageLayout { - storage: StorageVar[]; - types: Record | null; -} - -const UPGRADEABLE_CONTRACTS: { source: string; contract: string }[] = [ - { source: "contracts/Interfold.sol", contract: "Interfold" }, - { - source: "contracts/registry/CiphernodeRegistryOwnable.sol", - contract: "CiphernodeRegistryOwnable", - }, - { - source: "contracts/registry/BondingRegistry.sol", - contract: "BondingRegistry", - }, - { - source: "contracts/E3RefundManager.sol", - contract: "E3RefundManager", - }, -]; - -const SNAPSHOT_DIR = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "../audits/storage-layouts", -); -const BUILD_INFO_DIR = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "../artifacts/build-info", -); - -function latestBuildInfo(): string { - if (!fs.existsSync(BUILD_INFO_DIR)) { - throw new Error( - `No build-info dir at ${BUILD_INFO_DIR}. Run \`pnpm hardhat compile\` first.`, - ); - } - const outputs = fs - .readdirSync(BUILD_INFO_DIR) - .filter((f) => f.endsWith(".output.json")) - .map((f) => ({ - f, - mtime: fs.statSync(path.join(BUILD_INFO_DIR, f)).mtimeMs, - })) - .sort((a, b) => b.mtime - a.mtime); - if (outputs.length === 0) { - throw new Error(`No *.output.json under ${BUILD_INFO_DIR}.`); - } - return path.join(BUILD_INFO_DIR, outputs[0].f); -} +import { + SNAPSHOT_DIR, + type StorageSnapshot, + UPGRADEABLE_CONTRACTS, + diffLayouts, + findCurrentLayout, +} from "./storageLayouts"; -function loadLayout( - buildInfoPath: string, - source: string, - contract: string, -): StorageLayout { - const buildInfo = JSON.parse(fs.readFileSync(buildInfoPath, "utf8")) as { - output: { - contracts: Record< - string, - Record - >; - }; - }; - const node = buildInfo.output.contracts?.[source]?.[contract]; - if (!node) { - throw new Error(`Contract ${source}:${contract} not in build-info.`); - } - if (!node.storageLayout) { - throw new Error( - `No storageLayout for ${contract}. Ensure hardhat.config.ts outputSelection includes "storageLayout".`, - ); - } - return node.storageLayout; -} +async function main(): Promise { + let totalErrors = 0; -function diffLayouts( - contract: string, - prev: StorageLayout, - curr: StorageLayout, -): string[] { - const errors: string[] = []; - const prevByLabel = new Map(prev.storage.map((s) => [s.label, s])); - for (const p of prev.storage) { - const c = curr.storage.find((x) => x.label === p.label); - if (!c) { - // Removal is only safe if the variable was the LAST one (could be - // converted into a __gap entry). We still flag it for review. - errors.push( - `${contract}: state variable \`${p.label}\` (slot ${p.slot}) was removed.`, + for (const { source, contract } of UPGRADEABLE_CONTRACTS) { + const snapshotPath = path.join(SNAPSHOT_DIR, `${contract}.json`); + if (!fs.existsSync(snapshotPath)) { + console.error( + ` ✗ ${contract}: required baseline is missing at ${snapshotPath}.`, ); + totalErrors += 1; continue; } - if (c.slot !== p.slot || c.offset !== p.offset) { - errors.push( - `${contract}: \`${p.label}\` moved from slot ${p.slot}+${p.offset} to ${c.slot}+${c.offset}.`, - ); + + const snapshot = JSON.parse( + fs.readFileSync(snapshotPath, "utf8"), + ) as StorageSnapshot; + if ( + snapshot._format !== "interfold-storage-layout-v1" || + snapshot.contract !== contract || + snapshot.source !== source + ) { + console.error(` ✗ ${contract}: baseline metadata is invalid.`); + totalErrors += 1; + continue; } - if (c.type !== p.type) { + + const candidate = findCurrentLayout(source, contract); + const errors = diffLayouts(contract, snapshot, candidate.layout); + if ( + candidate.compiler !== snapshot.baseline.compiler || + candidate.evmVersion !== snapshot.baseline.evmVersion || + candidate.optimizerRuns !== snapshot.baseline.optimizerRuns + ) { errors.push( - `${contract}: \`${p.label}\` type changed from ${p.type} to ${c.type}.`, + `${contract}: candidate compiler settings differ from the production baseline.`, ); } - } - // Appended new variables are OK (consume __gap or appended). - for (const c of curr.storage) { - if (!prevByLabel.has(c.label)) { - // informational only + if (errors.length === 0) { console.log( - ` + ${contract}: new state variable \`${c.label}\` at slot ${c.slot}+${c.offset} (${c.type}).`, + ` ✓ ${contract}: compatible with ${snapshot.baseline.sourceCommit} ` + + `(${snapshot.baseline.buildInfoId}).`, ); - } - } - return errors; -} - -async function main(): Promise { - if (!fs.existsSync(SNAPSHOT_DIR)) { - fs.mkdirSync(SNAPSHOT_DIR, { recursive: true }); - } - const buildInfoPath = latestBuildInfo(); - console.log(`Using build-info: ${path.basename(buildInfoPath)}`); - - let totalErrors = 0; - for (const { source, contract } of UPGRADEABLE_CONTRACTS) { - const snapshotPath = path.join(SNAPSHOT_DIR, `${contract}.json`); - const layout = loadLayout(buildInfoPath, source, contract); - if (!fs.existsSync(snapshotPath)) { - fs.writeFileSync(snapshotPath, JSON.stringify(layout, null, 2) + "\n"); - console.log(` * ${contract}: snapshot CREATED at ${snapshotPath}.`); - continue; - } - const prev = JSON.parse( - fs.readFileSync(snapshotPath, "utf8"), - ) as StorageLayout; - const errs = diffLayouts(contract, prev, layout); - if (errs.length === 0) { - console.log(` ✓ ${contract}: storage layout compatible.`); } else { - totalErrors += errs.length; - for (const e of errs) console.error(` ✗ ${e}`); + totalErrors += errors.length; + for (const error of errors) console.error(` ✗ ${error}`); } } if (totalErrors > 0) { - console.error( - `\nvalidateUpgrade FAILED with ${totalErrors} storage incompatibilit${ - totalErrors === 1 ? "y" : "ies" - }.`, - ); - console.error( - `If the change is intentional, update the snapshot under audits/storage-layouts/ and re-run.`, + throw new Error( + `validateUpgrade failed with ${totalErrors} storage-layout error${ + totalErrors === 1 ? "" : "s" + }. Baselines are never created or modified by this command.`, ); - process.exit(1); } - console.log("\nvalidateUpgrade OK."); + + console.log("validateUpgrade OK (read-only)."); } -main().catch((err) => { - console.error(err); - process.exit(1); +main().catch((error) => { + console.error(error); + process.exitCode = 1; }); diff --git a/packages/interfold-contracts/tasks/interfold.ts b/packages/interfold-contracts/tasks/interfold.ts index c203381db..a2f540c08 100644 --- a/packages/interfold-contracts/tasks/interfold.ts +++ b/packages/interfold-contracts/tasks/interfold.ts @@ -131,12 +131,6 @@ export const requestCommittee = task( defaultValue: ZeroAddress, type: ArgumentType.STRING, }) - .addOption({ - name: "proofAggregationEnabled", - description: "whether to enable proof aggregation (default: false)", - defaultValue: false, - type: ArgumentType.BOOLEAN, - }) .setAction(async () => ({ default: async ( { @@ -147,7 +141,6 @@ export const requestCommittee = task( e3Params: _e3Params, computeParams, customParams, - proofAggregationEnabled, }, hre, ) => { @@ -238,7 +231,6 @@ export const requestCommittee = task( paramSet, computeProviderParams, customParams, - proofAggregationEnabled, }; console.log("Request parameters:", requestParams); @@ -326,20 +318,29 @@ export const publishCommittee = task( }) .addOption({ name: "pkCommitment", - description: - "Hash-based aggregated PK commitment (bytes32 hex); required even when proof aggregation is disabled", + description: "Hash-based aggregated PK commitment (bytes32 hex); required", defaultValue: "", type: ArgumentType.STRING, }) .addOption({ name: "proof", description: - "ABI-encoded DkgAggregator (EVM) proof (bytes rawProof, bytes32[] publicInputs); pass 0x when proof aggregation is disabled", + "Required ABI-encoded DkgAggregator (EVM) proof (bytes rawProof, bytes32[] publicInputs)", + defaultValue: "0x", + type: ArgumentType.STRING, + }) + .addOption({ + name: "dkgAttestationBundle", + description: + "Required ABI-encoded DKG fold attestation bundle (Attestation[], PartySlotBinding[])", defaultValue: "0x", type: ArgumentType.STRING, }) .setAction(async () => ({ - default: async ({ e3Id, nodes, publicKey, pkCommitment, proof }, hre) => { + default: async ( + { e3Id, nodes, publicKey, pkCommitment, proof, dkgAttestationBundle }, + hre, + ) => { const { deployAndSaveCiphernodeRegistryOwnable } = await import( "../scripts/deployAndSave/ciphernodeRegistryOwnable" ); @@ -383,13 +384,21 @@ export const publishCommittee = task( "publicKey is required and must be a non-empty hex string", ); } + if (!isHexString(proof) || proof === "0x") { + throw new Error("proof is required and must be a non-empty hex string"); + } + if (!isHexString(dkgAttestationBundle) || dkgAttestationBundle === "0x") { + throw new Error( + "dkgAttestationBundle is required and must be a non-empty hex string", + ); + } const tx = await ciphernodeRegistry.publishCommittee( e3Id, publicKey, pkCommitment, proof, - "0x", + dkgAttestationBundle, ); console.log("Publishing committee... ", tx.hash); diff --git a/packages/interfold-contracts/test/BfvDecryptionVerifier.spec.ts b/packages/interfold-contracts/test/BfvDecryptionVerifier.spec.ts index 7a71a27b3..6ce53e998 100644 --- a/packages/interfold-contracts/test/BfvDecryptionVerifier.spec.ts +++ b/packages/interfold-contracts/test/BfvDecryptionVerifier.spec.ts @@ -25,6 +25,7 @@ const MESSAGE_COEFFS_COUNT = 100; const EXPECTED_C6_FOLD_KEY_HASH = ethers.id("c6_fold"); const EXPECTED_C7_KEY_HASH = ethers.id("c7"); +const DECRYPTION_DOMAIN = ethers.id("e3-decryption-domain"); /** Must match `BfvDecryptionVerifier.threshold` / default circuit `T`. */ const THRESHOLD = BFV_THRESHOLD_T; @@ -35,6 +36,8 @@ const EXPECTED_PUBLIC_INPUTS_LEN = bfvDecExpectedPublicInputsLen(THRESHOLD); /** Indices for committee hash limbs (fixed layout). */ const COMMITTEE_HASH_HI_IDX = 2; const COMMITTEE_HASH_LO_IDX = 3; +const DECRYPTION_DOMAIN_HI_IDX = 4; +const DECRYPTION_DOMAIN_LO_IDX = 5; function committeeHashHi(committeeHash: string): string { const v = BigInt(committeeHash); @@ -55,6 +58,7 @@ function buildPublicInputsWithMessage( EXPECTED_C7_KEY_HASH, ], committeeHash = ethers.ZeroHash, + decryptionDomain = DECRYPTION_DOMAIN, ): string[] { const arr: string[] = new Array(totalInputs); arr[0] = subCircuitHashes[0]; @@ -64,6 +68,8 @@ function buildPublicInputsWithMessage( } arr[COMMITTEE_HASH_HI_IDX] = committeeHashHi(committeeHash); arr[COMMITTEE_HASH_LO_IDX] = committeeHashLo(committeeHash); + arr[DECRYPTION_DOMAIN_HI_IDX] = committeeHashHi(decryptionDomain); + arr[DECRYPTION_DOMAIN_LO_IDX] = committeeHashLo(decryptionDomain); const offset = totalInputs - MESSAGE_COEFFS_COUNT; for (let i = 0; i < messageCoeffs.length && i < MESSAGE_COEFFS_COUNT; i++) { arr[offset + i] = "0x" + messageCoeffs[i].toString(16).padStart(64, "0"); @@ -122,31 +128,65 @@ describe("BfvDecryptionVerifier", function () { return { bfvDecryptionVerifier: dv, mockCircuit: mc }; }; - /** Contextual params forwarded to verify; not checked against circuit outputs (future domain binding). */ + /** Domain supplied by Interfold after hashing the full E3 context. */ const ctx = () => { - const e3Id = 7n; - const root = BigInt(ethers.id("test-root")); - const nodes = [testSigner.address]; - const ciphertextHash = ethers.id("ct-hash"); - const committeePk = ethers.id("committee-pk"); - return { e3Id, root, nodes, ciphertextHash, committeePk }; + return { decryptionDomain: DECRYPTION_DOMAIN }; }; describe("reverts", function () { + it("rejects zero, EOA, and zero-hash trust anchors at deployment", async function () { + const factory = await ethers.getContractFactory("BfvDecryptionVerifier"); + + await expect( + factory.deploy( + ethers.ZeroAddress, + EXPECTED_C6_FOLD_KEY_HASH, + EXPECTED_C7_KEY_HASH, + THRESHOLD, + ), + ) + .to.be.revertedWithCustomError(factory, "InvalidCircuitVerifier") + .withArgs(ethers.ZeroAddress); + await expect( + factory.deploy( + testSigner.address, + EXPECTED_C6_FOLD_KEY_HASH, + EXPECTED_C7_KEY_HASH, + THRESHOLD, + ), + ) + .to.be.revertedWithCustomError(factory, "InvalidCircuitVerifier") + .withArgs(testSigner.address); + + const { mockCircuit } = await loadFixture(deployWithMockCircuit); + await expect( + factory.deploy( + await mockCircuit.getAddress(), + ethers.ZeroHash, + EXPECTED_C7_KEY_HASH, + THRESHOLD, + ), + ).to.be.revertedWithCustomError(factory, "InvalidVerificationKeyHash"); + await expect( + factory.deploy( + await mockCircuit.getAddress(), + EXPECTED_C6_FOLD_KEY_HASH, + ethers.ZeroHash, + THRESHOLD, + ), + ).to.be.revertedWithCustomError(factory, "InvalidVerificationKeyHash"); + }); + it("reverts on invalid proof encoding", async function () { const { bfvDecryptionVerifier } = await loadFixture( deployWithMockCircuit, ); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const plaintextHash = ethers.keccak256("0x1234"); await expect( bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, ethers.ZeroHash, "0xdeadbeef", @@ -159,7 +199,7 @@ describe("BfvDecryptionVerifier", function () { deployWithMockCircuit, ); await mockCircuit.setReturnValue(true); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const messageCoeffs = [1n, 2n, 3n]; const publicInputs = buildPublicInputsWithMessage(messageCoeffs).slice( @@ -171,11 +211,7 @@ describe("BfvDecryptionVerifier", function () { await expect( bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, ethers.ZeroHash, proof, @@ -191,7 +227,7 @@ describe("BfvDecryptionVerifier", function () { deployWithMockCircuit, ); await mockCircuit.setReturnValue(true); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const messageCoeffs = [1n, 2n, 3n]; const publicInputs = buildPublicInputsWithMessage( @@ -203,11 +239,7 @@ describe("BfvDecryptionVerifier", function () { await expect( bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, ethers.ZeroHash, proof, @@ -223,7 +255,7 @@ describe("BfvDecryptionVerifier", function () { deployWithMockCircuit, ); await mockCircuit.setReturnValue(true); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const messageCoeffs = [1n, 2n, 3n]; const publicInputs = buildPublicInputsWithMessage( @@ -236,11 +268,7 @@ describe("BfvDecryptionVerifier", function () { await expect( bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, ethers.ZeroHash, proof, @@ -253,7 +281,7 @@ describe("BfvDecryptionVerifier", function () { deployWithMockCircuit, ); await mockCircuit.setReturnValue(true); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const messageCoeffs = [1n, 2n, 3n]; const publicInputs = buildPublicInputsWithMessage( @@ -266,11 +294,7 @@ describe("BfvDecryptionVerifier", function () { await expect( bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, ethers.ZeroHash, proof, @@ -283,7 +307,7 @@ describe("BfvDecryptionVerifier", function () { deployWithMockCircuit, ); await mockCircuit.setReturnValue(true); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const committeeHash = ethers.id("real-committee"); const wrongCommitteeHash = ethers.id("wrong-committee"); @@ -301,11 +325,7 @@ describe("BfvDecryptionVerifier", function () { // pass wrong committeeHash to verify — hi/lo check should fail await expect( bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, wrongCommitteeHash, proof, @@ -316,12 +336,36 @@ describe("BfvDecryptionVerifier", function () { ); }); + it("rejects replaying a proof under a different E3 decryption domain (C-03)", async function () { + const { bfvDecryptionVerifier, mockCircuit } = await loadFixture( + deployWithMockCircuit, + ); + await mockCircuit.setReturnValue(true); + + const messageCoeffs = [1n, 2n, 3n]; + const publicInputs = buildPublicInputsWithMessage(messageCoeffs); + const plaintextHash = plaintextToHash(messageCoeffs); + const proof = encodeProof("0x01", publicInputs); + + await expect( + bfvDecryptionVerifier.verify.staticCall( + ethers.id("different-e3-domain"), + plaintextHash, + ethers.ZeroHash, + proof, + ), + ).to.be.revertedWithCustomError( + bfvDecryptionVerifier, + "DomainBindingMismatch", + ); + }); + it("reverts PlaintextHashMismatch when message coeffs don't hash to plaintextHash", async function () { const { bfvDecryptionVerifier, mockCircuit } = await loadFixture( deployWithMockCircuit, ); await mockCircuit.setReturnValue(true); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const messageCoeffs = [1n, 2n, 3n]; const wrongHash = ethers.keccak256("0x0000"); @@ -330,11 +374,7 @@ describe("BfvDecryptionVerifier", function () { await expect( bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, wrongHash, ethers.ZeroHash, proof, @@ -350,7 +390,7 @@ describe("BfvDecryptionVerifier", function () { deployWithMockCircuit, ); await mockCircuit.setReturnValue(false); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const messageCoeffs = [1n, 2n, 3n]; const publicInputs = buildPublicInputsWithMessage(messageCoeffs); @@ -359,11 +399,7 @@ describe("BfvDecryptionVerifier", function () { await expect( bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, ethers.ZeroHash, proof, @@ -375,7 +411,7 @@ describe("BfvDecryptionVerifier", function () { const { mockCircuit } = await loadFixture(deployWithMockCircuit); await mockCircuit.setReturnValue(true); const mockAddr = await mockCircuit.getAddress(); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const bfvDecryptionVerifier = await ( await ethers.getContractFactory("BfvDecryptionVerifier") @@ -394,11 +430,7 @@ describe("BfvDecryptionVerifier", function () { await expect( bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, ethers.ZeroHash, proof, @@ -413,7 +445,7 @@ describe("BfvDecryptionVerifier", function () { deployWithMockCircuit, ); await mockCircuit.setReturnValue(true); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const messageCoeffs = [1n, 2n, 3n, 42n, 100n]; const publicInputs = buildPublicInputsWithMessage(messageCoeffs); @@ -421,11 +453,7 @@ describe("BfvDecryptionVerifier", function () { const proof = encodeProof("0x0102", publicInputs); const result = await bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, ethers.ZeroHash, proof, @@ -438,7 +466,7 @@ describe("BfvDecryptionVerifier", function () { deployWithMockCircuit, ); await mockCircuit.setReturnValue(true); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const messageCoeffs = [1n, 2n, 3n]; const publicInputs = buildPublicInputsWithMessage( @@ -449,11 +477,7 @@ describe("BfvDecryptionVerifier", function () { const proof = encodeProof("0x01", publicInputs); const result = await bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, ethers.ZeroHash, proof, @@ -466,7 +490,7 @@ describe("BfvDecryptionVerifier", function () { deployWithMockCircuit, ); await mockCircuit.setReturnValue(true); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const committeeHash = ethers.id("the-committee"); const messageCoeffs = [10n, 20n, 30n]; @@ -480,11 +504,7 @@ describe("BfvDecryptionVerifier", function () { const proof = encodeProof("0x01", publicInputs); const result = await bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, committeeHash, proof, @@ -497,7 +517,7 @@ describe("BfvDecryptionVerifier", function () { deployWithMockCircuit, ); await mockCircuit.setReturnValue(true); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const messageCoeffs: bigint[] = []; const publicInputs = buildPublicInputsWithMessage(messageCoeffs); @@ -505,11 +525,7 @@ describe("BfvDecryptionVerifier", function () { const proof = encodeProof("0x01", publicInputs); const result = await bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, ethers.ZeroHash, proof, @@ -522,7 +538,7 @@ describe("BfvDecryptionVerifier", function () { deployWithMockCircuit, ); await mockCircuit.setReturnValue(true); - const { e3Id, root, nodes, ciphertextHash, committeePk } = ctx(); + const { decryptionDomain } = ctx(); const messageCoeffs = Array.from( { length: MESSAGE_COEFFS_COUNT }, @@ -533,11 +549,7 @@ describe("BfvDecryptionVerifier", function () { const proof = encodeProof("0x01", publicInputs); const result = await bfvDecryptionVerifier.verify.staticCall( - e3Id, - root, - nodes, - ciphertextHash, - committeePk, + decryptionDomain, plaintextHash, ethers.ZeroHash, proof, diff --git a/packages/interfold-contracts/test/BfvPkVerifier.spec.ts b/packages/interfold-contracts/test/BfvPkVerifier.spec.ts index d6066f807..2aa0e795b 100644 --- a/packages/interfold-contracts/test/BfvPkVerifier.spec.ts +++ b/packages/interfold-contracts/test/BfvPkVerifier.spec.ts @@ -84,6 +84,49 @@ describe("BfvPkVerifier", function () { }); describe("reverts", function () { + it("rejects zero, EOA, and zero-hash trust anchors at deployment", async function () { + const factory = await ethers.getContractFactory("BfvPkVerifier"); + + await expect( + factory.deploy( + ethers.ZeroAddress, + EXPECTED_NODES_FOLD_KEY_HASH, + EXPECTED_C5_KEY_HASH, + H, + ), + ) + .to.be.revertedWithCustomError(factory, "InvalidCircuitVerifier") + .withArgs(ethers.ZeroAddress); + await expect( + factory.deploy( + testSigner.address, + EXPECTED_NODES_FOLD_KEY_HASH, + EXPECTED_C5_KEY_HASH, + H, + ), + ) + .to.be.revertedWithCustomError(factory, "InvalidCircuitVerifier") + .withArgs(testSigner.address); + + const { mockCircuit } = await loadFixture(deployWithMockCircuit); + await expect( + factory.deploy( + await mockCircuit.getAddress(), + ethers.ZeroHash, + EXPECTED_C5_KEY_HASH, + H, + ), + ).to.be.revertedWithCustomError(factory, "InvalidVerificationKeyHash"); + await expect( + factory.deploy( + await mockCircuit.getAddress(), + EXPECTED_NODES_FOLD_KEY_HASH, + ethers.ZeroHash, + H, + ), + ).to.be.revertedWithCustomError(factory, "InvalidVerificationKeyHash"); + }); + it("reverts on invalid proof encoding", async function () { const { bfvPkVerifier } = await loadFixture(deployWithMockCircuit); const { e3Id, root, nodes } = ctx(); diff --git a/packages/interfold-contracts/test/BfvVkBindingIntegration.spec.ts b/packages/interfold-contracts/test/BfvVkBindingIntegration.spec.ts index 7d43ae1f4..98c49c855 100644 --- a/packages/interfold-contracts/test/BfvVkBindingIntegration.spec.ts +++ b/packages/interfold-contracts/test/BfvVkBindingIntegration.spec.ts @@ -15,6 +15,7 @@ import { assertBfvDecryptionVerifierSubCircuitVkHashes, assertBfvPkVerifierSubCircuitVkHashes, bfvDecCommitteeHashIndices, + bfvDecDomainIndices, bfvDecExpectedPublicInputsLen, bfvDkgCommitteeHashIndices, bfvPkExpectedPublicInputsLen, @@ -115,6 +116,7 @@ function hexToBytes32Array(hex: string): string[] { const DKG_COMMITTEE_HASH_IDX = bfvDkgCommitteeHashIndices(BFV_DKG_H); const DKG_EXPECTED_PUBLIC_INPUT_LEN = bfvPkExpectedPublicInputsLen(BFV_DKG_H); const DEC_COMMITTEE_HASH_IDX = bfvDecCommitteeHashIndices(); +const DEC_DOMAIN_IDX = bfvDecDomainIndices(); const DEC_EXPECTED_PUBLIC_INPUT_LEN = bfvDecExpectedPublicInputsLen(BFV_THRESHOLD_T); @@ -317,6 +319,10 @@ describe("BfvVkBindingIntegration", function () { decPublicInputs[DEC_COMMITTEE_HASH_IDX.hi], decPublicInputs[DEC_COMMITTEE_HASH_IDX.lo], ); + const decDomain = committeeHashFromLimbs( + decPublicInputs[DEC_DOMAIN_IDX.hi], + decPublicInputs[DEC_DOMAIN_IDX.lo], + ); if (isCoverageRun) { // Instrumented Honk verifiers can exceed any practical eth_call budget; @@ -357,11 +363,7 @@ describe("BfvVkBindingIntegration", function () { const plaintextHash = plaintextHashFromPublicInputs(decPublicInputs); expect( await bfvDec.verify.staticCall( - testE3Id, - testRoot, - [testSigner.address], - ethers.id("test-ciphertext"), - ethers.id("test-pubkey"), + decDomain, plaintextHash, decCommitteeHash, decEncoded, diff --git a/packages/interfold-contracts/test/E3Lifecycle/E3Integration.spec.ts b/packages/interfold-contracts/test/E3Lifecycle/E3Integration.spec.ts index 874e08bb8..395f3e623 100644 --- a/packages/interfold-contracts/test/E3Lifecycle/E3Integration.spec.ts +++ b/packages/interfold-contracts/test/E3Lifecycle/E3Integration.spec.ts @@ -7,9 +7,14 @@ import { expect } from "chai"; import type { Signer } from "ethers"; import InterfoldModule from "../../ignition/modules/interfold"; -import { Interfold__factory as InterfoldFactory } from "../../types"; +import type { MockBlacklistUSDC } from "../../types"; +import { + Interfold__factory as InterfoldFactory, + MockUSDC__factory as MockUSDCFactory, +} from "../../types"; import { deployInterfoldSystem, + encodeMockDkgProof, ethers, ignition, networkHelpers, @@ -70,6 +75,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { slashedFundsTreasury: treasury, timeoutConfig: defaultTimeoutConfig, treasury, + useBlacklistFeeToken: true, wireSlashingManager: true, }); @@ -115,7 +121,12 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const makeRequest = async ( signer: Signer = requester, committeeSize: number = 0, + requestToken = usdcToken, ): Promise<{ e3Id: number }> => { + // Ticket voting power is snapshotted at request timestamp - 1. EDR may + // mine consecutive setup transactions with the same timestamp, so move + // the request clock forward before taking that conservative snapshot. + await time.increase(1); const startTime = (await time.latest()) + 100; const requestParams = { @@ -131,11 +142,10 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, }; const fee = await interfold.getE3Quote(requestParams); - await usdcToken.connect(signer).approve(interfoldAddress, fee); + await requestToken.connect(signer).approve(interfoldAddress, fee); await interfold.connect(signer).request(requestParams); return { e3Id: 0 }; @@ -216,6 +226,232 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const storedRequester = await interfold.getRequester(0); expect(storedRequester).to.equal(await requester.getAddress()); }); + + it("classifies every supported failure reason by economic responsibility", async function () { + const { e3RefundManager } = await loadFixture(setup); + + for (const reason of [5, 6, 7, 8, 9]) { + expect(await e3RefundManager.getFailurePayer(reason)).to.equal(1); + } + for (const reason of [1, 2, 3, 4, 10, 11, 12]) { + expect(await e3RefundManager.getFailurePayer(reason)).to.equal(2); + } + + await expect( + e3RefundManager.getFailurePayer(0), + ).to.be.revertedWithCustomError(e3RefundManager, "InvalidFailureReason"); + await expect( + e3RefundManager.getFailurePayer(13), + ).to.be.revertedWithCustomError(e3RefundManager, "InvalidFailureReason"); + }); + + it("AUD-M07: snapshots failure allocation and treasury at request time", async function () { + const { + interfold, + e3RefundManager, + registry, + usdcToken, + makeRequest, + owner, + treasury, + computeProvider, + operator1, + operator2, + operator3, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + await makeRequest(); + + const originalTreasury = await treasury.getAddress(); + const rotatedTreasury = await computeProvider.getAddress(); + const snapshot = await e3RefundManager.getE3PolicySnapshot(0); + expect(snapshot.initialized).to.equal(true); + expect(snapshot.version).to.equal(1); + expect(snapshot.treasury).to.equal(originalTreasury); + expect(snapshot.registry).to.equal(await registry.getAddress()); + expect(snapshot.allocation.committeeFormationBps).to.equal(1000); + + await e3RefundManager.connect(owner).setWorkAllocation({ + committeeFormationBps: 2000, + dkgBps: 3000, + decryptionBps: 4500, + protocolBps: 500, + successSlashedNodeBps: 1000, + }); + await e3RefundManager.connect(owner).setTreasury(rotatedTreasury); + + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await registry.connect(operator3).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + const publicKey = "0x1234567890abcdef1234567890abcdef"; + const pkCommitment = ethers.keccak256(publicKey); + await registry.publishCommittee( + 0, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); + const deadlines = await interfold.getDeadlines(0); + await time.increaseTo(deadlines.computeDeadline + 1n); + await interfold.markE3Failed(0); + await interfold.processE3Failure(0); + + const distribution = await e3RefundManager.getRefundDistribution(0); + expect(distribution.honestNodeAmount).to.equal( + (distribution.originalPayment * 4000n) / 10000n, + ); + expect( + await e3RefundManager.pendingTreasuryClaim( + originalTreasury, + await usdcToken.getAddress(), + ), + ).to.equal(distribution.protocolAmount); + expect( + await e3RefundManager.pendingTreasuryClaim( + rotatedTreasury, + await usdcToken.getAddress(), + ), + ).to.equal(0); + + const unchanged = await e3RefundManager.getE3PolicySnapshot(0); + expect(unchanged.version).to.equal(1); + expect(unchanged.treasury).to.equal(originalTreasury); + expect(unchanged.allocation.committeeFormationBps).to.equal(1000); + }); + + it("AUD-M04: drains an in-flight E3 through request-time dependencies", async function () { + const { + interfold, + e3RefundManager, + bondingRegistry, + registry, + slashingManager, + usdcToken, + makeRequest, + owner, + requester, + treasury, + computeProvider, + operator1, + operator2, + operator3, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + const rotationTestWindow = 100; + await registry + .connect(owner) + .setSortitionSubmissionWindow(rotationTestWindow); + await makeRequest(); + + const interfoldAddress = await interfold.getAddress(); + const refundManagerAddress = await e3RefundManager.getAddress(); + const registryAddress = await registry.getAddress(); + const bondingAddress = await bondingRegistry.getAddress(); + + const policy = await e3RefundManager.getE3PolicySnapshot(0); + expect(policy.interfold).to.equal(interfoldAddress); + expect(policy.registry).to.equal(registryAddress); + const dependencies = await slashingManager.getE3Dependencies(0); + expect(dependencies.bonding).to.equal(bondingAddress); + expect(dependencies.registry).to.equal(registryAddress); + expect(dependencies.interfoldContract).to.equal(interfoldAddress); + expect(dependencies.refundManager).to.equal(refundManagerAddress); + + const rotatedRegistry = await requester.getAddress(); + const rotatedBonding = await computeProvider.getAddress(); + const rotatedRefundManager = await treasury.getAddress(); + const rotatedSlashingManager = await owner.getAddress(); + + // Rotate every global dependency after the E3 has been requested. EOAs are + // deliberate canaries: any accidental read through a live global pointer + // will fail instead of silently succeeding through another deployment. + await interfold.connect(owner).setCiphernodeRegistry(rotatedRegistry); + await interfold.connect(owner).setBondingRegistry(rotatedBonding); + await interfold.connect(owner).setE3RefundManager(rotatedRefundManager); + await interfold.connect(owner).setSlashingManager(rotatedSlashingManager); + await registry.connect(owner).setInterfold(rotatedRegistry); + await registry.connect(owner).setBondingRegistry(rotatedBonding); + await registry.connect(owner).setSlashingManager(rotatedSlashingManager); + await bondingRegistry + .connect(owner) + .setSlashingManager(rotatedSlashingManager); + await e3RefundManager.connect(owner).setInterfold(rotatedRegistry); + await slashingManager.connect(owner).setBondingRegistry(rotatedBonding); + await slashingManager + .connect(owner) + .setCiphernodeRegistry(rotatedRegistry); + await slashingManager.connect(owner).setInterfold(rotatedRegistry); + await slashingManager + .connect(owner) + .setE3RefundManager(rotatedRefundManager); + + // Committee selection still reads eligibility and ticket checkpoints from + // the original bonding registry, then calls back into the original Interfold. + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await registry.connect(operator3).submitTicket(0, 1); + await time.increase(rotationTestWindow + 1); + await registry.finalizeCommittee(0); + + const publicKey = "0x1234567890abcdef1234567890abcdef"; + await registry.publishCommittee( + 0, + publicKey, + ethers.keccak256(publicKey), + encodeMockDkgProof(ethers.keccak256(publicKey)), + "0x01", + ); + + // Slashing also stays bound to the original registry, bonding, Interfold, + // and refund manager even though all four global pointers were rotated. + const proof = await signAndEncodeAttestation( + [operator2, operator3], + 0, + await operator1.getAddress(), + await slashingManager.getAddress(), + ); + const refundBalanceBefore = + await usdcToken.balanceOf(refundManagerAddress); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + proof, + ); + expect( + await bondingRegistry.isAuthorizedSlashingManager( + await slashingManager.getAddress(), + ), + ).to.equal(true); + expect(await usdcToken.balanceOf(refundManagerAddress)).to.be.gt( + refundBalanceBefore, + ); + + const e3 = await interfold.getE3(0); + await time.increaseTo(Number(e3.inputWindow[1])); + await interfold.publishCiphertextOutput( + 0, + "0x" + "ab".repeat(100), + "0x1337", + ); + await interfold.publishPlaintextOutput( + 0, + "0x" + "cd".repeat(100), + "0x1337", + ); + + expect(await interfold.getE3Stage(0)).to.equal(5); // Complete + }); }); describe("Committee Formed Integration", function () { @@ -256,7 +492,13 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const publicKey = "0x1234567890abcdef1234567890abcdef"; const pkCommitment = ethers.keccak256(publicKey); - await registry.publishCommittee(0, publicKey, pkCommitment, "0x", "0x"); + await registry.publishCommittee( + 0, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); // Verify stage transitioned to KeyPublished (after publishCommittee which calls onKeyPublished) stage = await interfold.getE3Stage(0); @@ -297,7 +539,13 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const pkCommitment = ethers.keccak256(publicKey); await expect( - registry.publishCommittee(0, publicKey, pkCommitment, "0x", "0x"), + registry.publishCommittee( + 0, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ), ) .to.emit(interfold, "CommitteeFormed") .withArgs(0); @@ -402,7 +650,11 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const distribution = await e3RefundManager.getRefundDistribution(0); expect(distribution.calculated).to.be.true; - expect(distribution.requesterAmount).to.be.gt(0); + expect(distribution.requesterAmount).to.equal( + distribution.originalPayment, + ); + expect(distribution.honestNodeAmount).to.equal(0); + expect(distribution.protocolAmount).to.equal(0); }); it("allows requester to claim refund after failure processing", async function () { @@ -526,7 +778,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { }); describe("Slashed Funds Escrow", function () { - it("E2E: slash via SlashingManager escrows actual USDC to refund manager and requester can claim", async function () { + it("E2E: slash via SlashingManager pays honest nodes without reducing the requester refund", async function () { const { interfold, e3RefundManager, @@ -535,6 +787,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { bondingRegistry, usdcToken, makeRequest, + owner, requester, operator1, operator2, @@ -545,6 +798,17 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { await setupOperator(operator1); await setupOperator(operator2); await setupOperator(operator3); + await slashingManager.connect(owner).setSlashPolicy(REASON_PT_0, { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: true, + failureReason: 0, + }); // 1. Request E3, form committee, publish key await makeRequest(requester, 0); @@ -556,7 +820,13 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const publicKey = "0x1234567890abcdef1234567890abcdef"; const pkCommitment = ethers.keccak256(publicKey); - await registry.publishCommittee(0, publicKey, pkCommitment, "0x", "0x"); + await registry.publishCommittee( + 0, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); // 2. Wait past compute deadline → mark as failed const e3 = await interfold.getE3(0); @@ -578,9 +848,9 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { // Record BondingRegistry's slashedTicketBalance before slash const slashedBalanceBefore = await bondingRegistry.slashedTicketBalance(); - // 4. Slash operator1 via proposeSlash (Lane A) — real on-chain flow - // This triggers: _executeSlash → slashTicketBalance → redirectSlashedTicketFunds - // → ticketToken.payout(refundManager, amount) → interfold.escrowSlashedFunds → e3RefundManager.escrowSlashedFunds + // 4. Slash operator1 via proposeSlash (Lane A) — real on-chain flow. + // The manager reserves the slash, then atomically routes the reserved + // underlying through Interfold into E3RefundManager escrow. const proof = await signAndEncodeAttestation( [operator2, operator3], 0, @@ -608,18 +878,62 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { slashedBalanceBefore, // slash added then redirect removed the same amount ); - // 6. Verify distribution was updated with requester-first priority + // 6. Base refunds stay denominated in the E3 fee token; the slash is a + // separate claim in its actual underlying token. const distributionAfter = await e3RefundManager.getRefundDistribution(0); - expect(distributionAfter.totalSlashed).to.equal(actualSlashedAmount); - expect(distributionAfter.requesterAmount).to.be.gte( + expect(distributionAfter.requesterAmount).to.equal( distributionBefore.requesterAmount, ); + expect(distributionAfter.honestNodeAmount).to.equal( + distributionBefore.honestNodeAmount, + ); + const usdcAddress = await usdcToken.getAddress(); + const requesterSlashClaim = await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await requester.getAddress(), + ); + expect(requesterSlashClaim).to.equal(0); + expect(distributionAfter.totalSlashed).to.equal(actualSlashedAmount); + + let honestSlashClaims = 0n; + for (const node of [operator1, operator2, operator3]) { + honestSlashClaims += await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await node.getAddress(), + ); + } + expect(honestSlashClaims).to.equal(actualSlashedAmount); + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await operator1.getAddress(), + ), + ).to.equal(0); + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await operator2.getAddress(), + ), + ).to.equal( + await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await operator3.getAddress(), + ), + ); - // 7. Verify requester can actually claim and receives the correct USDC + // 7. The requester pulls only the fault-attributed base refund. const requesterBalanceBefore = await usdcToken.balanceOf( await requester.getAddress(), ); await e3RefundManager.connect(requester).claimRequesterRefund(0); + expect( + await usdcToken.balanceOf(await e3RefundManager.getAddress()), + ).to.be.gte(await e3RefundManager.tokenLiability(usdcAddress)); const requesterBalanceAfter = await usdcToken.balanceOf( await requester.getAddress(), ); @@ -628,6 +942,274 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { ); }); + it("AUD-M05: reserves a failed slash route and retries it permissionlessly", async function () { + const { + interfold, + e3RefundManager, + registry, + slashingManager, + bondingRegistry, + usdcToken, + makeRequest, + owner, + requester, + operator1, + operator2, + operator3, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + await slashingManager.connect(owner).setSlashPolicy(REASON_PT_0, { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: true, + failureReason: 0, + }); + + await makeRequest(requester, 0); + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await registry.connect(operator3).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + + const publicKey = "0x1234567890abcdef1234567890abcdef"; + await registry.publishCommittee( + 0, + publicKey, + ethers.keccak256(publicKey), + encodeMockDkgProof(ethers.keccak256(publicKey)), + "0x01", + ); + + const e3 = await interfold.getE3(0); + const computeDeadline = + Number(e3.inputWindow[1]) + defaultTimeoutConfig.computeWindow; + await time.increaseTo(computeDeadline + 1); + await interfold.markE3Failed(0); + await interfold.processE3Failure(0); + + const blacklistToken = usdcToken as unknown as MockBlacklistUSDC; + const refundManagerAddress = await e3RefundManager.getAddress(); + await blacklistToken.blacklist(refundManagerAddress); + + const proof = await signAndEncodeAttestation( + [operator2, operator3], + 0, + await operator1.getAddress(), + await slashingManager.getAddress(), + ); + await expect( + slashingManager.proposeSlash(0, await operator1.getAddress(), proof), + ).to.emit(slashingManager, "SlashRoutePending"); + + const pending = await slashingManager.getPendingSlashRoute(0); + expect(pending.pending).to.equal(true); + expect(pending.e3Id).to.equal(0); + expect(pending.token).to.equal(await usdcToken.getAddress()); + expect(pending.amount).to.be.gt(0); + expect(await bondingRegistry.reservedSlashedTicketBalance()).to.equal( + pending.amount, + ); + expect(await bondingRegistry.slashedTicketBalance()).to.equal( + pending.amount, + ); + + await expect( + bondingRegistry.connect(owner).withdrawSlashedFunds(pending.amount, 0), + ).to.be.revertedWithCustomError(bondingRegistry, "ReservedSlashedFunds"); + + await blacklistToken.unblacklist(refundManagerAddress); + const refundBalanceBefore = + await usdcToken.balanceOf(refundManagerAddress); + await expect(slashingManager.connect(requester).retrySlashRoute(0)) + .to.emit(slashingManager, "SlashRouteCompleted") + .withArgs(0, 0, await usdcToken.getAddress(), pending.amount) + .and.to.emit(interfold, "SlashedFundsEscrowed") + .withArgs(0, await usdcToken.getAddress(), pending.amount); + + expect( + (await usdcToken.balanceOf(refundManagerAddress)) - refundBalanceBefore, + ).to.equal(pending.amount); + expect((await slashingManager.getPendingSlashRoute(0)).pending).to.equal( + false, + ); + expect(await bondingRegistry.reservedSlashedTicketBalance()).to.equal(0); + expect(await bondingRegistry.slashedTicketBalance()).to.equal(0); + expect( + await slashingManager.connect(requester).retrySlashRoute.staticCall(0), + ).to.equal(false); + }); + + it("AUD-H01: preserves a slash token distinct from the E3 fee token", async function () { + const { + interfold, + e3RefundManager, + registry, + slashingManager, + usdcToken: ticketUnderlying, + makeRequest, + owner, + requester, + operator1, + operator2, + operator3, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + await slashingManager.connect(owner).setSlashPolicy(REASON_PT_0, { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: true, + failureReason: 0, + }); + + const feeToken = await new MockUSDCFactory(owner).deploy(0); + await feeToken.waitForDeployment(); + await feeToken.mint( + await requester.getAddress(), + ethers.parseUnits("10000", 6), + ); + await interfold.connect(owner).setFeeToken(await feeToken.getAddress()); + + await makeRequest(requester, 0, feeToken); + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await registry.connect(operator3).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + const publicKey = "0x1234567890abcdef1234567890abcdef"; + await registry.publishCommittee( + 0, + publicKey, + ethers.keccak256(publicKey), + encodeMockDkgProof(ethers.keccak256(publicKey)), + "0x01", + ); + + const proof = await signAndEncodeAttestation( + [operator2, operator3], + 0, + await operator1.getAddress(), + await slashingManager.getAddress(), + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + proof, + ); + + const underlyingAddress = await ticketUnderlying.getAddress(); + const feeTokenAddress = await feeToken.getAddress(); + const actualSlash = ethers.parseUnits("50", 6); + expect( + await e3RefundManager.pendingSlashedFunds(0, underlyingAddress), + ).to.equal(actualSlash); + + const e3 = await interfold.getE3(0); + await time.increaseTo( + Number(e3.inputWindow[1]) + defaultTimeoutConfig.computeWindow + 1, + ); + await interfold.markE3Failed(0); + await interfold.processE3Failure(0); + + // The fee-token settlement cannot consume or relabel the distinct + // ticket underlying. Anyone can settle that recorded token explicitly. + expect( + await e3RefundManager.pendingSlashedFunds(0, underlyingAddress), + ).to.equal(actualSlash); + await e3RefundManager + .connect(operator3) + .settleSlashedFunds(0, underlyingAddress); + + const distribution = await e3RefundManager.getRefundDistribution(0); + expect(distribution.feeToken).to.equal(feeTokenAddress); + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + feeTokenAddress, + await requester.getAddress(), + ), + ).to.equal(0); + + const recipients = [requester, operator1, operator2, operator3]; + let totalSlashCredits = 0n; + for (const recipient of recipients) { + totalSlashCredits += await e3RefundManager.pendingSlashedClaim( + 0, + underlyingAddress, + await recipient.getAddress(), + ); + } + expect(totalSlashCredits).to.equal(actualSlash); + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + underlyingAddress, + await requester.getAddress(), + ), + ).to.equal(0); + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + underlyingAddress, + await operator1.getAddress(), + ), + ).to.equal(0); + const operator2SlashClaim = await e3RefundManager.pendingSlashedClaim( + 0, + underlyingAddress, + await operator2.getAddress(), + ); + expect(operator2SlashClaim).to.be.gt(0); + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + underlyingAddress, + await operator3.getAddress(), + ), + ).to.equal(operator2SlashClaim); + expect(await e3RefundManager.tokenLiability(underlyingAddress)).to.equal( + totalSlashCredits, + ); + + const requesterAddress = await requester.getAddress(); + const feeBefore = await feeToken.balanceOf(requesterAddress); + const operator2Address = await operator2.getAddress(); + const underlyingBefore = + await ticketUnderlying.balanceOf(operator2Address); + await e3RefundManager.connect(requester).claimRequesterRefund(0); + await e3RefundManager + .connect(operator2) + .claimSlashedFunds(0, underlyingAddress); + + expect((await feeToken.balanceOf(requesterAddress)) - feeBefore).to.equal( + distribution.requesterAmount, + ); + expect( + (await ticketUnderlying.balanceOf(operator2Address)) - underlyingBefore, + ).to.equal(operator2SlashClaim); + expect(await e3RefundManager.tokenLiability(underlyingAddress)).to.equal( + totalSlashCredits - operator2SlashClaim, + ); + }); + it("E2E: honest nodes can claim their share after slashed funds are escrowed", async function () { const { interfold, @@ -636,6 +1218,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { slashingManager, usdcToken, makeRequest, + owner, operator1, operator2, operator3, @@ -645,6 +1228,17 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { await setupOperator(operator1); await setupOperator(operator2); await setupOperator(operator3); + await slashingManager.connect(owner).setSlashPolicy(REASON_PT_0, { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: true, + failureReason: 0, + }); // 1. Request E3, form committee, publish key await makeRequest(undefined, 0); @@ -656,7 +1250,13 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const publicKey = "0x1234567890abcdef1234567890abcdef"; const pkCommitment = ethers.keccak256(publicKey); - await registry.publishCommittee(0, publicKey, pkCommitment, "0x", "0x"); + await registry.publishCommittee( + 0, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); // 2. Fail via compute timeout const e3 = await interfold.getE3(0); @@ -666,7 +1266,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { await interfold.markE3Failed(0); await interfold.processE3Failure(0); - // 3. Record distribution BEFORE slash to verify it actually changes + // 3. Record the base distribution before slash. const distributionBefore = await e3RefundManager.getRefundDistribution(0); const honestNodeAmountBefore = distributionBefore.honestNodeAmount; @@ -685,30 +1285,56 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const distribution = await e3RefundManager.getRefundDistribution(0); expect(distribution.honestNodeCount).to.be.gt(0); - // Verify that honestNodeAmount INCREASED due to slashed funds escrow - expect(distribution.honestNodeAmount).to.be.gt(honestNodeAmountBefore); - expect(distribution.totalSlashed).to.be.gt(0); + expect(distribution.honestNodeAmount).to.equal(honestNodeAmountBefore); + const usdcAddress = await usdcToken.getAddress(); + const op2SlashClaim = await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await operator2.getAddress(), + ); + expect(op2SlashClaim).to.be.gt(0); + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await operator1.getAddress(), + ), + ).to.equal(0); + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await operator3.getAddress(), + ), + ).to.equal(op2SlashClaim); // 5. operator2 (honest node) claims their share const op2BalanceBefore = await usdcToken.balanceOf( await operator2.getAddress(), ); await e3RefundManager.connect(operator2).claimHonestNodeReward(0); + await e3RefundManager + .connect(operator2) + .claimSlashedFunds(0, usdcAddress); const op2BalanceAfter = await usdcToken.balanceOf( await operator2.getAddress(), ); const perNodeAmount = distribution.honestNodeAmount / BigInt(distribution.honestNodeCount); - expect(op2BalanceAfter - op2BalanceBefore).to.equal(perNodeAmount); + expect(op2BalanceAfter - op2BalanceBefore).to.equal( + perNodeAmount + op2SlashClaim, + ); }); - it("requester-first priority: requester gets filled before honest nodes", async function () { + it("routes failed-E3 slashes to treasury when no honest nodes exist", async function () { const { interfold, e3RefundManager, + usdcToken, makeRequest, - owner, + requester, + treasury, operator1, operator2, operator3, @@ -721,46 +1347,156 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { await makeRequest(); - // Fail the E3 at committee formation stage (no honest nodes, requester gets 95%) + // Fail at committee formation (no honest nodes, requester gets all escrow). await time.increase(SORTITION_SUBMISSION_WINDOW + 1); await interfold.markE3Failed(0); await interfold.processE3Failure(0); const distributionBefore = await e3RefundManager.getRefundDistribution(0); const slashedAmount = ethers.parseUnits("100", 6); + await usdcToken.mint(await e3RefundManager.getAddress(), slashedAmount); - // requesterGap = originalPayment - requesterAmount (how much more needed to be whole) - const requesterGap = - distributionBefore.originalPayment - distributionBefore.requesterAmount; - - // Escrow slashed funds via the interfold proxy (swap interfold address for test) + // Call from the Interfold frozen in the E3 policy snapshot. Rotating the + // manager's live pointer must not grant settlement authority for old E3s. const originalInterfold = await e3RefundManager.interfold(); - await e3RefundManager.setInterfold(await owner.getAddress()); - await e3RefundManager.connect(owner).escrowSlashedFunds(0, slashedAmount); - await e3RefundManager.setInterfold(originalInterfold); + await ethers.provider.send("hardhat_impersonateAccount", [ + originalInterfold, + ]); + await ethers.provider.send("hardhat_setBalance", [ + originalInterfold, + "0x1000000000000000000", + ]); + await e3RefundManager + .connect(await ethers.getSigner(originalInterfold)) + .escrowSlashedFunds(0, await usdcToken.getAddress(), slashedAmount); + await ethers.provider.send("hardhat_stopImpersonatingAccount", [ + originalInterfold, + ]); const distributionAfter = await e3RefundManager.getRefundDistribution(0); - - // H-08: with no honest nodes (failure at committee formation), the\n // node share is routed to the treasury pull-pool instead of being\n // stranded in `dist.honestNodeAmount`. Requester is still capped at\n // `originalPayment` via the requesterGap. - const expectedToRequester = - slashedAmount >= requesterGap ? requesterGap : slashedAmount; - const expectedToHonestNodes = 0n; + const tokenAddress = await usdcToken.getAddress(); expect(distributionAfter.requesterAmount).to.equal( - distributionBefore.requesterAmount + expectedToRequester, + distributionBefore.originalPayment, ); - expect(distributionAfter.honestNodeAmount).to.equal( - distributionBefore.honestNodeAmount + expectedToHonestNodes, + expect(distributionAfter.honestNodeAmount).to.equal(0); + expect(distributionAfter.protocolAmount).to.equal(0); + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + tokenAddress, + await requester.getAddress(), + ), + ).to.equal(0); + expect( + await e3RefundManager.pendingTreasuryClaim( + await treasury.getAddress(), + tokenAddress, + ), + ).to.equal(slashedAmount); + expect(await e3RefundManager.tokenLiability(tokenAddress)).to.equal( + slashedAmount, ); - expect(distributionAfter.totalSlashed).to.equal(slashedAmount); + }); + + it("credits every failed-E3 slash to honest nodes without requester compensation", async function () { + const { + interfold, + e3RefundManager, + registry, + usdcToken, + makeRequest, + requester, + operator1, + operator2, + operator3, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest(requester); + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await registry.connect(operator3).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + await time.increase(defaultTimeoutConfig.dkgWindow + 1); + await interfold.markE3Failed(0); + await interfold.processE3Failure(0); + + const distribution = await e3RefundManager.getRefundDistribution(0); + expect(distribution.requesterAmount).to.equal( + distribution.originalPayment, + ); + expect(distribution.honestNodeAmount).to.equal(0); + const firstSlash = ethers.parseUnits("25", 6); + const secondSlash = ethers.parseUnits("50", 6); + const totalSlash = firstSlash + secondSlash; + const refundManagerAddress = await e3RefundManager.getAddress(); + await usdcToken.mint(refundManagerAddress, totalSlash); + + const originalInterfold = await e3RefundManager.interfold(); + await ethers.provider.send("hardhat_impersonateAccount", [ + originalInterfold, + ]); + await ethers.provider.send("hardhat_setBalance", [ + originalInterfold, + "0x1000000000000000000", + ]); + const interfoldSigner = await ethers.getSigner(originalInterfold); + const usdcAddress = await usdcToken.getAddress(); + + await e3RefundManager + .connect(interfoldSigner) + .escrowSlashedFunds(0, usdcAddress, firstSlash); + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await requester.getAddress(), + ), + ).to.equal(0); + + await e3RefundManager + .connect(interfoldSigner) + .escrowSlashedFunds(0, usdcAddress, secondSlash); + await ethers.provider.send("hardhat_stopImpersonatingAccount", [ + originalInterfold, + ]); + + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await requester.getAddress(), + ), + ).to.equal(0); + + let honestSlashClaims = 0n; + for (const node of [operator1, operator2, operator3]) { + honestSlashClaims += await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await node.getAddress(), + ); + } + expect(honestSlashClaims).to.equal(totalSlash); + expect( + (await e3RefundManager.getRefundDistribution(0)).totalSlashed, + ).to.equal(totalSlash); }); it("queues slashed funds arriving before processE3Failure and applies on calculate", async function () { const { interfold, e3RefundManager, + usdcToken, makeRequest, - owner, + requester, + treasury, operator1, operator2, operator3, @@ -778,12 +1514,23 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { await interfold.markE3Failed(0); const slashedAmount = ethers.parseUnits("50", 6); + await usdcToken.mint(await e3RefundManager.getAddress(), slashedAmount); // Escrow slashed funds BEFORE processE3Failure — should be queued const originalInterfold = await e3RefundManager.interfold(); - await e3RefundManager.setInterfold(await owner.getAddress()); - await e3RefundManager.connect(owner).escrowSlashedFunds(0, slashedAmount); - await e3RefundManager.setInterfold(originalInterfold); + await ethers.provider.send("hardhat_impersonateAccount", [ + originalInterfold, + ]); + await ethers.provider.send("hardhat_setBalance", [ + originalInterfold, + "0x1000000000000000000", + ]); + await e3RefundManager + .connect(await ethers.getSigner(originalInterfold)) + .escrowSlashedFunds(0, await usdcToken.getAddress(), slashedAmount); + await ethers.provider.send("hardhat_stopImpersonatingAccount", [ + originalInterfold, + ]); // Distribution should not exist yet const distBefore = await e3RefundManager.getRefundDistribution(0); @@ -794,13 +1541,88 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const distAfter = await e3RefundManager.getRefundDistribution(0); expect(distAfter.calculated).to.be.true; - expect(distAfter.totalSlashed).to.equal(slashedAmount); - - // `totalSlashed` was already asserted above; the per-bucket split is\n // exercised by the dedicated requester-priority test — the residual\n // is routed to the treasury pull-pool, not to a single dist bucket. + const usdcAddress = await usdcToken.getAddress(); + expect( + await e3RefundManager.pendingSlashedFunds(0, usdcAddress), + ).to.equal(0); + expect( + await e3RefundManager.pendingSlashedClaim( + 0, + usdcAddress, + await requester.getAddress(), + ), + ).to.equal(0); + expect( + await e3RefundManager.pendingTreasuryClaim( + await treasury.getAddress(), + usdcAddress, + ), + ).to.equal(slashedAmount); }); }); - describe("Full Failure Flow - DKG Timeout", function () { + describe("Failure Claim Roles and DKG Timeout", function () { + it("AUD-M02: a requester who is also a node can claim both requester-fault allocations", async function () { + const { + interfold, + e3RefundManager, + registry, + makeRequest, + operator1, + operator2, + operator3, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + await makeRequest(operator1); + + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await registry.connect(operator3).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + const publicKey = "0x1234567890abcdef1234567890abcdef"; + const pkCommitment = ethers.keccak256(publicKey); + await registry.publishCommittee( + 0, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); + const e3 = await interfold.getE3(0); + await time.increaseTo( + Number(e3.inputWindow[1]) + defaultTimeoutConfig.computeWindow + 1, + ); + await interfold.markE3Failed(0); + await interfold.processE3Failure(0); + + await e3RefundManager.connect(operator1).claimRequesterRefund(0); + expect( + await e3RefundManager.hasRequesterClaimed( + 0, + await operator1.getAddress(), + ), + ).to.equal(true); + expect( + await e3RefundManager.hasHonestNodeClaimed( + 0, + await operator1.getAddress(), + ), + ).to.equal(false); + + await e3RefundManager.connect(operator1).claimHonestNodeReward(0); + expect( + await e3RefundManager.hasHonestNodeClaimed( + 0, + await operator1.getAddress(), + ), + ).to.equal(true); + }); + it("complete flow: request -> committee formed -> DKG timeout -> fail -> process -> claim", async function () { const { interfold, @@ -861,6 +1683,11 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { ); const distribution = await e3RefundManager.getRefundDistribution(0); + expect(distribution.requesterAmount).to.equal( + distribution.originalPayment, + ); + expect(distribution.honestNodeAmount).to.equal(0); + expect(distribution.protocolAmount).to.equal(0); expect(balanceAfter - balanceBefore).to.equal( distribution.requesterAmount, ); @@ -900,7 +1727,13 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const publicKey = "0x1234567890abcdef1234567890abcdef"; const pkCommitment = ethers.keccak256(publicKey); - await registry.publishCommittee(0, publicKey, pkCommitment, "0x", "0x"); + await registry.publishCommittee( + 0, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); stage = await interfold.getE3Stage(0); expect(stage).to.equal(3); // KeyPublished @@ -935,6 +1768,17 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { ); const distribution = await e3RefundManager.getRefundDistribution(0); + expect(distribution.requesterAmount).to.equal( + (distribution.originalPayment * 5500n) / 10000n, + ); + expect(distribution.honestNodeAmount).to.equal( + (distribution.originalPayment * 4000n) / 10000n, + ); + expect(distribution.protocolAmount).to.equal( + distribution.originalPayment - + distribution.requesterAmount - + distribution.honestNodeAmount, + ); expect(balanceAfter - balanceBefore).to.equal( distribution.requesterAmount, ); @@ -974,7 +1818,13 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const publicKey = "0x1234567890abcdef1234567890abcdef"; const pkCommitment = ethers.keccak256(publicKey); - await registry.publishCommittee(0, publicKey, pkCommitment, "0x", "0x"); + await registry.publishCommittee( + 0, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); stage = await interfold.getE3Stage(0); expect(stage).to.equal(3); // KeyPublished @@ -1016,10 +1866,14 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { ); const distribution = await e3RefundManager.getRefundDistribution(0); + expect(distribution.requesterAmount).to.equal( + distribution.originalPayment, + ); + expect(distribution.honestNodeAmount).to.equal(0); + expect(distribution.protocolAmount).to.equal(0); expect(balanceAfter - balanceBefore).to.equal( distribution.requesterAmount, ); - expect(distribution.requesterAmount).to.be.gt(0); }); }); @@ -1059,7 +1913,6 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, }; const fee = await interfold.getE3Quote(requestParams); await usdcToken.connect(requester).approve(interfoldAddress, fee); @@ -1142,7 +1995,6 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, }; const fee = await interfold.getE3Quote(requestParams); await usdcToken.connect(requester).approve(interfoldAddress, fee); @@ -1195,6 +2047,8 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { operator2, operator3, treasury, + computeProvider, + owner, setupOperator, } = await loadFixture(setup); @@ -1204,6 +2058,18 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { // 1. Request E3, form committee, publish key await makeRequest(undefined, 0); + // Governance changes after request must not alter this E3's success + // allocation or redirect its treasury share. + await e3RefundManager.connect(owner).setWorkAllocation({ + committeeFormationBps: 1000, + dkgBps: 3000, + decryptionBps: 5500, + protocolBps: 500, + successSlashedNodeBps: 1000, + }); + await e3RefundManager + .connect(owner) + .setTreasury(await computeProvider.getAddress()); await registry.connect(operator1).submitTicket(0, 1); await registry.connect(operator2).submitTicket(0, 1); await registry.connect(operator3).submitTicket(0, 1); @@ -1212,7 +2078,13 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const publicKey = "0x1234567890abcdef1234567890abcdef"; const pkCommitment = ethers.keccak256(publicKey); - await registry.publishCommittee(0, publicKey, pkCommitment, "0x", "0x"); + await registry.publishCommittee( + 0, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); expect(await interfold.getE3Stage(0)).to.equal(3); // KeyPublished @@ -1324,7 +2196,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { let slashedClaimedTotal = 0n; for (const op of ops) { const before = await usdcToken.balanceOf(await op.getAddress()); - await e3RefundManager.connect(op).claimSlashedFundsOnSuccess(0); + await e3RefundManager.connect(op).claimSlashedFunds(0, usdcAddress); const after = await usdcToken.balanceOf(await op.getAddress()); slashedClaimedTotal += after - before; } @@ -1366,7 +2238,13 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const publicKey = "0x1234567890abcdef1234567890abcdef"; const pkCommitment = ethers.keccak256(publicKey); - await registry.publishCommittee(0, publicKey, pkCommitment, "0x", "0x"); + await registry.publishCommittee( + 0, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); expect(await interfold.getE3Stage(0)).to.equal(3); // KeyPublished @@ -1420,7 +2298,13 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const publicKey = "0x1234567890abcdef1234567890abcdef"; const pkCommitment = ethers.keccak256(publicKey); - await registry.publishCommittee(0, publicKey, pkCommitment, "0x", "0x"); + await registry.publishCommittee( + 0, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); // Publish outputs const e3 = await interfold.getE3(0); diff --git a/packages/interfold-contracts/test/E3Lifecycle/Sortition.spec.ts b/packages/interfold-contracts/test/E3Lifecycle/Sortition.spec.ts index bd4775f3b..04c6c115e 100644 --- a/packages/interfold-contracts/test/E3Lifecycle/Sortition.spec.ts +++ b/packages/interfold-contracts/test/E3Lifecycle/Sortition.spec.ts @@ -114,7 +114,6 @@ async function deployStack() { ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, } as any; return interfold.connect(requester).request(req); }; diff --git a/packages/interfold-contracts/test/Interfold.spec.ts b/packages/interfold-contracts/test/Interfold.spec.ts index 300ea6679..b2e1205ca 100644 --- a/packages/interfold-contracts/test/Interfold.spec.ts +++ b/packages/interfold-contracts/test/Interfold.spec.ts @@ -44,13 +44,7 @@ describe("Interfold", function () { const inputWindowDuration = 300; const setup = async () => { - const sys = await deployInterfoldSystem({ wireSlashingManager: false }); - const dkgFoldAttestationVerifier = await ethers.deployContract( - "DkgFoldAttestationVerifier", - ); - await sys.ciphernodeRegistry.setInitialDkgFoldAttestationVerifier( - await dkgFoldAttestationVerifier.getAddress(), - ); + const sys = await deployInterfoldSystem({ wireSlashingManager: true }); return { owner: sys.owner, notTheOwner: sys.notTheOwner, @@ -64,7 +58,6 @@ describe("Interfold", function () { ticketToken: sys.ticketToken, usdcToken: sys.usdcToken, slashingManager: sys.slashingManager, - dkgFoldAttestationVerifier, request: sys.request, mocks: { decryptionVerifier: sys.mocks.decryptionVerifier, @@ -221,7 +214,6 @@ describe("Interfold", function () { paramSet: request.paramSet, computeProviderParams: request.computeProviderParams, customParams: request.customParams, - proofAggregationEnabled: false, }); const e3 = await interfold.getE3(0); @@ -383,7 +375,6 @@ describe("Interfold", function () { paramSet: request.paramSet, computeProviderParams: request.computeProviderParams, customParams: request.customParams, - proofAggregationEnabled: false, }), ).to.be.revertedWithCustomError(usdcToken, "ERC20InsufficientAllowance"); }); @@ -401,7 +392,6 @@ describe("Interfold", function () { paramSet: request.paramSet, computeProviderParams: request.computeProviderParams, customParams: request.customParams, - proofAggregationEnabled: false, }; // `CommitteeSizeNotConfigured(3)` reverts correctly on-chain; ethers cannot // decode the custom error when the enum arg is not a named variant (0..2). @@ -423,7 +413,6 @@ describe("Interfold", function () { paramSet: request.paramSet, computeProviderParams: request.computeProviderParams, customParams: request.customParams, - proofAggregationEnabled: false, }), ).to.be.revertedWithCustomError(interfold, "InvalidDuration"); }); @@ -438,7 +427,6 @@ describe("Interfold", function () { paramSet: request.paramSet, computeProviderParams: request.computeProviderParams, customParams: request.customParams, - proofAggregationEnabled: false, }), ) .to.be.revertedWithCustomError(interfold, "E3ProgramNotAllowed") @@ -455,7 +443,6 @@ describe("Interfold", function () { paramSet: request.paramSet, computeProviderParams: request.computeProviderParams, customParams: request.customParams, - proofAggregationEnabled: false, }), ) .to.be.revertedWithCustomError(interfold, "InvalidEncryptionScheme") @@ -471,7 +458,6 @@ describe("Interfold", function () { paramSet: request.paramSet, computeProviderParams: request.computeProviderParams, customParams: request.customParams, - proofAggregationEnabled: false, }); const e3 = await interfold.getE3(0); @@ -502,7 +488,6 @@ describe("Interfold", function () { paramSet: request.paramSet, computeProviderParams: request.computeProviderParams, customParams: request.customParams, - proofAggregationEnabled: false, }); const e3 = await interfold.getE3(0); @@ -540,7 +525,6 @@ describe("Interfold", function () { paramSet: request.paramSet, computeProviderParams: request.computeProviderParams, customParams: request.customParams, - proofAggregationEnabled: false, }); await setupAndPublishCommittee(ciphernodeRegistryContract, e3Id, data, [ @@ -603,7 +587,6 @@ describe("Interfold", function () { paramSet: request.paramSet, computeProviderParams: request.computeProviderParams, customParams: request.customParams, - proofAggregationEnabled: false, }); await setupAndPublishCommittee(ciphernodeRegistryContract, e3Id, data, [ @@ -764,6 +747,34 @@ describe("Interfold", function () { interfold.publishPlaintextOutput(e3Id, data, proof), ).to.be.revertedWithCustomError(interfold, "InvalidStage"); }); + it("AUD-C02: requires a final decryption proof", async function () { + const { + interfold, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + operator3, + } = await loadFixture(setup); + const e3Id = 0; + + await makeRequest(interfold, usdcToken, { + ...request, + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], + }); + await setupAndPublishCommittee(ciphernodeRegistryContract, e3Id, data, [ + operator1, + operator2, + operator3, + ]); + await mine(2, { interval: inputWindowDuration }); + await interfold.publishCiphertextOutput(e3Id, data, proof); + + await expect( + interfold.publishPlaintextOutput(e3Id, data, "0x"), + ).to.be.revertedWithCustomError(interfold, "ProofRequired"); + }); it("reverts if output is not valid", async function () { const { interfold, @@ -773,14 +784,12 @@ describe("Interfold", function () { operator1, operator2, operator3, - dkgFoldAttestationVerifier, } = await loadFixture(setup); const e3Id = 0; await makeRequest(interfold, usdcToken, { ...request, inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], - proofAggregationEnabled: true, }); const operators = [operator1, operator2, operator3]; @@ -788,7 +797,7 @@ describe("Interfold", function () { operators, e3Id, data, - await dkgFoldAttestationVerifier.getAddress(), + await ciphernodeRegistryContract.dkgFoldAttestationVerifier(), ); await setupAndPublishCommittee( ciphernodeRegistryContract, diff --git a/packages/interfold-contracts/test/Pricing/DustRotation.spec.ts b/packages/interfold-contracts/test/Pricing/DustRotation.spec.ts index 1e8a172be..5febe1a7e 100644 --- a/packages/interfold-contracts/test/Pricing/DustRotation.spec.ts +++ b/packages/interfold-contracts/test/Pricing/DustRotation.spec.ts @@ -14,6 +14,7 @@ import { SORTITION_SUBMISSION_WINDOW, DATA as data, deployInterfoldSystem, + encodeMockDkgProof, ethers, networkHelpers, PROOF as proof, @@ -37,7 +38,13 @@ describe("Pricing — per-E3 dust rotation across consecutive E3s", function () await time.increase(SORTITION_SUBMISSION_WINDOW + 1); await registry.finalizeCommittee(e3Id); const pkCommitment = ethers.keccak256(publicKey); - await registry.publishCommittee(e3Id, publicKey, pkCommitment, "0x", "0x"); + await registry.publishCommittee( + e3Id, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); }; const setup = async () => { @@ -105,7 +112,6 @@ describe("Pricing — per-E3 dust rotation across consecutive E3s", function () ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, } as any; }; @@ -124,7 +130,6 @@ describe("Pricing — per-E3 dust rotation across consecutive E3s", function () ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, }; await feeToken.approve(await interfold.getAddress(), ethers.MaxUint256); await interfold.request(req); @@ -142,7 +147,8 @@ describe("Pricing — per-E3 dust rotation across consecutive E3s", function () ); await time.increase(inputWindowDuration + 200); await interfold.publishCiphertextOutput(e3Id, data, proof); - await interfold.publishPlaintextOutput(e3Id, data, proof); + const e3Proof = ethers.concat([proof, ethers.toBeHex(e3Id, 32)]); + await interfold.publishPlaintextOutput(e3Id, data, e3Proof); return nodes; }; diff --git a/packages/interfold-contracts/test/Pricing/Pricing.spec.ts b/packages/interfold-contracts/test/Pricing/Pricing.spec.ts index 8de24cb50..86263fc46 100644 --- a/packages/interfold-contracts/test/Pricing/Pricing.spec.ts +++ b/packages/interfold-contracts/test/Pricing/Pricing.spec.ts @@ -64,7 +64,7 @@ describe("E3 Pricing", function () { const treasurySigner = signers[5]; const sys = await deployInterfoldSystem({ treasury: treasurySigner, - wireSlashingManager: false, + wireSlashingManager: true, }); await mine(1); return { @@ -358,7 +358,6 @@ describe("E3 Pricing", function () { ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, }; // Make request with large approval to avoid fee mismatch @@ -445,7 +444,6 @@ describe("E3 Pricing", function () { ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, }; // Make request with large approval diff --git a/packages/interfold-contracts/test/Pricing/PullPaymentsAndAllowlist.spec.ts b/packages/interfold-contracts/test/Pricing/PullPaymentsAndAllowlist.spec.ts index 93f70d2cb..372103e25 100644 --- a/packages/interfold-contracts/test/Pricing/PullPaymentsAndAllowlist.spec.ts +++ b/packages/interfold-contracts/test/Pricing/PullPaymentsAndAllowlist.spec.ts @@ -9,6 +9,7 @@ import { SORTITION_SUBMISSION_WINDOW, DATA as data, deployInterfoldSystem, + encodeMockDkgProof, ethers, networkHelpers, PROOF as proof, @@ -32,7 +33,13 @@ describe("Interfold — pull payments + fee-token allow-list", function () { await time.increase(SORTITION_SUBMISSION_WINDOW + 1); await registry.finalizeCommittee(e3Id); const pkCommitment = ethers.keccak256(publicKey); - await registry.publishCommittee(e3Id, publicKey, pkCommitment, "0x", "0x"); + await registry.publishCommittee( + e3Id, + publicKey, + pkCommitment, + encodeMockDkgProof(pkCommitment), + "0x01", + ); }; // Two fixtures: one using vanilla USDC (allow-list tests), @@ -94,7 +101,6 @@ describe("Interfold — pull payments + fee-token allow-list", function () { ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, }; return { @@ -190,7 +196,11 @@ describe("Interfold — pull payments + fee-token allow-list", function () { ); await time.increase(inputWindowDuration + 200); await interfold.publishCiphertextOutput(e3Id2, data, proof); - await interfold.publishPlaintextOutput(e3Id2, data, proof); + await interfold.publishPlaintextOutput( + e3Id2, + data, + ethers.concat([proof, ethers.toBeHex(e3Id2, 32)]), + ); const op1Addr = await operator1.getAddress(); const expected = diff --git a/packages/interfold-contracts/test/Registry/BondingRegistry.spec.ts b/packages/interfold-contracts/test/Registry/BondingRegistry.spec.ts index e7f7a0392..839d50db1 100644 --- a/packages/interfold-contracts/test/Registry/BondingRegistry.spec.ts +++ b/packages/interfold-contracts/test/Registry/BondingRegistry.spec.ts @@ -132,6 +132,9 @@ describe("BondingRegistry", function () { expect( await bondingRegistry.totalBonded(await operator1.getAddress()), ).to.equal(bondAmount); + expect(await bondingRegistry.totalLicenseLiability()).to.equal( + bondAmount, + ); }); it("reverts if amount is zero", async function () { @@ -267,12 +270,12 @@ describe("BondingRegistry", function () { const unbondAmount = ethers.parseEther("300"); const slashAmount = ethers.parseEther("800"); - await bondingRegistry.setSlashingManager(await notTheOwner.getAddress()); await licenseToken .connect(operator1) .approve(await bondingRegistry.getAddress(), bondAmount); await bondingRegistry.connect(operator1).bondLicense(bondAmount); await bondingRegistry.connect(operator1).unbondLicense(unbondAmount); + await bondingRegistry.setSlashingManager(await notTheOwner.getAddress()); await expect( bondingRegistry @@ -289,6 +292,9 @@ describe("BondingRegistry", function () { bondAmount - slashAmount, ); expect(await bondingRegistry.slashedLicenseBond()).to.equal(slashAmount); + expect(await bondingRegistry.totalLicenseLiability()).to.equal( + bondAmount, + ); }); }); @@ -915,6 +921,76 @@ describe("BondingRegistry", function () { }); }); + it("AUD-M03: governs eligibility parameters and refreshes cached status by policy version", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + operator2, + } = await loadFixture(setup); + const operator = await operator1.getAddress(); + + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), LICENSE_REQUIRED_BOND); + await bondingRegistry + .connect(operator1) + .bondLicense(LICENSE_REQUIRED_BOND); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = TICKET_PRICE * BigInt(MIN_TICKET_BALANCE); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + expect(await bondingRegistry.isActive(operator)).to.equal(true); + expect(await bondingRegistry.numActiveOperators()).to.equal(1); + + const initialVersion = + await bondingRegistry.eligibilityConfigurationVersion(); + await bondingRegistry.setTicketPrice(TICKET_PRICE * 2n); + + expect(await bondingRegistry.eligibilityConfigurationVersion()).to.equal( + initialVersion + 1n, + ); + expect(await bondingRegistry.isActive(operator)).to.equal(false); + expect(await bondingRegistry.numActiveOperators()).to.equal(0); + + // Refresh is permissionless and evaluates the new policy. The doubled + // ticket price leaves this operator below the five-ticket threshold. + await bondingRegistry.connect(operator2).refreshOperatorStatus(operator); + expect(await bondingRegistry.isActive(operator)).to.equal(false); + expect(await bondingRegistry.numActiveOperators()).to.equal(0); + + await bondingRegistry.setTicketPrice(TICKET_PRICE); + await bondingRegistry.refreshOperatorStatuses([operator]); + expect(await bondingRegistry.isActive(operator)).to.equal(true); + expect(await bondingRegistry.numActiveOperators()).to.equal(1); + + await bondingRegistry.setLicenseRequiredBond(LICENSE_REQUIRED_BOND * 2n); + await bondingRegistry.refreshOperatorStatus(operator); + expect(await bondingRegistry.isActive(operator)).to.equal(false); + + await bondingRegistry.setLicenseRequiredBond(LICENSE_REQUIRED_BOND); + await bondingRegistry.setLicenseActiveBps(9_000); + await bondingRegistry.refreshOperatorStatus(operator); + expect(await bondingRegistry.isActive(operator)).to.equal(true); + + await bondingRegistry.setMinTicketBalance(MIN_TICKET_BALANCE + 1); + await bondingRegistry.refreshOperatorStatus(operator); + expect(await bondingRegistry.isActive(operator)).to.equal(false); + }); + + it("AUD-M03: rejects a zero minimum ticket requirement", async function () { + const { bondingRegistry } = await loadFixture(setup); + await expect( + bondingRegistry.setMinTicketBalance(0), + ).to.be.revertedWithCustomError(bondingRegistry, "InvalidConfiguration"); + }); + describe("withdrawSlashedFunds()", function () { it("allows owner to withdraw slashed funds", async function () { const { bondingRegistry, treasury } = await loadFixture(setup); @@ -1254,55 +1330,129 @@ describe("BondingRegistry", function () { await expect( bondingRegistry.connect(operator1).removeTicketBalance(step), ).to.be.revertedWithCustomError(bondingRegistry, "TooManyTranches"); + + // Draining the 64 ticket-only tranches must release all 64 slots even + // though the independent license head never advanced through them. + await time.increase(SEVEN_DAYS_IN_SECONDS + 1); + await bondingRegistry.connect(operator1).claimExits(step * 64n, 0); + await bondingRegistry.connect(operator1).removeTicketBalance(step); }); - /** - * M-13 reproduction guard. - * - * `claimExits` / `withdrawSlashedFunds` previously called - * `licenseToken.safeTransfer` without measuring the registry's - * own balance delta. A fee-on-transfer / rebasing token configured - * via `setLicenseToken` would silently underpay the recipient while - * the registry's internal accounting was still decremented by the - * requested amount. The fix measures the delta and emits - * `LicenseTransferShortfall(recipient, expected, actual)`. - */ - it("M-13: emits LicenseTransferShortfall when license token charges a transfer fee", async function () { - const { bondingRegistry, licenseToken, operator1, operator1Address } = + it("AUD-M08: blocks license-token rotation until old liabilities are drained", async function () { + const { bondingRegistry, licenseToken, operator1 } = await loadFixture(setup); - // Bond + queue a license exit with the well-behaved token. const bondAmount = LICENSE_REQUIRED_BOND; await licenseToken .connect(operator1) .approve(await bondingRegistry.getAddress(), bondAmount); await bondingRegistry.connect(operator1).bondLicense(bondAmount); - await bondingRegistry.connect(operator1).registerOperator(); - await bondingRegistry.connect(operator1).unbondLicense(bondAmount); - await time.increase(SEVEN_DAYS_IN_SECONDS + 1); - // Swap the license token for a 1% fee-on-transfer token. const FoTFactory = await ethers.getContractFactory( "MockFeeOnTransferToken", ); const fot = await FoTFactory.deploy(100n); // 100 bps = 1% - // Seed the registry with enough FoT tokens to honor the (gross) claim - // amount: tests need to verify the delta-detection emits, not that the - // registry magically conjures tokens. We mint `bondAmount` directly to - // the registry's address. - await fot.mint(await bondingRegistry.getAddress(), bondAmount); - await bondingRegistry.setLicenseToken(await fot.getAddress()); - - // Claim — the safeTransfer will short by 1%. - const expectedFee = bondAmount / 100n; - const expectedActual = bondAmount - expectedFee; - - await expect(bondingRegistry.connect(operator1).claimExits(0, bondAmount)) - .to.emit(bondingRegistry, "LicenseTransferShortfall") - .withArgs(operator1Address, bondAmount, expectedActual); - - // Operator received the net (post-fee) amount. - expect(await fot.balanceOf(operator1Address)).to.equal(expectedActual); + await expect(bondingRegistry.setLicenseToken(ethers.ZeroAddress)) + .to.be.revertedWithCustomError(bondingRegistry, "InvalidBondingAsset") + .withArgs(ethers.ZeroAddress); + await expect(bondingRegistry.setLicenseToken(operator1.address)) + .to.be.revertedWithCustomError(bondingRegistry, "InvalidBondingAsset") + .withArgs(operator1.address); + await expect(bondingRegistry.setLicenseToken(await fot.getAddress())) + .to.be.revertedWithCustomError( + bondingRegistry, + "OutstandingAssetLiabilities", + ) + .withArgs(await licenseToken.getAddress(), bondAmount); + }); + + it("AUD-M08: sweeps donated license-token dust without touching liabilities", async function () { + const { + bondingRegistry, + licenseToken, + operator1, + treasury, + treasuryAddress, + } = await loadFixture(setup); + + const registryAddress = await bondingRegistry.getAddress(); + const dust = ethers.parseEther("1"); + await licenseToken.connect(operator1).transfer(registryAddress, dust); + + expect(await bondingRegistry.totalLicenseLiability()).to.equal(0); + + const replacement = await ( + await ethers.getContractFactory("MockFeeOnTransferToken") + ).deploy(0); + await expect( + bondingRegistry.setLicenseToken(await replacement.getAddress()), + ) + .to.be.revertedWithCustomError( + bondingRegistry, + "OutstandingAssetLiabilities", + ) + .withArgs(await licenseToken.getAddress(), dust); + + const treasuryBefore = await licenseToken.balanceOf(treasuryAddress); + await expect(bondingRegistry.sweepLicenseSurplus()) + .to.emit(bondingRegistry, "LicenseSurplusSwept") + .withArgs(await licenseToken.getAddress(), treasuryAddress, dust); + expect(await licenseToken.balanceOf(treasury)).to.equal( + treasuryBefore + dust, + ); + expect(await licenseToken.balanceOf(registryAddress)).to.equal(0); + + await expect( + bondingRegistry.setLicenseToken(await replacement.getAddress()), + ) + .to.emit(bondingRegistry, "LicenseTokenSet") + .withArgs(await replacement.getAddress()); + }); + + it("AUD-M08: blocks ticket-token rotation until supply and payouts are drained", async function () { + const { + bondingRegistry, + licenseToken, + ticketToken, + usdcToken, + operator1, + owner, + } = await loadFixture(setup); + + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), LICENSE_REQUIRED_BOND); + await bondingRegistry + .connect(operator1) + .bondLicense(LICENSE_REQUIRED_BOND); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("10", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + const replacement = await ( + await ethers.getContractFactory("InterfoldTicketToken") + ).deploy( + await usdcToken.getAddress(), + await bondingRegistry.getAddress(), + owner.address, + ); + await replacement.waitForDeployment(); + + await expect(bondingRegistry.setTicketToken(ethers.ZeroAddress)) + .to.be.revertedWithCustomError(bondingRegistry, "InvalidBondingAsset") + .withArgs(ethers.ZeroAddress); + await expect( + bondingRegistry.setTicketToken(await replacement.getAddress()), + ) + .to.be.revertedWithCustomError( + bondingRegistry, + "OutstandingAssetLiabilities", + ) + .withArgs(await ticketToken.getAddress(), ticketAmount); }); }); }); diff --git a/packages/interfold-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/interfold-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 54f15f5ae..8976b0e79 100644 --- a/packages/interfold-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/interfold-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -11,6 +11,7 @@ import { ADDRESS_ONE as AddressOne, ADDRESS_TWO as AddressTwo, deployInterfoldSystem, + encodeMockDkgProof, ethers, networkHelpers, } from "../fixtures"; @@ -77,7 +78,6 @@ describe("CiphernodeRegistryOwnable", function () { ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, }; const fee = await interfold.getE3Quote(requestParams); @@ -207,9 +207,87 @@ describe("CiphernodeRegistryOwnable", function () { ); expect(await registry.rootAt(0)).to.not.equal(0); }); + + it("AUD-M03: fails closed after governance updates until operators refresh", async function () { + const { + registry, + interfold, + bondingRegistry, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + operator1, + operator2, + operator3, + } = await loadFixture(setup); + + await bondingRegistry.setLicenseActiveBps(9_000); + expect(await bondingRegistry.numActiveOperators()).to.equal(0); + + await expect( + makeRequest( + interfold, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + ), + ) + .to.be.revertedWithCustomError(registry, "InsufficientCiphernodes") + .withArgs(3, 0); + + await bondingRegistry.refreshOperatorStatuses([ + await operator1.getAddress(), + await operator2.getAddress(), + await operator3.getAddress(), + ]); + expect(await bondingRegistry.numActiveOperators()).to.equal(3); + + await makeRequest( + interfold, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + ); + expect(await registry.rootAt(0)).to.equal(await registry.root()); + }); }); describe("publishCommittee()", function () { + it("AUD-C02: requires a final DKG proof and attestation bundle", async function () { + const { + registry, + interfold, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + operator1, + operator2, + operator3, + } = await loadFixture(setup); + await makeRequest( + interfold, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + ); + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await registry.connect(operator3).submitTicket(0, 1); + await finalizeCommitteeAfterWindow(registry, 0); + + await expect( + registry.publishCommittee(0, data, dataHash, "0x", "0x"), + ).to.be.revertedWithCustomError(registry, "DkgProofRequired"); + await expect( + registry.publishCommittee( + 0, + data, + dataHash, + encodeMockDkgProof(dataHash), + "0x", + ), + ).to.be.revertedWithCustomError(registry, "FoldAttestationsRequired"); + }); it("allows any caller to publish a finalized committee", async function () { const { registry, @@ -237,7 +315,13 @@ describe("CiphernodeRegistryOwnable", function () { await expect( registry .connect(notTheOwner) - .publishCommittee(0, data, dataHash, "0x", "0x"), + .publishCommittee( + 0, + data, + dataHash, + encodeMockDkgProof(dataHash), + "0x01", + ), ) .to.emit(registry, "CommitteePublished") .withArgs( @@ -249,7 +333,7 @@ describe("CiphernodeRegistryOwnable", function () { ], data, dataHash, - "0x", + encodeMockDkgProof(dataHash), ); }); it("stores the public key of the committee", async function () { @@ -275,7 +359,13 @@ describe("CiphernodeRegistryOwnable", function () { await registry.connect(operator3).submitTicket(0, 1); await finalizeCommitteeAfterWindow(registry, 0); - await registry.publishCommittee(0, data, dataHash, "0x", "0x"); + await registry.publishCommittee( + 0, + data, + dataHash, + encodeMockDkgProof(dataHash), + "0x01", + ); expect(await registry.committeePublicKey(0)).to.equal(dataHash); }); it("emits a CommitteePublished event", async function () { @@ -303,7 +393,13 @@ describe("CiphernodeRegistryOwnable", function () { await finalizeCommitteeAfterWindow(registry, 0); await expect( - await registry.publishCommittee(0, data, dataHash, "0x", "0x"), + await registry.publishCommittee( + 0, + data, + dataHash, + encodeMockDkgProof(dataHash), + "0x01", + ), ) .to.emit(registry, "CommitteePublished") .withArgs( @@ -315,7 +411,7 @@ describe("CiphernodeRegistryOwnable", function () { ], data, dataHash, - "0x", + encodeMockDkgProof(dataHash), ); }); }); @@ -475,7 +571,13 @@ describe("CiphernodeRegistryOwnable", function () { await registry.connect(operator3).submitTicket(e3Id, 1); await finalizeCommitteeAfterWindow(registry, e3Id); - await registry.publishCommittee(e3Id, data, dataHash, "0x", "0x"); + await registry.publishCommittee( + e3Id, + data, + dataHash, + encodeMockDkgProof(dataHash), + "0x01", + ); expect(await registry.committeePublicKey(e3Id)).to.equal(dataHash); }); it("reverts if the committee has not been published", async function () { diff --git a/packages/interfold-contracts/test/Registry/CiphernodeRegistryVerifierLifecycle.spec.ts b/packages/interfold-contracts/test/Registry/CiphernodeRegistryVerifierLifecycle.spec.ts index dd67634aa..1a77fdafc 100644 --- a/packages/interfold-contracts/test/Registry/CiphernodeRegistryVerifierLifecycle.spec.ts +++ b/packages/interfold-contracts/test/Registry/CiphernodeRegistryVerifierLifecycle.spec.ts @@ -11,7 +11,9 @@ const { loadFixture, time } = networkHelpers; describe("CiphernodeRegistryOwnable verifier lifecycle", function () { const setup = async () => { - const sys = await deployInterfoldSystem(); + const sys = await deployInterfoldSystem({ + wireMockDkgFoldAttestationVerifier: false, + }); const verifier1 = await ethers.deployContract("DkgFoldAttestationVerifier"); const verifier2 = await ethers.deployContract("DkgFoldAttestationVerifier"); const verifier3 = await ethers.deployContract("DkgFoldAttestationVerifier"); diff --git a/packages/interfold-contracts/test/Registry/ExitQueueLib.spec.ts b/packages/interfold-contracts/test/Registry/ExitQueueLib.spec.ts new file mode 100644 index 000000000..2f2c1cc20 --- /dev/null +++ b/packages/interfold-contracts/test/Registry/ExitQueueLib.spec.ts @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +import { expect } from "chai"; + +import { ethers } from "../fixtures"; + +describe("ExitQueueLib", function () { + it("counts a new tranche when a fully slashed tail has the same unlock timestamp", async function () { + const [operator] = await ethers.getSigners(); + const harness = await ethers.deployContract("ExitQueueHarness"); + const operatorAddress = await operator.getAddress(); + + await harness.queueSlashQueue(operatorAddress, 7 * 24 * 60 * 60, 10, 20); + + expect(await harness.liveTrancheCount(operatorAddress)).to.equal(1); + expect(await harness.queueLength(operatorAddress)).to.equal(1); + expect((await harness.tranche(operatorAddress, 0)).ticketAmount).to.equal( + 20, + ); + }); + + it("prunes drained tails and reuses both asset heads safely", async function () { + const [operator] = await ethers.getSigners(); + const harness = await ethers.deployContract("ExitQueueHarness"); + const operatorAddress = await operator.getAddress(); + + await harness.queue(operatorAddress, 0, 10, 20); + await harness.claim(operatorAddress, 10, 20); + expect(await harness.queueLength(operatorAddress)).to.equal(0); + expect(await harness.liveTrancheCount(operatorAddress)).to.equal(0); + + await harness.queue(operatorAddress, 0, 30, 40); + expect( + await harness.claim.staticCall(operatorAddress, 30, 40), + ).to.deep.equal([30n, 40n]); + await harness.claim(operatorAddress, 30, 40); + expect(await harness.queueLength(operatorAddress)).to.equal(0); + expect(await harness.liveTrancheCount(operatorAddress)).to.equal(0); + }); + + it("revives an asset head when a same-timestamp merge adds that asset", async function () { + const [operator] = await ethers.getSigners(); + const harness = await ethers.deployContract("ExitQueueHarness"); + const operatorAddress = await operator.getAddress(); + + await harness.queueTicketThenLicense(operatorAddress, 0, 10, 20); + expect(await harness.queueLength(operatorAddress)).to.equal(1); + expect( + await harness.claim.staticCall(operatorAddress, 10, 20), + ).to.deep.equal([10n, 20n]); + }); + + it("caps the physical scan span when fully drained holes remain between live tranches", async function () { + const [operator] = await ethers.getSigners(); + const harness = await ethers.deployContract("ExitQueueHarness"); + const operatorAddress = await operator.getAddress(); + + // Keep live licence-only tranches between ticket-only tranches so slashing + // the tickets creates interior holes that tail pruning cannot remove. + let ticketTotal = 0n; + for (let i = 0; i < 64; i++) { + const isTicket = i % 2 === 1; + await harness.queue( + operatorAddress, + i + 1, + isTicket ? 1 : 0, + isTicket ? 0 : 1, + ); + if (isTicket) ticketTotal++; + } + + await harness.slash(operatorAddress, ticketTotal, 0); + expect(await harness.queueLength(operatorAddress)).to.equal(63); + expect(await harness.liveTrancheCount(operatorAddress)).to.equal(32); + + // One new tranche fills the remaining span slot; another would make a + // claim/slash scan exceed MAX_ACTIVE_TRANCHES despite only 34 live entries. + await harness.queue(operatorAddress, 100, 1, 0); + await expect( + harness.queue(operatorAddress, 101, 1, 0), + ).to.be.revertedWithCustomError(harness, "TooManyTranches"); + }); +}); diff --git a/packages/interfold-contracts/test/Slashing/CommitteeExpulsion.spec.ts b/packages/interfold-contracts/test/Slashing/CommitteeExpulsion.spec.ts index 9ee2d1e8d..f3155a864 100644 --- a/packages/interfold-contracts/test/Slashing/CommitteeExpulsion.spec.ts +++ b/packages/interfold-contracts/test/Slashing/CommitteeExpulsion.spec.ts @@ -25,6 +25,7 @@ import { ONE_DAY, SORTITION_SUBMISSION_WINDOW, deployInterfoldSystem, + encodeMockDkgProof, ethers, networkHelpers, signAndEncodeAttestation, @@ -130,6 +131,10 @@ describe("Committee Expulsion & Fault Tolerance", function () { } async function makeRequest(committeeSize: number = COMMITTEE_SIZE_MINIMUM) { + // Ticket voting power is snapshotted at request timestamp - 1. EDR may + // mine consecutive setup transactions with the same timestamp, so move + // the request clock forward before taking that conservative snapshot. + await time.increase(1); const startTime = (await time.latest()) + 100; const requestParams = { committeeSize, @@ -144,7 +149,6 @@ describe("Committee Expulsion & Fault Tolerance", function () { ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, }; const fee = await interfold.getE3Quote(requestParams); @@ -168,8 +172,8 @@ describe("Committee Expulsion & Fault Tolerance", function () { e3Id, publicKey, pkCommitment, - "0x", - "0x", + encodeMockDkgProof(pkCommitment), + "0x01", ); } diff --git a/packages/interfold-contracts/test/Slashing/SlashingLanes.spec.ts b/packages/interfold-contracts/test/Slashing/SlashingLanes.spec.ts index 650dd7d60..a55feb629 100644 --- a/packages/interfold-contracts/test/Slashing/SlashingLanes.spec.ts +++ b/packages/interfold-contracts/test/Slashing/SlashingLanes.spec.ts @@ -9,8 +9,8 @@ // `CommitteeExpulsion.spec.ts`: // // * SLASHER_ROLE admin is GOVERNANCE_ROLE (not DEFAULT_ADMIN_ROLE). -// * `BondingRegistry.deregisterOperator` blocked while a Lane B slash -// proposal is open; unblocks after execution or upheld appeal. +// * Bonding collateral withdrawals and exit claims are blocked while any +// financial slash proposal is open; they unblock after terminal resolution. // * Lane A `proposeSlash` with a non-zero `appealWindow` defers // execution (no auto-execute) and respects the challenge window. // * EIP-712 attestation signatures are bound to the SlashingManager's @@ -45,6 +45,7 @@ describe("SlashingManager — lanes, roles, EIP-712 & admin handover", function const DEFAULT_ADMIN_ROLE = ethers.ZeroHash; const APPEAL_WINDOW = 7 * 24 * 60 * 60; + const APPEAL_RESOLUTION_GRACE = 7 * 24 * 60 * 60; // Constructor uses 2 days as the AccessControlDefaultAdminRules delay. const DEFAULT_ADMIN_DELAY = 2 * 24 * 60 * 60; @@ -122,6 +123,13 @@ describe("SlashingManager — lanes, roles, EIP-712 & admin handover", function ); await slashingManager.setInterfold(addressOne); await slashingManager.setE3RefundManager(addressOne); + await networkHelpers.setBalance(addressOne, ethers.parseEther("1")); + await networkHelpers.impersonateAccount(addressOne); + await slashingManager + .connect(await ethers.getSigner(addressOne)) + .snapshotE3Dependencies(0); + await networkHelpers.stopImpersonatingAccount(addressOne); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); return { owner, @@ -186,9 +194,9 @@ describe("SlashingManager — lanes, roles, EIP-712 & admin handover", function }); // -------------------------------------------------------------------------- - // deregisterOperator blocked while Lane B proposal open + // collateral exit blocked while financial proposal is open // -------------------------------------------------------------------------- - describe("deregisterOperator gated by open Lane B proposals", function () { + describe("operator exits gated by open slash proposals", function () { async function registerOperatorForExit( ctx: Awaited>, ) { @@ -202,12 +210,12 @@ describe("SlashingManager — lanes, roles, EIP-712 & admin handover", function await bondingRegistry.connect(operator).registerOperator(); } - it("hasOpenLaneBProposal flips true after proposeSlashEvidence and false after executeSlash", async function () { + it("hasOpenSlashProposal flips true after proposeSlashEvidence and false after executeSlash", async function () { const ctx = await loadFixture(setup); const { slashingManager, slasher, operatorAddress } = ctx; await setupLaneBPolicy(slashingManager); - expect(await slashingManager.hasOpenLaneBProposal(operatorAddress)).to.be + expect(await slashingManager.hasOpenSlashProposal(operatorAddress)).to.be .false; await slashingManager @@ -219,16 +227,83 @@ describe("SlashingManager — lanes, roles, EIP-712 & admin handover", function ethers.toUtf8Bytes("ev"), ); - expect(await slashingManager.hasOpenLaneBProposal(operatorAddress)).to.be + expect(await slashingManager.hasOpenSlashProposal(operatorAddress)).to.be .true; await time.increase(APPEAL_WINDOW + 1); await slashingManager.executeSlash(0); - expect(await slashingManager.hasOpenLaneBProposal(operatorAddress)).to.be + expect(await slashingManager.hasOpenSlashProposal(operatorAddress)).to.be .false; }); + it("retains an old manager's collateral authority and exit gate across rotation", async function () { + const ctx = await loadFixture(setup); + const { + owner, + slashingManager, + bondingRegistry, + slasher, + operator, + operatorAddress, + } = ctx; + await registerOperatorForExit(ctx); + await setupLaneBPolicy(slashingManager); + + await slashingManager + .connect(slasher) + .proposeSlashEvidence( + 0, + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes("rotation"), + ); + + const replacement = await ethers.deployContract("SlashingManager", [ + DEFAULT_ADMIN_DELAY, + await owner.getAddress(), + ]); + await bondingRegistry.setSlashingManager(await replacement.getAddress()); + + expect( + await bondingRegistry.isAuthorizedSlashingManager( + await slashingManager.getAddress(), + ), + ).to.equal(true); + expect( + await bondingRegistry.isAuthorizedSlashingManager( + await replacement.getAddress(), + ), + ).to.equal(true); + expect(await bondingRegistry.authorizedSlashingManagerCount()).to.equal( + 2, + ); + + await expect( + bondingRegistry.connect(operator).unbondLicense(1), + ).to.be.revertedWithCustomError(bondingRegistry, "OperatorUnderSlash"); + + await time.increase(APPEAL_WINDOW + 1); + await slashingManager.executeSlash(0); + + expect(await bondingRegistry.getLicenseBond(operatorAddress)).to.equal( + ethers.parseEther("950"), + ); + await bondingRegistry.connect(operator).unbondLicense(1); + + await bondingRegistry.revokeSlashingManager( + await slashingManager.getAddress(), + ); + expect( + await bondingRegistry.isAuthorizedSlashingManager( + await slashingManager.getAddress(), + ), + ).to.equal(false); + expect(await bondingRegistry.authorizedSlashingManagerCount()).to.equal( + 1, + ); + }); + it("deregisterOperator reverts OperatorUnderSlash while Lane B proposal open", async function () { const ctx = await loadFixture(setup); const { @@ -312,7 +387,7 @@ describe("SlashingManager — lanes, roles, EIP-712 & admin handover", function // Owner has GOVERNANCE_ROLE and can resolve appeals. await slashingManager.connect(owner).resolveAppeal(0, true, "upheld"); - expect(await slashingManager.hasOpenLaneBProposal(operatorAddress)).to.be + expect(await slashingManager.hasOpenSlashProposal(operatorAddress)).to.be .false; await bondingRegistry.connect(operator).deregisterOperator(); expect(await bondingRegistry.isRegistered(operatorAddress)).to.be.false; @@ -340,6 +415,21 @@ describe("SlashingManager — lanes, roles, EIP-712 & admin handover", function await mockCiphernodeRegistry.setThreshold(0, 2); } + async function registerOperatorWithQueuedExit( + ctx: Awaited>, + ) { + const { bondingRegistry, interfoldToken, operator } = ctx; + const licenseAmount = ethers.parseEther("1000"); + const queuedAmount = ethers.parseEther("100"); + await interfoldToken + .connect(operator) + .approve(await bondingRegistry.getAddress(), licenseAmount); + await bondingRegistry.connect(operator).bondLicense(licenseAmount); + await bondingRegistry.connect(operator).registerOperator(); + await bondingRegistry.connect(operator).unbondLicense(queuedAmount); + return queuedAmount; + } + it("proposeSlash with appealWindow>0 does NOT auto-execute and remains executable later", async function () { const ctx = await loadFixture(setup); const { slashingManager, proposer, operatorAddress, voter1, voter2 } = @@ -363,6 +453,8 @@ describe("SlashingManager — lanes, roles, EIP-712 & admin handover", function const p = await slashingManager.getSlashProposal(0); expect(p.executed).to.be.false; expect(p.executableAt).to.be.gt(p.proposedAt); + expect(await slashingManager.hasOpenSlashProposal(operatorAddress)).to.be + .true; // Cannot execute before window elapses await expect( @@ -374,6 +466,55 @@ describe("SlashingManager — lanes, roles, EIP-712 & admin handover", function slashingManager, "SlashExecuted", ); + expect(await slashingManager.hasOpenSlashProposal(operatorAddress)).to.be + .false; + }); + + it("AUD-H03: a deferred Lane A proposal freezes active and queued collateral", async function () { + const ctx = await loadFixture(setup); + const { + slashingManager, + bondingRegistry, + proposer, + operator, + operatorAddress, + voter1, + voter2, + } = ctx; + const queuedAmount = await registerOperatorWithQueuedExit(ctx); + await setupLaneAPolicy(slashingManager, APPEAL_WINDOW); + await setupCommittee(ctx); + + const proof = await signAndEncodeAttestation( + [voter1, voter2], + 0, + operatorAddress, + await slashingManager.getAddress(), + ); + await slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, proof); + + await expect( + bondingRegistry.connect(operator).deregisterOperator(), + ).to.be.revertedWithCustomError(bondingRegistry, "OperatorUnderSlash"); + await expect( + bondingRegistry.connect(operator).removeTicketBalance(1), + ).to.be.revertedWithCustomError(bondingRegistry, "OperatorUnderSlash"); + await expect( + bondingRegistry.connect(operator).unbondLicense(1), + ).to.be.revertedWithCustomError(bondingRegistry, "OperatorUnderSlash"); + + await time.increase(APPEAL_WINDOW + 1); + await expect( + bondingRegistry.connect(operator).claimExits(0, queuedAmount), + ).to.be.revertedWithCustomError(bondingRegistry, "OperatorUnderSlash"); + + await slashingManager.executeSlash(0); + await bondingRegistry.connect(operator).claimExits(0, queuedAmount); + expect((await bondingRegistry.pendingExits(operatorAddress))[1]).to.equal( + 0n, + ); }); it("operator can fileAppeal on a Lane A deferred proposal", async function () { @@ -403,6 +544,53 @@ describe("SlashingManager — lanes, roles, EIP-712 & admin handover", function slashingManager.connect(operator).fileAppeal(0, "not me"), ).to.emit(slashingManager, "AppealFiled"); }); + + it("expires an unresolved appeal after the governance grace period", async function () { + const ctx = await loadFixture(setup); + const { + slashingManager, + proposer, + operator, + operatorAddress, + voter1, + voter2, + } = ctx; + await setupLaneAPolicy(slashingManager, APPEAL_WINDOW); + await setupCommittee(ctx); + + const proof = await signAndEncodeAttestation( + [voter1, voter2], + 0, + operatorAddress, + await slashingManager.getAddress(), + ); + await slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, proof); + await slashingManager.connect(operator).fileAppeal(0, "not me"); + + await expect( + slashingManager.expireAppeal(0), + ).to.be.revertedWithCustomError( + slashingManager, + "AppealResolutionWindowActive", + ); + + const proposal = await slashingManager.getSlashProposal(0); + await time.increaseTo( + proposal.executableAt + BigInt(APPEAL_RESOLUTION_GRACE), + ); + await expect(slashingManager.expireAppeal(0)).to.emit( + slashingManager, + "AppealResolved", + ); + + expect(await slashingManager.hasOpenSlashProposal(operatorAddress)).to.be + .false; + await expect( + slashingManager.executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "AppealUpheld"); + }); }); // -------------------------------------------------------------------------- diff --git a/packages/interfold-contracts/test/Slashing/SlashingManager.spec.ts b/packages/interfold-contracts/test/Slashing/SlashingManager.spec.ts index 145e861a0..abbfe7d70 100644 --- a/packages/interfold-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/interfold-contracts/test/Slashing/SlashingManager.spec.ts @@ -148,6 +148,17 @@ describe("SlashingManager", function () { await slashingManager.setCiphernodeRegistry(mockCiphernodeRegistryAddress); await slashingManager.setInterfold(addressOne); await slashingManager.setE3RefundManager(addressOne); + await networkHelpers.setBalance(addressOne, ethers.parseEther("1")); + await networkHelpers.impersonateAccount(addressOne); + await slashingManager + .connect(await ethers.getSigner(addressOne)) + .snapshotE3Dependencies(0); + await slashingManager + .connect(await ethers.getSigner(addressOne)) + .snapshotE3Dependencies(1); + await networkHelpers.stopImpersonatingAccount(addressOne); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + await mockCiphernodeRegistry.setCommitteeNodes(1, [operatorAddress]); return { owner, @@ -348,6 +359,42 @@ describe("SlashingManager", function () { ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); }); + it("rejects a failure policy that does not expel the faulty operator", async function () { + const { slashingManager } = await loadFixture(setup); + + await expect( + slashingManager.setSlashPolicy(REASON_PT_0, { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: 0, + requiresProof: true, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: false, + failureReason: 4, + }), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + + it("rejects a failure policy with the sentinel failure reason", async function () { + const { slashingManager } = await loadFixture(setup); + + await expect( + slashingManager.setSlashPolicy(REASON_PT_0, { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: 0, + requiresProof: true, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: true, + failureReason: 13, + }), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + it("should allow proof-based policy without verifier (attestation model)", async function () { const { slashingManager } = await loadFixture(setup); @@ -1094,6 +1141,27 @@ describe("SlashingManager", function () { ), ).to.be.revertedWithCustomError(slashingManager, "ZeroAddress"); }); + + it("should reject evidence for an operator outside the E3 committee", async function () { + const { slashingManager, slasher, notTheOwner } = + await loadFixture(setup); + + await setupPolicies(slashingManager); + + await expect( + slashingManager + .connect(slasher) + .proposeSlashEvidence( + 0, + await notTheOwner.getAddress(), + REASON_INACTIVITY, + ethers.toUtf8Bytes("unrelated operator"), + ), + ).to.be.revertedWithCustomError( + slashingManager, + "OperatorNotInCommittee", + ); + }); }); describe("executeSlash() — Lane B execution", function () { diff --git a/packages/interfold-contracts/test/Token/InterfoldTokenSaleDeployer.spec.ts b/packages/interfold-contracts/test/Token/InterfoldTokenSaleDeployer.spec.ts index 7cc6c2265..396157559 100644 --- a/packages/interfold-contracts/test/Token/InterfoldTokenSaleDeployer.spec.ts +++ b/packages/interfold-contracts/test/Token/InterfoldTokenSaleDeployer.spec.ts @@ -293,6 +293,28 @@ describe("InterfoldTokenSaleDeployer", function () { it("captures the Safe as protocolAdmin (no hardcoded address)", async function () { const ctx = await setup(); expect(await ctx.saleDeployer.protocolAdmin()).to.equal(ctx.safeAddress); + expect(await ctx.saleDeployer.deploymentOperator()).to.equal( + await ctx.operator.getAddress(), + ); + }); + + it("rejects a copied sale deployment from a non-operator", async function () { + const ctx = await setup(); + const plan = await buildLbpSaleConfig(ctx); + + await expect( + ctx.saleDeployer + .connect(ctx.stranger) + .deploySaleWithLiquidityLauncher(plan.lbpSaleConfig, plan.foldInitCode), + ) + .to.be.revertedWithCustomError(ctx.saleDeployer, "UnauthorizedOperator") + .withArgs(ctx.stranger.address); + + expect( + await ctx.saleDeployer.usedConfigHashes( + await ctx.saleDeployer.hashLbpConfig(plan.lbpSaleConfig), + ), + ).to.equal(false); }); it("deploys FOLD + CCA through LiquidityLauncher/LBPStrategy without a predicted auction", async function () { diff --git a/packages/interfold-contracts/test/fixtures/bfv_vk_binding/folded_artifacts.json b/packages/interfold-contracts/test/fixtures/bfv_vk_binding/folded_artifacts.json index eb1378844..5e42acfb2 100644 --- a/packages/interfold-contracts/test/fixtures/bfv_vk_binding/folded_artifacts.json +++ b/packages/interfold-contracts/test/fixtures/bfv_vk_binding/folded_artifacts.json @@ -1,10 +1,10 @@ { "dkg_aggregator": { - "proof_hex": "0x000000000000000000000000000000000000000000000001e75e6e6bf316ec7900000000000000000000000000000000000000000000000e5e66538093519ec700000000000000000000000000000000000000000000000ea1d99185bf57a67100000000000000000000000000000000000000000000000000027a81f6b520c100000000000000000000000000000000000000000000000300a975ba6f13dbee000000000000000000000000000000000000000000000004a477a61f80a9e6d100000000000000000000000000000000000000000000000fd219fd84fb4defd70000000000000000000000000000000000000000000000000002fef0aaa80f5700000000000000000000000000000000000000000000000587f4bf0b7f2c5edb00000000000000000000000000000000000000000000000e89f1f317a49cf14f00000000000000000000000000000000000000000000000176869939c2c359c0000000000000000000000000000000000000000000000000000129747f04f15f000000000000000000000000000000000000000000000002d178d9b11de53f8f0000000000000000000000000000000000000000000000009044587fc21f92c1000000000000000000000000000000000000000000000009fdf61347211a781000000000000000000000000000000000000000000000000000008cdfd42e77de1980c43e1a2bdb19cf0131ff4fad06d6f11d7fd3279bd66410f2388c088f70a31e30bb64c3895db6a3301e9be27e79c07a3027013b0e68eb9f15025acf14e1972ecf23db1d3f80e4db8d3a8ce5e61a085775421a4ef5a8e770f626dc0fce3db71d0f0ff62f59bef95993de4ff0fc655fba80d47d6dd49ac6a99222e6b74f833800c35d744198e39965e71b95e55f6b34b670494a4279fb653f46cdce9e7d9e910a06fdd4d860032388619541f32f14d021660e8fa5497b849a703d63a58e930a195f63db46d4043c35c1fc90f21d2251f27e90874b191252eea17c8946d69d060959a597a665f3f225822e7cd537ed3be57beb9ea1ca04fc1baf6135541631ab1500bf70062fecf66545666e5f092e35c6655aa6eaf8d8bf20727575f7fc6c2c26c51b65b1a2161c60550ff91579866608e5f84a22bd9d525db9c3c0910d5a9d257d8b9fb3278bf7e3983384d4569ef101962e973fdc50af677e61249f7659180a6357f6d55ff9c3d0c3ded3bbcb37b1c49d08a76bba3d8ade40e097310dc2d2162b769f4d49f4a18af470a46bbe04ee29c054b4d644f034bed1b330b7f8624916ab19c29d424d52039e2f724aece13e882b30e012c588d515212a73f8539ef1167250177895980aea7471392e3ba028ec0715fae1cf2539f351dcf30b5d48790b4c6789f819bad95e55d1789e239dbd28d7cdcc56ad0bc1359ba6868a569d5d0bee03d62aec658dfd96444f35a5254fb150e5273acc5c643f0c6232b71c27d7109e197faa332c23d8b7c3f590a4a9972c5dc7f072121d62db7e63389c59c03726436b338944ea9825f719c9763ef1c8633e4ba2ee18556f162d735ae3c56d9e09e8ac442f6d947881166eef26a7b6fa538cbd84f3eebe8dbb6853e7399ec4520677fd7801b63bd7fa27430251fa14ea07fc97371a20021362a971be8ff709462c2e2be304a8baf3c6f57cecfb7e2dcb804656a16b3533262110bfd6bb8983cb16f23b3025152a5a67b3ba9d5f073fde73e67ff05ee11a29924d74412cf1555709498cba2023bf4d40d0fdbc85670c51082e006b173761c4183d75815fb12cda2b696fad8923295ae21e8958d220435e0a1b7bed0da50cee047f64c599848aab17beebb6cc9e802e65596603823a70658891f4d5da2d768e5acba89a796f3b8b3034f1a2c01add7ccb7fed20cc7068b76c2738cdc9f4c0cb7b752dba7393173a28f16aafb2f8423dc1c661fe8bfb5339cc45482f1df073b995b7e97f2a6c2a5824adc1dc1cfcdfeb2a64daef86e37da39bc9c8197716f12be4903f3228fbaab02bd7959a5ef295739062e15917703d8fd32f6a2a7c7667561282eb80fdb3fb6b2b123ef2586fd658281d49dca1b320e2b10851631c9b11f42550bf7a0bf737c3005918962545897986fb73a7cfacd6960fdef24f2a08d0d9909425f1b401e0572f6c2d7f7f1b16621e22948e190ffada5611905e724e14031cf80a38fd2431500edd1c857e0d04bfe118ba0e6620899d7e77a2d87d2a74adcca704a1b2c0186526acfe031abc4da8b5acfa7d50b2c69790ebf6c47f70cc3f43c6984704592a4e28316ef07835a8b598db038d52417ec5f81bf7f1b3697f5f1bc45e95a34112300f0b77c2213b2a827c30939cfd9282d84dcaa386c25bee8d7da7ca98b02968b31e1c714facf0d484cf017398d694cc979513571261b7df8c6b2c32a28820004f27df14770753cc82ad82fc23820185c486444867a8080bb57075d9c3cf32e23d1d643fda915e0ad53332c302f5e2804bdc2581874b0104eedc116923256237f41adb04fe2fb7c053b4716f2bbfada9b87cc1b4c22af9b77248e0a26bb622c3521f0e4244c66a7af714d4f840273335d1aa8d2e3159cddfd166e3d567c9268b472dc2d46705f03557130da85c7d69b53de20961e3850b6e8fb8591beaa86d0fd10b765c64e3a18d3a3171538b4a236aa5a0598d8e0678617a8cb90c62c8f941010680b196b8fefd31a1948ffda6faa57093c0e5bdb15ab793644f278ceb0ff43f12090bdafbc4674113de9a445ce1aaa725587a8e3d5aaa1ba49ff4930ec7447e0dbc9bfbdceafd180cfa38214128df7f02b436865f4ab2e5dc449557d5386c301affca4aeac69ec4f8167d8db4ad57d0e4ea4b4e60ac2f62187edfef5efbc7b2009d2829cf84a14526960e56b022d48ede9b144f6440242904da9cacb7f8db180bbed0140d8a53ef6cebbf37bb856bf73bd8fa5317e59044f72aa111b65483550994ca20ca1b0b1236fc724c0863806e5f7382ad322d66b21a8e1756f01ad84504a8c28f257e89a6369320f29a2369a69ea93a8a5afc6781483cbf31c33091a400513cca8308b55905d0cbdb38a453c8db8833cdba8c6ae2ff057fcd60e43a561ddfc683a046f7584dc4e2861eb9fcce302bf211e9693589c6e77a11cccedefb2dd16cf71ac62163775ddb1c3832a7b7ec669461968961ceccb928d387ee35e10a040c3f1cb63e84e1d359e36fe76052c8c44d4c7448e50cdd148dccfd5868a009d8b8e6fb0ca9f7e41ff668cde4ebe82df366177918e950849d8107e9a338c822b015efb43ba64949f8bcb267a544cd2e2485d66cc1525ee4e54f0d3dd0b3221e77b6ca1ad4a95d905c06145fd1a9d2821708e1ae3000e868b4f4707f38af1404d399bb89c3f4d4c4a5730271048b5d96072f147d106ac16cc13f20367e67b8142a20c858c21ee4e5e081ea05813323e4b76020518dd83c4e3eb040aac1ba862a4870e2f07eb53071ef39daa2cd981381a6adc2fe595944b16b8f3f487a870f2dfb23a1114ebbd6a5de16cc1f0fb09af8d174afba1902c520a90117ecb45afc04f60d3d3105d013af7696668a02aa56e31ac9cc3670265bb8a0d2103cfc3d072cc2af914aa524cdbf165a4f3190afc28b5810774e6c6b77669f24c3b35fc3f4119e0006c49411d8ae9918d6c3ff4207bea8185e836c24ba66c4f6c6caea6d341386396dde428fe86014248ef438f5ab688a18dbb82b516e360a706f83e414f119573fe6cc28a0d6d52ec6c133bdfcecfd4e547d43816e6fa064b6d7e93f532825ab2a81e8db45f8efb64ae9972a7cca6a163df9f2033de5a2b79240304f782d2dd93fa04491da05424e25dc8c6cbefcbffae80b6c8e792c0e5f43edc39dd7df23f9d0f14541bafae611a59f5c8aa26e03380b723934162c19a459608d98b347299086598038e2a91c73b7e0782ce39331f6d8512fbe07a7fc7b08c9b98f35082cbe354c1fb739964ac30f61d4b1f71d08a8b6a196574934238a87028427dfec27fc4436566de283beb42dbdc55e87188162e72844fcba35e452d654f06d80840a0f19ab6ecb4bab013f2e50d3069b47664158b0c63fe98f22dda6ce21cc87ad039f328fbc56d4394b0c8d0c42d6acd596912931ed363a6054458ae54789198e2d3bbc4b536f974d0b8bd865e9909e2e5bd297d45b64da2dfd9ae5c9a3a687c208e2a4ff1134488d967ddbc64b3107a9b23e7b7ca83402be13743a67d66c9a5e18418af5a327d3d8ddcd5e15421465c4b889bfa0e31c1126a743aa2f515d3c8b2a2f5598ad35c7bde623ae2bd70be128018bab38332186f3c8ea5b0ed790046421e604996d3d77c0257ce4598a66ffa6c39c755cc330dd0f827298ff230a11502223b2d5017042fb406f1cd386e642521d9f761c3c6d99b16cd03b8253982d331303a9d35dd122d9f49cc010e33c00eb51cf199abdcaf56f8648edea566a574d2e69611d85210c46ad4702245353f3616f899c74847da4456bd3fc09b84d396e234d15ca8b10caf34411fa4d9b16c44c0ae469bdeef548b7ad3a26c772810cf225a4af0997e2f4c9145a0b0e91ab7a1cc519dd271a6c3b58f2bc45c24a790bcb0bebca54fbef6125eb192cb92b1fa3c6bcb0a61c73d1efbd57d138b402953e5100ae1c1e7d107647636bee7a9309e1b1eb66e1cc1faf8ab5e8f77ddc06121a3f0244f4f4304b06453fdc75fa41db3e1b15bc1b04256ae0896b00f3268f67ff511f363f7632cadbbbac7997eb2722ddfc9d59e21c8d842b5251c4d3d76aed8fc50042272b8de59329dff728a309ff3d13bccc8913f2a664e2ba6fd8bdc0d0cbf42d453ece719b816784dc9aebe3e3e9053b6c22d14aabbec84bd9fd670e1ccbe7055be7145f3995da1a2187f9f16cbcd1b3d757624bedbf93346352a131c1840f187b4964ab692aad4d6107b75b2d22cdf9efeb5536c24a0ce84b4a57d30b42262d404e0f3178cbc291d51a96ba7ee2f7e14025059d8ff407aac7d03758f4c4260114dda8a818c704dfd406e98d804128cab1c04f35bb681799ee4a30b656ec202fb62bccc933a1416ad7b85aad49eb3180167ca52842b89ba82ac83b8d6554950ac7cf180f14c8bf1fb3b4362be359524d47bfac31b78affb5dc5c6d3d3026ef181ea0ec299ebdf35bf046ad6e9c45b6f18a4010de317e26295723ba5743fd700d3ad9a07c060be8eaf1c903412063aece07c05331fd2dfa95ef1060d2f601fd08bf69a7b0a6f2a4527320ca4f4969abce4099492c007166abe37419d20826cc1f0a166adb3e36c8f5a8e072a4aff8ca9bec1181beaa2b87e9a0f70f1c18c2db10c751f37041d1362319dcfb04fc5793083903e75cb4b510b777e81fc4ae7e3d27d853dc7a2f500f08e29ec406bd29da17b3c2477fc65aa413c13ae19d66f9b120edca4a240d2c8848d9fe4e33f695538043af26e4f6f3f19fed0f13f01ec1f103eb9cd40eafecb1d2fa1baab9e43ae7d150426827416f64a93865dc1f97b0610eb497f1d8364fdf8cb9a23478d77d66aa4a120752fa843f0c7dd4b37ed3d3d8117864f2c913d928bee9f700024ce3e597abd468289f4ba5acf754817f751a831e80aa16566b4f413037a9b7fd9fdd21400a23091807b764e3010226586b9ab929c21f256d595a95444a4e09cc4e7a5ff59d047ea54260df1d4a7a603a3a18a108984ca89e76f6ffc7369eb3215e572e865e2c05ca1c108dd3e592371a7f7bb626a0dc75a6b3d61c907c9d3f35895b219ba5d50f573e739942737cacfcaef3f70fea9682b9104dae31342cfafc5832621000bc409403687fef64c904c7d4e9a3249c19a2b07c46eb1d0667837580a380a5bf8d4501792b51fdbd8e215e44e307251e8b962cc1c1cbe05e792db1e0d4870bceacd545709283af3a9b5c3e917c690e4cff620d02eda82401c1349026a6a9809c49f899d86499d1c7900523454b7623569eeca508db51e54938b1ff954bd5abfd4b2ab5c7f67e7a11adb60119a07f0bfdb09da284b1450ba5b230fff60c7f4be779eb8d3cde7c0580fb5205f4802e0b8d987dfa249b777f42cd3d213f5945759a31c79211426f0bae9a138c9b90892c8f6912ac582724062ee8b6192acb157553f51f6317fd6ebb50af0e51419e6f25c2fbeb3e2814a21016c8489f37fe016d82b563409565692bedab6449f7095c24e77e51ee36c1d4daf80c9ef363a2f9b3673a0b3da7ae46993667adfabb47d80d6d7a3d15e84d0c42cffbdf6a2e75dc2815f6d5e0a1f64aa7d3dd22a838b11f1f6293522395fc29b60109bbadcf3de1d3a461d75131580ed56675b7648064e12626c930ca131799f18d1a050b8f2bf5e84366b8ae90d4dc77f15e98a84a3a290b13dde5201fa4ba401b2959be9d60793da5cafc8c11cf3e891aea323b5bf15d0e24a049c82597ac0781bab9dd41edfb58268a690625911be4f01c7cb94d934c1b25176d969dfe9b34d6fed67f9ae47d57d833d9af528dbaaf91285ddc45eaf9122dbe16539cf24a0c3dacc44ec98f8d88ab7adcb74bfee1a543c88f4e2e62d022541213eaeef891e4debbe5ea26f90f3474e55099d4b7f5ee86eef7772989fe1d1c25d9420a2e0d332e755af7b835a05e47a493edbd291f92126cf8f1855f5c1a244c568838828c4d494aaf5b88e13acbd8c3d7c4be49b0b3fd8b7ab4c9fb630e8afa9e4bf719bf5632d8910b7fb79b4cbba77f5810c1e6fe7401b5b41b91c61f9a5940657a8bcbafcf34afc0e9a434569a4f1919307a325dd86c8c10e1aa2a133fd94a58aac7c3be622a6f994261eac893484f9aea9826feaf71c375f073520cc58a69a35ceb6dbb2c1c65df95947118cd406728a12eb3854ee662074247b417b5a11b5daf6791ed17986906542a20f94515f08f218a8ab502e1b11e0aac770081d293b47933c806076012139832a8e64423c19a1074e9e0de79531196f2ea0bfcc8bc150670f2be8274e8ae922f2bbfad37eebc9077f98aa4ee75a28f4d7905a64017a72c409e12e9cf6ada08ac13b2dba318d32bd083c672a6c614d0bf9822bf7306835cc4c6de854e0d688c9f9cbb2e1232afd03be44541fa60eaac5c8a1f63971da1ccfa9936179c5019c8ff81a58b59edf5999d04508ed13c89645c6613ab155d18e2619002fd0a72004b09d224cb4148f271591cb2a4426ed904fb2d2212ad06ad516d91c6bf5327f0eb369bce8c8b21e7565d02cea64b2d7f3c34c91a5a0c816b61884489c8b3276e5561350da01cb1a119a6731003f2b17e86c1b30bdc6dc3b26d9151f5431e081c634ea1a9b614d7ae882a25d795af76b912eb841c150e88557ca4b44af991b82f7de755d4194ffd7451cdce97a08e72519e2bda060743c8fdba876bba64d622d3a327b26d7b2afa91a01a50d1d01d185360b8ae0170c51968c9f474009653bd419140cad4617e717a1bfa931648040b94e327da062e27394520550f3306ef2e382620771b6f8fbeee6cd62843a3250439de314019c4b41cc44de9007f07a2e5080ab05ac265f887744c70327792f6049a1fcccb27da87d4ab427b9b4c85af0927600f93fdc236d2289da94542315d39b7bdb768060b03b1d14a9cdc60b1557c6fd8b6bac4542231bca105e635f3281e7c62cc2b1f48c7159380fd0d6216d9554aba836c201b849f2a605040e9121314be4b866d1a2fae8c523bbdff12f569e21fce88ffa46cb9ced93c840921992dea2efd19460757f5c2f64cd24c7715a82de62612724ae70c8ca5d3cc85b63cc9c7fc299ce7233846d0922dcc43f9add1a83bbef63484e773b158ee55a5d30f4578266b1dfa2373becaaff6cf6b7170b4c2085fc791eff1de8917f17c9431bf4cc8ee95f61713c630086899abd3d7f20e0e8268815fd7983d5f3537e1c3c468c2c230cd39f0139087845c94cb7b81f812f650f9380f4cb21477b3a38b803134e9a22c24f67d29dfa788f5121f5f0d9eb5525984578f241189170113154c11b2254cab4bc5341007ddf7059c3331775cfb74f91f6191831e9cb87307dd294081121ffada8aaa073cc372846273c03b391c56748ddf263f87d4a60fc4b995ae8cb2b20221305b1aa0cc91d7da4aab51b807dde0675cc29c484c3c02c3456307b7eeb667a831d523c695ef210c609ba4cba27c99da6e51dd302b8fdae82d6d9f6a9aa396a6c3e30795648859df738869d97752561a1d88841995749366464fdfcbd661586ceadb144bb1217b3c7c2f44ecbe837472b2664dc8332fef90f9510cc2487d88b5927c2433456419d301db11a097d3a25997a3d2eeefbef2a7a8db4f2c7245b8485e932c50919631c4ce6b5ce255fb3a5cc858c3e3544c506117a619f707bc19c3efba1d95b287af15f587081deebabcdfb4c4f908b3b8c8693691b096df4f6dc52a41305543367023d60fd114cc4bf0affd9b5d9ffa07ae31d8c7f25cc412930046350c203cf0ae151b4ca11593001a15458bbce8485d4482fce3d2cc3403c04c77aa2f20209eaf549ebd9cc1a981171ababf3ec2ba1ed8e65ca6551c7788d52d515a0b089e0b1524de80ab0fd091d1214e4d5bdba3565a7b0efcc62d8e0fa9f404dd21f4dec3ff782e6ab2b5ae3ee011ee9d11a0d1a78b1e210366fd5b3f8edc769c2db8fe731d318984acb0ac6f4623ee3da9efdc6f9ff6ff3ba48567db397df4ea289db1c6510d93632c53679ff55101b789440208421573fd0c444df98007f3492ae39f895998fbf498f649ed257cda5f907d6ed197e2fff53a5419db41ac81a50d483775cecb08ae4c0bf98c73fced128605c54cdc366204ea89b06d2334a7ac22c92bd95e7c976ce697aa0c4f2dd1c14f78a292df8a922e8fef5e70f3a71e6e12a3ecba8a005ff5b5f0907cde6addd618f87dc3af2282c7fd224c4b81489e5c21e04a303f14fd115821b8d2c9c0c87ddef37c2f8b75a38706ace5b753a14cd404d0070d8090b5b1a0c558531a7626b40b0793f8ba8222bf81c038a3873d56881446ccea637d62a1649fb1ef247d0a342eb4a80b80e50d9b3bde954cb36da862292fc562e6ea29b4e4540670f6091f8e208f3654dc8775fe7cb2d0396c9098912f3841a820514ab2888d1336989d54d731f343bc117e3e5ea9c44273500bdf4c1ade9f6df986799bdea8d7f971c6914e70d406ca623d827a38c06e8b3824e9792853e50587a4b6d3dd71d34debc7e16ca04ca6b341b156b7393f409ded9f6f2c15cdf3390434653ccc1001e4122760cedec3a26aef2447d1ea5ee1af1035e28e1c6bdfeb3b32736479f04b19a4a1145dc0c458661d5b7d1318bb173d15385635180271dc91dd237698a29c921bd0894c593b7baa8ae6a7acd3611b5d0f4412852d476bb32f265ad24625d3e0682a8287d30a28e80d927243ebd4d31f233fc390176f79482c591fb0dc511b571e2c5f3542d52f32262ce0dd0226d4a0289de5c40550e19c3a7ae98cb29bc08cf3b6839097693797d5ca83fe66d684f44f9e9044232f95842896713cf8d85fadad0cd8292644b1a544e5a1313668f27af4f17cd600b28b913f5decc071740c52d7743edc53cda78277d9aff4e5ee8155c0854b4f135d1f7ac80785ddf791093fd7f722f19b2adcfd954f7aa838f2fe64c22214ce19c24064dc86e1c55333c3c32439bc8e8a71e8124af4078a7e9e50762bd0f4331327baf15995e2559ba95f44067b3e51a2038379b66ca898763030856451c29b2ea529401bcaf41b1b223ef9984d1c50793dfbb0744b871ac7385b1bed63140508a63c8806d470c88868cd900442157edcab4575b1a4a4841dc59e05610a17881163a53193b4f46038ec16d3a2acbfb22a828e1a66624123169cb5a837c2b2011ad3085e87c50e9c2090861347696909b69c5e58b7815ddcd0643ba8878b1e8f036cee3022f1da9114688c17a2ebf421eaafd7361287c4c521181d54cba83b3817308c53918ee0943f330dbbc80cf4cc32b9ba6b6a4c062a8f43b4376f1d14b52559c7ad92155bd042d3c5eebfb3507f296f6d43c94c22498614015eff3695f70e0ff18ee349bdf38f952f695bf514c1df01bf656937c556ac772dd985c440922409ee81e639296fdc8c12260c32447264782affadcc5bf4a79dd22811caab34269b381426ce376540fc588cf3619c022e7eadc42b5922677c22c2ff713ca3cf107db447a08cf0613c5bc706a050c83c06471958fa6469a56d77e3f45e8d3c331a535823cee65ac7c8c4afdb19949f24181d7c7f21e1aa4799980622831538c8151cc5566f52a7b13833b80415eac513ec1c379d80deb19bd856e201a18dfebc2eee4f168918883e7340b94437016cfa000c73edfdcb415138dd4f2e7d1f1b5500564a9eac0cf699202924402233d3cf17fb0cc71c2d36a5756e6c9a66a4d0e82e54d53a42d0f6c6e68b4fc6201138af40afdafdb86dd19c3743f5c1bd756b3a0129e5d2912421adc8599fd2153517102b90b3e61e6057536869bc28db874754062177ae022f1f1b0d740d39449cd7180f6e9e5a1ae0f570ab236b4296d4306c1a3fd3f59866db6bb3853c890f8993b31fadd42ad1c44c9c054f18a41db4facf2a7a8a5a2fc146bd14040dcd9dee96424fcff3a3d942093bb298b8a23f9721ee12abfb3f889e0bf1fdb61379cf4b45319e071b6e2a6d6c37be28099addceef0a0b65e63b0f17706ad47fb2cc73a40de4dfe018a4e3fa93c7b873a2456c35610a1272f488091e34387fd1db5b9e5b3fa673dd169652af9eafd7070bd4e353ce961b0b0f58259760aba424f2216f090ca085bfcce32299bce70cf9b1c4167f08ec2842f0022eeb47eccd16484a32e08299e9f7f6688c3d21d74780c284d98af5d22077f2b2d8792de1960e21ed61e1af6df5948ffb2f92d82cd8d00efae054c6730f75d06e2287eba7416dcefe8bddc2fc68b222de614f5642a8a1787b4bf652411071c6e00b058da7aedceb2923578667f49be6451e2f68336c6a63a3f1c8dbbf2126794ee71fd02d449d1b0febcdfd42713447a8023cd91866df9eb83cb0c84b2ea1de622cb3cf144613d6f7a002d159653c9f96d02de3d21bc23192c9f78f911f2c41152c4066d401b95984d314f961f38b6743f440a3f04550d4d4e150b7110d1b91be8944aa03a26fa598db4a5a0f69e75b533a2207c46bc39c351c9ba7f223fda2324aea7101eafad809321f8415dbab2aff593cb49291b7e8945641d8f425cb352ec3a15786cdc3597cd032670839a4c551ab6574edb82c6e76dcba9333268c043a2982fa0d4317237502c90a1200ee11e53feb3637ec36f551f122e1e50083ae2b39be1cd219b0d78c6958e6b5cfd88996fd748a14e99a6d2fc2962bc114690032e6d81fa9d2ac6aed38b94d92af501ff32c9b3ab5aee0b0e76d84d5cd235088e4efd4d536a88ce0b512f98777f9b0198e44e865876669cd498d3a55ab0dd10534d18582c409a543c9c9f87a00a79e62382382765eba9bef2b1c5e91be0031fd9f3b1fb2d76b661d1d0f7b9a0cd08ad31881809c625913a6eb18fe23f50f119e9782aecc36fee9ac6f56a3b3fe0c5240297a01afc7cb0f0675af297a790755a4ccbcd4f1866e1f561b5f03e8a9448e57528caf7148b9725cac6831ca982740bc1864a832bf1c7df354412f5e31780a951386ff364ea1f01ce880fc974c176e1209efff827a6e99ff93a5626451cbf51c4bc89b2f14e5aa376ad61e9fc22f7fd42b0f4378cd0326b21031805568ac8fd7d64ccf4e872e7cdff021f48f7d162205911801d4a6ac2cd45882dc7e02c465034d976c358a781079831f87a889242de0921ca6e9ec53f785ec9b695b3cd0bb080a6109a2d5158e9d9573bf42ab2c6f3fb93baeb4cb45d84d16ff83ebcf879cacef6a2c47f6eddd47f08cb9649b0b1ab3c4334de9a157252c42ed94d64ce65ab4666fdd06e256489ccef93a9fe5041380b0aba4610c03a7cad4f8d205c8c7477cf65e79edf520e85f0752dc4f9427bc42b9359c1a8498f935dc47fbc843d4fff6eaa3d0eb3a361edfe1e31a10502f073561801dae02a117682fcbd044310886caf0f756bee1388d1c260d1e0dd320cc47a959354d31a31e6f8aa49c74a388f2e6515dfb55b712cfc9e54397d24918b47a2576194194a6efcdc203bb3e67524336a62e288fb251ba07d348954ef92fea075d7e50785d56376bcff1157cc85e2670076c58a9d2ce39cb1be88abbc00413d4ccd164c11e57b2f8c0e122cebd75ad03376dc7da3355b0ccb87dc125a31c3176bfdd507b2fe2eab4a020b0bb85a2e75a40b46f7e25b4b7df5dd0a92ccd01bb041eb5abfce247954c34fa5a4b5cbec62d0cd0860db0f1a6cbf9d5f106e202c3c76759bd8ed2d8b45dee6b1470428729ea03d0dac9a65e9da2048c7f99ce04c3329559fa0290eac1a9a42e1bae853424cfd3fada637d50067aa58d1d4d943015b03c2309ccaca5a209459c4bf2c301e4274b1e3045453a918d8a2e214db618dff718f0e8287927480d5a3ae118be697a50cff71b45211e65eacf0357d8d8084a5a7663313d67403bd7ecb9140c76fbaf2387e480f89302b47bcc31d1e6d7005b0b9e5c5d626e9a6619c4ce5587637b45c44a486f7c62e075adc727eb01cb2213bcb92edf2e81be9f2a49ad512fea35fd981727f3140cc114f24f28e9ce950f6e1d41030ca825b20c02bafa07da5283d31726260659ea0575a470e9f3862612594ae7da751efe342fbf919a5ac503522de63fd4cdb644d7560d435f237f7c22eabffdcda551aacaa33bfbfd7b264b0de538421e87c5b6112d4c10fa837a670872ab3fd883ca6be74ec4a41888ea76efccbda23a48f3876c20007bb5e8fd6608bcf1f22036ccf763f19291926e1ae21061c1381f42bb82af11bc3bb8f34f1d25c566966289b286af824e626be44fa609fb9f8e5c280b11ab25da9eae47547d1134ba4a20c37fa9f9c9119b3f0946765dc30ae229db3828469d37c4af2a3d43255889f4da75691e9009aed63ec762f684754cc16909e6f55c5269ff4ed7626917c8a264883124b54e4c661f42400179867e83256dd6c8b5c70a6eecf33f69fd2e3c04920a72f5f2249b63e1e08881e8273da02e502582e13dbe9d0b5d79b1b226a8698609fc8716a362b03401118b9ffd1a8d4151e25d3aab6864751a3f82db0affa91866f5748099a7a04ecf6c582401622c3aee7b56e9926ed231141c26f1034408199b216c95816741706ac3a241e10725fb30888123fbcd7609ae4cc233041e412d88d21b96c2c899f9d3464d3be3ada75bfa0a939522cb1222f6fd2d201250460ce0f1a7fb9074a0f6fa3b172e262e1724003855416d8485c2d780f07f1195cae1b14cd3e8b70a23cf4667c9913433b4f2ca4488685c0d83f58f24902023ce9091da8bd491e977134e6d24ba3e0d62afba9b9f0c9596ce5e207a75f02e05a488621b6b1b2f82eab998582f6da1b507d69d16de20539f5ea50ca1e89bf3043c3d2bb158ea3be35187e000b92298549448cd4d5b896af4c10ff7c292a9b6234888e4059dd68e03f9d2f6ba50284c55a63594c727efccb615a7dc0336d3641caec9c366f569ff0f2cecb46452264a800c5963aafcc862f4ef840e4839dc1c25c64913bb18e4943f1bca953e7bc4ca7238ffb61f87ccb9b4f411cca65172d82a9b37ff2faf9238ab9a8cd548f01a5c06a2f0c852b173ed8bb2f16e4e49e7491a0841bc464caab75ddb9afac16a3c4c9979a16067072e41ec38ac45384fc7db175ce8432e258234b709460386d667b25e97f32010e41cec2cab1c959c68d23b09d014d1421ec2b3fecd14fa25e9c288f5e508fa5606c89a8678bf87d7b7c938002486328b9b29b627d0f6ebf8a77cb2e8a8038986c07da3a8b718c3fb80602705be98a904bb2f54859cc16434cde0b7e8a6de36b5ec8a6ea22f10f227be5b060f1dcb994fad2f44350307e50d5481ce1a3cde7147806383acb56cf1cb59bd25244acc1d62e14a82954b17b519a1cb03b5d4103beb62feb96e13350c9c7fa5bb07bd3a2c8ea44417d8a935a9e6099406c7b36215c35285a0614326bf40fccab901a9a0fcb2319ffbade7f8d15ced7f3f51f979809c892ac9f6bfd4c9a2a1a5aa04a128248d3cb749415d418a4384da9bebaeca135d0f28ba169e4947a16ca1f52b82b51b5263c99d50552714a9a2fea1a5d0bd680d2fe441f8abe121960053680300e3f56615a750fc1d8e8bbdc1a74feb42de4f686920a55b33c9d37364c1261f9637c6e7ca508cd210914189000700c7299658e9f007bacaa37980f64bd6cb249cc42f44e8f86a0afca5ea0f982453b72208aa9fbb69df5b2d91e1cbcefebc21027e3be6e9bafc8efa9000169e7fdc38fe27e534e786e5b363fd2fb3c706ca24bb1c43e8e4d331239503d6c0820a75a673526b2b4c8e33d810f0cd432caeac2961cae418d33d3f5fc24a83452e897af28d3d2cb3e94d177fd1eccfd030ebc1186974863f6fbeed1d5b16ff8eb4fff18629f25349caeda0f588bee9bf6342c42f5b264f5b6c882fa61c8e9d80c4fd68019c6220c9c04a4fb50fda44c1294f0417e5c37beb768112252becc0cb87f4f7b3668e7acf882e119b83517186944b6402d290f7c84aca9901c8c2e39bf76cdc6e6dc58affb9a31cc2eda3b04ffb86700a2cffaa7330da0604b73af6be18bb8c028958709e814773086372e6f300e1a31df307488514ba7a83b1776aadac1d20c306b12776e8050edc6d21e3cd6e23910a90de67c3d41d621aa294394bfad19c0cade227c268ab93bd9c69fea9d554c4253a34614ea3eb7565f89a7b76c6294edc4359819409b436da27f11206bd097b26c2e4d4e0b551afde56514b47c71407c1b1faf7470958c3cd29ddc98d4d57782f6fafc9c9b673e30ba8387fc5d09b2d6af4fb50260aad9cd5efbc5232f9c79b30612b7f03f037f16a8a5b587aa1556ceacd8c1e3cb7a936a7dc132341ecb5d0067578f8637ba3fdb938deedfe74d89f87891021566578a7f2c238794d73d0571a77b344f2989ebe251e7e1ff9029bd686e1608af6d7c5b275671f67d2083830134ab23a331c5f5d8208748b39f75da694aa518406fcb20273227668c7e598121a8f82dfb5d84f876fba9a258d1a1817968395ee75da396f59abbd3bc8d3143a11a434282eeac18c25ed67af3b2733e69375ecc8b0e604e168f2f6bdf41a4c9f0ba4b7b622754e332144a32a7678575beeffc8cd293964509b5d9bc054a0792e2d967ff97e72a34fcde286456d9a94e248963c6fd50338647d8996d23f27298118e03f44699f28d7ff31f31c5cc651b47df9ec3b9b869ac0bc4f66bef4e5b14c0e186b0227bba2e618527749c1c041b9e4b5c39abb3a77e9328fcbc0cb025d4225f3bb62ce3504f467d07fc5ece3b1ba8a5ad7682a503bc7a6502f37f4f0fe13106355c8bda71d12797aaca34008c3a006fdd066b64da246671b4d0f37152bd4", - "public_inputs_hex": "0x25832c071d6d99ad42c7b885e48b10100726dc0de03c0a779b282c519ccf538a25fc312a5f0009edd084811b739bcbd529987df1cea36f5a24515386662573960000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000c946233fabee579beb961008103a4c8d00000000000000000000000000000000026a297ba8fb72294cced25b49dae7c92208484402633db0cfcb2af11d3b66ff07cd7b6b12c3a35e9581183e399d53f72b942ad53baa7c6e1a7616e4f0aa2eedf7d67eacc63696345525988225847479273fc58215abae6a73503410e8209a542c92bbea2ba8e6fb129c971def4ca8452ddcd2c1e0b08c268d942b52a49e7597873ccad9df1b02ea1ab854e7111eb2dd188f793a89424a83a3e746a72d4cdfd5702a7944deb0fa2ee04f84489f4af3c22f5ffac9352fe35d950a4ce548798309f5d896dd11f304f93d83cdfe284b37a6" + "proof_hex": "0x0000000000000000000000000000000000000000000000038e146b049f5a2ebc0000000000000000000000000000000000000000000000018fbadc502ad14036000000000000000000000000000000000000000000000008a2cf332394158a790000000000000000000000000000000000000000000000000001f710378be6e900000000000000000000000000000000000000000000000c08a0a49dcc1784f700000000000000000000000000000000000000000000000479a1f04a4849464900000000000000000000000000000000000000000000000ecdd4f70261b68b8e0000000000000000000000000000000000000000000000000002bdc67e780e270000000000000000000000000000000000000000000000068c088e96890e815e0000000000000000000000000000000000000000000000019927ec72a8a0dea300000000000000000000000000000000000000000000000bc30ab5296a7e92200000000000000000000000000000000000000000000000000001b1768dcaf05300000000000000000000000000000000000000000000000514324a6e0f591c0a000000000000000000000000000000000000000000000002e31090bcd0a36cca00000000000000000000000000000000000000000000000eee316dba6c9c144300000000000000000000000000000000000000000000000000000fea047711602896aebdb264dac06725b3966345b9e4ef9019cf6b810f3dcc51860c5764920f2014eca6c2f04e7cf3b91e2628946b6a3718640954ab71d723bde7bf502a9d1a2f34db5ecb0806f11af806f3bb030c60338ec88b64ffe73e839f5c5870542b041441320f7b67e34ad210322d2f5fb516ac74a4f27d68106a4b6bb8862f403fea0fde735d225bf57a92f107fcaf3280cbae16c9c7029987ff0a7f4ee7a20274e10bff5a51024341ee9a5663df40a9283f6b04fc17bae3b7bb5a1581785ff55d700c1e06cfb9c565754142bcaa7b5b4cbdfe8d1a68400b427ad5a3859a91572c9a07739d0572ceabbe0d52bd4b99c0f620ca8a5321b1b804a8a139818f04a251b12a1ddf209a0d7b853ec134beb579539c5f1c5c12272c86207ade7f36723a0c1808564b49f500341dc967a931ef95cdf14fe2d6e213f59a4c08241a0a630c0a20094d9edbb54cc50d8085bd1ae3f24c32b350465caa4ee2c1f3f0e98d99ef1f280d566dbffac05a37803f1edd0b55a4915e64dd5cc8040dfa02d698b48ed45c2b0512682837918095625fc2f6808d921af9f7809b5769a1d571d5ae35c1ecedd22eb10c42bf1697e343247de4678b4739d8468c15ff49d0789daad865c2a4dfd520a710c69acd71d2e39b1b66bca361a77383686b095267d483e9a65a7106fbc129b9018e43e5304f0560ef448d298fd8c68d0bbfa7f415422f83b3a7c0c1149d100a6294b4a17081217254d550f784ac02a667e1d23540040f976e34a6d2059b0ee597f3b9db445f113f30e5498a99ff0bbc642ec6ec2902fcf42cfc0dc9c8ce290fc0e267a0be0f576ff0af58e4a68a01ccc44047c891eff4f54421844362872cc5f85a0b7a4a2c5683e6927c0129ebf5aabd8df471e02ae39565e11f3ce0831c28465d49309ddf5bce3974f68fefaa445b99e5aec1d308991ef090362bc9611951db034f99dd86a24366d10c24747aa78338a374f51595dec1de2f4e35604f0fda9c6dc2f4a1d965603db25df75368cfdcaf4a4e5c8defa96b67e839008bf02d308407c26a577d97b478ea360252b08fae8d535c3c54649a262d2c9725450e1db0494dbc8db5b571cb6e856c23cbd330154826c02ce5ba29b4bb2720ae713e0e751541f9b404bc211cacc9691d3c0917a149cb51e4ac5ddd944b7d1bc9668d2bc5615a7d472255e69005a613c4f949a3bab81819ae67ec962bbcc2635ab94e1dce72569ae638d012c6ca6cf21634eb535f80177106ee176eea6d1a4c3c3f032e3839c2b0f5c9165b77e0af36691c7451341d88be0a519f7a87b72170406ea908f56f676ef12fdbd5cdd673d77a9fe1106d29f5be7ab396ef83fc3cdbbf70152b69c3d9cf3c2da2f6e32224297dfd0d86e41bcbd3a04343c5c05547f1fa94412933a7e458060e19ce1faf9db3c9ca6e069a30c1c4ee3368ea06f3f29022bfdb118c7245fc64000f57ffdb78ef63bc87ee99108ec5ae0f3b79c18eb49a5a87e8174a8fdb7cacc0af74eb2668d45b1d999a92c3ac7234b0c504b22981c0f44fce2d45cd8817e21eea6a471a53ceb7aad4e2f20efe82889e70284075c63df31f1802f49619073b093b611c5d89f0b69bbe6510cd5c0098862491d94c229b9fefeb301a1359c653d0d5a264cc62a8ae6c08b988e588ea96f11355bdf497f26207530a887415ad40f0da37e84607ef267fef2a4392874e3e035db64ed8b4e804a5bf27ca07f83779e8ae2cf14654363fe4f88f25093ed819e433a9510367bea91d9f2db1e26383a4c8a0df489007f12ea00f91d6d00a39eef342cfce4049cb1005331a81e08d651c53177fee573a87fb0652bf0f8b6ae2eabdab0cd53a222a97bb1f2599e409604b872622a24d9f0eda3e56a7cc78ec9780f28a329e531e56b416540277caf2f5367e0c6ab8463bb850496fb67960433bd6fbfdc0f384b3734ce64902f8aaa512adc680bf8b4ee6e8805006c28ac3fe58625d3eff6c55ed1418b2d80af9a08acd5b9cb6bc4dc70c131282f2aaa7b77da31a88736f45781f136f23841e512e5a081aa544a6d5b4b7da8fcfb871abc7c2aeffb7d5c5f36c3a47ef542d0a2f0e77853444ef1bd396138b4692c68bcbc71b49273d6b85c43bd3d636d8171f9b8f9e1a6cce6b155ca1b8941e29f9384fb8675df1b925fe9d049934b57d2e0344065e99856c0f58560c7b61969d8f8a1b22185d7cee49e155dc41eeddaf392055b9fbff12a248c53aa009b31d8d898fa411dc0ae2f8832e8286f7a2502af118c76caca54feb76a5660e2e925f0a2fc3e76a09df485fad25d6454f2078345d247e5986d23f8130dde014cc70ce4d6600102584d37d2ae1ee3339cca8ebaacd1b0bc9c2cfeba8bf0d26baf663766f1a41511b6643f268b8a4b18e72dfc133af07299dd37a8e816a3301dfeb7186b727fcf0d937e36aedf38061ec699e43aa9622ba36764425a1f2d8e63cf07b25d5eea48a81658ceafff1eb99284fd40ba0ea0e8070b0f18b508f8dc8b73ad007a52a97b95997da9c88e51d725fb0322dfcb52b15678857422cd69a0a38d0b739c4640cb3a65bb1387a18b9c597802a7008b307335192168f583e82957952e50c1bb0872a59617318249d8f63aa6db33db7d90fa9601322e78210e75087f0f74d98f6de55c577d149bb5323f10274cc323a571e1a58d0bd353b5cea492d943f2b9ce4a7c1c597290edf32132364b69af83381298d05687cd395bfa6845e299ba59cfc67314a07cc4347892ef618d0f36a3fb62442db09f8bd01600b9d4797b1a1855cdd9c2fcbe56ddcf7aa3c9a6f7808d7d602c5ecf4a2882da2d6b21cb057b030bf46f1c58e6177a97ba2aed6b67bdbf42715a3daede130d745cf8524e663319b106661a34762cedd498bc1f8670ea78a042158e87a0661db99505ee3a6aae62d8ca251044674fde2521bbe3dd236f6c35a2d7f3bed582584f5791362da4c8b52a5c85a7d9d698e0d2ca59ea2fc4aea21c2256ee0cbe2b992e43e977d082c1f7979c49189644d6e4f56a293347de835599703e0ada4a22af6d5fde5b44821ad479e26d73e1af7994d26d04794cbeb8ec61c05d6afa779be840e98bd1c3ed40836c5741c99fc55edb8f7203823f6a50dd44e19c061d28ad101a955c8358078cd5490855af88af9eb62704daeea6382ddc35400d11c2b43f4373b045010f285bd78c8912d74b74f7d294126ac5e72155dcea405e9031605c04becdb06f8b84e244e6baad4146c7dab4572f9d1d4ba038d452018a742401211aea375ff3f2b56acfdb64639a433002aca37cf20cebb3be5c60715ef324c76ea04192181fa5121703321e84713478c02f63a9192f50067ae13421931e93801854687d8e60ddd847100095fcd9bb2ede66d370e647fe4f0d7fc3a2f363c468eef65a3793cd4bfb3e56d79ab68a3b09be67ab2bc3c2da6cf38eb631310185da9dfb841b5467e8183bc21132f88582589c25856117e167d80dd692c076cbda0ce6d6d2d5dcfd89311658456916c62de398c5638e43a843366d817cc0ddc7d7de125fc1c474608004580282688063f5aca6f06690dfc491a8ae9c5c02ecb8837abd0b8af52d74854a8f15b57dad2ce4346c3a57b9f16e8708730a6cb064b6fe9fb0089a65dd35b6aa970b411c78f7b2aa5b9400d87fcc1acacb3e369171c47f3230a9f6a371d420d11eafa30fd4658145178f4dcf5c28c34bb19e1a123b681a4887939e0a351aaf8fe25680434596806ca11ea526051c082247bb87a209e2e9c586721be984a8f864c9d3be80c8c7523c21113e1b18b3b706aa7d8c21d657c054d8769747e011d716dddfbefdc0cc7fa8d471e1c58ca31cf89c1d196218dc8b201131c3a34389ce3fee2ec9ed61ff3959c451e5a0f020c8033efcdfb0b5bcf51236b0b6541029662b8a3c50376f053a85124291f173411ced41748f009c22127832ab9f36659c9c87f116365b90af9e2392f5e92086b801e6fe6376a2ac461be885eea8608ddbfa4db732feb4e936819e6446f9697249b38e1f716292348b3287168eb24314e2425373dd6756868bf66717995f106cd9c871a5a68572cee61ede06b11f37d231a609bea864675dec910763e710754c461b778f906bd095df757103c44f4bdebdbc1409647a6892f67ceadc6bad48589824e199226432fafb8eda60a1202f598713bcfcb94681048b4980b98760455343316b5b35a9b295941a67f7b4ac4f1a24c8b3ed045247feb50d3670a5a918355ad64e5cf7e302775f423dd2acf738d94430a22803d157ac60ae399707c08adcec7d41e03d118052a31d8a16b8a3aef7c9830df6815906f0440531b33a10571f1ed27d84cb75510495df8efcb714197c94f8c9fd53b198efe93334064d84b5c6c22c822c643d02c3a053da66d5c4cc774b5fd04740d89c3839be5157aa9ad2d2169bdecfb020c002199232930c8eb7fd2e59c70ca689f838a282b03d2372e35f3b52fb332d8e7118f60a499489c36626769c99700537dd8f5e80422d92dd0683b5efc3f21690e288089688a1421854d4653247bb20bed1dccd20b56c9dc4daa52cab2a984fa03024b5ad9af64d50703a4de10752bf3a82f835938640c7ab80e0a0190b0a2261d2ab6a42d129566586bf0db21334d4336c2754b368cd5ce3bce6196007c51a2b52cb2a7f29f13b502784ba00d49726cdd8f2315488c2511acb7f64acb5e86a6fd2506d1d03a141d4247fee5336d6210577a55cb80c60607c3ddf33d9e3ca1c46f0d62d50ce69948edfbb234bbf9216a1aff75647b311f859620385c0acaa4193f2e5f309fabc40436ac440f39da8db73698e0a85edafc0836ff33e234321278352394536d28d050f56da00b11fbb40fd5f8a4566c72be8e5ecfacaa7d38c2cf3e0f9e2384d56265c064dc420fdbc6a73f8c0e6e04b644e9cbc61df97d88e9c529145184b011f9eb91fc5096842a3aaec5b6caee0899dbc249e19b2d0a176d35b82cb08a43f16b3b82da140b313618120d6906492df6fe2f189703dcb70023b64c28fe97dc5ee19d42a34cc5e7abadd24ffd49c250e34a4741c8956e6616ec6efe20ae8c503e52ce778350c20b65b6f09a5051a7e95694565ea4bf3cf1f7a93dba00e28eaf5dbcae67ebfdba29b7068b913ea43126ce9a9a39170ce0e05aff43bc00119d5e9e43798de0b003442bbf4b3e07af206186e1a96eb324bb77eb0f8c242b4d00117d41cb40900378b7235ea285c4f1dd4cc09843501eb4e301e1a7fe7606f17485221a20da729dca07ec410db76f9c69f7b723e209e79a605b5fbfdc431f6420d8788a71c1af68e3f2d8a68768e632346dd1cd22d6a4a2c6268c6224810247c6cc0c8cd333a78b736a02f0d029e760f7f68589298a4919b407290a97dd2083d9af9cb142c55efde8b4c69193ac087c976a0dee9ee7b5f2630707a1632209af5ed7b86d5f08d8abcde1dc1ff387ad7a31e9388ca089e2f5f8340d54aeb0232f5ad00ce41cec1f6647b1edd63ccd207531485b06b8874ac25d769e17b22c1c8f6d4b1b5251c23e315c2f0a6b18b5521fe3ffb7fc36c156e935e34a3d918c2ab04f3bbdcb1307886b001258e4ca0efe37f22a80fbc035788ea7704d8681a4277cf318460ff17c8f2790f78a22a05264e33715a085c7429e7181944af22fb329334c5f59fed35528ec577f296de352adb6d95822e85dd1493d81be726aaef9165c3501ed7bc205db385083ad76207c9f586daae7f8c78b4ce4eb3de59338c426c7fb9191c9f897feec6174af661fd70b303f6f50a23266aa7cee73e15d320a0cad09345c02e98c86f16b6b80b94c18edf982583b4f96e047c1fbcc45c4205618b64c24cd8c14c082dc62b338b3c048a4293481ee0bef71dd3cdadd40fbd4f419a02b9f28c205241b18241d5e117306e89dd23a884a3fc54cb8079a924bebbe11deebc2e717ffce26862b8b7804bdd09db9704b0623a397c429054666741d2302640a3ab0551496af654bf68fd032082432e7e8596aefae1ed73ec7546b6fe41f13b641fdaa7396a87890b29dd70425b2c60daf38e4fac39375c7da128620a42cb6b472caa894b66488e4056e20adee4b5051750d58f3c8f9e21e5f69dcb03f0eac9e69b653a0485052211d6495de5aa9b499a123aad61eb126d430172d07e2090d8abf78ae916581c51b2e3abfa8c68740a7dd7f47dc90d8ea573a6460059a273621d0211dc09cf4274902a2c2681eb4d62fbb27ffdc5483473734e87e78621e05869d22c3adc90ced561fa5d5b023d494c41c392a690b001dedbda3dd7fdd20f479ce52973dba498a97f2358bb08cbd67c571c90d6cdc69a1fee0ad59de442bfb9bcdbd21e349693af3522519fb8c32f21a588e1f426b1d95683e4446ef340e9b664df02bf2b8ff0ad4148f50d517ff24acb4b9b33a01c3e9e0d13ebc789c0b4686f8844ffc6f638b7a90dc67bbe0a6b00d0174f3135d48eedab7fddaaf4c165a4460fca15426dde3b0718d64c4697d296912edc702e1f5258f0d26939ff4220fb00ff5792b4efb016789b009d6812f1c81d5a4dd82dbee54d9864a15068121598b4701b74147dce1853d2ce745710ef36f85432d5bee18b5fca0662be8bd0a622e484d95516e6a425a4919a4fc52c8764d106b4bcb6242dea3a8b3cb8b58306313fc37588ec9f6aa94c22ab4151908b675806ad6c9a95df02143ddc913ef054bf6e35f383708a2d5259d0a2336ddbc2a4ef93772e289a8a1710c78898caf0dc63980253765e3ec76174601394171065781698d149637a81dbf295fc6fb80020d85ea2aadf1f6b2d34f425aaf14d4a7b2d32afcd5eafa3013f9f76d12d0e70ca208763a96a78410d65d925326ac2a85f000eb77f77517672807b5b279bbba1914c7955fbdd9ba738021e16940413339f9fc19702af01758a8964a3648eb5f2b38cb8f8170de80e05194bae8de9fc3fc98e7d99456bf7ed4e9280dcd188a2909e5b07d0f283c47d1f7f9f0703a70c3d068d48040b976324abea0d70af38dad20fd871e487c34122fb7fbc2e808ce4923f17beea6b0c78baf481c0e7a7782e6301d886ffd464f271910ff70eae27784767ee6cad1b7c706568b8448f2ae1db00007db90b02058d7c9a26e29392545cc731e629682b34cc76007a0e2c672503c02b62b0a2861aa8b1541eafe096469fd0fc7671db638eac0d7a3c861af4e041b06358dd08a1d1fabf41e2caf56c93619dcaa1c152f84f123b433b5de6f526d652dfdbb3cf6ca3b495bf4e119d3192744aaa057e59b25c4dbcbcb1551531c925a1fed585cbba1e3e4832dbe93ef798ea3104aed3c15ebb35567a1b0fba49c080f1b259db8e480c19b86f74d3f12242ad8ddf276551b5579f889a925699391618b2fc3fb82abb5dabc4b164696a158efa88188d644926aa14601c5e62ef22038b12fe800ae4bbd980802c5321da39fa532fb6546a3996cd3d7c89e3a22e390bba90555e59695339ceeaa217f88e74826783c3a1efa8de6714f2275cd9067e427bd21bf502ff800c2cdef2d00ff1baab89b91b68c2c0541964433c87a099d90ddbe0a8798761cd55fee0f4d5ff34e9a58ba87decf37f3cf420687bf0371171ca8252f6e8b2f4617c0fa96b9c65542f0cb40662789e0f2e00b7f05db17c528cfe216108f9162d4eefd554a028a1db73813040a371fad8c790996a7084a20eaf0e67507873b7fa254aa0638156ce5e8b739179fc965a19dffd739a07b3a1a6a71bfb42c1b02e90a38270855f423d40e83bfa56b31993377625174ab859dd3a66a2c880c1aa42a078a47519c83c3718af991f3475f0951012ec4643a53495feb2f534715a02251c6da883227283e90750f5c68b6d14a712291ad78b16e8663ba0ada3d25809bd3d1c6bf98b92f28bc97921c7b6cf346f57797a2f7d8355f8247f29fcf1e70b2e59b61d4a90e90204e3e843a475f1525a80f27f2c139aa440bee68db3b1b272f57c8719c8d01e533e12e6d01c477823c46343278281d910d26a6c4f3c32f8a20f59a9785a883d74bfd8f621578d57a4b149b01eb7cfb4b6918dcb7b68f139b1ba1b7a052b79be15a40085bf9b91ec9937f78e54d385f2a09a747a7138f2e909b486a842c9943fac57c89900e32d4c0eccd701f3d439fe00aa9bf910d2827178724f0614e8084a6c472f74c0289623b10ba0edf264e88f45dc9d80079cb17ba5e3ece6e6c3c76f57b6b4459f8536cbb511054face7d1636809a52d7904d0bad02e0ef3e5992417d9c6c356646d4ae6f8ef5ca2f8b45d00d22e7f3f43a321d402a81c0794c86c6cbcc0b8f0af9cdf907836ecc4dcc159ac76e7e9b2d4ab90476fefeceb4acff387ae5e1eb6abe96c97be936ff5523f2b2767e3e3b1f0d4128ba97d019c749862c304731f9462a2d22583235f00df1f812f7fe18ba6afd24260768907290435becbac3106b72c5289bda8f55ac47bc0b45a0337cef3f77f51faac7cc16a64e8d42db7fe0de579a842761f013e5c1ac835ec7a7a4d5a1c50e061863ba5f8315e1d829c7c1f0c507a69075f5c5d39c659a49f0e766abbf080711f8623621c44771dc91147349f6208f5578b294a9ebb3fc3ed64da4c19c98982ef5d818424ad862d1f5dbd75d9200c40ae912792a93633ca6fbf0bb199000da10ca86481b98e624f81d5c0a4acb3bc1abc06c5d113683ec1c40e345ec9ac66607bb8da713163665fd11d4c11bc6ea47bf2647ba10234b4f4e7a20c32f8ced0d125d027030726358be036baa138cbef9ad966d57399ed14cce73aa45921d5254099bd00d5f347f292bae143b759c9448fcbcca7ea6cbee944f8a00fc8aeceae222362079175d66ee0c74e798127385d4e7ab0103a050e05e16345d82f4e3f98b1ac557f7a3dc1a6d7fda9a7b5ac1ed91ed23fbaa48afe8376e6b3c52e0c40bd10574bcd3a407a0a700420cf3ff6bf4e9d2c92603b0e3ba2fe76a86601f5b58b32b1d5c029f31a366f0a64a899988644c135db3aabe1188c1937e0e67d43784cb259c6df89d2678b2eda7d8f7c59d3742b405fc7dcbe70eb947e80cdabfe853cd0ab21de8f0cd93b0e975f77dfa85612421e01fb87848c26875a6423b34de9a820d6ca2314c7d276f56fa815f374af9a3c15ade27bfd6c74a94d8df257ec68dc401969ab545a44962b4480d7787827e9220791ccfab4ec5ed310f331ac84d41d620b975acd3d6d7244e0c9bb86cc0a45d5b86e6a8e0a8387020337602fb9664fa1f42a5e9bace6f4a0adf7a680459ac831e9148dd067aec1e6247b703bf3fd541262169439bbbced69403ebe737c5b1bf602ec254585f3533875af458d75f4b6b04904c93b04484c354815a73fa7a2f84c69b33822bd91db96ba88c06b4e7083f11ad2f5ee09720c05014344b0a80575011bfc838f5691b8401628a0bf8f137e10ad62be00adddc2e69fdc00dac0bba8a8dd99116dd8c0475879304f478e4d0b419780617665f19a173d472938bde6fc5443a221a8bb6ae4fce38148cb97e9a3c2faca00d80752a8daf6e02a7af2b014f0ad3d05f16ad4cb3155fc320b777805b1e7a4de59fd26fec3113ec4ff03fe8cace4e8a5c352032020155f6df973172802d4120638812c9ee71880c4e97cb48d2e1cddc2777d093912a2b6ee71de7d54e12e877b827611ca16f81f1dc67a480230e997c5b8437c9b4a99997b227bc034216731efbe60e9f9b3fc4a0f877a973aed8e2a29baf5de5b7a63406987c15e0670464c90090c236014d4b77d3455b4d09048f5123409f32d104cb7f6d59f3a2550f13d808471f826a1338c92e579bd02cc4f12e97833c52547dc31c1c207fb3f7024ca9687a18b57a64626be3d6c34563e9bf612404c60dff70ca16804759818329d8f0fae167bb6649e51caa8831278645f950c16888e24ececc5378976da9c82c894e200a8ef1fd2dc188d41e70e336ea373cf1ef81751147d768bf50c2aad418eb384eb68613754260e9d2bea84c850924d171bbda4c856a78b74f4bf9e5242bebf28c2c3c53437b81528c77b5e14145392417ac24b77b634ea8709293dffc03b4d4d57e67ce15a550d6d3003bc839d1beadcddd0abc5157f06c3542ecf35a030ef12b79dd6dbd1f86bafdef9613f63ad08a03cf7978dbcbcb9ee67aff50230c810be573ac148f794c0650424b0e7992692d96f482481aa5638d16c88e31812f60dcb63c920e69f88d14ff0d18d076f8fd0faa93c4b8c2bbc6f41fa2c0857f2a656284d2218612e128640002e4af05c6be31686edf9974791630d4ae3197b8211e31a04979b215d23a9834258fe87c12d45d77944db3606cdfbabdd010a05c0d3e23bc252d9cd18a0cdf2d9cc94865bfd0a803a4ec27de80a01ffa5dee9b9e0ca42cb9c506e92f339d0f1504199b9e624a71e581ea1e531e48659f75e7ed992c62e94ad931b28ae7495f5fec6b73eb59707c8cdf58149dc4ac231f7b2b770911fe287c9f702d993205d349896c2a4e491b9a6c5f923c6195ef8dd2d064cce0249c1753b3c38b42b2e67c8e75d0eba600108c3c400fbbbc2da699a5fb635a0707212d96fea2841bdb3a184a7bee263731ef6f4184fc903ae392c4012977f4e32b41f65e453c9892c2f685f43636eecf49d451735342f95e842551c9f2817dd104b05865b8c0e3fca15f8c9f236b8ac32287d9b79f1a2b2a978641c76bf86e1a280d8af6b20b402423863ffad8270c35656613c8eaa25d2bddf64ed473508f442a0ddcaf8fb65ff255b991762c99df15a0f3e406b47e367a5290e9a93502cda808f5003de148bdafa621f1056d271f7d5b3d9ed3b3564f7ca2321bcf5e9f728e15e13d7b117abfc92f42a4ab68c260e37f27187224246a90a2040d0e133ceb3b194a4dfe23338883d6cedf803623c9e7e319320fa414208a7e9e083e334e7a102ffaf3053c5ec4dbc1afc1fd0a1e2ec41fb236993d167299e4ac66fb39a6ff7b2dddf7de35183aa28dbd3e7f55085df1e85823b7a2a1415afa97ce1fba342d0c1b76ce8c291da4eab121b9bf1ed337fc3eaa48c0519f81beb378a21cf30de0211a36c491e17075f9d390f74af0c916b4b72a52cca839a8ec77453f5069a11ee606a0a221273d418f367330a250f685df6535c0f011535a661f5e0329507faba7086cb2591cb49bb2d7f9f36cb318bcb71b3a0c36a764de9159da2b359b42bfa40c2be677dbac3cbf3257f23add29381424b2c501161f347275a0eeaabfe55c84297b2d313b2aefea9bea73663caf91539278db07b06b54ade556bc5f863552870016e61cddec04e209f65b23a294dec5f51ada9a71877524071a6ea37e323abf10477b2c9ed66ea8cdd54cea94061109bb65d0c735cc408c908a3f03d4082a2208a7fc84c0a72caf1ffc0e6ed1e76508a1b12281c8c2c9c689539c18df34d9110b40a9a11243c885df0264afe639e088f4eb0b58e37f66646193e462e31328bb219dab1e270ed096487efaaeffea549d1155f6880c13af3e2cb5b0911d2a464a01415ef211740de758492628c66e9dd91e7f4e34764d67f6752df3b1d916f8d2145e0e5f0f89f34315af664917caa889c19aea42b0d33dc42316513ad09688731a9d0847762e6e23049166ec03a3806de98ca8f294bb1d5a048d4edcf28e099f109305e145b3d4eeb96745bbccf43a9539ee56cf4e98272ab0748490a629e802252bbfe3be8fe6b8385583cc012128fe518bcaa30542f39f988b9fb67442b0fb0e7504643c613e4d19af19ac28c4c5836e764648abd2e35ffa5686349d7809c70ce07a4620bc5fd09ebb0ceeab9325bab28cccbbef4dd184df360140a1d4615c202b344fe389b9dc87b319c09e0dbb55a2e43baeb87f1436120c1252986972141ba8ecc47065d5ec5f8cb112daf5090184e5feb2a314be75e75d274315c55af2213ffd564c562bc0cca937ecbdc9f30ff98354721e0dbf5cde8c067ce35cd6851ee308e251e86c55bba859435f09869c38758ea576ece6900fa2b8be2f89abd908ff57ade95004643698340099abbfd7e03c84b0318c007f4f05278d99644dd92a8c2dcc238e60a4dbf7d3d55c9944a71904123e26ffa3fb020ee020881ec78e002a17629d2c4c3964192849fda0e6adb70e34bc84f1467ebb9e22b1c565fe9e01bd5f58733c2da2eba1144d50afec878d11547b4b6c50bafd213a7dfea0081c28601df6765d0e4ee2c62ea450f53ce44526df86f32d1defd826168a72a6b920213190556ae50722f3880e4af3b1edd283ca0350c6a283fdf00ad92bdaa18bf2157c350f6ee718a1a3cde639733fc2349d24c78270fdc21200b686dad3608802168371758ded5ccc436d431141f79d62b38cef19f5a47555da701ed8ae18231825b7c906ce453bb755d3bc1676eec754c0dab65f27c40e59a55398dc0fba641a0892b81895428beefb7ed284886e138b82014911fb433cd864aa73aa714613402189ecfbeeb18f9b16b55c507adb769f2c4c5f835c7648df6b5262fb07fd0a2817fe3da73410af4651d7a63b68e23928fd166e3f267fce54cf78292a56ab39e20fb21d359fa63fb3e1ec851eca417207415d1a31c3d555e7f37ecf615cfc28880368029a43fb583cc9ec4a17a734ae0a8049756ee5ff77ff74f8111ec484ae801983d6932fb35c81a7b46486eba0913f01076f46d206f3a489a23d140886dd571cbda8df5b840b54f0a7d916fba3a2a113386c7dbe0f549148d3e707d9a9d6b6072a9b6033b5be4c53b68aa9f7b477c40091461f2e2f02e16d4415d7a2ecd09024f69b28b9794c45f96282ae002d52b4a8220df57fefd31e37c19c2fd036b7ff2cb77fa1ccfc2c67146766aa5f35a98f4dcd212d7279c899818285fbb9b83e6c259c7a955c3493e431b5bbde4227647df6a83c86e3769f1f0d9c89e2c5a480e91738e2dc7e605d15367ea67ce4c71cc11f99bc44eb2182437c564e6773e46a9a0d5a77619a1a6331ecf0ff39d75636169e1c0b4e38fcfd6ed67c848b93022faf1d8e51615c0774abeea8a79ffc4d2caec987768a90d93237d016e7713eb8118316c1bc2b8b9ccc7aa4faf4a50be342596ad7b4c8edc8ed7fe5a7ff5f5939591900df5a5284d15388b073a0af365eb1c24592b64b1c82d038ed563e553092e6d7048e132898602a07c0f960b2570b85d5fa2e1b4decabecfccdf1112b8b861bd626aa202c37ebec7772441b5f603bc932a664de98260e9d49ccb0755d9c7d822428b6d3ed257e5ac8ea166103154e36c20aa5f22c7939d0c03ddc9792cde75d4826794267b7ce2691bcf03f39f3d22b0880db9d479b70844c8a8a77fa1c60907e14d06a5afc5131c18866c61c265c54a18724f68c19c8b82709b1bf888b5c73292ba7cc7276f19d4202234cf42211e8469e760aa814e2c294e8c2382e7e99294b127ee3cc14a3c6cfe5134b9a6bade9114d816f61113f52b41198c21ff59a6c6820d1486a96ba8b765442a01b6038a1fd73d5ffaa332b3e550ee414c45e231d0e2d1423d06f425b2ce5df3a31e84885399841165521a56b4eeb752ff05e0c40310e9ce5f10b9e04a82d4b4a8ded7f583b45545e1f9d1c968ebf17db91821b9c873011955b2e280d34e3438df5326ad21c9f1ed98d6381eafec7d79a0080d0acab0b34e2d1daaf718033987daa5b33ac16ca645236f57a240a68b56bb18876f6592b1ed337f1e257d929a91f65e86638d0acab0b6888283c291fe9ef9e954ea08510e39719e12227711908eab00635ef7924f1faa04716e4a34d4c8807bedb8997256608e44ac98289e2fcd9b02c0b0ab0f8052577ff841ff5e5b79488f0b7fd432ca2ca5e920866c51eeaccec44e4058cb5117f0616d9d116aa4fc7022499ee1b10b9f5fc832077a33189c4aadd1556c83f06b77f925691ccc5686fb376c6c49a17937868fb780b8e3f922a603dcff6f04c6dcecb61c049f57ab204b26d9ede8e28d4ac29a8bb31d996cb32fa7ecc34188e070a5aabdd6cf44e901df3fe0324cd0b4d45d3abfe6f26bf5518fedaded225a1035af7ce5718cfcc45f680f1cdd5462201443bbb8b3c7a170e5a6c149204fc0aaf929c596dc73e0892cc48491a58cf1be03855cc91be6d8bf06a7fa80feddce1fd960863852ecfb7cdc46a7b286f0c29a51bf98b25b04881d1a0212e05e6969f39fa4b7952e2413bcbf31fcd2734362db6cd58cc76bf3b72c19014acff4c67ef4cdc24684f1320d63c269a189be80521208b163f2041974ed84a52863c756b994eaf8742f40c82c0cffffc9181184626865e3d548de611cb99f1a9b17e2b56f32db2f9191c07a9a604cbe44c3ad0182de47bd199a0fb45ee54a0822cd8e96d3557e8038a3d6e349979edd20261bfb92ff749ce13423317b1f5c5ea4098f22e55539d2ddb4aa1ba6629460f46976276157df52e4c2296fae7f02811a91b132aa57ebbbfcd9fefd8d553ed1a0214e0a21bf873377ecb470024fb9ecfed2e815120d5d428ca9f0b05de050081fe146e7d02a448638f8bcfea0900ff7c9f3708147674b69b2c7ca201efdf77cbb1318a2f1bde19219ba1ed0a9d8e938573fc5f080b86b7b9806980f26e202cde0805f99f2632811d2237127a1461fb0a2e7f96966c3217f6cef17878964d4858dcb3d8d40e779558453eee9fc19844fce26ad920446fbb4196dbf31525e5453e2dbf01a61cc54ed7971d0096ba4dd210313a41b3b826ddcec371851e8c1f65505a210e3e2c3916f5eafc1c6d84ee035d17bec092aca97468727448cb5af027e190a40a00", + "public_inputs_hex": "0x25832c071d6d99ad42c7b885e48b10100726dc0de03c0a779b282c519ccf538a25fc312a5f0009edd084811b739bcbd529987df1cea36f5a24515386662573960000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000cb80a0dfbeaeb79af355a70d8f19855c00000000000000000000000000000000915681ec51465d5a3404f61907428b712208484402633db0cfcb2af11d3b66ff07cd7b6b12c3a35e9581183e399d53f714ae98e2b0527277d74d7898e5a1d76041a613ee74f2db890bd6db2fd3a76e9d30329528532c70174aa6a80f652ef9fb86217d722ac4e756dac8ec699873be87009de2f6efc0e2420bebb740d52be9795045be2c08d2f61c665fd8c2946f25a70037344482bad1ff4c46d1ed3887ee2f37d4bc2dd3a1c34c3c6a0b0d75d4fa63077e46b34c8c83b27e71c711f27ad90e03f985d92330211d3eb4c6c88abb18ae" }, "decryption_aggregator": { - "proof_hex": "0x0000000000000000000000000000000000000000000000046462eded1bbde4bf000000000000000000000000000000000000000000000009f2fdc2ae91c95310000000000000000000000000000000000000000000000000b2ea26392102492c00000000000000000000000000000000000000000000000000004afc85764e79000000000000000000000000000000000000000000000004c469c087756779e800000000000000000000000000000000000000000000000d3dc12dc70ceba4e500000000000000000000000000000000000000000000000d09dfd159e66bd2360000000000000000000000000000000000000000000000000001d0742afdbf00000000000000000000000000000000000000000000000009104c8f5d91fa2bf500000000000000000000000000000000000000000000000836b1660e0992fc3c0000000000000000000000000000000000000000000000031380efa2b7ff3ce80000000000000000000000000000000000000000000000000001d837d5ff0a5500000000000000000000000000000000000000000000000db4b2385e73fbe68b00000000000000000000000000000000000000000000000e4f7388c73406d61700000000000000000000000000000000000000000000000e8623e231e09b7f3a000000000000000000000000000000000000000000000000000132c5f9251f0e1f3f0033a84e44de526080bdfdef3d2c4e9be76761bf24b2d0e31ac487ab12b12f3b1b559ce6d9ce7c97b5f31aae0e6529acf2d329858733b8c0215b3e3314c51eb174b690f7d4f832df6574dd9e4bfdd41cc47539b0301c66a88984cc0347a312b53b6e93390fa5cff0fbb34c9a55b3bfa61ab7841f31779fec5644f07e5f9c17d0eb1194403db98035cef3a0d47e70ddedfbfb47bd33c44fc0f7f4921d50180f10ba9e083af9917dde07ffbe45a53e65983092dc0f35e1c84f92b7189595390810dddf63209c3f94b856eae91571feb451a665cd1a1035b990c887ecc43cba2433686433d56359e92f6bacf0501d6d7662ce00d33aebf14dbe64164c036caf16519af50842a2f1478375a011705e1b6e3cc8d1427b463a370002e3a70896ad27996d20e8ff12e70916e72471a5dbaef7f378d7232905aba8cecc91e8c077c72a272b8af0c6bc297b6b8563e218a17a1d510b271bf12dd8d3aa90e56d6519a32d29635466a7bd4dfdf51438ccceb51d486058596bd2e201d320c9b63fe239251273008b48d56b8a01c916ad52aace95cfaaae3232df4a22af016a830f00b2280dc4090249ebc6c4f4977b3d2f3e92603ca1e86d0c65fb91f757747e5b3c64f229c98b47e1adbdae651e8c93d1b5e22b26a27a4004b26a4cc6dece078c1e1f420e928f4810f467c371466a5ad780e27de4b7cc7d34c4bcf9bc4d1a8d69382b5c26a661451205b6074193728e248ef7585707509e09eae306e78710f1fb3288251b20d0edf70c5a978f32b92a869fe73975d955f5281b23392981a6f19a2f0f58271d29171ed9a120fb70186dd4f920f56f767149d663c103ad123bb86de7d5ab1678aaa63cc7e6039049df417f336220a6115e3e860bf85aa96a44d22587562527980f69c45463860ee9df44482e0f52c5a10107aa1bd1fa8a3ffa215868740307471504d1532b017118223f73cec88bdb2514519f0eb31f1c14aa42fccc0d541f5b3ea28bd0f5a00d90defbeeb8e25bdb73835f623fa6bfee1e5ea3032e649608c59db0e1d6d963d552f4b4633c5767b0e8bddd6f350c179ed032448ecb129c15f26bb9e1ca1f3529eef918c8ff9788b597b6befad98ef7b67aa0eb9134d5b4210870b8989b0adbbeabe3dfebe6ea30bd7d60659f4e39e266184b8243691b2e0552aae18e9cd627f7c5ec8693c5fa0d609eded0c8d047ec9ea6c224418278250112560148af7398eef1f9dd48561fa9c0441801dbdd69c8bd46e5c4af99ffe4109f7ed54f9cb7f83e617e6b2f93b922bce7e1c07151e676a7ece630dd747bcc142aeccbc157a93950eed8401606e1b16e66571b7f8ef663ccf4e93fb82995212bc9e169ea1c0594356b171e607e54fc02180a1059edf5acb8983064734fd9f8209e394bb72f81f207edcfaf9f2ed0b8c0689b85c47181abcfc8d36e08b0c5ae05832a231dcb4f6da7f1ff2ac341a08b210acdf2bf48f315d5920175f3d5cec908b4961c1e62cb178fd865c31734ac843d19fe58175f36c3bd84dd20e8069f6b0a624f48a0e47210130d1e6697959e2fc351c28cdc4228fe9fe37fd5abaa897e269391d032b12bcccdfe6c5b4157526f2325f20b13b8a81f8ee1492e0008cc770dc6aa7a1be35998418d9d54e3eeb7bc87d1ae47d20d47645c4973519428a79b15d9121c942e03cc0ccd93c2158ec1e33c2b59bb09173e7b7c9662f2baba474b01d7183e0570580d82094361ca63a614ebb95d875ff403af22cabd79cc4e8f400424df7b0904cb8d32decc3e907e6a0b511bec4ce2fe9331afdf13b26b4b010b0b3f755f1a444cc372cbbb860872fa0493f8c492a7f23867ff93dda7b637a2e90efee7b19c99a3476e8f399e1c68944d4bac5b988ae883dac4f406ec18abb7ac0c45dca453ba41f5b9c28dcca7905816fc9600d2298b31a000bda8fd06731f6f26b119a02a7fc2a4ae33307e9f2798f2502ba08f0cba86fa82624386a8a7cf1a1bcc4583eb73ca4ac0b5285cef38d0a4a6302218e5ff0184f9c9908eca82c01a26e711272a4d2dc6c57ad49857ecb1b4adab7807061cb4f5edfdde5930058ee71e8bc31fedd59323de5af8e5713c7ceb21d48c3f73611979e7e2712684589c4711f24d5a2b83af9233bea1cf2268234eac399129feda79cc48c5cd77735d9b000d2d0a40be4f80ee8003228f36bd9602fff33d890bec95c8c23e00e7cae127c41075162afa6c2b18a81f299e7eae0f590c18989adf321dfd19a846924be23a7918c08a3dd27cd71b39d3e3562973ca37074ec000f69aa3824710bc344bd5ea292ae856b14a9a14635879ccbf87f082eddf2dbd240b9fa4702f5fc5d88ee72b2a096808a9862cbf8ae30240058d22dd69272b346467520339fda95a07f6a8403325be8b2eb1744fef4514ad7158c52488019b3eafa4d8cf7df694c3bf47c3a59c1a1fc4a36060bc9af14ebf2eb79d07cfda2528c398b145317c2cc84de6137d44221226dbd233ab0c7c0263ddef6811487072f0e218b94a6c72fc7e39bc27141401b11d54d55aba111509422d31d6cb92dea31534ace379c0427c6c3a92d4f90f02251ec156fd36045aa90adf78403f49e8479b8ca9778326da1425427815fbd6274bf80015317e3ecdca8795d60e025b58a56dc77f5a4b9acb453d3e78deee5c14a5cd11d5d16c49fef05d37c81548bcc270ef9f21a79b449a30d540ee0b9a8307e991ea9b46f57eb69d7c1ebe87a8fdf4f8048b4f41d4808162f8e0f7cfb3e11a5c3cdec6eecb39b1351814f82418b9170740fff3f5681d48fce7310e527b6a03d5ae1282295d1a77b314268efd8cac07fc028290a1c8657e45227f4b6ee63a24de15adf0cd89fa38336b41277d5fbb3dd19e281b815b2c0d2e9c3e5511797e00e3f664895d68974f56c39c9889994ab59f8cab6b8a8ecb2d703646a2236aa91198ae08f77f02dfc879c800f7c84e398ed8a43f20f2aea496b004cc58fcf4780b4966b97a4be732ee4358cbdfa657c322cd78549ee3780e0350374ea4a3d8900f6418d683ec071a1273be730e362daf287de8c335727321143ba40ee0115c241f9ecad9f72e9925e3de109f1e566341752dd39b45d512d2388dacf23b281ce3108c6659ba45864d7bb6836042b9097f20f3f2a053046045dc613ef501a2eea81281834f5e8f9d7140f709b79d53fd264556d70139ed47d25666027aa69c58e42b34757fdfd0b9d122191696b4b4bf4eca578e24fd9b844b34d73c31ac3ddfa801ff012ca13ccd6b0f5e561e0e6430b4be2b0b2d374ea129e470491ed7c3b13b183e35a8ce4cd1853425fe864158971415c7a65f11dfe69d5074f950809a122a28063e5d27dab57ae4f0b3ae9fc361028adf438677dfb5dfb988f8b8b6ae57a3076e5ea1a0b2f974df0ffbfe0a060e3637c3127776fa81d9b01ea0d657dabda901d6807a490c87e46e4e394192ec98b42ed04f334b5a1c0f5057e4a4c557c01a1043c947cc968219522c1abe82b20d6a4db9c2b388a24b71cced988a4b883a481d25e3810be1b850a40508bc3fa632b2e5fcff0a7b1a49340915865bd52f501f1f78f68122727eb366d619ffc67d08d4d38cf0e91ea66920699c3a2dcd37656724c44f6274e12fa976dcaa3d6cb603e2b323f8dcacbcfda2d2371a78596fd7e42e52e2ba095e30b7021a480d05dad2f2838df1fbbaf38e0df2af304c8b1175b80cb447d4e3ba6d26637a624eec49a5617a8380020addb7879bc212fe1c600425147bf057f7596b5f2c938d61049a8cd1889ab5c40ab9386a13aa7022d00ad42d0e1b1338ff64fa766858440e350fa32f5fc613ccd8b46c95090843de78343271006a68bf23479c2223f60d11d64a98c2865fd34f92e687eb8c2b7a28deca830818a2208adc5a09a52dcfdc04f6e0e53bbc232b046d60528ffb794238b3e64b5417d6dfa423205c8a1ab17a90537a19ef9e5931c33a757771a62c7288865270fa1762f450463e4256850c78446525c13e0f55928414615620e1c41fc5f2a62bbe2ec7351b1d969bf8399ed677fcdd79a6689cb8c08682aa486ed19431e2fea30326c3b9cb74eb998fdd93bd366ca54253b2ac0e213d65275b7e15be6f55b87e5326ae1e2391bfc71e63cb96fe8badbb8eea59b1bfbbc89d9fa9c76214c968a9ef10f4697708d257e2caa80176fce9c9dd0bf7f08db787efff22e8c50a2b3d851c03bc1a26a9f96c6c7519fa1f9f69e89d21f3a846cfe8c7825b32cd3b1df30dba1d5dbd1e3883130ed44f9f1708164d7c037f7110fc42a2569005b099cd6c03561737abb36b4e6751c47f50a1749e53575fb2d60336dfb156eea527c39e6044fd0fa4c2d07af7ebb89646540fb42fce47fe70faaf75eeef6741a570a90a39e2212e25594c3141f72b1caabdbcdd33ae4e941fe07d0a9fd16698c6856b74619a810c0cfff04663e7bee110fefa623a4db43fad9bd5d72829e65d9a71427a0d5c2f0bc57df006430e7edbc07970858f1f006b42a6b8e510300fc604821abe5d9ed612879f61da683c655452ab2d76bf7345c63299b02b7ccaf83dcb03552c5048020354ed28001f40fb66a7fc55ff272aa8de1844409e56a6c2a3fcb89c94218f082674c00541fe4443178d227abef44644d4abfd5e9303fff2e68bf3e1d140235910cd28a2f0ac9cd54303522a68c80e618d937536c1d26897c8f85b4acf4aa1060c9d7cd154657431320a619ea0a603fd86a52edf39d34cbfd2327b95c4d02dac2df4af3fda5d0fb1411f7499e9228e69f7baba0aeed7b1e31f6d17aad11c2c4a1fda0b5b8b20f3f9963100e0dcd1ab2346b12f602bc30a2bb453e615a7c7df7a1e209bafa91194c0e3da621aea9637db24336c649d82aed522c6ebdc9e3079df264481411d0846d3f82043e90df085bb974d2ff8c5094b56b0721e622a3ece1e2c72afee9cce381b1ab0a042d7a2a0d32bfe34d76e43ae24cfa00802038ce3482e328423d2b01a87447801dd0fc1e2c33563bf35c6bf7ee27f8ea7808e26da630559777a16193d3cbaa060b686fdf5756e395ca3c942d5415ea321930d9bc4581da056043fe7c53176c8fcd66835b9ef69bd815564d1bc7fc71c1373bfc03d480b7aacb1b71a1d5cb97773bd8420ecedb4abeac3762884e73eb9701d5e192c1e2798ea30398aa7c6aa711720294d914419284a52a9794048ad73895e21c73f2e27ee0aeb6cbfc1ed89266847420a154aa825df70f24eb1b1f0b958c046771a911baa6453cba72219ac18b8e31e8b660828ed849b2cd1b642f2494e7430d440552ccde3ff0ea6860a58505c5d2c029cbb4f5fa598fe4ac011751790e0e606322c13d130dfeb16f00ef3a180f8e4886c9a3f367a36a4b2fa215a60511d77a4d1ab2e1d16ee35174f5333bacc3170eb79641811ab8d999ca78bd6aeb778c04bfe4b022405d81d43d828fa1363837759b1d688f9bc423f18067c1e63e7ddcae77b252d284dd9ebdebff77e6f0f9e799a7461a0f4899e54c7d34d148cb21cc3d29c76292c218095c7386baa2c5304e29fd4c70bd82e6062cf160ebb110230f444c2d71f3187f7f4932fda50d850dc56b3ef554e7745311c76955f8eb116e15d3f72d81a267864acfa25775724ef1f370daa11bc9983df1f6d897ed8f040899ecaad740b34dfb34c45c325bd21c38b8634103c639653053c1a72bf63e3a074b437a978169d1d7cf13831621c414bd3d03f307e7bf3c2bc654afdd857c43c0e1e47c4e726f6703cf414f9bef59c90a72eb596013447cc3cf7a8576cecd1674d8607f22e0cf5d470df13e216be6c2e9e8ea0ff0a179c00cac95375a14e02787cacb54a901fd68c2ae7eb0ee85c442aae3d8304039a8209c8090040c83dc3e287ce5187731d9b8e516f91b73a1475c4c4077544dbd325ee6ccfa3aabd1144352afdbd457c2f39054b92189663deb023ab48081a80b8e28841188838be3981e7e93840ac35199412daa396d472a6a2cd403a92a8a80995a1092e99f0dab79a2c024258bff1009161d190a6eff111d46fd2e4fb3528475d89ed0f4acbd2a83ed488cd0d14fb2864e012328fa9fa8234ef040bd7e64c94d8e5dd435f85564aed36b3c59bf50b0611681a82df36ab4b44b009b20fe02e8689ea64df791546cd3db0588d08074c241768008e3feedafd677164b7e19c9dfb2a1f5b4c91abe6b2716dda9027a8cc020340435acab05014bc0b5fba39be8a5dfe4751ec0cd65a54e3acaf559fc45310001c7116a956f02d2c919493c1972c2a8c37c5c7018371de046603f943861d069b44e45d5c59aa7b8e14620ef592b42e43dcd8022db7fd3d652bc292fdebed066f0db566282ce6fd05432d2bf063cc05c2372a442e4eb1ae95d943750660e129ab4a81c1e699cb974b471bb885af4c42f8be4be4a44f6c16cf33b22bfacc7122532e5f78e3da9ca7b8da40ad3651c7a3cde3c028a39f76ee378a5df8e7122d117b0ada3274420f4169996ff8ad7f0644294b964bd02e43b573e027e56c31f30281ded22c23f5318715d0f675c4c697a2c257f22112738f30ba3d9abfab6ffa1b183c5a511100513b515950e7995ccf0f082cb5b0d62b57269e2c7cf3e4c0eb0bffb7398d6bcbc8c3a742ce3bfb2975d365c391409d9590552733692a1a62b7138cea6bdb50758166ea8db309d0ad52dd680140c79d82fe429ceced64c6c214103a90d27fdc490bf99777ffd713275db4329c3cc42f5d631a29af86d0e449a5298a95d8f509c980f7b11abc90efb5b6ec019ba2abe05ff97971e3cc4494a8d11c320855d17be84acbdabcc29634e10451cc55f7b18e63385dee746c70927b271c9d654c07f30212ba3d0b34ab1c7d81484353e7303ba8739122ea0b4d054c5905b3b6fc0e3482c9d1f92261d1caf129228350cf179238a409ae49a4887645050da8ffcf50fa4ef02b22e5cd98fee5625348f8dd72c4806e6a6028da2514e05314cfd201047def07dbac92fc3f31197a0f94659a6264cd08c7b7b1b73f5cc19d074a8c82be071ccd4ad401a5907a3816735668a55c2c2254cc94d9fd42d2990419d4ce2b03bae535d55557127ecd66370f5a555fa5b3d3255b0e3e18040e7c9718c174a2ecd7916c33c950e946764b5a2490252a6c8b4e8a3f4e4200160e12472f73ca99a627089fc5affdb380da4506c458ac642d6a8f151f60b09261abddb2092edcbd085881db6d5b96013108dabdc80a3b38fa8d2e638be26c78465bbeaf2c06f7a360a2cc343d6d9dc2db8f6424302e9718b8884ec3d80e99407770f64a15777f2da7f4aab9408e3ae2f3414bbffd74a290cd14195ef7fff6b31d737bc22f2a1491ff76b75114b2105c8ced13258e504a7b36c5102321daa348581a551729aa4fd8e5c8a411c89f29eb17ccbbdb02141008bbdf46c7274a2c719595fb7a2f5854c0f6543b85ec7221d1e15b8e867c80848016f25e72617f27e537cd721c05b0e96a7801ee3fc43a534ff819962b1e8282ed7fe5437df8026f12931affc92a45a488eb039c2285d69ebb9cc721d0c0481ade8e27986cc2424bd63f09ea13281fa330a68c69d21ea8262d617a8784c5c3130f9fe2638a28d3cdda46fa9c3f110d509bb1dfefd5f72c7af0c2d6cf3036ea02de515f6876005bbac8d8c51d6300ac7fc8e451e423e4ba7c25eb016814dbbe5a9847c8d72a519dee86749635c52fc29948f9db22787989d3d2630dabd1b4a74528011a8a2df437b4fa220098850a3e4753fadb485e012a8cc00da2cd5919c51598f8ff98d82ff813d03bedaedf2fea1e5e0c489ac9b0400e0d949e92115373e6350c76f49c345b79f2ff56bcd001b956017661a2f6b6b167f5f12b4894a32157334c60f29d0521166d13d15de9302c9b0c0bb5c7880f6acbf2acb78d97a66d12e898b7ec0d8c7322bf62e51aec263770b749a733e1842af368cb129d636ca2c23890cd74c44f69a427e6dcd095119e080b300e82de5fc8b08396e97d027b8e781f0cbd63489c5d496949b3daee08a8d6b94c056c4b29bdbefdfb87f715725d48e2aa2c8ecb2e72e4a6329b861c11a1733f664320f80bb5e0624c847534380dd10bf3b9d7b0b3f3cbc00ed6d8d72ea19cc0831d47f5142d7699110a13d154190e29363598288d9e87582621ec8518aa844817a33cf7385a4867a624e6d35d19f08689cb4694d9eb4068a5a89cee14fe3cfd331d9edeba664f921f4e2af1b182eff2ffca37f65408d3d1b42eb324193f27aea86433f3a6aaa1f0ccc589243d364a6d284678652a5727709a417c212fda27231f6e20f52417f284a5641a6d80362cf3dd402b81e6dda971168329fc14aa355a2874208110adb7169be2c53ef263bdf59336ba78618087e36be1f3c9277257150ffe847f6d86895f1197dcaa651987c33d02f5ba5b482311f817d2752f96ec1b41838e338b2af7008a0d5949ff9f18dc7be79ef5739211659f266d621462e12c1be7bc4b4934fdb8699f993d2caf7b1c570cf25b802c98a25d69b994073b68b01ae79b6a69eb211a914afceb059e78d2a636d304a48f6b05bc32993f1eb3e3e642813c85af8b5f1de0888cb13c53fdc5d5d479c70ccbbe61744c1239002bf3e62cebcaeb5e7303b3e12a1f30fd0db795b804875570e7fed3230bd8052871b4aea05a90ad7b63f4113cef0be4dda60693a503b2378172a58c4624a2eb2d5737e779f841a33c28eb444b62d3d23520278eb740181fda57f544f3aee7e30252680255206ff7d0a4a519afb865069d8a05da1132d795895891fa94f8f29904f4324c26aceac3104c97d39759801a293e22e65a381aa86de3b50159ca9fca09cf8f2106c1ecba895d547e1dc71ed0d9e8db14b9db90888777e29ef793b337179e7d5a6852b64d326b7fff80c4434942d3585902b1c534450562c6ffd93e8c0d157d7f9386ead62dbe90615399b0c8fb87e5fca00721a2a3fb68d14d36819b09983dca3f3792b69abc99b3f3f5afd33d28cba0999b8968f72566f05de37d660436b2b1da3001d81f914ab0dc20eb913507e7e526d5f96f842dbc6d461b96df2c259d17a61cc499cc16d0ecb910225076caef6a3d6c2fa282b2bf34a1deb87129450563c1427f7e543ebbd7aa31c01a16f7f9ef377414182129f183ba1246f91048c125b304aec8c136a897807710b6a9a73cf827b2c53e733c2eb1df3fb2fb0210be8239b1631db74571a2e30ff1dc5e77a7be632890b5bed671bdaf6e2a960bdf75213571caa91dd52fab661a9e4fa96e23b6d814748fc4bd6a032ca1cbc71a6e2915cc8eb694d63a591eae3f5aec153656fb2311b4ba339a7e871ef872702461a3b251b88bd55cea092af0c1cf7ec9f96f35e46cd359223da833d8e518990775f13a8a47b5df373d0d46a9098a0b23c1dd6aa76b286f6708e7713811025a23a510820428c3768c4dfc8965fb9d34ed574ea7ca0d2eb426afe5f8fe0681ef02a7a0700344716e9466f589aaee49d2860b604052684091a9e0ea23dc12661a18dc0d314a36ae8c600e6a886d4343a2b42f3419ba19e793ede386baaa303a402f234bee6497b9e517ac24aaab0fb01afb0b1707423554a869d1bea6a5bf743f11f32baa913c5500c9df9db6f92f49d30b7adb7cda475c5cbcfbdab514f391b12c3e705687b7ca3381e1363090cbf57ff7a86d1fc0c9b110893d6537435e223b0966964c5e464aa76eb466b04c4d5c7037f19229aa437e7e490d1bf9b19eec700cee2eaf6b694fa0b6b912b86ac7bddd35f7270baf326abc5234e02798047f6100dd53510a41dc8fe6369350a1b458bf042e8204d4453a359a5d107ad266d8bd0a393b07ac21e86143568945dea47579f961da100dcd17c89e800299d1dac22413736f9bcfe75432de59c190fec427a659e0878ae078596ccfca7ce18070706a27da89553eba9079775003ae2cb47c214b82ea2eefefe40da9638e912cfe60822ae649c2057442471a471a9f952eff46a2babf1a83e19a4ddab3794cdac562db101a09b7a278b2dd6fb6e2a9d393579c92db556817f6dbdc7733599f140cb4382e6b319f574a62dbebd2af4cc151e0773ab2fec6239c69681263c76fd83cff19220271f850682cc592aaaa6eea28d6bc0cb73007fd4bd1bd995a099bd82c024f0a2f782d188ed96e6e3bf4087c26d3d237a2b12204a57053ad307c344ae34fa4213bd4ee4f056cb822fe91f44421f97b9553e372050526ddbec9a9810f0b77cd05e7dd08def408b601eea156f02988103cdc6af0fb020894d84e00680fc6cb690af351f694b00379f093c09cf78cb4261d49857bea4be6f8dd3f4a7b32f59a230e3189ede5dddf5124a8caf0482b511ba11705b8e97b0ae8daff89770da3fb7e20cb1d9be77cebb1509930887487b1599d90171d64bffc6e929ec317bf7e5aec2f7740dbe894189de87675d7e978196e17b528ac35b67d091c6b144dccebf6770a01a0077e8d2c021ad57fb50a34e3158e55e57f94290cb6db1438e73c26dd5f03367dc364867a8930faff385c5e3805b3f5d61b5fe90d04e51b1e14373054971a06811bbc967bd9eec33c892ae9c4ef7274d5e4351abbabd2bcb803a5f5c70f02860e11bdf399a1dfe38969bf66e7da76086fd1368b68e47963db7f9565b3130d69008e708fc5f8961db2d87f77fe3aa722795a6c711cdaa2d4da8f38eaef83196be74e4cf2a837a79259f60cff97b4901f7f48941807f07784fcbf4db57ba80c897759c9ca3474d1f1c03abf18050430fe347a17998ad5498c2afd48c8a98415b32a411758c283b4c28c1d5ca62fc3dc246c62fae1b7450b168c5854c3f6f81f25ec496179eca02a8934cceb7f9ab9fc81a0b5c0982e045b5742e3d21791562bf709a403a8ff7f4c3d3ed483bd94c60426e37242a53fb501d465f6771bdd1c01e4364a0bcd48a20d4fff5363f75c85d02ce552a1c66784f8c393df093885b02286068214029f61fe4946410855f18276aac139a43f0b328f281a40f607dc65006694ad288bedeb4d931e515e3d739eb1c0cc95d7c5284f3ae66f43286a98a92480eb742d842cf382e23e07211c6767bc86b65efcaf58199ad5bbcfaeb67b8116ab42ba4929e61ef40822b47b3b77e1411f2f8dbc72c1e930cd08e4eb38f37b0bea7c2020d7ee87ea54c1b3696fdbc146aa584a23042c69e6efe33f7556c1762b3e3ba8c773badbf5104256d6a599fd5b20145beefa188347e080716230875a1f23ad5fd8f118d14793db2a001b386410d01cae4501bde987126554aa1f943809b93a34284851e0092f0ddadd9d27066c2568c8ebf91bbd5e1554c3ebbc6eb509d2ea1544f9c6b1dce97cd8584418ced461121ffcdec9e3dd1fa14f95d2e4ea08cf6a13709220c27a0a49bb9334e11e182a621cde1bde5f384b1b0ad51d19833033a68bb8ad808d6c9c7b07ab154119abbcbb85ef10d40fb251969dc2108c90221aec3e7930b31580525bb5871112787777aea6ad16614536a5a667520debea18baad0cb9f041125b510f5f1fd48f41cd60268c6ec4149908e7f9c9a1c3fcf813a79c776a2eea726579809ef20c267d839f3a208120f8736779796e9dfa7bca253a82820d90f4f5f7fb4b94a870394dbb688fdcc0bb429e43546b8b88743f3f114951cfc8e34a21110a24cfb809985668ea681e55f068d8f260436dfcbaffc91c32edff5ee1777d2ff82c80ea797077225f691258424cf5f5d64b3c312d6f48257b1ca15daafc6d830d0d38e74669add7907e870f0f8e896837af7e0104b15d103b7ad3d25f8de54bcc7ba08a0b3bb9f30e238fe4e3321a523728c554b162110489739821241ca55e4ecbcc5f6f25bfb44763a32e19fe977940ce8800db666317bdcdf1f29834dc6b2f07c60f7076ea4f4c73881e89734d46a31a160380704e0e4d0b30a60fae2bd517e3c939cf57682a076d4f6f364126bb2975f940ee8fdd0976ffd75e34827735162044bff075b04735ee3468e51d80a7a09bed81b48942205981554c5a78f504f763531fdb93d2861ef929be2f873e59dcec85631807b70484a7f6ec144b234ea375bdd48188e15ada0eb56ad1b987d75ed356bb65d5e01bdf10e80176f7be41f9d31008b8274f1b0a8e37108cfd37fa80c5be57fdc6450207b4b4209e983e6aa97e26c81afb4d57059de74276ffde689f883d5ed67abd2ccd9fbc681e291fed67130e43f755601bae4c8849909e9030484cf3c71aea07227f95a36dffb88c8d0131284fba36b597c0607f0ab3b1bb6c215a19fd80cdc92c0ae72660453b0973ebfa890718c2939f1207e3ae5ae18bde7208392f708a9a093cbd866e4426034e015fa66d8b9379509021996d41841c44374f7e4ec77a8e0bcb2604e0783d0a7981f77addc0d9d00dcdba536950e38f1893b138696ca20828cc6f843d63ce7c068774807ac2f6a52bd2adc3649c130a35b854e2965dda9a2a39eab89126cc289a350e2bf0eda765a5bc5da8640047bf12423fd54224d0d6053505f8e689a403440ad11e799b653515ad5ebadac03c3c17a2e6a40ad8b829025a520eabe6104fefc0349b83a1292f1713d4e56131708398777532728fcd2b0b712b5802b896281422c1bc219dfd0d9df3be491a20443ed5119ae62c468d2a22adda3834369b2cefc68f1033b217baa21720c4e4813cd4b52388f3bce5eb3e21ea381f1ee6305814a07f4742b52311720f7aa836766eb57fc2504d89fa68be03b747454dcc12f3b4434342eab6d95eb64c2dccd735983c553b76a009c18e6e2e8e783a9067cda1f5109f7371fcaa2cc1658c50fc513effba99a62f84479ede1c2afee100f375462e12d611ca1e58b350f4713e43436f8d44ed129d36d741772485b57e2cde8766e19956bf15fe1a3f715f462ff70e3e66911ca8b0349ccac50d0c4d59a71edabc1844172fce54e10bcba6f74cbd36c2c6cb8cdf86b07fcf272bbeae03157930ff5bf215a8fbeae00610efde36e8803758c44b71611ad8b7530f77a243c70d35bcbdf3d390e0e1f358a53bb7b271748486ae81cecf1d3bd7b312e91ec7312eb62f7f4465e9f133c90b40b5aabadf897db276170982e3682959027d4f12eee5a6720a6863c259d5569d30604063277d5afa87a04ff1db7cbbd8048b5f6ba32e0972746c42fada98ecc8139f01726b225c661b7131c962a6af431c46da1b18603580e0eb36b1b3a78803784752e95178856a52cdf293be7d20ca28afd91216a390b4c5fa200d13796d66a09e67c0dcad2e5cc6ba969843f35de110a26385709f5eaa333c3dbc672805554507e4492dd88e4e1fb37d30f00b100b0b73ef0f2805b3dcee77ee3c518d4d2b5e10a6e74139d13ec437fb20caf21f6b14258b219d2d7c874ff865b46668c9a98a6f95f7513c4840eac1fcd8c0d5983f2fc417fa028662ef9622af0c8b9a8370044569515953bc3351e7879256a1682f1913c6f71242142890f49878f50a88254967744ee98fc8f39b077df24f896475138b353992d43a58ab679391c189def4abb585c95fc78f8560ed1578334a54ef06c4e242e99763949a6214d2f1e6b2e0084716b5acb458f0132451aca43dbe9002d447989ac471fc47f751e9ab748f01dba71e41478c7ba6cba186c59ab2b22e0ae2cd982005400350f2b1fd5804f85a943324dcd60e01aec98058ec872048fc2a3aa79499437917db1f087117dc5627fb97486c24adaf4cdea43ec70a5ecb1115482b3eccdfff6508c76655387abd8db57b01b170d5e1870faa1384ba4365e7054331d92e9f9f0b1ad97b7b530669a248a4c26ceaf174cf6479efbc7da6ea54099c32957b8e46d8efb77b50d25e3c705ec7679f33abae0b097fd615c45f9d461b03dc3ca49cfa79c813d003bc5fe4ec9cfa94a1acd523d23b88d35ebb5a6e9a2fea8939d0a2391f5ba2f337b17972354cbee48f9f4316611ca4747267fb899a2c3deec9cbe9907b63384af2f8949c18e82d0599a14c794393c329254a5497322b8f054ff0c0c835e7176bef90eebe63c9caf6838cf97942832984d19839ab9a15ff2ee3f4e15a05e49a92af53f986375f372836e8b0871f4c4ec28a690e5e631d2dd1341b55a31770bbeb958ca73627dcb539875b11846f8129714880e708ee260d85ed2385e58c520b25866ee5635882ab26c9019999d6fbf748f1e799852121d87673b8e00f15076cdc02771e293c1ca49972b74ab8790702fbc2d08d89be1c26e413b5ff7ff00f30fdc45ed794caecb4f8e943d341f7ec1f9cbe686e8408302d278a2da6ae5b6328b996f93480f99dcbf66ac56244f780859f9bc5e6d61413b5b5724c4eaff5aa4d5113e19f2072cc4486528b4f864c0b2fdd8dc12c07f70799dcb83bccdf08e82e1fb2354171bec979c58e2dc82f05fb9018859906ff3803fc845ea83bfb869fca929234cedf2c7ed172a4a857031bd37d4a65886916451a01d80e3d40970692f0ab31cc53c737a8ffd3408641dd18b12b59d00880a1a51bf1201219c156f4687c89b3bba7966515c263a6da337c70219b18cbd7e1a7ca07f77287b04f62adf3ba0a00f903a4de156b402aa92faec6737f1be36050a4810eb34741a55cfc86f22b7e734e338f600f09df00574221e9f861fe9b68b5f8501ec4c809a6c85ede1d9410633478be10ac32a14f2d77664f050a16f2b0a73b470a99616c27a96a7b747c365d9191b9a0a92f96eb10e8cfd399d35a186ab9ee7f1680f3102a604fc69d1b1e5058113ac9d917ec32e1cb339755b5536e4715758f", - "public_inputs_hex": "0x1a207628cc6936816ccb62a7b56fdbbf8e975293b677c988644e018fc402e4411dfdc7cb6265f100524012f038ee1f205bf8789b70b46c57463dedb4998f7ac800000000000000000000000000000000c946233fabee579beb961008103a4c8d00000000000000000000000000000000026a297ba8fb72294cced25b49dae7c91806572c19ee2f3532e970d617919b3aa8cdc582782dfad0699badc631f75128000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000022b942ad53baa7c6e1a7616e4f0aa2eedf7d67eacc63696345525988225847479273fc58215abae6a73503410e8209a542c92bbea2ba8e6fb129c971def4ca8452ddcd2c1e0b08c268d942b52a49e7597873ccad9df1b02ea1ab854e7111eb2dd188f793a89424a83a3e746a72d4cdfd5702a7944deb0fa2ee04f84489f4af3c2000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "proof_hex": "0x00000000000000000000000000000000000000000000000b848f7067da04597300000000000000000000000000000000000000000000000cfb8eacb33dc5026500000000000000000000000000000000000000000000000a140f61a0ae7da9830000000000000000000000000000000000000000000000000001b35adc769dec000000000000000000000000000000000000000000000005529748f6a3df43120000000000000000000000000000000000000000000000041e2b70da2e142a38000000000000000000000000000000000000000000000002489ca9f8182b061f000000000000000000000000000000000000000000000000000069af3676bfe700000000000000000000000000000000000000000000000bc2f7c98198f35216000000000000000000000000000000000000000000000001de57a498803e7aa800000000000000000000000000000000000000000000000e40fd6827d61103c1000000000000000000000000000000000000000000000000000084230bc9e5b600000000000000000000000000000000000000000000000c20745ecc0a9b617d0000000000000000000000000000000000000000000000098615242b257ddaf500000000000000000000000000000000000000000000000d98b13f67a2b6a5220000000000000000000000000000000000000000000000000000df689a73e63b1a953e2b375615edc3058e7d1ca5cc4bcc12d1207ee7045581b3b6d157383acb008447d2b42adf3a25787526050a2c464130614c700a9cf0b996d758e0ea3101207ba4219ef22ce8b381e24a97633135e5ae877a3905824a6b2117d2c52adb6121f18d2e4a72128ba85103c6084065a0a957c369df32907adc47c59f3457ae3524cfaf1eb600e22c8222d5c7f3d05b89dfb6a6252ed158a263b28cf1d704e00722d53425971fa3915cb5842157f175ef3b6c978bd686b6bb911469c0a0bb04e524323ec504346fc6d320aefb7c8a5aadf1728b7a7598f38ee2595296f4aa053a27c5f74b1cb2d97320928bf8e5004eaea31e7ec579c77a628c4d06c323adffc62948c40c873573a3a100f76bfd152510de33a30d61a10d2b6d1c93de13958e1c18776cc9836534e19b35a0f44676561fe123ab6651f2926973483bed88c85e5c1ed56dc8a8651f15acc31d6d6bcf2e97b303b160aff49148eaae0a509cf30cd9294fb69b8ae360bd1e988a8fd0b940a61d4811e0e8b18fb2e137aa4e76109a151c5d96eba7b16f7543505b2e217df4e672a962b340c011df2743b79be60e0b0c05a2155c315ad8038be74112c3c0a11a2ba8bb424fe02ff440851c431f3a0117298170f8e7b02639cab117e04b8477192bc4e2feb6dc7d675bd3a0e09017ee842e52ae8728ea51e4a18da1ff28088f42d38ff18e60317977213e9c0d60b20d2828872222cc0f34e17aa5c9267d627f4ac25bf6c7969cc08f69079cc5182cf58c183cfc115d5179470fd7395d888cfca34a1913a3ac918e490bb8231b59359ef909ab3830af8794f8ad241303954d3f8c794969cf24f7f171196fd1267895e1e42daea16482d70b755aeac34e0b1442d10765de80604fff4fb678bc7f0400f5db1065748acf1169718b09f0aa0f601350f0a894f8d32b81d408c6d02b1906426515448f4e0aae668ea694fbbc2a7280df188e758dcd850ad11c7fa74268dd35f910a27b4c1b260afcbcd7f6837d48fc77d4a6da0cb7b8500247d05df4e3154c3024c6488ba33e3fd241db9e47eadb5c7ecdd8246a0532a85ea5c12ad12077537a256a2e262e3ba7450fcb7709ad3b2b9857b7326ea74d55f5ceb973b149f8cf7e0a8ab8172a80b043633b24ae10967d6489570114fe7c5bcc2419711b7d4be07121e64aa42315eaadd92cad2c7dfaba18ac683b7ba27997eec1d15edee22fd752017f1e3ea133164724fb9730c4863fac6f3c79aac2204d17c154dfeaa11ad78e1c9c38a7affeb9d26b70487b79b739b2daa0d2b89b0a57de0138d35875d5fd23095e676dc7daa828b9c87b55c7a3518b138dda5947ad0dd1b14a160547a853850ac087cc19fc4ec1f18b50f448df96d3b691645c3f02ae58749b108889cc72921344c9da4fcc45375fc89d97646e8a7d8ac9fc1507cfcb38fe716ca264c886db2146a1ccd11f884504c374b7a78a3396c92c52412791af6611b89bec51c8ba232444938535ffa6ecb26f8768571cb60fcb4e413ce7ccf34fd796bee3fbe9753322b0fa40c4ece2d11c0a0fbe4caf01ee10217d9713e3da320a49d2e44fa046fa2d95725fde9404a03323ed83df779d69954a6159ad108a0cec3860d6790373240e33e2916f05c0e22bf7ef3e1e55026f54b16bac5918c42a031e3cd75368a3ea0911eacdce98241da2874f049bf64456301ae43029d846360fc0b7157f3ccf9c2a618b5f19970b3569e8e6c296cb17ded8d661167462409d035d33f8f410a1af1c9d7c94e3a65ea5bb07307f1472f9bf7d3a77d4d738bafefbbb37b11d76a8592e9c374b65d060a6e0432b1e507bc3121a282a9098f9b1c56d75ca6cca35104415bbbb52c70c65afa557e9a5bda404374ce6d4933c90b2bb75b3ad3579f19bfe2479c82140a0b2d0dc1c358799373f4b0054141fdc63e26b4d40735cb98b412e0a92db0db0d383f17d1a3c5b32c14c7e00ecce12114b2a14c522a007a951e5d02562699300507e2202c4ec98a17de017c2a98f9f696f74f012a63e0ddea2b4c40e88b9ea9eba8c6a614ee3d17688edd8bfd425e968efd6e41f18fa4212a02c471d1fe5f93f27bbc092530c4ac6f2ad3a6560773c63da42312ebe80209dbe7af5221fa153635f616561ac1701355d572dda9f76e1858ec211a1cddd0c571472cc1524e7a358c86623765427aff53132b5a43e43735b0724cf4f7888341d96c119180e1cfed86fd01c41ab9a7b9ed10c57679055a8590d57581a6d0249971f93e12e33f3c568d000fc1ed9f0f12a741a42ec1a40e365c1ef0da413aa7292fdb61c0e994f53d3698d0be3dafb87348b8de14bef041dc9e7928909858239b791c7981e35ba93a4550b707bde9024dae26188c586bd851e350ae151480f3b787fc3db0053c854ac64e45cfd0c7526783b1d9066c34520dad62fd5c2195a94f05dcd90289201e5ef356c1ef34c84a1f3908211af44c1a9e44f63c34bc1077872f831cf2d6227dc6d78093ae08420e2c0ee695b9b8876fcbe7a5e27f7167c33cdfb5d3315023c8573bd927cd69c7dd64f6725215180bc8ec72d43dbe87904c30e01fd9e1315d81e115af8bef91661176d0d43d12c5281e09189e4fa8da6ef31867c5e49229b5e9f1e762ad2820ad879257ae7d089e7c29665fe3350109b817b2ba1cff502fc48497b60a3fcf25520e674245d397e728388b0885fb2254724111ce886c3008b363f7fa3ee256c6d2b66b2ee8ee6706129c381a305f06e0bf6427cb59d072472eff3c40da57fa40bad15e9c5fe639305d72ea09049d2a4598b72aa20bbe11559f8a819c5bc7278828b036cbf3cdbad90b31710103fa95493a3e1d95f14311b48fe60856b93baa932b1264f8203a3c78d7c04f10295c5ee7acf6d430edc2028a04b182925feeca261c0d7721177163c92b273fdc65ea9e8dc8644588ea1090684ef3bb32cd09380e7e12c59b402bd772c4418952cc8a294ff4ff1e46364041d295cc8f55949a82ae31c2f69523f349d5a258bacccd74e56231e958f8fee981ff582ff63854b92917c9bba45cf614b18c5b4632989de011ed79138ab62362d26ef1fd5eebd2afa99b676fdf9ba3ca972fba10c25aa0cb6dab126aa1a390be51ea1f33fa64b2d28c185a7a108e7dab30c1b6294c066e945d9eb6fec0dfdb9691c8121fe3c63bf90063464f2d891bdd627ae213e03b80d381676e921583af3cd21a651eee9fed187b8e69df4575045e3917026bfd4bf22a9aa5c680ce3d5cc0e0637807164449acdb6f3b12ac057411106e158e043f2ae60fa35ef009d6037eb0a92bf7d3b84212cfbf3ec6bc236b7821f0b17152ef2f1dbd536790d8060aaf81de5ee880095e83cca416a8783a531b8b9affc26f14848e3ea28aa826521b91d20a4b663e9fd51696ad6dcfd0068afb00e91ff5a813da0d09e4e27ddd02d477c294acafe501df227625d3ec378c8ebce32a57811927421e2e173efe6c0222a7e111dce77d8ff4a253b43bb277027570fc74609252c0c480a91326a694214312e2f5cab400667c72202f583340647f2910a182c44d174422c75d287fbed54792a2d54d768ea9152152da70641eabc415a78251bf2cd430f248bd5e22089e8d574231c9823edd290fb508e77034dc2f163bdc0126715cc646beef40785e43a7a6c10fad4cc07ac70c73a86c05ab097eae1fa23091df78485e6c00c7e9ab187eb8e09177ebd746640f923894114b8a1c066370d18c21cd3f0779187e04600feb1150e3812fe80bee8cd54a1622028c75ca001bb037e569533f3d9c127b688309a0026ba070ab9931c737db933bff1d7feff03fde071d5d47462704098e3f37e18e509b1f0c3190850f84d9ce5bef54dd7fc2f3c3c578ee43c40ee92e6256f605ded019e527d4b2c3f5e4e29cc85841ff0e94697affc8a31cb61062bc94a0e39bb140656f880eae3933627a1239b9b85969b64e9bffd551162916a19cd0f363259ab1a90ab085bb36bace2486c4692d034964ac64b049d51febf49b19dcfa5f99c170b17bd9df0b4b930338d1ef2d5696406b529fbd7dc830fc8c14c6bd8c16f00f1253c1e478d7e88fd2978ba0bd327c11331425cf4b985fd58b14d879b9839e0620bf079427cc9eeb05626ce30880a78ce60ae4d6ac366b9102d69765aec3752b40bc86f3d0a057736d13e3c85e4713dca46a775894be6a86afc029e0f8a76cea40f2182cf1d36ca720da6d2b5b679660c5061add32b5fc152a820b782087ba66b0a07809344a0134d129ba7f4e19956cfd5c47b410b7302525494b9a3e397b51e3039ad1ef896965dec0bff9b358868014c10ca1fb842e3b901b0e510122498551f2e9d7794aad63d358a377b42b0c6d1d78415abe68f58df1e04dc5bf11769b1051f90bc0f3e131d24df2c55c2e7050e7d33d22ad39e5db73e9af8b1c00128081b8ce0ce2a9951c146ffeff3e1fcd55fa601217e3da2193448fb858c06be8d3c1f7e565a2e821cba698bc9a13112bdfe25250d3823deb0e7a8fb419d9e772a922eb38484d054315f4418bb9a05efd6b9bfc3e5ee052f6a7fde779216815dfc4f13478e9b3d87b0a82f217cd9b0e782f61a8697d5d6707d5e0e04ae039d32acb62cbb49c04501ac0643d2524d3709215390667fd7b63a2c0c85f63f7a392074fd094f6c07f5ef4742de0a6aba56f4b16d8ce2e6f7270e5906c9cac6bbd72e0440214dd6d3ec8f200878ca27d4d36ca018bd7d9092b0aa1b86e6ea37d637778c3808dd56099b383a37fe3b60526ab524475ac22dd37d44a10380cf05c26d795edf159e18b8e9e07abf492f5ad7031ab13106b34b66be1ecc8366bb6933be7233d02250b41b4fb8f028195bc6f56fb47efbaa729680c0a89e3bab9c2d109baaf5231fed54a986f8a867b839ba0b3394cab7b638f79fb5fd0003bbb4f17236d2178b244a32093464ef748333651ad7798fefe65c89e14efea7bf094f077db3dcaaa2153a977ffc12f1b060c3fc74f4cac4c8852b1b86683e693d11c4978c2879e757056e7665ae4ec522122f631b3e7be0e247c70e498be722e31864c30b9e131f00069729385ad3114bbf616f1ee7b0da8bcacb110bf9e27cb437bedf1e283c5261008c0347f73b653e6bf9ac59c203749cca809dd1c666d128eaa074865773063400f1a2f125d9711cbd61c211db9c22d974b11205d2af4aff9f8f5795a2aa02991bc661edf40dcd7704f835795a7eaaa5d2ffca5a355400fd8eb053cd2eb201b72cecc94ff7c922e1351c74bec7068a18db3043d9048ebd87ffccbadae1ec64fa2b05d1c7370963c3ce5976f94d3c3cf4337bc97e2862c5371d7de5fbf8b08cf00a469637d69d340a3acad48ee62a6da336abe55d5a82d1c88e1a4cc0d64019010879351b300c0d4f5c8ec7593bcc6a9afa75eb0b529fe079bd4e99238be11c171ced5a0a55a72439584a10a548ff26452e842426bd0572527c6bb2e48b5f9ecb117d9f4717426d96ddc8c4e5c2d3cf707a3a44a20e5d564401a54fcbb2bec9c81527aed8153ecda2dfe77976690a75f4ad60038a20a8287fba638619b1bd8acf09bb43d9e15613ca424fb0f4b15117d8bf4cc72ceeabc45b803d6a4657ac4acf0bfbd262fd8158150068a3e2e6e6b97148231ab66fedb12b089fbf9ac106e6d70aec7ed98cc0a00c75a21498193b1249a75d4a121c8de1da14e8f03d76530171172b24f9cf58ed5bb84c96a80bff5fb44d8276815de2b11f63280aaf0910c977015b57b509baedf1a0abb96552ded4ffec30c1aa214f478ffbad0332bf585026177219291f2a7a5d6a47eb312e333eebadb57634b2c6b7b42340883b9444a9cf0d756bc8ec5020031ab0255720813527bf9ea6fa107c276178962953db9abb091a6dfccc0ee73df060ed6671445cb8ab79645d36a4c81daf02949f2ccac2f2b50d8c680c18a8038e2e4fb9e4613caec509de7e672fea040c71ade0f84ddf82442060de941d416b6cda862fb8a064d0721b031c7579c9b917480ee6e05054b3b70b73cb22a12cbebf44319d890f99ae72567d4fea41d298a200676aed5c9c440d2c1da3167a37b4a6a2a8e3cab3ab003d0a9bd75c1f3565db7440a50840afa0d820ed8e1a347f5ed5f14306fa9d6580d65de70e3d9e6ff372855141bf3baab542143e686a3af2c0ad7eec32397fea1d8261d7ad9fd280a835cfa66015b1e27517225c5c595103ebf1dae43c83a1b20d5d48a99fdd13de8804b71a36e29bb9220d2eb9e83179fb7d6d036a24a977f063539add79293bc4b8a514db5b794b578e0719e14d05a12be872f6d6d6eac49bf338195d99a4a767b10a4ef31b8df5c8b2240e27dfa9b58ab84a85882f45235480b01b40a00c94a4f7030f0963ce2a4cd8b0263bbcac11527f42563962bd99da5db443d0b1e0033795ca9a8c7903f1f339f6255933c3493c3bf79745abbd70be31483d1cac03942d2168a4f1847170fc85d302fff1ab84401d0eab0359433eeaf7cd60090c9d81e6c8e5c07e205eb049b2f10c72d392a223087ab81ca2a4f0077e20706fab5f7f305bdde4d0a9153a4915de2000df2d1e2967f24ba50b979f116f205af7492a83adf7a55eaa247ee60a2c421e43b1b9e7b656041004affd6a20ea5bb0ef63b91c467780eb487c6070fab41811ce6b8ad866d10db953803612ed1bbf5988481c603f79276edf7e871459055802f3d0053b41b8792dfdd36153e6ed6258f93c1f8e0b8ac021b9b94f525408b202a84935cd7589deadc831c1c5605db0e200fad3e10317a51d3fec67451b252c292e7b1774e660407e5a15b34a8c9f4d61f0e733d8c85ca0963b9f22a8435baa12070dbd1df83e54b0b2a1acc4171d1caee9a39fd3c7923cc6871804cd6a45241522f4d78196ee7eaffc973e1267f5890b93c6d0653bcef6aa53b62ed9b61b841e8bab815b1ded0129cb1b38c56e9a754538a399e7800b170ca36571923260a12d6bb79a7fb8e6dc8e5b7f00ca46cab952bce4483ad5cd24ffbf0f717380323122c46ba422755404ae8510b406b2fd907d9aaeb3963a3c27ececec99a33edaa9101f93327e93488b4a619d33020e760bf00158724f273434655cbb6f8d1a975c132e4be33e35e0efc792875a03699d80227c2f4a82cc1c41779b595ec6b229ca1aeb4878e91d33f5c1f8e6f4fc6dc0272fc57bc3f5695d22c8d8c48d43c1bdc31d2da24ac5e9316f10b959e21550a2cfd755abaefd3f38123733ffcd44e99736179ac0bae52fe1f58c61775f68d32f624cd87d2feb6cb4dbdbf4373a9d66b80d12eb80cbbe95ba9303626558d899af8106ced7de5ee279a67156c0a3c331aec02e87e29957b9c0ac857cce8a96e3f1df0c20d5de2919263398f66b9d600a73211678262826d92f4bcb00a9d6eb2c2eb2a7ecc4938a4014ac4c51c552607b6dc10cd7a5bd4df68a63f60e99a4217feee85660182837c340fc514b1babb8999f22252b722718ed1b4cba7feeea9e62cf1f928561264ffccd96ba25a5b2d251034805640ec7a51cd89c8c15551954728e5c4a0fb51a2eb096f42476fa59c85cede303994fab2c2b79d77b62c0e603897a38e965fefa190cb8f5a56e41f3e5d982ec1d07f555fdc8fb2ad2f4ffe98295c150230780734aa03b45309488fd2833b8381d4757d04fa792cc07684c3779d6e7925b62e7a9c7e4a480bbcb4d6fedac1f18128d5eb739d4b69e8b21071a541c26179848912a0d4cc4ff0a781cb6b31f878f02974c4f50900ed33f113e0bb0f26468012cc533c2ca3992893bab98c90ffc870737d1e3dbd4fefb3f3cad23798bd53a8582110eaffd4d743e338d4d722f37ce1ebcc857a827e17f21b11559e1967b8d77375ab7496a26c4b0b074a869f4dba80ee390a805941102fcfd60ae53451ffdea972f2540da6e0158f526d3746ad4151bbfc38f1e51d61b972ba86463812c0757425438511fb52e822cfaf6de308b220969aa9f86e81ea726dd04aec3f9d5ac8a8f17b0f20f9d609076fc00b14bd3fe02345a027a720c6e69b4a63e5e36183dbfd0f8cc4e91bbe3c3068c619a1b1d3403aff3af1b56cc7b1739136e60067ae287c98d9e2ce149030a202ac73941a6ff0f94086685820732dd374210e88b9f4b63485d7aedd71fb847236dba844137ce0450b47164a5f527c08afd78e8f640c14d87643dd72e423cf6fbba79157dcc420566c93478dd666c41ffa31c2ecd7be1dbce9d9bdcf168d4ada263e0d0f430011c777c6b3b40b46080e53d4119b244eff42c6e69ed48279af0cf4770293edaf511a16c3963ec99669ac601b463cbf66e3fab9c15d3060e000fa88e3d05bc656a1ba70800b6c5741a4423df85e4054f05520df212d9281dc34f7fb9249ecf8c3c0f4d2c8f7acc0e83aee1d251a75f8b91adcb606c62d3ede2690a94ac2278b8bb19f63535ec4947928b83cb975772c1af82294fa24b19f01347a7152ac2c3ddc62079fcae6d7b3760bb8b4c789def8284a0c967e2a4952a1a004f5f0eacd2cc75056ccd1e5bb952dff6b0d686e0518547fa090b71c61135b66143c3902bb940381e174fb8cabadfc57cf2b221173a632bb2118164ee73e7551653239d0f26b0ab0251555276a22689f249497e1aa2fdb80c1ccb10f1c9251356172afcc8daff492210ecc2afbe7e22c86e233d33094753e2d99ebb44400299bfadba8f00742d9d1a3407043af6996354516cea8df2b32a4a812e495d44034010823b33d462803f0b1a6e0bbd39623cc963d33a0a091ca88a609129f5ca0aa07564b4ddf1cf3fda284e629a09f29850659d05d665d6239a15df892595a38396e55b60e15f582ba40f41fd33fc05b2dfdcd49d4d2c57699a0ed952941b8cad789dcf07e30b681da50407bed8c5b81c23c1b3b42c545d849a58c8a573234b62976474fcf2a8fcaeb807573f4628bc97825e7f54b1a3ad043bea287e4445e53f042c4c6a15464841ae11ff27679c65cd10dc23087e03e9e29be3c1f3a2e9cb0ae4fa6854e09b9d26f1063cbac8be17a250addbc6bd7abaae71d56e594050d5c1d7df6b7442966069fd243dd956498931f6cf7cb430a0cd69c77ff8e4607c1afe006c8d74c3f813f7b41f1087a949f426d578c92f78fae3702cc06f55f93ea35fe7b8d001c8f615ce1d1ffe8754bad06ea54d0a3112a3810bf1bc5b9ae3baa0d2ab3c099718bf98bf4820658f23fbdb337379fddaaff6303ce007a89157a5a9a8b2509cb6f2e0c72114244743184f8156393fe128396af8c48d5a74dc8946b61d66137aa6ee7d4930cf264a587fdbd7aa0c068804057b2215c264c3c77663dd4cb58f2bdc2de0b5ab112b104da9bac0d9db69a448412588863ea869d4f890eb8e4faab5827299a111b42ee9463654303cd9f4dcff5e58afe88724abcd3fe4cb3168aef9b6ceb7e9b1a6156679fd2085e6db996c6540ada72d5ea0cff41cdb9483648f9f2685347e7bc512a9d6e535978c5ed0ce38f4cd903a38b3b61ddc2d79e14080166c08fb5272b52644a0ee32396a63c02d3746844ceec71816272a612ce308778c70a75119f5b0140916fafdc97b1f9dcadb8fd0735e7c5e57c9da0c017770e01e20ea42b73f771522f9921eeb1daa7e293c702d0f61b4569b5b516fca3820d31f6dd4ceda2f9728c0cfef56256dfb1a8d61cd21a3f45f7eb98e3091e05328e13e50cdbc6843ce2d8f397f0050b564b3ebd0acbafc6510b3e86e22b2d2047f8ec5257c46db621209a2a078c9dec80cc74b327642819a2f99c7be4b2e1d2199e2f2beac3dc2e5022d6d91efdb7b76b09877ceed20947ac3cab1538083c437423814930ef96dea4c15090bc6cebea4517d208e2e824892c6e16377ee7929e61d65ad7237dd82598010f7abaf49e01ec9e6e08a82c4e853284210a8c4702df5bf12123f71a1f0f5160d6794f9c7a9279b930eea4d341380919281804a4e94872273e2e5cc539c76901209f99b730c355459134fe2c0f31cf6b4588b1d44d3ebac7719797d95bd035208afabe41e1975f751fc36aaa3407e07c6d57413529e35a7c1b38491422ef1291f6a30c0c6daf41e7058c2591f6b097d9b2edde98f1b20ce6fd5aece66bea7fa2915ce7d77023ae83c91694c5f0b737add3e40e4843a6c18f6d707c3d2c85df0299529419ac1919c5bc769faedd0f12b209ccb799f5dc915a985c0708c680aa11d7895f6e24a12d2bfd0a60807702e15814b1520e87f3409de46e5209973232d01db4272a97d90adb153e7cc27dd74f4185a29398200a656d1d9dfd683aaf39313946aea52c413519d6b3bc374cc173cff10809b8909baa02b1e5159edee8bb62eac59720ed6dcacfe763a0b7cfe322855ebf7bff78973198d6938e4ffd88c8b0d32ae740ef3e56931f09c0c29f96ab6c186380f5071cec86000f7a22df3a72d0b895de61a03eac7b5558fbe848e4f10ff02e3908077f41ad02d24fd4cd0bb6b26e7e931f92d07c7fb6398efe14d166d8636b74c50f36125536e8d4e54b8ef490f9613caac20bc9595db4415c3c2220cdde4da13c96c7fc5aec1fe4d3959b9f924f963f560c6b25f8fde46885119f57a6f7371258532c6993d273a15634e653f02b022ab922892ed90592aab7ecbc248ca426f6c781699f05b94fd9699d9193d1fac31588805eb9e9c2b0f6d07d672153267fa971b65d7919fc06fed449ec9f10cfa1765e6fe32511c5cd9964110fa2e8c31067d0aca0594a8e30930f7cad4df0852eeb8f4ee0118d291d2d6bc65e845de6259392a3a23f0ebb4b8044e9b033a0e02436c782b517fd3571f5e640fc633f9eaea7afb3e5e52d7fb7ae85002bd43280e9cb71ca103b038f5440f07ffb5c07022c63c71b7b13c5d02d95611745d5e163831b7d8405c891e99b0187be7e16ee91a4df4d51a1f1e7471a4df113b506b27d0bd739504efa54410a51805c7a0c3473591bc5e1fb407b88989972bc23efa2d26ee8732f82d458d22a48f6f87698fb230510278f71e6110d3c824c27b0ad00a8cfa8560bd7d8182893afe1d9fb2977cb50d6fa56f26d1517d2f71e0ccb42622faa7016742636fb83a916407f2d4efc89f9946cc1cd1e0d1cc3807759633cb0510e8ac665d0a45803f1c9568411eef994874bfcc7931df17395ff51241ff5c124400dfcdd47389cb656691a889fe095e97f339d36646df49e57b965d8b33e214b4f694f8f0925e03fd5f40981b9286d6e407f18f92ffc8c85f83ef8d07181312b7055d28e732a517a8521eaaaa90a3ca67de07dc28e701b85c5dbdacfd5a5008b34dfe53d6fc73f30cb18c00518041bfc57d9f64b64e8678cd4c1188676e530227175b1eef5ff44203e722278bb91f98fe8b911573c39b9f761e7f73dad7a92dbf76cec5be468770a1978606c24b0d3e522f5120b693eff54d38b1d10cd80f114153657e6e4376f300f59fecc5e7709e17b7e14fe4af20642b7ad7bfe91bb0300fd5d9db6bf4df35703fe05bff89f997c274d95ccf9bf07eb5ddb8c8b8fc9c2f48ece63e29a592ea41c2685c73cd1097a2134a4c484b2b53d6b4bb234739e22abdc8030545ac4df71a37aaefb9c7a704ce293764bbbd30c4baadbcf6c369eb0ec714758cb33b4e304652545922f487f96c6a267ffd7fc0d7c11a25d6e0429608c2f6df6ef67e6390eff059329d36ba9db3eac7bca7a8f2ca56840f6141305f2c116a3db192620c0a131bbaefac4da65c54529043350d609fcad09ade9a24402e8725b3338cfb990f458ae7a4c52ccaf53ce9ae282547a422a26827d1d3dce2274e9b51112988587a3ab714f0ef42a1d876dc39e3c112a79cae3896a33d9a13072177ee6d6fea94f01eb78f935f8c9de33b62221ba77b347b9229a334d417182e59c4a6cd9fa657cc2a8667b92f6bb818b472c5d7715b61bc5989755ee041240801037f656357e6c0fa53e6b42cee535390a6876123780b29ba90d7c558da9f12d9a37b69f6f5b3e924428e8b08fc14af078a468c81f4c3c6c1e06c277de2a620a0f69e36deb8c19cddcb73fa68c8301e1ebdf39a72ade861dc02e5ab7f8ab428cab45a4ff96e733274d6d87f1df3e42f219b2d613394a1755de67872ffbeed131a535a20e7c418741bd73ca355044fa144a7936e930279ed6e3ded182d8cc61d89de26952b776cf1d17ed04d61968d07370a8d3950445b4bd9642394f003ff14a6c73f993dc9ca33bfb497cfcb7f27a2f3f2452fead8ab31a0182e3d3b70632ed9d410f61054261e79d7a3ac90233c3b473a3576fff10fcfd468e95749cbdd2add7cff11960f67ed0ef73691934d54f99e0ee94202a349f03436c695b561a81e808338a7d39286e584ac61ee5d2d94c0c22bd24bec13e7e10ef3125e2ae98301659ef85444a286a35b4837e5a034f3f1e4acfa0ae181957571788a160dec670c479afbd10fc99caf3c58705fe8f1fa7cd599743838be1e46742adc2f33a8691dd72b5b0c3d6dc48a5d7fb4bfe2e7112ab91b325348739db3d804d56d207583091d5c455a120de60438a23ac79f773c4f3a7d3f6ba689cd73e78d8fcb11c34928e9f3e2950b59524e5c978460553925ee38e93f4a36ba9fc319a2cb9554530603f73f6d43197e47255602bdbfa9d0f42763a3d7c827fe7652119d280f8bfcb1291f0c1ef60919a98e8fd034a894a276ef544c3c726a4d7765486549002b0b6401c820c3ae13077142caa1a9cf4dfeb037b575379d09bbfdfad8e9a3a65b442a2eb998ee8036dc8ee44b19765f3ad89c68250136bdaebcd6f20a64a2a996a0ee2a2e15af6c67e71250d9fb7dc0c4f5396938174d1836f598c91312cc390edbac22082e3de6dc6c42bbfc782edb4735308a414b581425c46fa1899c43dffab7ad1470b789f3a376d8f05e382e4eef5ad2f730a4c6ca71b3dc181bfedc014a3d2b00dea01112567d0465b3e605cbca12e841543e9f39ecc126724ac802fbccaa8c0238a905a0a909ba0c4d302b30dc3b77073e601e426d5ecdcd9e991f8cb9a833291480283cb121f1bdc9f638f91033e3aa6b5c64dc4006f5aae7cfd5d5bbfac70ba40e9cb25808a0a689515f811e67e7d63cc307cc75654506f44405570f452418754d1712c04993196cab35c7ea1f2144ecc1543a2c4e1591f3b94ecd495fee1c1bb846112a5aae6cb6a19faf9fb27b33277f32daa6f82c824fbb3055507bc818f96b1ec9ef2de81da729dca128fdcda775a39fab77b88be963fb242a099ccc0ece6a91d0bf6d93df0e19f975ad7da6e9f10a546949ebd6b18180ed3825a77e1c6e15d81d01398ec10c8fd8724f52c696b34de7e26ac48375189ddd503e19ed293aa1cd6eae95bcf17f3add4b4dfa09c3433fd656e0c3dadf111b8f1143a7e22516d371c2a38e182f341290fbb5056ac9f1ea51058ee260825918bb89a393502df87d475d7fc3d36b0dc606bcf6c369c51a422d9b95752543704a8d9c2ac374271e2d61cd899fffba684b47845bda1683342d76d8ee853a801288cb933b889f1446f0ad542d89e6721fae1dcf14690775cf7b2bea19715f14e2ce869bfe48871f14cafb1ebc870c16ad964b0571772d77f785dc4d116ac3ec7dd3e6e33a316d292cf4826fc7534dd40a58496a31a6698165ed2d15d6b65f4cf65a083b233081182c75c5c7256093280baf3e6039b485c966a4adc93e968128151b789e493478216822e156940e5383997307d339c3cd0f6e6b797ed3c20efa02724897beb560224117481c8293bacccec8b5930f3debc7459fa6c8e71d9f3277b14c5fa01c4311b90c808a4fdf40dc95186163f4e28cc568c5e5acbd27b7618bd0e5c9bb7efe2219af0f6204b7497060c1d9601f9efc7f317f7c6f7081ab661059f573305d9e2fd2eb51c85ac7b45341c2cc132192bc506304df61661e833aa262d3fa75e21208e014fe42ce0846f8b16168a662a57a5b55d0d27af4237a165855fd27f122851019b4c04fb5d022c676fc313ffb296a03eef785638c893853cfc602561ad32b101fd6ad5ea0fea4d4d5a492017fcad531bc307455c1b7905de3408a9d8850ce25e8e3a1e6ed3c3306eb705fe3785cb93b49278b24fb6a96793798d2f0a057052df7591a5099f3fcbb2e0e7410709cc4bbbe1b05460812d481bb5f02b59603601e677bb528c3a003004de33da4925468554eac3cb3229f138b19a1278a83d7ab15e9be54c76e61c983702c43cffc2d2e9b91907bc8d7274aa2212996a91e1c880655f31c7e5235bd1859b5362ec0d4e1c591b9da7391a509c56d6f7a331e6e7e0e8bd781ec70faec75a6bfb9b6405e5ad2ffe2acf7ab915b986a456f16b61c3f0e1244ad90e3f92e0b892ee2608af2845ec819cb196bfb2cdbda39e30bc912d81b93a3d81947e6fddc6b4f3efb313fed09e5c0d9f6a27b1e97bb5a143474d3b51a0a41cc70485edfc31889587523d0da2943be33c2ef8f1298b0a6194efa03ac29ccc15bb12ba5b38d399585be59fb9ece3a9e74f5a92249c7da5ff28469859c0770a3a910361232dad8292a99fcd4f06d55ad29a3abb22aa9de47d58d9567572260eb019429970a087a1bc0f84db9d940b67051598e083885cf0a54dd7c7f41166e9923e928060b8c82bd127cdddeb969328bd242e5043220916323ddef3e8f09c3519aac68604cb8f6cc8a01aa514bbe678b3c2d57c6a5d69459f3c6adbcd0064007fa97d6dd95157d7281574bb25d82d45f1357627bd5426b55360c9e1c022d671d7c295d43f13f84d21247c8b1ae74a060e60e8aca8c6e9520e0b6defe58", + "public_inputs_hex": "0x15c9b404c344691839abf638a132bd083b09e98c2fadb268923013737d217ad41dfdc7cb6265f100524012f038ee1f205bf8789b70b46c57463dedb4998f7ac800000000000000000000000000000000cb80a0dfbeaeb79af355a70d8f19855c00000000000000000000000000000000915681ec51465d5a3404f61907428b7100000000000000000000000000000000666965e3fee8efc43146d3b5cc157df900000000000000000000000000000000b2039cf00b2cd6017d01a8e38e5d2d102b26ea14534f288a6affb3a0aa4953baf450f69fa48000890435a8ee45258a0a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000214ae98e2b0527277d74d7898e5a1d76041a613ee74f2db890bd6db2fd3a76e9d30329528532c70174aa6a80f652ef9fb86217d722ac4e756dac8ec699873be87009de2f6efc0e2420bebb740d52be9795045be2c08d2f61c665fd8c2946f25a70037344482bad1ff4c46d1ed3887ee2f37d4bc2dd3a1c34c3c6a0b0d75d4fa630000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" } } diff --git a/packages/interfold-contracts/test/fixtures/helpers.ts b/packages/interfold-contracts/test/fixtures/helpers.ts index f9eec7897..3c9aaab2f 100644 --- a/packages/interfold-contracts/test/fixtures/helpers.ts +++ b/packages/interfold-contracts/test/fixtures/helpers.ts @@ -13,6 +13,7 @@ import { COMMITTEE_SIZE_MINIMUM, SORTITION_SUBMISSION_WINDOW, } from "./constants"; +import { buildMockDkgAttestationFixtureData } from "./dkgAttestation"; const { time } = networkHelpers; const abiCoder = ethers.AbiCoder.defaultAbiCoder(); @@ -50,6 +51,25 @@ export const setupAndPublishCommittee = async ( await time.increase(SORTITION_SUBMISSION_WINDOW + 1); await registry.finalizeCommittee(e3Id); const pkCommitment = ethers.keccak256(publicKey); + if (committeeProof === "0x" && dkgAttestationBundle === "0x") { + let verifierAddress = await registry.dkgFoldAttestationVerifier(); + if (verifierAddress === ethers.ZeroAddress) { + const verifier = await ethers.deployContract( + "DkgFoldAttestationVerifier", + ); + await verifier.waitForDeployment(); + verifierAddress = await verifier.getAddress(); + await registry.setInitialDkgFoldAttestationVerifier(verifierAddress); + } + const fixture = await buildMockDkgAttestationFixtureData( + operators, + e3Id, + pkCommitment, + verifierAddress, + ); + committeeProof = fixture.proof; + dkgAttestationBundle = fixture.bundle; + } await registry.publishCommittee( e3Id, publicKey, @@ -90,8 +110,6 @@ export interface BuildRequestParamsOptions { startOffset?: number; /** `inputWindow[1] - time.latest()`. Defaults to `300` (5 minutes). */ windowDuration?: number; - /** Defaults to `false`. */ - proofAggregationEnabled?: boolean; /** Override custom params bytes. Defaults to an ABI-encoded throwaway address. */ customParams?: string; /** Param-set id registered on the Interfold. Defaults to `0`. */ @@ -131,6 +149,5 @@ export const buildRequestParams = async ( ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: opts.proofAggregationEnabled ?? false, }; }; diff --git a/packages/interfold-contracts/test/fixtures/system.ts b/packages/interfold-contracts/test/fixtures/system.ts index 8b36e31b6..584b47ecd 100644 --- a/packages/interfold-contracts/test/fixtures/system.ts +++ b/packages/interfold-contracts/test/fixtures/system.ts @@ -97,6 +97,12 @@ export interface DeployInterfoldSystemOptions { maxDuration?: number; /** Override the timeout config. Defaults to {@link DEFAULT_TIMEOUT_CONFIG}. */ timeoutConfig?: TimeoutConfig; + /** + * Install the permissive DKG fold-attestation verifier used by ordinary tests. + * Defaults to `true`. Set to `false` for verifier lifecycle tests that need an + * initially unset registry. + */ + wireMockDkgFoldAttestationVerifier?: boolean; /** Treasury for `E3RefundManager`. Defaults to `"owner"`. */ treasury?: "owner" | Signer; /** `slashedFundsTreasury` passed to `BondingRegistry`. Defaults to `"owner"`. */ @@ -107,7 +113,7 @@ export interface DeployInterfoldSystemOptions { * - `registry.setSlashingManager` * - `slashingManager.{setCiphernodeRegistry,setInterfold,setE3RefundManager}` * - * Pass `false` for legacy fixtures that only wire the + * Pass `false` for isolated fixtures that only wire the * `bondingRegistry <-> slashingManager` link (always wired). */ wireSlashingManager?: boolean; @@ -341,7 +347,7 @@ export async function deployInterfoldSystem( BondingRegistry: { owner: ownerAddress, ticketToken: await ticketToken.getAddress(), - licenseToken: ADDRESS_ONE, // placeholder — fixed below + licenseToken: ethers.ZeroAddress, // one-time placeholder — fixed below registry: effectiveRegistryAddress, slashedFundsTreasury: slashedFundsTreasuryAddress, ticketPrice: TICKET_PRICE, @@ -499,6 +505,18 @@ export async function deployInterfoldSystem( ENCRYPTION_SCHEME_ID, await pkVerifier.getAddress(), ); + if ( + !mockCiphernodeRegistry && + (opts.wireMockDkgFoldAttestationVerifier ?? true) + ) { + const mockDkgFoldAttestationVerifier = await ethers.deployContract( + "MockDkgFoldAttestationVerifier", + ); + await mockDkgFoldAttestationVerifier.waitForDeployment(); + await ciphernodeRegistry.setInitialDkgFoldAttestationVerifier( + await mockDkgFoldAttestationVerifier.getAddress(), + ); + } // ── Committee thresholds ([M, N] per CommitteeSize) ───────────────────── for (const [size, [m, n]] of committeeThresholds) { @@ -546,7 +564,6 @@ export async function deployInterfoldSystem( ["address"], ["0x1234567890123456789012345678901234567890"], ), - proofAggregationEnabled: false, }; return { diff --git a/packages/interfold-sdk/README.md b/packages/interfold-sdk/README.md index dc2c06bd5..280b2b83f 100644 --- a/packages/interfold-sdk/README.md +++ b/packages/interfold-sdk/README.md @@ -351,6 +351,24 @@ await sdk.startEventPolling(); sdk.stopEventPolling(); ``` +`CommitteePublished.publicKey` is an untrusted transport value. Validate it against the event's +on-chain commitment before using it for encryption: + +```typescript +import { hexToBytes } from 'viem' + +await sdk.onInterfoldEvent(RegistryEventType.COMMITTEE_PUBLISHED, async (event) => { + const publicKey = hexToBytes(event.data.publicKey) + const expectedCommitment = hexToBytes(event.data.pkCommitment) + + if (!(await sdk.validatePublicKeyCommitment(publicKey, expectedCommitment))) { + throw new Error('Committee public-key commitment mismatch') + } + + // The key is now safe to pass to the encryption methods. +}) +``` + #### Encryption ```typescript @@ -360,6 +378,7 @@ const params = await sdk.getThresholdBfvParamsSet(); // Key generation const publicKey = await sdk.generatePublicKey(); const commitment = await sdk.computePublicKeyCommitment(publicKey); +const isValid = await sdk.validatePublicKeyCommitment(publicKey, commitment); // Encrypt data const encrypted = await sdk.encryptNumber(data: bigint, publicKey: Uint8Array); diff --git a/packages/interfold-sdk/src/contracts/contract-client.ts b/packages/interfold-sdk/src/contracts/contract-client.ts index 48432c5e9..62737d4f1 100644 --- a/packages/interfold-sdk/src/contracts/contract-client.ts +++ b/packages/interfold-sdk/src/contracts/contract-client.ts @@ -158,7 +158,6 @@ export class ContractClient { paramSet: params.paramSet, computeProviderParams: params.computeProviderParams, customParams: params.customParams || '0x', - proofAggregationEnabled: params.proofAggregationEnabled ?? true, }, ], account, @@ -233,7 +232,6 @@ export class ContractClient { paramSet: requestParams.paramSet, computeProviderParams: requestParams.computeProviderParams, customParams: requestParams.customParams || '0x', - proofAggregationEnabled: requestParams.proofAggregationEnabled ?? true, }, ], }) diff --git a/packages/interfold-sdk/src/contracts/types.ts b/packages/interfold-sdk/src/contracts/types.ts index 7897bb365..36162b15e 100644 --- a/packages/interfold-sdk/src/contracts/types.ts +++ b/packages/interfold-sdk/src/contracts/types.ts @@ -60,9 +60,6 @@ export interface E3RequestParams extends RequestParams { paramSet: number computeProviderParams: `0x${string}` customParams?: `0x${string}` - /** When true, ciphernodes generate wrapper/fold proofs for DKG proof aggregation. - * When false, proof aggregation is skipped for faster computation. Defaults to true. */ - proofAggregationEnabled?: boolean } export enum E3Stage { diff --git a/packages/interfold-sdk/src/events/event-listener.ts b/packages/interfold-sdk/src/events/event-listener.ts index e48c585b9..dfaf651c5 100644 --- a/packages/interfold-sdk/src/events/event-listener.ts +++ b/packages/interfold-sdk/src/events/event-listener.ts @@ -218,7 +218,12 @@ export class EventListener implements SDKEventEmitter { if (callbacks) { callbacks.forEach((callback) => { try { - void (callback as EventCallback)(event) + const result = (callback as EventCallback)(event) + if (result) { + void result.catch((error) => { + console.error(`Error in event callback for ${event.type}:`, error) + }) + } } catch (error) { console.error(`Error in event callback for ${event.type}:`, error) } diff --git a/packages/interfold-sdk/src/events/types.ts b/packages/interfold-sdk/src/events/types.ts index add662a21..b1583615f 100644 --- a/packages/interfold-sdk/src/events/types.ts +++ b/packages/interfold-sdk/src/events/types.ts @@ -93,6 +93,7 @@ export interface CommitteePublishedData { e3Id: bigint nodes: string[] publicKey: string + pkCommitment: string proof: string } diff --git a/packages/interfold-sdk/src/interfold-sdk.ts b/packages/interfold-sdk/src/interfold-sdk.ts index 39c2a62d8..6ade6fd76 100644 --- a/packages/interfold-sdk/src/interfold-sdk.ts +++ b/packages/interfold-sdk/src/interfold-sdk.ts @@ -103,6 +103,24 @@ export class InterfoldSDK { return computePublicKeyCommitment(publicKey, this.thresholdBfvParamsPresetName) } + /** + * Validate serialized BFV public-key bytes before using them for encryption. + * + * `CommitteePublished.publicKey` is an untrusted transport value. Consumers + * must compare its semantic BFV commitment with the event's on-chain + * `pkCommitment` before accepting it. + */ + public async validatePublicKeyCommitment(publicKey: Uint8Array, expectedCommitment: Uint8Array): Promise { + if (expectedCommitment.length !== 32) { + return false + } + + const actualCommitment = await this.computePublicKeyCommitment(publicKey) + return ( + actualCommitment.length === expectedCommitment.length && actualCommitment.every((byte, index) => byte === expectedCommitment[index]) + ) + } + public async encryptNumber(data: bigint, publicKey: Uint8Array): Promise { return encryptNumber(data, publicKey, this.thresholdBfvParamsPresetName) } @@ -140,7 +158,6 @@ export class InterfoldSDK { paramSet: number computeProviderParams: `0x${string}` customParams?: `0x${string}` - proofAggregationEnabled?: boolean gasLimit?: bigint }): Promise { return this.contractClient.requestE3(params) diff --git a/packages/interfold-sdk/tests/events.test.ts b/packages/interfold-sdk/tests/events.test.ts index f800d675b..836fe5056 100644 --- a/packages/interfold-sdk/tests/events.test.ts +++ b/packages/interfold-sdk/tests/events.test.ts @@ -5,9 +5,15 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import { CiphernodeRegistryOwnable__factory } from '@interfold/contracts/types' -import { describe, expect, it } from 'vitest' +import type { Log, PublicClient } from 'viem' +import { afterEach, describe, expect, it, vi } from 'vitest' -import { RegistryEventType } from '../src/events/types' +import { EventListener } from '../src/events/event-listener' +import { RegistryEventType, type InterfoldEvent } from '../src/events/types' + +afterEach(() => { + vi.restoreAllMocks() +}) describe('RegistryEventType', () => { it('uses the finalized-committee event name exposed by the registry ABI', () => { @@ -16,4 +22,39 @@ describe('RegistryEventType', () => { expect(RegistryEventType.COMMITTEE_FINALIZED).toBe('SortitionCommitteeFinalized') expect(registryEventNames).toContain(RegistryEventType.COMMITTEE_FINALIZED) }) + + it('handles asynchronous event callback failures', async () => { + const error = new Error('invalid committee key') + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined) + const listener = new EventListener({ + publicClient: {} as PublicClient, + contracts: { + interfold: '0x0000000000000000000000000000000000000001', + ciphernodeRegistry: '0x0000000000000000000000000000000000000002', + feeToken: '0x0000000000000000000000000000000000000003', + }, + }) + const event: InterfoldEvent = { + type: RegistryEventType.COMMITTEE_PUBLISHED, + data: { + e3Id: 1n, + nodes: [], + publicKey: '0x', + pkCommitment: `0x${'00'.repeat(32)}`, + proof: '0x', + }, + log: {} as Log, + timestamp: new Date(), + blockNumber: 1n, + transactionHash: '0x', + } + + listener.on(RegistryEventType.COMMITTEE_PUBLISHED, async () => { + throw error + }) + listener.emit(event) + await Promise.resolve() + + expect(consoleError).toHaveBeenCalledWith(`Error in event callback for ${RegistryEventType.COMMITTEE_PUBLISHED}:`, error) + }) }) diff --git a/packages/interfold-sdk/tests/sdk.test.ts b/packages/interfold-sdk/tests/sdk.test.ts index 098a14165..a945fbc3a 100644 --- a/packages/interfold-sdk/tests/sdk.test.ts +++ b/packages/interfold-sdk/tests/sdk.test.ts @@ -50,6 +50,18 @@ describe('encryptNumber', () => { expect(value.length).to.equal(9_242) }) + it('should validate a committee public key against its on-chain commitment', async () => { + const publicKey = await sdk.generatePublicKey() + const commitment = await sdk.computePublicKeyCommitment(publicKey) + + expect(await sdk.validatePublicKeyCommitment(publicKey, commitment)).to.equal(true) + + const differentCommitment = commitment.slice() + differentCommitment[0] ^= 1 + expect(await sdk.validatePublicKeyCommitment(publicKey, differentCommitment)).to.equal(false) + expect(await sdk.validatePublicKeyCommitment(publicKey, new Uint8Array(31))).to.equal(false) + }) + it('should encrypt a vector and generate a proof without crashing in a node environent', async () => { const publicKey = await sdk.generatePublicKey() diff --git a/templates/default/client/src/pages/steps/RequestComputation.tsx b/templates/default/client/src/pages/steps/RequestComputation.tsx index 162ba3307..62d66fc3f 100644 --- a/templates/default/client/src/pages/steps/RequestComputation.tsx +++ b/templates/default/client/src/pages/steps/RequestComputation.tsx @@ -6,6 +6,8 @@ import React, { useState, useEffect } from 'react' import { CalculatorIcon } from '@phosphor-icons/react' +import { hexToBytes } from 'viem' +import type { CommitteePublishedData } from '@interfold/sdk' import CardContent from '../components/CardContent' import Spinner from '../components/Spinner' import ErrorDisplay from '../components/ErrorDisplay' @@ -45,18 +47,33 @@ const RequestComputation: React.FC = () => { })) } - const handleCommitteePublished = (event: any) => { - const { e3Id, publicKey } = event.data - setE3State((prev) => { - if (prev.id !== null && e3Id === prev.id) { - return { - ...prev, - isCommitteePublished: true, - publicKey: publicKey as `0x${string}`, - } + const handleCommitteePublished = async (event: { data: CommitteePublishedData }) => { + const { e3Id, publicKey, pkCommitment } = event.data + if (e3State.id === null || e3Id !== e3State.id || !sdk.sdk) return + + try { + const isBoundKey = await sdk.sdk.validatePublicKeyCommitment( + hexToBytes(publicKey as `0x${string}`), + hexToBytes(pkCommitment as `0x${string}`), + ) + if (!isBoundKey) { + throw new Error(`Rejected committee public key for E3 ${e3Id}: commitment mismatch`) } - return prev - }) + + setE3State((prev) => { + if (prev.id !== null && e3Id === prev.id) { + return { + ...prev, + isCommitteePublished: true, + publicKey: publicKey as `0x${string}`, + } + } + return prev + }) + } catch (error) { + setRequestError(error) + console.error(`Rejected committee public key for E3 ${e3Id}:`, error) + } } onInterfoldEvent(InterfoldEventType.E3_REQUESTED, handleE3Requested) @@ -66,7 +83,7 @@ const RequestComputation: React.FC = () => { off(InterfoldEventType.E3_REQUESTED, handleE3Requested) off(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublished) } - }, [isInitialized, onInterfoldEvent, off, InterfoldEventType, RegistryEventType, setE3State]) + }, [isInitialized, onInterfoldEvent, off, InterfoldEventType, RegistryEventType, setE3State, e3State.id, sdk.sdk]) // Auto-advance to next step when committee publishes useEffect(() => { @@ -109,7 +126,6 @@ const RequestComputation: React.FC = () => { e3Program: contracts.e3Program, paramSet: 0, // ParamSet.Insecure512 computeProviderParams, - proofAggregationEnabled: false, } const fee = await sdk.sdk.getE3Quote(requestParams) diff --git a/templates/default/hardhat.config.ts b/templates/default/hardhat.config.ts index 1aa00a599..7d74fab3a 100644 --- a/templates/default/hardhat.config.ts +++ b/templates/default/hardhat.config.ts @@ -131,6 +131,7 @@ const config: HardhatUserConfig = { '@interfold/contracts/contracts/test/MockCiphernodeRegistry.sol', '@interfold/contracts/contracts/test/MockComputeProvider.sol', '@interfold/contracts/contracts/test/MockDecryptionVerifier.sol', + '@interfold/contracts/contracts/test/MockDkgFoldAttestationVerifier.sol', '@interfold/contracts/contracts/test/MockE3Program.sol', '@interfold/contracts/contracts/test/MockPkVerifier.sol', '@interfold/contracts/contracts/test/MockSlashingVerifier.sol', diff --git a/templates/default/interfold.config.yaml b/templates/default/interfold.config.yaml index 7dec47d07..fda781360 100644 --- a/templates/default/interfold.config.yaml +++ b/templates/default/interfold.config.yaml @@ -43,6 +43,9 @@ program: # node: # multithread_reserve_threads: 1 # multithread_concurrent_jobs: 8 + # # Test/CI only. Requires mock on-chain proof verifiers and an Interfold binary built with + # # the `test-only-skip-proof-aggregation` Cargo feature. + # skip_proof_aggregation: false nodes: cn1: address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' diff --git a/templates/default/scripts/test_integration.sh b/templates/default/scripts/test_integration.sh index fd5141d64..79e14edbe 100755 --- a/templates/default/scripts/test_integration.sh +++ b/templates/default/scripts/test_integration.sh @@ -4,6 +4,14 @@ set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." +# The template integration deploys mock proof verifiers. Keep recursive proof +# aggregation enabled by default for users, but skip it in this bounded CI test. +export E3_NODES__CN1__SKIP_PROOF_AGGREGATION=true +export E3_NODES__CN2__SKIP_PROOF_AGGREGATION=true +export E3_NODES__CN3__SKIP_PROOF_AGGREGATION=true +export E3_NODES__CN4__SKIP_PROOF_AGGREGATION=true +export E3_NODES__CN5__SKIP_PROOF_AGGREGATION=true + passed_message() { echo "" echo "------------------------" diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index 3c67049f9..f8541808f 100644 --- a/templates/default/tests/integration.spec.ts +++ b/templates/default/tests/integration.spec.ts @@ -209,7 +209,6 @@ describe('Integration', () => { e3Program: contracts.e3Program, paramSet: 0, // ParamSet.InsecureThreshold512 computeProviderParams, - proofAggregationEnabled: false, } const quote = await sdk.getE3Quote(requestParams) console.log('E3 quote:', quote) diff --git a/tests/integration/base.sh b/tests/integration/base.sh index ee14ae48f..15b00c04f 100755 --- a/tests/integration/base.sh +++ b/tests/integration/base.sh @@ -19,7 +19,7 @@ done pnpm evm:clean -if [[ "$PROOF_AGGREGATION_ENABLED" == "true" ]]; then +if [[ "$FULL_PROOF_AGGREGATION" == "true" ]]; then heading "Deploy contracts (ZK verification + fold attestation verifier)" ENABLE_ZK_VERIFICATION=true pnpm evm:deploy else @@ -63,7 +63,7 @@ pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_4 --network localho heading "Add ciphernode $CIPHERNODE_ADDRESS_5" pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_5 --network localhost -heading "Request Committee (proof-aggregation-enabled=$PROOF_AGGREGATION_ENABLED)" +heading "Request Committee" ENCODED_PARAMS=0x$($SCRIPT_DIR/lib/pack_e3_params.sh \ --moduli 0xffffee001 \ @@ -82,12 +82,11 @@ pnpm committee:new \ --input-window-start "$INPUT_WINDOW_START" \ --input-window-end "$INPUT_WINDOW_END" \ --e3-params "$ENCODED_PARAMS" \ - --committee-size 0 \ - --proof-aggregation-enabled "$PROOF_AGGREGATION_ENABLED" + --committee-size 0 wait_for_committee_pubkey 0 "$SCRIPT_DIR/output/pubkey.bin" "$INTEGRATION_DKG_TIMEOUT" -if [[ "$PROOF_AGGREGATION_ENABLED" == "true" ]]; then +if [[ "$FULL_PROOF_AGGREGATION" == "true" ]]; then heading "Verify active aggregator (proof aggregation / DKG path)" ACTIVE_AGG_ADDRESS=$(wait_for_active_aggregator_address 0 "$INTEGRATION_DKG_TIMEOUT") echo "Active aggregator: $ACTIVE_AGG_ADDRESS" @@ -98,7 +97,7 @@ daemon_query_events cn1 "$SCRIPT_DIR/output/events.txt" check_last_line "$SCRIPT_DIR/output/events.txt" '{"Next":10}' -if [[ "$PROOF_AGGREGATION_ENABLED" == "true" ]]; then +if [[ "$FULL_PROOF_AGGREGATION" == "true" ]]; then heading "Wire MockE3Program → Interfold so publishInput triggers decryption" pnpm e3-program:setMockInterfold --network localhost diff --git a/tests/integration/fns.sh b/tests/integration/fns.sh index 11e15c368..8256b4eb7 100644 --- a/tests/integration/fns.sh +++ b/tests/integration/fns.sh @@ -7,7 +7,7 @@ ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" PLAINTEXT="4,0" # Set by test.sh via export_integration_flags -PROOF_AGGREGATION_ENABLED="${PROOF_AGGREGATION_ENABLED:-false}" +FULL_PROOF_AGGREGATION="${FULL_PROOF_AGGREGATION:-false}" INTEGRATION_DKG_TIMEOUT="${INTEGRATION_DKG_TIMEOUT:-1300}" ID=$(date +%s) @@ -34,7 +34,15 @@ CIPHERNODE_ADDRESS_4="0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" CIPHERNODE_ADDRESS_5="0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc" -if command -v interfold >/dev/null 2>&1; then +if [[ -n "${INTERFOLD_BIN:-}" ]]; then + if [[ ! -x "$INTERFOLD_BIN" ]]; then + echo "Configured INTERFOLD_BIN is not executable: $INTERFOLD_BIN" >&2 + exit 1 + fi +elif [[ "${CIPHERNODE_SKIP_PROOF_AGGREGATION:-false}" == "true" ]]; then + cargo build --locked --bin interfold --features e3-cli/test-only-skip-proof-aggregation + INTERFOLD_BIN="$ROOT_DIR/target/debug/interfold" +elif command -v interfold >/dev/null 2>&1; then INTERFOLD_BIN="interfold" elif [[ -f "$ROOT_DIR/target/debug/interfold" ]]; then INTERFOLD_BIN="$ROOT_DIR/target/debug/interfold" diff --git a/tests/integration/interfold.config.yaml b/tests/integration/interfold.config.yaml index 44d4f35ae..16fd9f85e 100644 --- a/tests/integration/interfold.config.yaml +++ b/tests/integration/interfold.config.yaml @@ -45,6 +45,9 @@ program: # node: # multithread_reserve_threads: 1 # multithread_concurrent_jobs: 8 + # # Test/CI only. Requires mock on-chain proof verifiers and an Interfold binary built with + # # the `test-only-skip-proof-aggregation` Cargo feature. + # skip_proof_aggregation: false nodes: cn1: diff --git a/tests/integration/lib/clean_folders.sh b/tests/integration/lib/clean_folders.sh index db12e5a43..b121d2901 100755 --- a/tests/integration/lib/clean_folders.sh +++ b/tests/integration/lib/clean_folders.sh @@ -4,7 +4,15 @@ clean_folders() { # Delete output artifacts rm -rf "$SCRIPT_DIR/output/"* - rm -rf "$SCRIPT_DIR/.interfold/" + + # Reset per-run node state without deleting the source-aligned Noir + # artifacts staged by prebuild.sh. Removing the whole .interfold directory + # makes `interfold noir setup` download the released circuit bundle, which + # can have an older ABI than the Rust witness code under test. + rm -rf \ + "$SCRIPT_DIR/.interfold/config" \ + "$SCRIPT_DIR/.interfold/data" \ + "$SCRIPT_DIR/.interfold/noir/work" } clean_folders $1 diff --git a/tests/integration/lib/prebuild.sh b/tests/integration/lib/prebuild.sh index 2116a0e8f..356d751c5 100755 --- a/tests/integration/lib/prebuild.sh +++ b/tests/integration/lib/prebuild.sh @@ -15,17 +15,17 @@ echo "" echo "FINISHED PREBUILDING BINARIES" echo "" -if [[ "${PROOF_AGGREGATION_ENABLED:-false}" == "true" ]]; then - echo "" - echo "BUILDING ZK CIRCUITS + ON-CHAIN VERIFIERS (proof aggregation enabled)..." - echo "" +echo "" +echo "BUILDING SOURCE-ALIGNED ZK CIRCUITS..." +echo "" - # Nodes must prove with the same VKs as deployed Honk verifiers. `interfold noir setup` - # otherwise installs the release tarball (crates/zk-prover/versions.json), which can - # disagree with locally rebuilt circuits/verifiers after `build:circuits`. - rm -rf "${INTEGRATION_NOIR}/circuits" - mkdir -p "${INTEGRATION_NOIR}/circuits" +# The ciphernode always generates leaf proofs, even when recursive aggregation +# is skipped. Never let integration tests combine current Rust witnesses with +# the older release circuit ABI. +rm -rf "${INTEGRATION_NOIR}/circuits" +mkdir -p "${INTEGRATION_NOIR}/circuits" "${INTEGRATION_NOIR}/bin" +if [[ "${FULL_PROOF_AGGREGATION:-false}" == "true" ]]; then (cd "$ROOT_DIR" && pnpm build:circuits --preset insecure-512 -o "${INTEGRATION_NOIR}/circuits") # `--check`: verify the committed Honk Solidity verifiers in # packages/interfold-contracts/contracts/verifiers/bfv/honk/ match the @@ -33,22 +33,33 @@ if [[ "${PROOF_AGGREGATION_ENABLED:-false}" == "true" ]]; then # silently rewriting committed contracts mid-test. If this errors, run # `pnpm generate:verifiers --write` and commit the diff. (cd "$ROOT_DIR" && pnpm generate:verifiers --check --no-compile --no-clean-targets) +else + # C5/C7 final aggregation is skipped, but DKG and decryption leaf proofs + # still execute. Build only those two source groups for the fast CI profile. + (cd "$ROOT_DIR" && pnpm build:circuits --preset insecure-512 --group dkg,threshold -o "${INTEGRATION_NOIR}/circuits") +fi - if ! command -v jq >/dev/null 2>&1; then - echo "jq is required to pin noir/version.json for integration ZK fixtures" >&2 - exit 1 - fi - REQUIRED_BB="$(jq -r '.required_bb_version' "$VERSIONS_JSON")" - REQUIRED_CIRCUITS="$(jq -r '.required_circuits_version' "$VERSIONS_JSON")" - jq -n \ - --arg bb "$REQUIRED_BB" \ - --arg circuits "$REQUIRED_CIRCUITS" \ - '{bb_version: $bb, circuits_version: $circuits}' \ - > "${INTEGRATION_NOIR}/version.json" - - echo "Staged circuits under ${INTEGRATION_NOIR}/circuits/insecure-512" - echo "Pinned noir version.json (bb=${REQUIRED_BB}, circuits=${REQUIRED_CIRCUITS})" - echo "" - echo "FINISHED BUILDING ZK CIRCUITS + VERIFIERS" - echo "" +if ! command -v jq >/dev/null 2>&1; then + echo "jq is required to pin noir/version.json for integration ZK fixtures" >&2 + exit 1 +fi +if ! command -v bb >/dev/null 2>&1; then + echo "bb is required to build source-aligned integration circuits" >&2 + exit 1 fi + +REQUIRED_BB="$(jq -r '.required_bb_version' "$VERSIONS_JSON")" +REQUIRED_CIRCUITS="$(jq -r '.required_circuits_version' "$VERSIONS_JSON")" +jq -n \ + --arg bb "$REQUIRED_BB" \ + --arg circuits "$REQUIRED_CIRCUITS" \ + '{bb_version: $bb, circuits_version: $circuits}' \ + > "${INTEGRATION_NOIR}/version.json" +cp "$(command -v bb)" "${INTEGRATION_NOIR}/bin/bb" +chmod +x "${INTEGRATION_NOIR}/bin/bb" + +echo "Staged circuits under ${INTEGRATION_NOIR}/circuits/insecure-512" +echo "Pinned noir version.json (bb=${REQUIRED_BB}, circuits=${REQUIRED_CIRCUITS})" +echo "" +echo "FINISHED BUILDING SOURCE-ALIGNED ZK CIRCUITS" +echo "" diff --git a/tests/integration/persist.sh b/tests/integration/persist.sh index 0350c521d..0c168876b 100755 --- a/tests/integration/persist.sh +++ b/tests/integration/persist.sh @@ -19,7 +19,7 @@ done pnpm evm:clean -if [[ "${PROOF_AGGREGATION_ENABLED:-false}" == "true" ]]; then +if [[ "${FULL_PROOF_AGGREGATION:-false}" == "true" ]]; then ENABLE_ZK_VERIFICATION=true pnpm evm:deploy else pnpm evm:deploy @@ -73,8 +73,7 @@ pnpm committee:new \ --input-window-start "$INPUT_WINDOW_START" \ --input-window-end "$INPUT_WINDOW_END" \ --e3-params "$ENCODED_PARAMS" \ - --committee-size 0 \ - --proof-aggregation-enabled "${PROOF_AGGREGATION_ENABLED:-false}" + --committee-size 0 wait_for_committee_pubkey 0 "$SCRIPT_DIR/output/pubkey.bin" "${INTEGRATION_DKG_TIMEOUT:-1300}" diff --git a/tests/integration/test.sh b/tests/integration/test.sh index f8bde2cc0..f199bd74b 100755 --- a/tests/integration/test.sh +++ b/tests/integration/test.sh @@ -4,15 +4,15 @@ set -eu # Exit immediately if a command exits with a non-zero status THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PROOF_AGGREGATION_ENABLED="${PROOF_AGGREGATION_ENABLED:-false}" +CIPHERNODE_SKIP_PROOF_AGGREGATION="${CIPHERNODE_SKIP_PROOF_AGGREGATION:-true}" SKIP_PREBUILD=false parse_integration_args() { while [[ $# -gt 0 ]]; do case "$1" in - --proof-aggregation-enabled) + --skip-proof-aggregation) shift - PROOF_AGGREGATION_ENABLED="${1:-true}" + CIPHERNODE_SKIP_PROOF_AGGREGATION="${1:-true}" shift ;; --no-prebuild) @@ -21,7 +21,7 @@ parse_integration_args() { ;; *) echo "Unknown integration argument: $1" >&2 - echo "Usage: ./test.sh [base|persist|net|restart] [--proof-aggregation-enabled true|false] [--no-prebuild]" >&2 + echo "Usage: ./test.sh [base|persist|net|restart] [--skip-proof-aggregation true|false] [--no-prebuild]" >&2 exit 1 ;; esac @@ -29,18 +29,29 @@ parse_integration_args() { } export_integration_flags() { - export PROOF_AGGREGATION_ENABLED - if [[ "$PROOF_AGGREGATION_ENABLED" == "true" ]]; then + if [[ "$CIPHERNODE_SKIP_PROOF_AGGREGATION" == "true" ]]; then + FULL_PROOF_AGGREGATION=false + else + FULL_PROOF_AGGREGATION=true + fi + export FULL_PROOF_AGGREGATION + if [[ "$FULL_PROOF_AGGREGATION" == "true" ]]; then export ENABLE_ZK_VERIFICATION=true export INTEGRATION_DKG_TIMEOUT="${INTEGRATION_DKG_TIMEOUT:-3600}" else export ENABLE_ZK_VERIFICATION=false export INTEGRATION_DKG_TIMEOUT="${INTEGRATION_DKG_TIMEOUT:-1300}" fi + export CIPHERNODE_SKIP_PROOF_AGGREGATION + export E3_NODES__CN1__SKIP_PROOF_AGGREGATION="$CIPHERNODE_SKIP_PROOF_AGGREGATION" + export E3_NODES__CN2__SKIP_PROOF_AGGREGATION="$CIPHERNODE_SKIP_PROOF_AGGREGATION" + export E3_NODES__CN3__SKIP_PROOF_AGGREGATION="$CIPHERNODE_SKIP_PROOF_AGGREGATION" + export E3_NODES__CN4__SKIP_PROOF_AGGREGATION="$CIPHERNODE_SKIP_PROOF_AGGREGATION" + export E3_NODES__CN5__SKIP_PROOF_AGGREGATION="$CIPHERNODE_SKIP_PROOF_AGGREGATION" } if [ $# -eq 0 ]; then - export PROOF_AGGREGATION_ENABLED=false + export CIPHERNODE_SKIP_PROOF_AGGREGATION=true export_integration_flags "$THIS_DIR/lib/prebuild.sh" "$THIS_DIR/persist.sh"