Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 33 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ members = [
"miner-api",
"node",
"pallets/frame-system",
"pallets/miner-aggregation",
"pallets/mining-rewards",
"pallets/multisig",
"pallets/qpow",
Expand Down Expand Up @@ -132,6 +133,7 @@ zeroize = { version = "1.7.0", default-features = false }

# Own dependencies
pallet-balances = { version = "46.0.0", default-features = false }
pallet-miner-aggregation = { path = "./pallets/miner-aggregation", default-features = false }
pallet-mining-rewards = { path = "./pallets/mining-rewards", default-features = false }
pallet-multisig = { path = "./pallets/multisig", default-features = false }
pallet-qpow = { path = "./pallets/qpow", default-features = false }
Expand All @@ -155,11 +157,11 @@ qp-poseidon = { version = "1.4.0", default-features = false }
qp-poseidon-core = { version = "1.4.0", default-features = false }
qp-rusty-crystals-dilithium = { version = "2.4.0", default-features = false }
qp-rusty-crystals-hdwallet = { version = "2.3.1" }
qp-wormhole-circuit = { version = "2.0.1", default-features = false }
qp-wormhole-circuit-builder = { version = "2.0.1", default-features = false }
qp-wormhole-prover = { version = "2.0.1", default-features = false }
qp-wormhole-verifier = { version = "2.0.1", default-features = false }
qp-zk-circuits-common = { version = "2.0.1", default-features = false }
qp-wormhole-circuit = { version = "2.0.1", git = "https://github.com/Quantus-Network/qp-zk-circuits", rev = "982fd4de36019c108169fedcff348003a06ad465", default-features = false }
qp-wormhole-circuit-builder = { version = "2.0.1", git = "https://github.com/Quantus-Network/qp-zk-circuits", rev = "982fd4de36019c108169fedcff348003a06ad465", default-features = false }
qp-wormhole-prover = { version = "2.0.1", git = "https://github.com/Quantus-Network/qp-zk-circuits", rev = "982fd4de36019c108169fedcff348003a06ad465", default-features = false }
qp-wormhole-verifier = { version = "2.0.1", git = "https://github.com/Quantus-Network/qp-zk-circuits", rev = "982fd4de36019c108169fedcff348003a06ad465", default-features = false }
qp-zk-circuits-common = { version = "2.0.1", git = "https://github.com/Quantus-Network/qp-zk-circuits", rev = "982fd4de36019c108169fedcff348003a06ad465", default-features = false }


# polkadot-sdk dependencies
Expand Down
111 changes: 111 additions & 0 deletions docs/delegated-l1-aggregation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Delegated L1 Aggregation

Wormhole proving is split into two layers:

- Layer 0 remains client-side because L0 proof generation touches private witness data.
- Layer 1 is delegated to bonded aggregation miners and only aggregates already-public L0 aggregate proofs.

The protocol invariant is:

```text
Candidate submission does not lock nullifiers.
Bundle claim locks nullifiers.
L1 settlement consumes locked nullifiers and marks them used.
Direct L0 verification rejects used or locked nullifiers.
```

The MVP candidate path stores bounded L0 aggregate proof bytes on-chain in
`pallet-miner-aggregation`. Submission parses public metadata and queues candidates by
`BundleGroupKey`, but it does not run full ZK verification, settle exits, lock nullifiers, or mark
nullifiers used.

Nullifier state is owned by `pallet-wormhole`:

- `UsedNullifiers` tracks nullifiers consumed by settlement.
- `LockedNullifiers` tracks nullifiers exclusively leased to an aggregation bundle.

This prevents the direct-L0 race after a miner claims a bundle, while avoiding the opposite attack
where unverified candidate spam locks nullifiers.

Current reward and bond model:

- candidate submitters reserve a storage bond, validity bond, and aggregation tip
- aggregation miners register a reward address, active-job limit, and bond
- bundle claim reserves a miner bond and locks all bundle nullifiers
- timeout releases bundle locks, returns unexpired candidates to the queue, and releases the miner bond
- successful L1 settlement consumes locked nullifiers, marks candidates settled, releases candidate bonds, and pays candidate tips to the configured aggregation reward account
- pending expired candidates can be dropped and refunded
- pending invalid candidates can be challenged; if full L0 verification fails, the candidate is marked invalid and its validity bond is burned

MVP limitations:

- L1 public inputs do not yet expose a constrained `bundle_root`; settlement compares the reconstructed public effects instead.
- `Bundle.bundle_root` is metadata until the L1 circuit exposes a constrained public root.
- The live external-miner RPC adapter is intentionally separated behind the miner service
`AggregationChainClient` trait; the worker core can be exercised with mocked clients and real
local prover artifacts.

Dependency strategy:

1. Merge the `qp-zk-circuits` delegated L1 aggregation changes first, or keep downstream repos
pinned to an exact `qp-zk-circuits` commit.
2. The chain workspace currently pins `qp-zk-circuits` crates to
`982fd4de36019c108169fedcff348003a06ad465`.
3. The `quantus-miner` delegated aggregation worker must use the same `qp-zk-circuits` commit until
these crates are published or upgraded together.
4. Preferred merge order is `qp-zk-circuits`, then `chain`, then `quantus-miner`.

Runtime weights:

- `pallet-miner-aggregation/src/weights.rs` currently uses conservative placeholder weights.
- `pallet-miner-aggregation/src/benchmarking.rs` contains benchmark scaffolding and
`runtime/src/benchmarks.rs` registers `pallet_miner_aggregation`.
- Placeholders are explicit for all extrinsics:
`submit_l0_candidate`, `register_aggregator`, `update_aggregator`, `unregister_aggregator`,
`claim_bundle`, `submit_l1_aggregate_cheap_reject`, `submit_l1_aggregate_valid_proof`,
`timeout_bundle`, `challenge_invalid_l0_candidate`, `challenge_invalid_l0_in_bundle`, and
`drop_expired_candidate`.
- The dispatchable weight for `submit_l1_aggregate` is the valid-proof placeholder because it is the
conservative upper bound. Cheap rejects are separately documented for future benchmark mapping.
- These values are not benchmark-generated. Replace them with autogenerated benchmark output before
release.

Benchmark regeneration:

```bash
cargo test --locked -p pallet-miner-aggregation --features runtime-benchmarks
cargo build --release --locked --features runtime-benchmarks
./target/release/quantus-node benchmark pallet \
--chain dev \
--pallet pallet_miner_aggregation \
--extrinsic '*' \
--steps 50 \
--repeat 20 \
--output pallets/miner-aggregation/src/weights.rs
```

Known benchmark blocker: the checked-in L1 settlement fixture is configured for one layer-0 proof,
while the runtime currently configures `MinerAggregationNumLayer0Proofs = 2`. Either regenerate the
fixture for the runtime setting or run the valid L1 benchmark with `QP_GENERATE_LAYER1=true` and
`QP_NUM_LAYER0_PROOFS=1` before committing generated weights.

ZK proving tests must use release mode:

```bash
cargo test --release -p <zk-crate> <test_name> -- --nocapture
cargo run --release -p <prover-binary> -- <args>
```

Parser-only, storage-only, mock-runtime, and precomputed-proof fixture tests may run without
`--release`.

Local E2E fixture command:

```bash
./chain/scripts/e2e-delegated-l1-aggregation.sh
```

This command regenerates fixtures with release-mode proving, runs the release-mode chain settlement
fixture, runs the miner worker state-machine test, and runs the miner real-prover fixture. The live
external-miner RPC adapter remains behind `AggregationChainClient` and is not exercised by this
fixture command.
55 changes: 55 additions & 0 deletions pallets/miner-aggregation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[package]
authors.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
name = "pallet-miner-aggregation"
repository.workspace = true
version = "0.1.0"

[dependencies]
codec = { workspace = true, default-features = false, features = ["derive"] }
frame-benchmarking = { workspace = true, optional = true }
frame-support.workspace = true
frame-system.workspace = true
hex = { workspace = true, optional = true, features = ["alloc"] }
pallet-wormhole.workspace = true
qp-wormhole-verifier = { workspace = true, default-features = false }
scale-info = { workspace = true, default-features = false, features = ["derive"] }
sp-io.workspace = true
sp-runtime.workspace = true

[dev-dependencies]
hex = { workspace = true, features = ["alloc"] }
pallet-assets = { workspace = true, features = ["std"] }
pallet-balances = { workspace = true, features = ["std"] }
pallet-zk-tree = { workspace = true, features = ["std"] }
qp-dilithium-crypto = { workspace = true, features = ["std"] }
qp-header = { workspace = true, features = ["serde"] }
qp-poseidon = { workspace = true, features = ["std"] }
qp-wormhole = { workspace = true, features = ["std"] }
sp-core.workspace = true
sp-state-machine.workspace = true

[features]
default = ["std"]
runtime-benchmarks = [
"dep:frame-benchmarking",
"dep:hex",
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"pallet-wormhole/runtime-benchmarks",
]
std = [
"codec/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"hex?/std",
"pallet-wormhole/std",
"qp-wormhole-verifier/std",
"scale-info/std",
"sp-io/std",
"sp-runtime/std",
]
63 changes: 63 additions & 0 deletions pallets/miner-aggregation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Miner Aggregation Pallet

MVP pallet for delegated Wormhole L1 aggregation.

Current scope:

- signed users submit bounded on-chain L0 aggregate proof candidates
- candidate submission parses public L0 metadata but does not verify the proof
- candidate submission does not lock or mark nullifiers
- compatible candidates are queued by `BundleGroupKey`
- submitter storage bond, validity bond, and aggregation tip are reserved
- aggregation miners register a reward address, job limit, and bond
- registered miners can claim a full compatible bundle
- bundle claim locks nullifiers through `pallet-wormhole`
- bundle timeout unlocks nullifiers and returns unexpired candidates to the queue
- L1 submission performs cheap public-input/effects checks before full L1 verification
- pending expired candidates can be cleaned up and refunded
- pending invalid candidates can be challenged and have their validity bond burned
- claimed invalid candidates can be challenged before L1 settlement

Nullifier locking remains owned by `pallet-wormhole`. Bundle claim must call the wormhole lock
helpers; candidate submission must never lock nullifiers.

Any test or command that generates ZK proofs must run with `--release`. The default pallet test
suite uses precomputed fixture proof bytes and does not generate proofs.

MVP limitations:

- L1 fixture regeneration must use `chain/scripts/generate-delegated-l1-fixture.sh`, which runs
proving in release mode.
- The local delegated aggregation E2E fixture can be run with
`./chain/scripts/e2e-delegated-l1-aggregation.sh` from the workspace root.
- The positive L1 settlement fixture test requires `QP_GENERATE_LAYER1=true` and
`QP_NUM_LAYER0_PROOFS=1` so `pallet-wormhole` embeds matching L1 verifier artifacts.
- `Bundle.bundle_root` remains metadata until the L1 circuit exposes a constrained public root.

Weights:

- `src/weights.rs` contains explicit conservative placeholder weights for every pallet extrinsic.
- `submit_l1_aggregate_cheap_reject` and `submit_l1_aggregate_valid_proof` are both recorded; the
dispatchable declares the valid-proof placeholder because it is the safe upper bound.
- Benchmark scaffolding lives in `src/benchmarking.rs` and is registered in
`runtime/src/benchmarks.rs`.
- These weights are not benchmark-generated and must be replaced with runtime benchmark output before release.

Benchmark regeneration:

```bash
cargo test --locked -p pallet-miner-aggregation --features runtime-benchmarks
cargo build --release --locked --features runtime-benchmarks
./target/release/quantus-node benchmark pallet \
--chain dev \
--pallet pallet_miner_aggregation \
--extrinsic '*' \
--steps 50 \
--repeat 20 \
--output pallets/miner-aggregation/src/weights.rs
```

The `submit_l1_aggregate_valid_proof` benchmark requires matching L1 verifier artifacts and the
current one-candidate L1 fixture. Run it with `QP_GENERATE_LAYER1=true` and
`QP_NUM_LAYER0_PROOFS=1`, or regenerate the fixture for the runtime's configured
`NumLayer0Proofs` before replacing `weights.rs`.
Loading
Loading