diff --git a/circom_circuits/Blend/poq.circom b/circom_circuits/Blend/poq.circom new file mode 100644 index 0000000..1d385e7 --- /dev/null +++ b/circom_circuits/Blend/poq.circom @@ -0,0 +1,98 @@ +// PoQ.circom +pragma circom 2.1.9; + +include "../hash_bn/poseidon2_hash.circom"; +include "../misc/constants.circom"; // defines NOMOS_KDF, SELECTION_RANDOMNESS, PROOF_NULLIFIER +include "../misc/comparator.circom"; +include "../circomlib/circuits/bitify.circom"; +include "../ledger/notes.circom"; // defines proof_of_membership +include "../Mantle/pol.circom"; // defines proof_of_leadership + +/** + * ProofOfQuota(nLevelsPK, nLevelsPol) + * + * - nLevelsPK : depth of the core-node public-key registry Merkle tree + * - nLevelsPol : depth of the slot-secret tree used in PoL (25) + */ +template ProofOfQuota(nLevelsPK, nLevelsPol, bitsQuota) { + // Public Inputs + signal input session; // session s + signal input Qc; // core quota Q_C + signal input Ql; // leadership quota Q_L + signal input pk_root; // Merkle root of registered core-node public keys + signal input aged_root; // PoL: aged notes root + signal input latest_root; // PoL: latest notes root + signal input K; // Blend: one-time signature public key + + signal output nullifier; + + // Private Inputs + signal input selector; // 0 = core, 1 = leader + signal input index; // nullifier index + + // Core-nodes inputs + signal input core_sk; // core node secret key + signal input core_path[nLevelsPK]; // Merkle path for core PK + signal input core_selectors[nLevelsPK]; // path selectors (bits) + + // Leaders inputs (all PoL inputs) + component pol = proof_of_leadership(); + + + // Constraints + selector * (1 - selector) === 0; + + // derive pk_core = Poseidon(NOMOS_KDF || core_sk) + component kdf = Poseidon2_hash(2); + component dstKdf = NOMOS_KDF(); + kdf.inp[0] <== dstKdf.out; + kdf.inp[1] <== core_sk; + signal pk_core; + pk_core <== kdf.out; + + // Merkle‐verify pk_core in pk_root + component coreReg = proof_of_membership(nLevelsPK); + for (var i = 0; i < nLevelsPK; i++) { + core_selectors[i] * (1 - core_selectors[i]) === 0; + coreReg.nodes[i] <== core_path[i]; + coreReg.selector[i] <== core_selectors[i]; + } + coreReg.root <== pk_root; + coreReg.leaf <== pk_core; + + // enforce PoL + // (All constraints inside pol ensure LeadershipVerify) + // /!\ copy the PoL constraints here /!\ + is_winning <== //0 or 1 for PoL + + // Enforce the selected role is correct + selector * (is_winning - coreReg.out) + coreReg.out === 1; + + + + // Quota check: index < Qc if core, index < Ql if leader + component cmp = SafeLessThan(bitsQuota); + cmp.a <== index; + cmp.b <== selector * (Ql - Qc) + Qc; + cmp.out === 1; + + // Derive selection_randomness + component randomness = Poseidon2_hash(4); + component dstSel = SELECTION_RANDOMNESS(); + randomness.inp[0] <== dstSel.out; + // choose core_sk or pol.secret_key: + randomness.inp[1] <== selector * (pol.secret_key - core_sk ) + core_sk; + randomness.inp[2] <== index; + randomness.inp[3] <== session; + + // Derive proof_nullifier + component nf = Poseidon2_hash(2); + component dstNF = PROOF_NULLIFIER(); + nf.inp[0] <== dstNF.out; + nf.inp[1] <== randomness.out; + nullifier <== nf.out; +} + +// Instantiate with chosen depths: 32 for core PK tree, 25 for PoL slot tree +component main { public [ session, Qc, Ql, pk_root, aged_root, latest_root, K ] } + = ProofOfQuota(32, 25, 6); \ No newline at end of file diff --git a/circom_circuits/Mantle/pol.circom b/circom_circuits/Mantle/pol.circom index d62ba25..0292036 100644 --- a/circom_circuits/Mantle/pol.circom +++ b/circom_circuits/Mantle/pol.circom @@ -11,7 +11,7 @@ include "../misc/constants.circom"; template ticket_calculator(){ signal input epoch_nonce; signal input slot; - signal input commitment; + signal input note_id; signal input secret_key; signal output out; @@ -20,7 +20,7 @@ template ticket_calculator(){ hash.inp[0] <== dst.out; hash.inp[1] <== epoch_nonce; hash.inp[2] <== slot; - hash.inp[3] <== commitment; + hash.inp[3] <== note_id; hash.inp[4] <== secret_key; out <== hash.out; @@ -42,28 +42,27 @@ template derive_secret_key(){ template derive_entropy(){ signal input slot; - signal input commitment; + signal input note_id; signal input secret_key; signal output out; component hash = Poseidon2_hash(4); - component dst = NOMOS_NONCE_CONTRIB(); + component dst = NOMOS_NONCE_CONTRIB_V1(); hash.inp[0] <== dst.out; hash.inp[1] <== slot; - hash.inp[2] <== commitment; + hash.inp[2] <== note_id; hash.inp[3] <== secret_key; out <== hash.out; } - -template proof_of_leadership(){ +template is_winning_leadership(secret_depth){ signal input slot; signal input epoch_nonce; signal input t0; signal input t1; signal input slot_secret; - signal input slot_secret_path[25]; + signal input slot_secret_path[secret_depth]; //Part of the note id proof of membership to prove aged signal input aged_nodes[32]; @@ -83,19 +82,10 @@ template proof_of_leadership(){ signal input starting_slot; signal input secrets_root; - // The winning note. The unit is supposed to be NMO and the ZoneID is MANTLE - signal input state; + // The winning note value signal input value; - signal input nonce; - // One time signing key used to sign the block proposal and the block - signal input one_time_key; - - //Avoid the circom optimisation that removes unused public input - signal dummy; - dummy <== one_time_key * one_time_key; - - signal output entropy_contrib; + signal output out; // Derive the secret key @@ -109,28 +99,17 @@ template proof_of_leadership(){ pk.secret_key <== sk.out; - - // Derive the commitment from the note and the public key - component cm = commitment(); - cm.state <== state; - cm.value <== value; - component nmo = NMO(); - cm.unit <== nmo.out; - cm.nonce <== nonce; - component staking = STAKING(); - cm.zoneID <== staking.out; - cm.public_key <== pk.out; - // Derive the note id - component note_id = Poseidon2_hash(4); - component dst_note_id = NOMOS_NOTE_ID(); + component note_id = Poseidon2_hash(5); + component dst_note_id = NOMOS_NOTE_ID_V1(); note_id.inp[0] <== dst_note_id.out; note_id.inp[1] <== transaction_hash; note_id.inp[2] <== output_number; - note_id.inp[3] <== cm.out; + note_id.inp[3] <== value; + note_id.inp[4] <== pk.out; - // Check the note is aged enough + // Check the note ID is aged enough //First check selectors are indeed bits for(var i = 0; i < 32; i++){ aged_selectors[i] * (1 - aged_selectors[i]) === 0; @@ -141,7 +120,7 @@ template proof_of_leadership(){ aged_membership.nodes[i] <== aged_nodes[i]; aged_membership.selector[i] <== aged_selectors[i]; } - aged_membership.root <== aged_root; + aged_membership.root <== aged_root; aged_membership.leaf <== note_id.out; @@ -149,7 +128,7 @@ template proof_of_leadership(){ component ticket = ticket_calculator(); ticket.epoch_nonce <== epoch_nonce; ticket.slot <== slot; - ticket.commitment <== cm.out; + ticket.note_id <== note_id.out; ticket.secret_key <== sk.out; @@ -164,7 +143,6 @@ template proof_of_leadership(){ component winning = FullLessThan(); winning.a <== ticket.out; winning.b <== threshold; - winning.out === 1; // Check that the note is unspent @@ -187,27 +165,101 @@ template proof_of_leadership(){ component checker = SafeLessEqThan(252); checker.in[0] <== starting_slot; checker.in[1] <== slot; - checker.out === 1; - // Compute the positions related to slot - starting_slot - component bits = Num2Bits(25); + // Compute the positions related to slot - starting_slot (and make sure it's 25 bits) + component bits = Num2Bits(secret_depth); bits.in <== slot - starting_slot; // Check the membership of the secret_slot against the secrets_root - component secret_membership = proof_of_membership(25); - for(var i =0; i<25; i++){ + component secret_membership = proof_of_membership(secret_depth); + for(var i =0; i