feat(pol): derive t0/t1_constant from f (#2155)

This commit is contained in:
Youngjoon Lee
2026-02-18 09:23:09 +09:00
committed by GitHub
parent f27772ba2f
commit 9a5f550405
57 changed files with 731 additions and 286 deletions
@@ -70,16 +70,12 @@ jobs:
with:
tool: cargo-nextest
# Build release binary for logos-blockchain-node (so runtimes can be reduced) with required test features
# Build release binary for logos-blockchain-node (so runtimes can be reduced)
- name: Build binaries with required features
uses: actions-rs/cargo@9e120dd99b0fbad1c065f686657e914e76bd7b72
with:
command: build
# Note: Trying without the `high-active-slot-coefficient` feature flag for now, as locally tests seem to
# converge faster without it, especially with release mode binaries. We can re-enable it later if
# needed.
args: --locked --release --all
# args: --locked --all --features high-active-slot-coefficient
env:
CARGO_INCREMENTAL: 0 # Disable incremental compilation
+2 -2
View File
@@ -77,7 +77,7 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build node binary
run: cargo build -p logos-blockchain-node --features high-active-slot-coefficient --release --target ${{ matrix.platform.target_triple }}
run: cargo build -p logos-blockchain-node --release --target ${{ matrix.platform.target_triple }}
- name: Tar node binary
run: tar -czf logos-blockchain-node-${{ matrix.platform.os }}-${{ matrix.platform.arch }}-${{ needs.configure-version.outputs.version }}.tar.gz -C target/${{ matrix.platform.target_triple }}/release logos-blockchain-node
@@ -230,4 +230,4 @@ jobs:
upload_url: ${{ env.UPLOAD_URL }}
asset_path: ${{ env.DOWNLOAD_DIRECTORY }}/${{ env.LB_CIRCUIT_NAME }}
asset_name: ${{ env.LB_CIRCUIT_NAME }}
asset_content_type: application/gzip
asset_content_type: application/gzip
Generated
+40 -1
View File
@@ -507,6 +507,40 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247"
[[package]]
name = "astro-float"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96034cc871c05bb65ad7fb77e6a8bebf45d8b055ed0311769e2f83a1d373c1ec"
dependencies = [
"astro-float-macro",
"astro-float-num",
]
[[package]]
name = "astro-float-macro"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05cfe0f6df5a74fb25b9e713470ad77e091f35f798730673c8772f26ed438963"
dependencies = [
"astro-float-num",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "astro-float-num"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86887daca11d02e0b04f37a9cb81888aae881397fb48ff66494e356aea97554a"
dependencies = [
"itertools 0.10.5",
"lazy_static",
"rand 0.8.5",
"serde",
]
[[package]]
name = "async-channel"
version = "1.9.0"
@@ -4092,7 +4126,9 @@ dependencies = [
"logos-blockchain-core 0.1.0",
"logos-blockchain-groth16 0.1.0",
"logos-blockchain-key-management-system-keys 0.1.0",
"logos-blockchain-pol 0.1.0",
"logos-blockchain-poq 0.1.0",
"logos-blockchain-utils 0.1.0",
"num-bigint",
"serde",
"thiserror 1.0.69",
@@ -4647,6 +4683,7 @@ name = "logos-blockchain-cryptarchia-engine"
version = "0.1.0"
dependencies = [
"cfg_eval",
"logos-blockchain-pol 0.1.0",
"logos-blockchain-utils 0.1.0",
"serde",
"serde_with",
@@ -5284,9 +5321,11 @@ dependencies = [
name = "logos-blockchain-pol"
version = "0.1.0"
dependencies = [
"astro-float",
"logos-blockchain-circuits-prover 0.1.0",
"logos-blockchain-circuits-utils 0.1.0",
"logos-blockchain-groth16 0.1.0",
"logos-blockchain-utils 0.1.0",
"logos-blockchain-witness-generator 0.1.0",
"num-bigint",
"num-traits",
@@ -5320,6 +5359,7 @@ dependencies = [
"logos-blockchain-circuits-utils 0.1.0",
"logos-blockchain-groth16 0.1.0",
"logos-blockchain-pol 0.1.0",
"logos-blockchain-utils 0.1.0",
"logos-blockchain-witness-generator 0.1.0",
"num-bigint",
"serde",
@@ -5506,7 +5546,6 @@ dependencies = [
"logos-blockchain-libp2p 0.1.0",
"logos-blockchain-network-service 0.1.0",
"logos-blockchain-node 0.1.0",
"logos-blockchain-pol 0.1.0",
"logos-blockchain-tx-service 0.1.0",
"logos-blockchain-utils 0.1.0",
"logos-blockchain-zone-sdk",
-6
View File
@@ -245,12 +245,6 @@ target/debug/logos-blockchain-node nodes/node/standalone-node-config.yaml
Node stores its state inside the `store` directory. If there are any issues when restarting the node, please try removing
`store` directory.
If you would like to test a node with a more frequent blocks per slot, please usea `--features high-active-slot-coefficient` feature flag when building:
```bash
cargo build --all-features --all-targets --features high-active-slot-coefficient
```
#### Running Logos Blockchain Node with integration test
To run the node programatically, one can use `local_testnet_one_node` integration test.
+3 -1
View File
@@ -30,6 +30,7 @@ pub struct PoQVerificationInputsMinusSigningKey {
impl Default for PoQVerificationInputsMinusSigningKey {
fn default() -> Self {
use lb_core::crypto::ZkHash;
use lb_groth16::{Field as _, Fr};
Self {
session: 1,
@@ -41,7 +42,8 @@ impl Default for PoQVerificationInputsMinusSigningKey {
pol_ledger_aged: ZkHash::default(),
pol_epoch_nonce: ZkHash::default(),
message_quota: 1,
total_stake: 1,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
}
}
@@ -13,7 +13,7 @@ use lb_blend_message::{
use lb_blend_proofs::quota::inputs::prove::public::{CoreInputs, LeaderInputs};
use lb_blend_scheduling::membership::{Membership, Node};
use lb_core::{crypto::ZkHash, sdp::SessionNumber};
use lb_groth16::Field as _;
use lb_groth16::{Field as _, Fr};
use lb_key_management_system_keys::keys::{Ed25519PublicKey, UnsecuredEd25519Key};
use lb_libp2p::{NetworkBehaviour, SwarmEvent};
use libp2p::{
@@ -182,7 +182,8 @@ pub fn default_poq_verification_inputs_for_session(
pol_ledger_aged: ZkHash::ZERO,
pol_epoch_nonce: ZkHash::ZERO,
message_quota: 0,
total_stake: 0,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
}
}
+2
View File
@@ -17,7 +17,9 @@ ed25519-dalek = { workspace = true }
generic-array = { default-features = false, version = "1.2" }
lb-blend-crypto = { workspace = true }
lb-groth16 = { workspace = true }
lb-pol = { workspace = true }
lb-poq = { workspace = true }
lb-utils = { workspace = true }
num-bigint = { default-features = false, version = "0.4" }
serde = { workspace = true }
thiserror = "1"
+16 -3
View File
@@ -1,3 +1,5 @@
use lb_pol::LotteryConstants;
use lb_utils::math::NonNegativeRatio;
use num_bigint::BigUint;
use crate::quota::{
@@ -19,6 +21,9 @@ pub fn valid_proof_of_core_quota_inputs(
signing_key: Ed25519PublicKey,
core_quota: u64,
) -> (PublicInputs, ProofOfCoreQuotaInputs) {
let (lottery_0, lottery_1) =
LotteryConstants::new(NonNegativeRatio::new(1, 10.try_into().unwrap()))
.compute_lottery_values(1);
let public_inputs = PublicInputs {
signing_key,
core: CoreInputs {
@@ -35,7 +40,8 @@ pub fn valid_proof_of_core_quota_inputs(
pol_epoch_nonce: BigUint::from(1u64).into(),
pol_ledger_aged: BigUint::from(1u64).into(),
message_quota: 1,
total_stake: 1,
lottery_0,
lottery_1,
},
};
@@ -134,11 +140,17 @@ pub fn valid_proof_of_core_quota_inputs(
// Reference values taken from the tests within the `poq` crate.
#[must_use]
#[expect(clippy::too_many_lines, reason = "This fixture is simply too long.")]
#[expect(
clippy::too_many_lines,
reason = "Fixture function. Only used for tests."
)]
pub fn valid_proof_of_leadership_quota_inputs(
signing_key: Ed25519PublicKey,
leader_quota: u64,
) -> (PublicInputs, ProofOfLeadershipQuotaInputs) {
let (lottery_0, lottery_1) =
LotteryConstants::new(NonNegativeRatio::new(1, 10.try_into().unwrap()))
.compute_lottery_values(1);
let public_inputs = PublicInputs {
signing_key,
leader: LeaderInputs {
@@ -153,7 +165,8 @@ pub fn valid_proof_of_leadership_quota_inputs(
.unwrap()
.into(),
message_quota: leader_quota,
total_stake: 1,
lottery_0,
lottery_1,
},
// Not relevant for leadership quota proofs
session: 1,
+2 -1
View File
@@ -30,7 +30,8 @@ impl TryFrom<Inputs> for PoQWitnessInputs {
pol_epoch_nonce: value.public.leader.pol_epoch_nonce,
pol_ledger_aged: value.public.leader.pol_ledger_aged,
session: value.public.session,
total_stake: value.public.leader.total_stake,
lottery_0: value.public.leader.lottery_0,
lottery_1: value.public.leader.lottery_1,
};
let common_input_data = PoQCommonInputsData {
core_quota: value.public.core.quota,
@@ -1,3 +1,4 @@
use lb_groth16::Fr;
use serde::{Deserialize, Serialize};
use crate::{ZkHash, quota::Ed25519PublicKey};
@@ -41,5 +42,8 @@ pub struct LeaderInputs {
#[serde(with = "lb_groth16::serde::serde_fr")]
pub pol_epoch_nonce: ZkHash,
pub message_quota: u64,
pub total_stake: u64,
#[serde(with = "lb_groth16::serde::serde_fr")]
pub lottery_0: Fr,
#[serde(with = "lb_groth16::serde::serde_fr")]
pub lottery_1: Fr,
}
+2 -1
View File
@@ -46,7 +46,8 @@ impl From<Inputs> for PoQVerifierInput {
pol_epoch_nonce: value.prove_inputs.leader.pol_epoch_nonce,
pol_ledger_aged: value.prove_inputs.leader.pol_ledger_aged,
session: value.prove_inputs.session,
total_stake: value.prove_inputs.leader.total_stake,
lottery_0: value.prove_inputs.leader.lottery_0,
lottery_1: value.prove_inputs.leader.lottery_1,
}
.into()
}
+3 -2
View File
@@ -1,6 +1,6 @@
use const_hex::FromHex as _;
use lb_blend_crypto::{ZkHash, merkle::MerkleTree};
use lb_groth16::{Field as _, fr_from_bytes_unchecked};
use lb_groth16::{Field as _, Fr, fr_from_bytes_unchecked};
use lb_key_management_system_keys::keys::UnsecuredZkKey;
use crate::{
@@ -130,7 +130,8 @@ fn generate_inputs<const INPUTS: usize>() -> PoQInputs<INPUTS> {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
total_stake: 1,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
};
let session = 1;
let signing_key = Ed25519PublicKey::from_bytes(&[10; ED25519_PUBLIC_KEY_SIZE]).unwrap();
@@ -196,7 +196,7 @@ mod test {
public::{CoreInputs, LeaderInputs},
};
use lb_core::crypto::ZkHash;
use lb_groth16::Field as _;
use lb_groth16::{Field as _, Fr};
use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519PublicKey};
use multiaddr::{Multiaddr, PeerId};
@@ -235,7 +235,8 @@ mod test {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
total_stake: 1,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
},
MockCorePoQGenerator,
@@ -245,7 +246,8 @@ mod test {
pol_ledger_aged: ZkHash::ONE,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 2,
total_stake: 2,
lottery_0: Fr::ONE,
lottery_1: Fr::ONE,
};
processor.rotate_epoch(new_leader_inputs);
@@ -282,7 +284,8 @@ mod test {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
total_stake: 1,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
},
MockCorePoQGenerator,
@@ -140,7 +140,7 @@ mod test {
use lb_blend_message::crypto::proofs::PoQVerificationInputsMinusSigningKey;
use lb_blend_proofs::quota::inputs::prove::public::{CoreInputs, LeaderInputs};
use lb_core::crypto::ZkHash;
use lb_groth16::Field as _;
use lb_groth16::{Field as _, Fr};
use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519PublicKey};
use multiaddr::{Multiaddr, PeerId};
@@ -183,7 +183,8 @@ mod test {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
total_stake: 1,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
},
MockCorePoQGenerator,
@@ -193,7 +194,8 @@ mod test {
pol_ledger_aged: ZkHash::ONE,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 2,
total_stake: 2,
lottery_0: Fr::ONE,
lottery_1: Fr::ONE,
};
processor.rotate_epoch(new_leader_inputs);
@@ -205,7 +207,8 @@ mod test {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
total_stake: 1,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
})
);
assert_eq!(
@@ -146,7 +146,7 @@ mod test {
public::{CoreInputs, LeaderInputs},
};
use lb_core::crypto::ZkHash;
use lb_groth16::Field as _;
use lb_groth16::{Field as _, Fr};
use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519PublicKey};
use libp2p::{Multiaddr, PeerId};
@@ -177,7 +177,8 @@ mod test {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
total_stake: 1,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
},
ProofOfLeadershipQuotaInputs {
@@ -194,7 +195,8 @@ mod test {
pol_ledger_aged: ZkHash::ONE,
pol_epoch_nonce: ZkHash::ONE,
message_quota: 2,
total_stake: 2,
lottery_0: Fr::ONE,
lottery_1: Fr::ONE,
};
let new_private_inputs = ProofOfLeadershipQuotaInputs {
aged_path_and_selectors: [(ZkHash::ONE, true); _],
+3 -2
View File
@@ -18,7 +18,8 @@ workspace = true
[dependencies]
cfg_eval = { optional = true, version = "0.1" }
lb-utils = { features = ["serde", "time"], workspace = true }
lb-pol = { workspace = true }
lb-utils = { workspace = true }
serde = { optional = true, workspace = true }
serde_with = { features = ["macros"], optional = true, workspace = true }
thiserror = "1"
@@ -27,5 +28,5 @@ tokio = { default-features = false, features = ["time"], optional = true, v
tracing = { workspace = true }
[features]
serde = ["dep:cfg_eval", "dep:serde", "dep:serde_with"]
serde = ["dep:cfg_eval", "dep:serde", "dep:serde_with", "lb-utils/serde", "lb-utils/time"]
tokio = ["dep:tokio"]
+62 -35
View File
@@ -1,37 +1,60 @@
use std::num::NonZero;
use lb_utils::math::NonNegativeF64;
use lb_pol::LotteryConstants;
use lb_utils::math::{NonNegativeF64, NonNegativeRatio};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct Config {
// The k parameter in the Common Prefix property.
// Blocks deeper than k are generally considered stable and forks deeper than that
// trigger the additional fork selection rule, which is however only expected to be used
// during bootstrapping.
/// The `k` parameter in the Common Prefix property.
/// Blocks deeper than k are generally considered stable and forks deeper
/// than that trigger the additional fork selection rule, which is
/// however only expected to be used during bootstrapping.
security_param: NonZero<u32>,
base_period_length: NonZero<u64>,
/// sufficient time measured in slots to measure the density of block
/// production with enough statistical significance.
s_gen: NonZero<u64>,
/// `f`, the rate of occupied slots
slot_activation_coeff: NonNegativeRatio,
stake_inference_learning_rate: NonNegativeF64,
/// Lottery approximation constants computed from `slot_activation_coeff`
#[cfg_attr(feature = "serde", serde(skip))]
lottery_constants: LotteryConstants,
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Config {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct RawConfig {
security_param: NonZero<u32>,
slot_activation_coeff: NonNegativeRatio,
stake_inference_learning_rate: NonNegativeF64,
}
let raw = RawConfig::deserialize(deserializer)?;
Ok(Self {
security_param: raw.security_param,
slot_activation_coeff: raw.slot_activation_coeff,
stake_inference_learning_rate: raw.stake_inference_learning_rate,
lottery_constants: LotteryConstants::new(raw.slot_activation_coeff),
})
}
}
impl Config {
#[must_use]
pub const fn new(
pub fn new(
security_param: NonZero<u32>,
active_slot_coefficient: f64,
slot_activation_coeff: NonNegativeRatio,
stake_inference_learning_rate: NonNegativeF64,
) -> Self {
Self {
security_param,
base_period_length: Self::compute_base_period_length(
security_param,
active_slot_coefficient,
),
s_gen: Self::compute_s_gen(security_param, active_slot_coefficient),
slot_activation_coeff,
stake_inference_learning_rate,
lottery_constants: LotteryConstants::new(slot_activation_coeff),
}
}
@@ -41,28 +64,22 @@ impl Config {
}
#[must_use]
const fn compute_base_period_length(
security_param: NonZero<u32>,
active_slot_coefficient: f64,
) -> NonZero<u64> {
NonZero::new(((security_param.get() as f64) / active_slot_coefficient).floor() as u64)
.expect("base_period_length with proper configuration should never be zero")
pub const fn slot_activation_coeff(&self) -> NonNegativeRatio {
self.slot_activation_coeff
}
#[must_use]
const fn compute_s_gen(
security_param: NonZero<u32>,
active_slot_coefficient: f64,
) -> NonZero<u64> {
NonZero::new(
((security_param.get() as f64) / (4.0 * active_slot_coefficient)).floor() as u64,
)
.expect("s_gen with proper configuration should never be zero")
pub const fn lottery_constants(&self) -> &LotteryConstants {
&self.lottery_constants
}
#[must_use]
pub const fn base_period_length(&self) -> NonZero<u64> {
self.base_period_length
NonZero::new(
((self.security_param.get() as f64) / self.slot_activation_coeff.as_f64()).floor()
as u64,
)
.expect("base_period_length with proper configuration should never be zero")
}
#[must_use]
@@ -70,9 +87,15 @@ impl Config {
self.stake_inference_learning_rate.get()
}
/// sufficient time measured in slots to measure the density of block
/// production with enough statistical significance.
#[must_use]
pub const fn s_gen(&self) -> NonZero<u64> {
self.s_gen
NonZero::new(
((self.security_param.get() as f64) / (4.0 * self.slot_activation_coeff.as_f64()))
.floor() as u64,
)
.expect("s_gen with proper configuration should never be zero")
}
}
@@ -84,7 +107,11 @@ mod tests {
#[test]
fn test_config() {
let config = Config::new(NonZero::new(10).unwrap(), 0.2, 0.1.try_into().unwrap());
let config = Config::new(
NonZero::new(10).unwrap(),
NonNegativeRatio::new(1, 5.try_into().unwrap()),
0.1.try_into().unwrap(),
);
assert_eq!(config.security_param(), NonZero::new(10).unwrap());
assert_eq!(config.base_period_length(), NonZero::new(50).unwrap());
assert_eq!(config.s_gen(), NonZero::new(12).unwrap());
+7 -1
View File
@@ -582,6 +582,10 @@ where
let pruned_blocks = new.update_lib();
(new, pruned_blocks)
}
pub const fn config(&self) -> &Config {
&self.config
}
}
/// The output of applying a new block to [`Cryptarchia`]
@@ -699,6 +703,8 @@ pub mod tests {
num::NonZero,
};
use lb_utils::math::NonNegativeRatio;
use super::{Cryptarchia, Error, Slot, maxvalid_bg};
use crate::{Config, ReorgedBlocks, State, UpdatedCryptarchia};
@@ -711,7 +717,7 @@ pub mod tests {
pub fn config_with(security_param: u32) -> Config {
Config::new(
NonZero::new(security_param).unwrap(),
0.1,
NonNegativeRatio::new(1, 10.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
)
}
+22 -3
View File
@@ -220,6 +220,8 @@ mod tests {
use lb_groth16::Fr;
use lb_key_management_system_keys::keys::UnsecuredZkKey;
use lb_pol::LotteryConstants;
use lb_utils::math::NonNegativeRatio;
use lb_utxotree::UtxoTree;
use num_bigint::BigUint;
@@ -244,12 +246,22 @@ mod tests {
let utxo_tree_root = utxo_tree.root();
let utxo_merkle_path = utxo_tree.path(&utxo.id()).expect("note must exist in tree");
let (lottery_0, lottery_1) =
LotteryConstants::new(NonNegativeRatio::new(1, 10.try_into().unwrap()))
.compute_lottery_values(1000);
// We grind the nonce here to find a winning PoL
let public_inputs = {
let mut nonce = 0;
while nonce < 1000 {
let inputs =
LeaderPublic::new(utxo_tree_root, utxo_tree_root, Fr::from(nonce), 0, 1000);
let inputs = LeaderPublic::new(
utxo_tree_root,
utxo_tree_root,
Fr::from(nonce),
0,
lottery_0,
lottery_1,
);
if inputs.check_winning(utxo.note.value, *utxo.id().as_fr(), *leader_sk.as_fr()) {
break;
@@ -257,7 +269,14 @@ mod tests {
nonce += 1;
}
LeaderPublic::new(utxo_tree_root, utxo_tree_root, Fr::from(nonce), 0, 1000)
LeaderPublic::new(
utxo_tree_root,
utxo_tree_root,
Fr::from(nonce),
0,
lottery_0,
lottery_1,
)
};
let signing_key = Ed25519Key::from_bytes(&[0; 32]);
+24 -19
View File
@@ -5,7 +5,6 @@ use lb_groth16::{Fr, fr_from_bytes, serde::serde_fr};
use lb_key_management_system_keys::keys::ZkPublicKey;
use lb_poseidon2::{Digest as _, Poseidon2Bn254Hasher};
use lb_utxotree::MerklePath;
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -95,7 +94,8 @@ impl LeaderProof for Groth16LeaderProof {
public_inputs.epoch_nonce,
public_inputs.aged_root,
public_inputs.latest_root,
public_inputs.total_stake,
public_inputs.lottery_0,
public_inputs.lottery_1,
leader_pk,
),
)
@@ -128,7 +128,10 @@ pub struct LeaderPublic {
pub slot: u64,
#[serde(with = "serde_fr")]
pub epoch_nonce: Fr,
pub total_stake: u64,
#[serde(with = "serde_fr")]
pub lottery_0: Fr,
#[serde(with = "serde_fr")]
pub lottery_1: Fr,
#[serde(with = "serde_fr")]
pub aged_root: Fr,
#[serde(with = "serde_fr")]
@@ -155,12 +158,14 @@ impl LeaderPublic {
latest_root: Fr,
epoch_nonce: Fr,
slot: u64,
total_stake: u64,
lottery_0: Fr,
lottery_1: Fr,
) -> Self {
Self {
slot,
epoch_nonce,
total_stake,
lottery_0,
lottery_1,
aged_root,
latest_root,
}
@@ -168,20 +173,12 @@ impl LeaderPublic {
#[must_use]
pub fn check_winning(&self, value: u64, note_id: Fr, sk: Fr) -> bool {
let (t0, t1) = self.scaled_phi_approx();
let threshold =
Self::phi_approx(&Fr::from(value), &(Fr::from(t0), Fr::from(t1))).into_bigint();
Self::phi_approx(&Fr::from(value), &(self.lottery_0, self.lottery_1)).into_bigint();
let ticket = Self::ticket(note_id, sk, self.epoch_nonce, Fr::from(self.slot)).into_bigint();
ticket < threshold
}
fn scaled_phi_approx(&self) -> (BigUint, BigUint) {
let t0 = &*lb_pol::T0_CONSTANT / &BigUint::from(self.total_stake);
let total_stake_sq = &BigUint::from(self.total_stake) * &BigUint::from(self.total_stake);
let t1 = &*lb_pol::P - (&*lb_pol::T1_CONSTANT / &total_stake_sq);
(t0, t1)
}
fn phi_approx(stake: &Fr, approx: &(Fr, Fr)) -> Fr {
// stake * (t0 + t1 * stake)
*stake * (approx.0 + (approx.1 * *stake))
@@ -216,7 +213,8 @@ impl LeaderPrivate {
let chain = lb_pol::PolChainInputsData {
slot_number: public.slot,
epoch_nonce: public.epoch_nonce,
total_stake: public.total_stake,
lottery_0: public.lottery_0,
lottery_1: public.lottery_1,
aged_root: public.aged_root,
latest_root: public.latest_root,
leader_pk,
@@ -287,6 +285,8 @@ fn ed25519_pk_to_fr_tuple(pk: &Ed25519PublicKey) -> (Fr, Fr) {
mod tests {
use std::str::FromStr as _;
use lb_pol::LotteryConstants;
use lb_utils::math::NonNegativeRatio;
use rand::RngCore as _;
use super::*;
@@ -334,14 +334,16 @@ mod tests {
);
}
fn rand_inputs() -> (LeaderPublic, Fr, Fr) {
fn rand_inputs(lottery_constants: &LotteryConstants) -> (LeaderPublic, Fr, Fr) {
let (lottery_0, lottery_1) = lottery_constants.compute_lottery_values(1);
let mut rng = rand::thread_rng();
let public = LeaderPublic::new(
Fr::ZERO,
Fr::ZERO,
Fr::ZERO,
rng.next_u64(),
1, // total stake
lottery_0,
lottery_1,
);
let note = Fr::from(rng.next_u64()); // note value
let sk = Fr::from(rng.next_u64()); // secret key
@@ -376,9 +378,12 @@ mod tests {
#[test]
fn test_check_winning() {
let slot_activation_coeff = NonNegativeRatio::new(1, 10.try_into().unwrap());
let constants = LotteryConstants::new(slot_activation_coeff);
// winning rate of all the stake should be ~ active slot coeff
check_prob(lb_pol::slot_activation_coefficient(), || {
let (public, note_id, sk) = rand_inputs();
check_prob(slot_activation_coeff.as_f64(), || {
let (public, note_id, sk) = rand_inputs(&constants);
public.check_winning(1, note_id, sk)
});
}
+9 -3
View File
@@ -1,6 +1,7 @@
use std::num::{NonZero, NonZeroU64};
use lb_cryptarchia_engine::{Epoch, Slot};
use lb_pol::LotteryConstants;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq)]
@@ -11,6 +12,11 @@ pub struct Config {
}
impl Config {
#[must_use]
pub const fn lottery_constants(&self) -> &LotteryConstants {
self.consensus_config.lottery_constants()
}
#[must_use]
pub const fn base_period_length(&self) -> NonZero<u64> {
self.consensus_config.base_period_length()
@@ -58,7 +64,7 @@ mod tests {
use lb_core::sdp::{MinStake, ServiceParameters, ServiceType};
use lb_cryptarchia_engine::EpochConfig;
use lb_utils::math::NonNegativeF64;
use lb_utils::math::{NonNegativeF64, NonNegativeRatio};
use crate::mantle::sdp::{ServiceRewardsParameters, rewards::blend::RewardsParameters};
@@ -72,7 +78,7 @@ mod tests {
},
consensus_config: lb_cryptarchia_engine::Config::new(
NonZero::new(5).unwrap(),
0.5,
NonNegativeRatio::new(1, 2.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
),
sdp_config: crate::mantle::sdp::Config {
@@ -122,7 +128,7 @@ mod tests {
},
consensus_config: lb_cryptarchia_engine::Config::new(
NonZero::new(5).unwrap(),
0.5,
NonNegativeRatio::new(1, 2.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
),
sdp_config: crate::mantle::sdp::Config {
+61 -16
View File
@@ -12,7 +12,6 @@ use lb_core::{
use lb_cryptarchia_engine::{Epoch, Slot};
use lb_groth16::{Fr, fr_from_bytes};
use lb_key_management_system_keys::keys::ZkPublicKey;
use lb_pol::slot_activation_coefficient;
use lb_utxotree::MerklePath;
use crate::cryptarchia::{
@@ -27,17 +26,22 @@ use crate::mantle::sdp::locked_notes::LockedNotes;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EpochState {
// The epoch this snapshot is for
/// The epoch this snapshot is for
pub epoch: Epoch,
// value of the ledger nonce after 'epoch_period_nonce_buffer' slots from the beginning of the
// epoch
/// value of the ledger nonce after `epoch_period_nonce_buffer` slots from
/// the beginning of the epoch
#[cfg_attr(feature = "serde", serde(with = "lb_groth16::serde::serde_fr"))]
pub nonce: Fr,
// stake distribution snapshot taken at the beginning of the epoch
// (in practice, this is equivalent to the utxos the are spendable at the beginning of the
// epoch)
/// stake distribution snapshot taken at the beginning of the epoch
/// (in practice, this is equivalent to the utxos the are spendable at the
/// beginning of the epoch)
pub utxos: UtxoTree,
pub total_stake: Value,
/// Lottery values computed based on `total_stake`
#[cfg_attr(feature = "serde", serde(with = "lb_groth16::serde::serde_fr"))]
pub lottery_0: Fr,
#[cfg_attr(feature = "serde", serde(with = "lb_groth16::serde::serde_fr"))]
pub lottery_1: Fr,
}
impl EpochState {
@@ -60,6 +64,8 @@ impl EpochState {
nonce,
utxos,
total_stake: self.total_stake,
lottery_0: self.lottery_0,
lottery_1: self.lottery_1,
}
}
@@ -78,6 +84,11 @@ impl EpochState {
self.total_stake
}
#[must_use]
pub const fn lottery_values(&self) -> (Fr, Fr) {
(self.lottery_0, self.lottery_1)
}
#[must_use]
pub fn utxo_merkle_root(&self) -> Fr {
self.utxos.root()
@@ -131,6 +142,9 @@ impl LedgerState {
self.epoch_state.total_stake,
block_density_inference.current_block_density(),
);
let (lottery_0, lottery_1) = config
.lottery_constants()
.compute_lottery_values(total_stake);
let current_epoch = config.epoch(self.slot);
let new_epoch = config.epoch(slot);
@@ -169,6 +183,8 @@ impl LedgerState {
nonce: self.nonce,
utxos: self.utxos.clone(),
total_stake,
lottery_0,
lottery_1,
};
Ok(Self {
slot,
@@ -193,12 +209,16 @@ impl LedgerState {
nonce: self.nonce,
utxos: self.utxos.clone(),
total_stake,
lottery_0,
lottery_1,
};
let next_epoch_state = EpochState {
epoch: new_epoch + 1,
nonce: self.nonce,
utxos: self.utxos.clone(),
total_stake,
lottery_0,
lottery_1,
};
Ok(Self {
slot,
@@ -225,7 +245,8 @@ impl LedgerState {
self.latest_utxos().root(),
self.epoch_state.nonce,
slot.into(),
self.epoch_state.total_stake,
self.epoch_state.lottery_0,
self.epoch_state.lottery_1,
);
if !proof.verify(&public_inputs) {
return Err(LedgerError::InvalidProof);
@@ -365,11 +386,17 @@ impl LedgerState {
total_stake
);
let (lottery_0, lottery_1) = config
.lottery_constants()
.compute_lottery_values(total_stake);
Some(EpochState {
epoch: requested_epoch,
nonce: self.nonce,
utxos: self.utxos.clone(),
total_stake,
lottery_0,
lottery_1,
})
} else {
// Requested epoch is in the past
@@ -404,11 +431,15 @@ impl LedgerState {
.utxos()
.iter()
.map(|(_, (utxo, _))| utxo.note.value)
.sum::<Value>();
.sum::<Value>()
.max(1); // TODO: Change total_stake to NonZeroU64: https://github.com/logos-blockchain/logos-blockchain/issues/2166
let (lottery_0, lottery_1) = config
.lottery_constants()
.compute_lottery_values(total_stake);
let slot: Slot = 0.into();
let stake_inference = Arc::new(StakeInference::new(
config.consensus_config.stake_inference_learning_rate(),
slot_activation_coefficient(),
config.consensus_config.slot_activation_coeff().as_f64(),
config.consensus_config.security_param().get().into(),
));
let block_density = BlockDensity::new(stake_inference.period(), slot);
@@ -421,12 +452,16 @@ impl LedgerState {
nonce,
utxos: utxos.clone(),
total_stake,
lottery_0,
lottery_1,
},
epoch_state: EpochState {
epoch: 0.into(),
nonce,
utxos,
total_stake,
lottery_0,
lottery_1,
},
block_density,
stake_inference,
@@ -463,7 +498,7 @@ pub mod tests {
use lb_cryptarchia_engine::EpochConfig;
use lb_groth16::Field as _;
use lb_key_management_system_keys::keys::{Ed25519PublicKey, ZkKey};
use lb_utils::math::NonNegativeF64;
use lb_utils::math::{NonNegativeF64, NonNegativeRatio};
use num_bigint::BigUint;
use rand::{RngCore as _, thread_rng};
@@ -579,7 +614,8 @@ pub mod tests {
},
ledger_state.epoch_state.nonce,
slot.into(),
ledger_state.epoch_state.total_stake,
ledger_state.epoch_state.lottery_0,
ledger_state.epoch_state.lottery_1,
),
leader_key: Ed25519PublicKey::from_bytes(&[0u8; 32]).unwrap(),
voucher_cm: VoucherCm::default(),
@@ -608,7 +644,7 @@ pub mod tests {
},
consensus_config: lb_cryptarchia_engine::Config::new(
NonZero::new(1).unwrap(),
0.1,
NonNegativeRatio::new(1, 10.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
),
sdp_config: crate::mantle::sdp::Config {
@@ -635,13 +671,16 @@ pub mod tests {
pub fn genesis_state(utxos: &[Utxo]) -> LedgerState {
let config = config();
let total_stake = utxos.iter().map(|u| u.note.value).sum();
let (lottery_0, lottery_1) = config
.lottery_constants()
.compute_lottery_values(total_stake);
let utxos = utxos
.iter()
.map(|utxo| (utxo.id(), *utxo))
.collect::<UtxoTree>();
let stake_inference = Arc::new(StakeInference::new(
config.consensus_config.stake_inference_learning_rate(),
slot_activation_coefficient(),
config.consensus_config.slot_activation_coeff().as_f64(),
config.consensus_config.security_param().get().into(),
));
let block_density_inference = BlockDensity::new(stake_inference.period(), 0.into());
@@ -654,12 +693,16 @@ pub mod tests {
nonce: Fr::ZERO,
utxos: utxos.clone(),
total_stake,
lottery_0,
lottery_1,
},
epoch_state: EpochState {
epoch: 0.into(),
nonce: Fr::ZERO,
utxos,
total_stake,
lottery_0,
lottery_1,
},
stake_inference,
block_density: block_density_inference,
@@ -848,7 +891,8 @@ pub mod tests {
latest_root: ledger_state.latest_utxos().root(),
epoch_nonce: ledger_state.epoch_state.nonce,
slot: slot.into(),
total_stake: ledger_state.epoch_state.total_stake,
lottery_0: ledger_state.epoch_state.lottery_0,
lottery_1: ledger_state.epoch_state.lottery_1,
},
leader_key: Ed25519PublicKey::from_bytes(&[0u8; 32]).unwrap(),
voucher_cm: VoucherCm::default(),
@@ -872,7 +916,8 @@ pub mod tests {
latest_root: BigUint::from(1u8).into(), // Invalid latest root
epoch_nonce: ledger_state.epoch_state.nonce,
slot: slot.into(),
total_stake: ledger_state.epoch_state.total_stake,
lottery_0: ledger_state.epoch_state.lottery_0,
lottery_1: ledger_state.epoch_state.lottery_1,
},
leader_key: Ed25519PublicKey::from_bytes(&[0u8; 32]).unwrap(),
voucher_cm: VoucherCm::default(),
+2
View File
@@ -717,6 +717,8 @@ mod tests {
nonce: ZkHash::ZERO,
utxos: UtxoTree::default(),
total_stake: 100,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
}
}
+2 -1
View File
@@ -268,7 +268,8 @@ impl RewardsParameters {
pol_ledger_aged: epoch_state.utxos.root(),
pol_epoch_nonce: epoch_state.nonce,
message_quota,
total_stake: epoch_state.total_stake,
lottery_0: epoch_state.lottery_0,
lottery_1: epoch_state.lottery_1,
}
}
}
+3 -1
View File
@@ -2,7 +2,7 @@ use lb_core::{
crypto::ZkHash,
sdp::{Declaration, DeclarationId, ProviderId, ServiceParameters, ServiceType, SessionNumber},
};
use lb_groth16::Fr;
use lb_groth16::{Field as _, Fr};
use lb_key_management_system_keys::keys::{Ed25519Key, ZkPublicKey};
use num_bigint::BigUint;
@@ -61,5 +61,7 @@ pub fn dummy_epoch_state_with(epoch: u32, nonce: u64) -> EpochState {
nonce: ZkHash::from(BigUint::from(nonce)),
utxos: UtxoTree::default(),
total_stake: 0,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
}
}
+6 -7
View File
@@ -85,10 +85,9 @@ tower-http = { default-features = false, features = ["cors", "limit", "timeout",
rand = { workspace = true }
[features]
config-gen = ["lb-key-management-system-service/unsafe"]
default = ["config-gen", "tracing"]
high-active-slot-coefficient = ["lb-pol/high-active-slot-coefficient"]
instrumentation = []
profiling = ["lb-http-api-common/profiling", "lb-tracing-service/profiling"]
testing = ["lb-key-management-system-service/unsafe"]
tracing = []
config-gen = ["lb-key-management-system-service/unsafe"]
default = ["config-gen", "tracing"]
instrumentation = []
profiling = ["lb-http-api-common/profiling", "lb-tracing-service/profiling"]
testing = ["lb-key-management-system-service/unsafe"]
tracing = []
@@ -7,14 +7,14 @@ use lb_core::{
sdp::{MinStake, ServiceType},
};
use lb_cryptarchia_engine::Config as ConsensusConfig;
use lb_pol::slot_activation_coefficient;
use lb_utils::math::NonNegativeF64;
use lb_utils::math::{NonNegativeF64, NonNegativeRatio};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Settings {
pub epoch_config: EpochConfig,
pub security_param: NonZeroU32,
pub slot_activation_coeff: NonNegativeRatio,
pub learning_rate: NonNegativeF64,
pub sdp_config: SdpConfig,
pub gossipsub_protocol: String,
@@ -37,10 +37,10 @@ pub struct EpochConfig {
impl Settings {
#[must_use]
pub const fn consensus_config(&self) -> ConsensusConfig {
pub fn consensus_config(&self) -> ConsensusConfig {
ConsensusConfig::new(
self.security_param,
slot_activation_coefficient(),
self.slot_activation_coeff,
self.learning_rate,
)
}
@@ -34,6 +34,9 @@ cryptarchia:
epoch_period_nonce_buffer: 3
epoch_period_nonce_stabilization: 4
security_param: 20
slot_activation_coeff:
numerator: 1
denominator: 10
learning_rate: 0.1
sdp_config:
service_params:
@@ -31,6 +31,9 @@ cryptarchia:
epoch_period_nonce_buffer: 3
epoch_period_nonce_stabilization: 4
security_param: 20
slot_activation_coeff:
numerator: 1
denominator: 10
learning_rate: 0.1
sdp_config:
service_params:
+6 -4
View File
@@ -38,14 +38,15 @@ where
{
fn default() -> Self {
use lb_core::crypto::ZkHash;
use lb_groth16::Field as _;
use lb_groth16::{Field as _, Fr};
Self {
epoch: LeaderInputs {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
total_stake: 1,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
session: SessionInfo {
membership: Membership::new_without_local(&[]),
@@ -63,14 +64,15 @@ where
impl<NodeId> From<Membership<NodeId>> for PublicInfo<NodeId> {
fn from(value: Membership<NodeId>) -> Self {
use lb_core::crypto::ZkHash;
use lb_groth16::Field as _;
use lb_groth16::{Field as _, Fr};
Self {
epoch: LeaderInputs {
message_quota: 1,
pol_epoch_nonce: ZkHash::ZERO,
pol_ledger_aged: ZkHash::ZERO,
total_stake: 1,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
session: SessionInfo {
membership: value,
+12 -6
View File
@@ -609,7 +609,8 @@ where
LeaderInputsMinusQuota {
pol_epoch_nonce,
pol_ledger_aged,
total_stake,
lottery_0,
lottery_1,
},
remaining_clock_stream,
) = async {
@@ -637,7 +638,8 @@ where
pol_ledger_aged,
pol_epoch_nonce,
message_quota: blend_config.session_leadership_quota(),
total_stake,
lottery_0,
lottery_1,
},
session: SessionInfo {
membership: current_membership_info.public.membership.clone(),
@@ -1814,13 +1816,15 @@ where
EpochEvent::NewEpoch(LeaderInputsMinusQuota {
pol_epoch_nonce,
pol_ledger_aged,
total_stake,
lottery_0,
lottery_1,
}) => {
let new_leader_inputs = LeaderInputs {
message_quota: settings.session_leadership_quota(),
pol_epoch_nonce,
pol_ledger_aged,
total_stake,
lottery_0,
lottery_1,
};
let new_public_info = PublicInfo {
epoch: new_leader_inputs,
@@ -1841,13 +1845,15 @@ where
EpochEvent::NewEpochAndOldEpochTransitionExpired(LeaderInputsMinusQuota {
pol_epoch_nonce,
pol_ledger_aged,
total_stake,
lottery_0,
lottery_1,
}) => {
let new_leader_inputs = LeaderInputs {
message_quota: settings.session_leadership_quota(),
pol_epoch_nonce,
pol_ledger_aged,
total_stake,
lottery_0,
lottery_1,
};
let new_public_inputs = PublicInfo {
epoch: new_leader_inputs,
+3 -1
View File
@@ -241,6 +241,7 @@ mod tests {
scheduling::message_blend::crypto::SessionCryptographicProcessorSettings,
};
use lb_core::crypto::ZkHash;
use lb_groth16::Fr;
use lb_key_management_system_service::keys::{Ed25519PublicKey, UnsecuredEd25519Key};
use crate::{
@@ -264,7 +265,8 @@ mod tests {
pol_ledger_aged: ZkHash::ZERO,
pol_epoch_nonce: ZkHash::ZERO,
message_quota: 1,
total_stake: 1,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
}
}
+3 -2
View File
@@ -34,7 +34,7 @@ use lb_blend::{
},
};
use lb_core::{crypto::ZkHash, sdp::SessionNumber};
use lb_groth16::Field as _;
use lb_groth16::{Field as _, Fr};
use lb_key_management_system_service::keys::{Ed25519PublicKey, UnsecuredEd25519Key};
use lb_network_service::{NetworkService, backends::NetworkBackend};
use lb_poq::CorePathAndSelectors;
@@ -339,7 +339,8 @@ pub fn new_public_info<BackendSettings>(
pol_ledger_aged: ZkHash::ZERO,
pol_epoch_nonce: ZkHash::ZERO,
message_quota: settings.session_leadership_quota(),
total_stake: 10,
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
},
}
}
+10 -5
View File
@@ -363,7 +363,8 @@ where
let EpochEvent::NewEpoch(LeaderInputsMinusQuota {
pol_epoch_nonce,
pol_ledger_aged,
total_stake,
lottery_0,
lottery_1,
}) = epoch_handler
.tick(slot_tick)
.await
@@ -376,7 +377,8 @@ where
message_quota: settings.session_leadership_quota(),
pol_epoch_nonce,
pol_ledger_aged,
total_stake,
lottery_0,
lottery_1,
},
remaining_clock_stream,
)
@@ -573,17 +575,20 @@ where
EpochEvent::NewEpoch(LeaderInputsMinusQuota {
pol_epoch_nonce,
pol_ledger_aged,
total_stake,
lottery_0,
lottery_1,
})
| EpochEvent::NewEpochAndOldEpochTransitionExpired(LeaderInputsMinusQuota {
pol_epoch_nonce,
pol_ledger_aged,
total_stake,
lottery_0,
lottery_1,
}) => LeaderInputs {
message_quota: settings.session_leadership_quota(),
pol_epoch_nonce,
pol_ledger_aged,
total_stake,
lottery_0,
lottery_1,
},
// We don't handle the epoch transitions in edge node.
EpochEvent::OldEpochTransitionPeriodExpired => current_public_inputs.leader,
+7 -3
View File
@@ -6,6 +6,7 @@ use lb_blend::proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInput
use lb_chain_service::api::{CryptarchiaServiceApi, CryptarchiaServiceData};
use lb_core::crypto::ZkHash;
use lb_cryptarchia_engine::{Epoch, Slot};
use lb_groth16::Fr;
use lb_ledger::EpochState;
use lb_time_service::SlotTick;
use overwatch::overwatch::OverwatchHandle;
@@ -59,22 +60,25 @@ where
pub struct LeaderInputsMinusQuota {
pub pol_ledger_aged: ZkHash,
pub pol_epoch_nonce: ZkHash,
pub total_stake: u64,
pub lottery_0: Fr,
pub lottery_1: Fr,
}
impl From<EpochState> for LeaderInputsMinusQuota {
fn from(
EpochState {
nonce,
total_stake,
utxos,
lottery_0,
lottery_1,
..
}: EpochState,
) -> Self {
Self {
pol_epoch_nonce: nonce,
pol_ledger_aged: utxos.root(),
total_stake,
lottery_0,
lottery_1,
}
}
}
+3 -1
View File
@@ -3,7 +3,7 @@ use futures::{Stream, future::ready, stream::once};
use lb_blend::proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs;
use lb_chain_service::Slot;
use lb_core::crypto::ZkHash;
use lb_groth16::Field as _;
use lb_groth16::{Field as _, Fr};
use lb_ledger::EpochState;
use overwatch::overwatch::OverwatchHandle;
@@ -17,6 +17,8 @@ pub fn default_epoch_state() -> EpochState {
nonce: ZkHash::ZERO,
total_stake: 1_000,
utxos: UtxoTree::new(),
lottery_0: Fr::ZERO,
lottery_1: Fr::ZERO,
}
}
+15 -6
View File
@@ -156,7 +156,8 @@ fn public_inputs_for_slot(
latest_tree.root(),
epoch_state.nonce,
slot.into(),
epoch_state.total_stake(),
epoch_state.lottery_0,
epoch_state.lottery_1,
)
}
@@ -342,7 +343,7 @@ mod pol_tests {
use lb_ledger::mantle::sdp::{
Config as SdpConfig, ServiceRewardsParameters, rewards::blend::RewardsParameters,
};
use lb_utils::math::NonNegativeF64;
use lb_utils::math::{NonNegativeF64, NonNegativeRatio};
use lb_wallet_service::{WalletMsg, WalletServiceSettings, api::WalletServiceData};
use overwatch::services::{
ServiceData,
@@ -357,12 +358,13 @@ mod pol_tests {
/// verified successfully.
#[tokio::test]
async fn test_build_proof_for() {
let config = test_config();
// Create secret key and leader
let kms = DummyKms;
let key_id = KeyId::from("0");
let sk = UnsecuredZkKey::new(Fr::from(0u64));
let pk = sk.to_public_key();
let config = test_config();
// Create a UTXO
let utxo = Tx::new(vec![], vec![Note::new(1000u64, pk)])
@@ -374,11 +376,17 @@ mod pol_tests {
let latest_tree = UtxoTree::new().insert(utxo.id(), utxo).0;
// Create EpochState
let total_stake = utxo.note.value;
let (lottery_0, lottery_1) = config
.lottery_constants()
.compute_lottery_values(total_stake);
let epoch_state = EpochState {
epoch: 1.into(),
nonce: Fr::from(999u64),
utxos: aged_tree.clone(),
total_stake: utxo.note.value,
total_stake,
lottery_0,
lottery_1,
};
// Create notifier channel (not used in this test)
@@ -408,7 +416,8 @@ mod pol_tests {
latest_tree.root(),
epoch_state.nonce,
winning_slot.into(),
utxo.note.value,
epoch_state.lottery_0,
epoch_state.lottery_1,
);
assert!(
proof.verify(&public_inputs),
@@ -453,7 +462,7 @@ mod pol_tests {
},
consensus_config: lb_cryptarchia_engine::Config::new(
NonZero::new(5).unwrap(),
0.05,
NonNegativeRatio::new(1, 10.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
),
sdp_config: SdpConfig {
@@ -423,7 +423,7 @@ mod tests {
mantle::sdp::{ServiceRewardsParameters, rewards},
};
use lb_network_service::{NetworkService, backends::NetworkBackend, message::ChainSyncEvent};
use lb_utils::math::NonNegativeF64;
use lb_utils::math::{NonNegativeF64, NonNegativeRatio};
use overwatch::{
overwatch::OverwatchHandle,
services::{ServiceData, relay::OutboundRelay},
@@ -1117,7 +1117,7 @@ mod tests {
},
consensus_config: lb_cryptarchia_engine::Config::new(
NonZero::new(1).unwrap(),
0.1,
NonNegativeRatio::new(1, 10.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
),
sdp_config: lb_ledger::mantle::sdp::Config {
+1 -1
View File
@@ -223,7 +223,7 @@ impl Cryptarchia {
Self {
consensus: <lb_cryptarchia_engine::Cryptarchia<_>>::from_lib(
lib_id,
ledger_config.consensus_config,
ledger_config.consensus_config.clone(),
state,
lib_slot,
lib_length,
+5 -5
View File
@@ -113,7 +113,7 @@ mod tests {
use lb_core::sdp::{MinStake, ServiceParameters, ServiceType};
use lb_cryptarchia_engine::State::Bootstrapping;
use lb_ledger::mantle::sdp::{ServiceRewardsParameters, rewards};
use lb_utils::math::NonNegativeF64;
use lb_utils::math::{NonNegativeF64, NonNegativeRatio};
use super::*;
@@ -125,7 +125,7 @@ mod tests {
let security_param: NonZero<u32> = 2.try_into().unwrap();
let cryptarchia_engine_config = lb_cryptarchia_engine::Config::new(
security_param,
0.1,
NonNegativeRatio::new(1, 10.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
);
let ledger_config = lb_ledger::Config {
@@ -134,7 +134,7 @@ mod tests {
epoch_period_nonce_buffer: 1.try_into().unwrap(),
epoch_period_nonce_stabilization: 1.try_into().unwrap(),
},
consensus_config: cryptarchia_engine_config,
consensus_config: cryptarchia_engine_config.clone(),
sdp_config: lb_ledger::mantle::sdp::Config {
service_params: Arc::new(
[(
@@ -259,7 +259,7 @@ mod tests {
let security_param: NonZero<u32> = 2.try_into().unwrap();
let cryptarchia_engine_config = lb_cryptarchia_engine::Config::new(
security_param,
0.1,
NonNegativeRatio::new(1, 10.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
);
let ledger_config = lb_ledger::Config {
@@ -268,7 +268,7 @@ mod tests {
epoch_period_nonce_buffer: 1.try_into().unwrap(),
epoch_period_nonce_stabilization: 1.try_into().unwrap(),
},
consensus_config: cryptarchia_engine_config,
consensus_config: cryptarchia_engine_config.clone(),
sdp_config: lb_ledger::mantle::sdp::Config {
service_params: Arc::new(
[(
@@ -568,6 +568,7 @@ mod tests {
StorageService,
backends::rocksdb::{RocksBackend, RocksBackendSettings},
};
use lb_utils::math::NonNegativeRatio;
use lb_utxotree::UtxoTree;
use num_bigint::BigUint;
use overwatch::{derive_services, overwatch::OverwatchRunner};
@@ -697,8 +698,11 @@ mod tests {
impl TestEnv {
async fn new() -> Self {
let (service, storage_relay) = Self::setup_storage().await;
let cryptarchia = Self::new_cryptarchia(HeaderId::from([0; 32]));
let proof = Self::make_test_proof();
let cryptarchia = Self::new_cryptarchia(
HeaderId::from([0; 32]),
NonNegativeRatio::new(1, 10.try_into().unwrap()),
);
let proof = Self::make_test_proof(&cryptarchia);
let provider = BlockProvider::new(storage_relay.clone());
Self {
@@ -795,7 +799,10 @@ mod tests {
for (i, (block, header_id, prev_header, slot)) in blocks.iter().enumerate() {
if i == 0 {
self.cryptarchia = Self::new_cryptarchia(*header_id);
self.cryptarchia = Self::new_cryptarchia(
*header_id,
self.cryptarchia.config().slot_activation_coeff(),
);
}
if i > 0 {
@@ -946,7 +953,9 @@ mod tests {
);
}
fn make_test_proof() -> lb_core::proofs::leader_proof::Groth16LeaderProof {
fn make_test_proof(
cryptarchia: &lb_cryptarchia_engine::Cryptarchia<HeaderId>,
) -> lb_core::proofs::leader_proof::Groth16LeaderProof {
let leader_sk = UnsecuredZkKey::zero();
let utxo = Utxo {
tx_hash: Fr::from(BigUint::from(1u8)).into(),
@@ -956,13 +965,23 @@ mod tests {
let utxo_tree = UtxoTree::<_, _, ZkHasher>::new().insert(utxo.id(), utxo).0;
let utxo_tree_root = utxo_tree.root();
let utxo_merkle_path = utxo_tree.path(&utxo.id()).expect("note must exist in tree");
let (lottery_0, lottery_1) = cryptarchia
.config()
.lottery_constants()
.compute_lottery_values(1000);
// We grind the nonce here to find a winning PoL
let public_inputs = {
let mut nonce = 0;
while nonce < 1000 {
let inputs =
LeaderPublic::new(utxo_tree_root, utxo_tree_root, Fr::from(nonce), 0, 1000);
let inputs = LeaderPublic::new(
utxo_tree_root,
utxo_tree_root,
Fr::from(nonce),
0,
lottery_0,
lottery_1,
);
if inputs.check_winning(utxo.note.value, *utxo.id().as_fr(), *leader_sk.as_fr())
{
@@ -971,7 +990,14 @@ mod tests {
nonce += 1;
}
LeaderPublic::new(utxo_tree_root, utxo_tree_root, Fr::from(nonce), 0, 1000)
LeaderPublic::new(
utxo_tree_root,
utxo_tree_root,
Fr::from(nonce),
0,
lottery_0,
lottery_1,
)
};
let private_inputs = LeaderPrivate::new(
@@ -990,12 +1016,15 @@ mod tests {
.expect("Proof generation should succeed")
}
fn new_cryptarchia(lib: HeaderId) -> lb_cryptarchia_engine::Cryptarchia<HeaderId> {
fn new_cryptarchia(
lib: HeaderId,
slot_activation_coeff: NonNegativeRatio,
) -> lb_cryptarchia_engine::Cryptarchia<HeaderId> {
<lb_cryptarchia_engine::Cryptarchia<_>>::from_lib(
lib,
Config::new(
NonZero::new(1).unwrap(),
0.1,
slot_activation_coeff,
1f64.try_into().expect("1 > 0"),
),
lb_cryptarchia_engine::State::Bootstrapping,
-1
View File
@@ -32,7 +32,6 @@ lb-key-management-system-service = { workspace = true }
lb-libp2p = { workspace = true }
lb-network-service = { features = ["libp2p"], workspace = true }
lb-node = { default-features = false, features = ["testing"], workspace = true }
lb-pol = { features = ["high-active-slot-coefficient"], workspace = true }
lb-tx-service = { features = ["libp2p", "mock"], workspace = true }
lb-utils = { workspace = true }
logos-blockchain-zone-sdk = { path = "../zone-sdk" }
+1 -10
View File
@@ -18,11 +18,7 @@ Ensure that the following are installed on your system:
Integration tests involving nodes run the binaries directly by spawning. Ensure the binaries are built and available in
your `target/debug` or `target/release` directory. You can build the project using:
`cargo build --features high-active-slot-coefficient` or `cargo build --release --features high-active-slot-coefficient`
**Important:**
Make sure to build the node binaries in advance with the `high-active-slot-coefficient` feature enabled, which increases the lottery winning rate and results in more frequent block production.
Integration tests rely on these pre-built binaries and will not build them automatically.
`cargo build` or `cargo build --release`
## Setup and Usage (using Docker)
@@ -86,11 +82,6 @@ $env:USE_RELEASE_BINARIES="1"; cargo test --test test_cryptarchia_happy_path two
```
**Important:**
Make sure to build the node binaries in advance with the `high-active-slot-coefficient` feature enabled, which increases the lottery winning rate and results in more frequent block production.
Integration tests rely on these pre-built binaries and will not build them automatically.
### 2. Run Tests with Debug Feature Flag
To execute the test suite with the debug feature flag, use the following command:
+1 -2
View File
@@ -4,7 +4,6 @@ use std::{
};
use lb_node::config::{blend, deployment::DeploymentSettings};
use lb_pol::slot_activation_coefficient;
/// Calculates the maximum time required for `num_blocks` blocks to be proposed
/// and fully propagated across the network.
@@ -20,7 +19,7 @@ pub fn max_block_propagation_time(
let proposal_interval = deployment
.time
.slot_duration
.div_f64(slot_activation_coefficient());
.div_f64(deployment.cryptarchia.slot_activation_coeff.as_f64());
let blend_latency = max_blend_latency_per_block(blend_network_size, &deployment.blend);
+1 -2
View File
@@ -5,7 +5,6 @@ use std::{
use futures::stream::{self, StreamExt as _};
use lb_libp2p::PeerId;
use lb_pol::slot_activation_coefficient;
use logos_blockchain_tests::{
common::{
sync::{wait_for_validators_mode, wait_for_validators_mode_and_height},
@@ -133,7 +132,7 @@ async fn test_ibd_behind_nodes() {
// So, calculate an acceptable height margin for safe comparison.
let height_margin = acceptable_height_margin(
config.deployment.time.slot_duration,
slot_activation_coefficient(),
config.deployment.cryptarchia.slot_activation_coeff.as_f64(),
height_check_timestamp.elapsed(),
);
+2 -1
View File
@@ -20,7 +20,7 @@ use lb_node::config::{
network::deployment::Settings as NetworkDeploymentSettings,
time::deployment::Settings as TimeDeploymentSettings,
};
use lb_utils::math::NonNegativeF64;
use lb_utils::math::{NonNegativeF64, NonNegativeRatio};
use time::OffsetDateTime;
use crate::topology::configs::time::{CONSENSUS_SLOT_TIME_VAR, DEFAULT_SLOT_TIME_IN_SECS};
@@ -98,6 +98,7 @@ pub fn e2e_deployment_settings_with_genesis_tx(genesis_tx: GenesisTx) -> Deploym
// nodes have some time to sync before deciding on the
// longest chain.
security_param: NonZero::new(10).unwrap(),
slot_activation_coeff: NonNegativeRatio::new(1, 10.try_into().unwrap()),
epoch_config: EpochConfig {
epoch_stake_distribution_stabilization: NonZero::new(3).unwrap(),
epoch_period_nonce_buffer: NonZero::new(3).unwrap(),
+23
View File
@@ -1,4 +1,5 @@
use core::ops::{Deref, DerefMut};
use std::num::NonZeroU32;
#[derive(Clone, Copy, PartialEq, PartialOrd, Debug)]
#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
@@ -252,6 +253,28 @@ impl TryFrom<u64> for F64Ge1 {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
pub struct NonNegativeRatio {
pub numerator: u32,
pub denominator: NonZeroU32,
}
impl NonNegativeRatio {
#[must_use]
pub const fn new(numerator: u32, denominator: NonZeroU32) -> Self {
Self {
numerator,
denominator,
}
}
#[must_use]
pub const fn as_f64(&self) -> f64 {
self.numerator as f64 / self.denominator.get() as f64
}
}
#[cfg(feature = "serde")]
mod serde {
use serde::Deserialize as _;
+2 -2
View File
@@ -331,7 +331,7 @@ mod tests {
};
use lb_cryptarchia_engine::EpochConfig;
use lb_ledger::mantle::sdp::{ServiceRewardsParameters, rewards};
use lb_utils::math::NonNegativeF64;
use lb_utils::math::{NonNegativeF64, NonNegativeRatio};
use num_bigint::BigUint;
use super::*;
@@ -682,7 +682,7 @@ mod tests {
},
consensus_config: lb_cryptarchia_engine::Config::new(
NonZero::new(1).unwrap(),
0.1,
NonNegativeRatio::new(1, 10.try_into().unwrap()),
1f64.try_into().expect("1 > 0"),
),
sdp_config: lb_ledger::mantle::sdp::Config {
+4 -3
View File
@@ -10,9 +10,11 @@ repository = { workspace = true }
version = { workspace = true }
[dependencies]
astro-float = "0.9"
lb-circuits-prover = { workspace = true }
lb-circuits-utils = { workspace = true }
lb-groth16 = { features = ["deser"], workspace = true }
lb-utils = { workspace = true }
lb-witness-generator = { workspace = true }
num-bigint = "0.4"
num-traits = "0.2.19"
@@ -28,6 +30,5 @@ lb-circuits-utils = { workspace = true }
workspace = true
[features]
build-verification-key = []
default = ["build-verification-key"]
high-active-slot-coefficient = []
build-verification-key = []
default = ["build-verification-key"]
+7 -11
View File
@@ -1,8 +1,6 @@
use lb_groth16::{Fr, Groth16Input, Groth16InputDeser};
use serde::{Deserialize, Serialize};
use crate::lottery::compute_lottery_values;
/// Public inputs of the POL cirmcom circuit with circom specific types.
#[derive(Copy, Clone, Debug)]
pub struct PolChainInputs {
@@ -21,7 +19,8 @@ pub struct PolChainInputs {
pub struct PolChainInputsData {
pub slot_number: u64,
pub epoch_nonce: Fr,
pub total_stake: u64,
pub lottery_0: Fr,
pub lottery_1: Fr,
pub aged_root: Fr,
pub latest_root: Fr,
pub leader_pk: (Fr, Fr),
@@ -105,21 +104,18 @@ impl From<PolChainInputsData> for PolChainInputs {
PolChainInputsData {
slot_number,
epoch_nonce,
total_stake,
lottery_0,
lottery_1,
aged_root,
latest_root,
leader_pk: (pk1, pk2),
}: PolChainInputsData,
) -> Self {
let slot_number = Fr::from(slot_number);
let (lottery_0, lottery_1) = compute_lottery_values(total_stake);
Self {
slot_number: Groth16Input::new(slot_number),
slot_number: Groth16Input::new(slot_number.into()),
epoch_nonce: Groth16Input::new(epoch_nonce),
lottery_0: Groth16Input::new(lottery_0.into()),
lottery_1: Groth16Input::new(lottery_1.into()),
lottery_0: Groth16Input::new(lottery_0),
lottery_1: Groth16Input::new(lottery_1),
aged_root: aged_root.into(),
latest_root: latest_root.into(),
leader_pk1: pk1.into(),
+5 -4
View File
@@ -139,22 +139,23 @@ impl PolVerifierInput {
}
#[must_use]
#[expect(clippy::too_many_arguments, reason = "all needed")]
pub fn new(
entropy_contribution: Fr,
slot_number: u64,
epoch_nonce: Fr,
aged_root: Fr,
latest_root: Fr,
total_stake: u64,
lottery_0: Fr,
lottery_1: Fr,
leader_pk: (Fr, Fr),
) -> Self {
let (lottery_0, lottery_1) = crate::lottery::compute_lottery_values(total_stake);
Self {
entropy_contribution: entropy_contribution.into(),
slot_number: Fr::from(slot_number).into(),
epoch_nonce: epoch_nonce.into(),
lottery_0: Fr::from(lottery_0).into(),
lottery_1: Fr::from(lottery_1).into(),
lottery_0: lottery_0.into(),
lottery_1: lottery_1.into(),
aged_root: aged_root.into(),
latest_root: latest_root.into(),
leader_pk1: leader_pk.0.into(),
+8 -4
View File
@@ -46,9 +46,7 @@ use tracing::error;
pub use wallet_inputs::{PolWalletInputs, PolWalletInputsData};
pub use witness::Witness;
pub use crate::lottery::{
P, T0_CONSTANT, T1_CONSTANT, compute_lottery_values, slot_activation_coefficient,
};
pub use crate::lottery::{LotteryConstants, P};
use crate::{inputs::PolVerifierInputJson, proving_key::POL_PROVING_KEY_PATH};
pub type PoLProof = CompressedGroth16Proof;
@@ -145,6 +143,7 @@ mod tests {
use std::str::FromStr as _;
use lb_groth16::Fr;
use lb_utils::math::NonNegativeRatio;
use num_bigint::BigUint;
use super::*;
@@ -152,10 +151,15 @@ mod tests {
#[expect(clippy::too_many_lines, reason = "For the sake of the test let it be")]
#[test]
fn test_full_flow() {
let (lottery_0, lottery_1) =
LotteryConstants::new(NonNegativeRatio::new(1, 10.try_into().unwrap()))
.compute_lottery_values(5000);
let chain_data = PolChainInputsData {
slot_number: 135,
epoch_nonce: Fr::from(510u64),
total_stake: 5000,
lottery_0,
lottery_1,
aged_root: BigUint::from_str(
"16524395010779500501330992017298834046369952285388149958144954382059764408785",
)
+233 -55
View File
@@ -1,22 +1,14 @@
use std::{
ops::{Deref as _, Div as _},
sync::LazyLock,
};
use std::sync::LazyLock;
use astro_float::{BigFloat, Consts, Radix, RoundingMode, Sign};
use lb_groth16::Fr;
use lb_utils::math::NonNegativeRatio;
use num_bigint::BigUint;
use num_traits::{CheckedSub as _, Num as _};
#[must_use]
pub const fn slot_activation_coefficient() -> f64 {
#[cfg(not(feature = "high-active-slot-coefficient"))]
{
1f64 / 30f64
}
#[cfg(feature = "high-active-slot-coefficient")]
0.1f64
}
/// From [Proof of Leadership spec](https://www.notion.so/nomos-tech/Proof-of-Leadership-Specification-21c261aa09df819ba5b6d95d0fe3066d?source=copy_link#256261aa09df800fbc88e5aae5ea7e06)
/// The BN254 scalar field order,
///
/// The value is defined in [Proof of Leadership spec](https://nomos-tech.notion.site/v1-1-Proof-of-Leadership-Specification-2e9261aa09df80058244c902defc6da2#2e9261aa09df807abe78c815e7a31809)
pub static P: LazyLock<BigUint> = LazyLock::new(|| {
BigUint::from_str_radix(
"30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001",
@@ -25,52 +17,238 @@ pub static P: LazyLock<BigUint> = LazyLock::new(|| {
.expect("P constant should parse")
});
/// From [Proof of Leadership spec](https://www.notion.so/nomos-tech/Proof-of-Leadership-Specification-21c261aa09df819ba5b6d95d0fe3066d?source=copy_link#256261aa09df800fbc88e5aae5ea7e06)
pub static T0_CONSTANT: LazyLock<BigUint> = LazyLock::new(|| {
#[cfg(not(feature = "high-active-slot-coefficient"))]
{
BigUint::from_str_radix(
"1a3fb997fd58374772808c13d1c2ddacb5ab3ea77413f86fd6e0d3d978e5438",
16,
/// Lottery approximation constants used for computing t₀ and t₁.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct LotteryConstants {
pub t0_constant: BigUint,
pub t1_constant: BigUint,
}
impl LotteryConstants {
const PRECISION: usize = 512;
const ROUNDING_MODE: RoundingMode = RoundingMode::ToEven;
/// Computes the lottery approximation constants with 512 bits of precision.
///
/// Formulas:
/// - t₀_constant = floor(p * (-ln(1-f)))
/// - t₁_constant = floor(p * ln²(1-f) / 2)
///
/// Where `p` is the BN254 scalar field order and `f` is the slot activation
/// coefficient.
///
/// The calculations are defined in the [Proof of Leadership spec](https://nomos-tech.notion.site/v1-1-Proof-of-Leadership-Specification-2e9261aa09df80058244c902defc6da2#2e9261aa09df807abe78c815e7a31809).
#[expect(clippy::doc_markdown, reason = "math formulas")]
#[must_use]
pub fn new(f: NonNegativeRatio) -> Self {
let mut cc = Consts::new().expect("memory allocation should succeed");
let p = Self::p_as_bigfloat(&mut cc);
// f = f_numerator / f_denominator (exact rational arithmetic)
let f_num = BigFloat::from_u32(f.numerator, Self::PRECISION);
let f_den = BigFloat::from_u32(f.denominator.get(), Self::PRECISION);
let f = f_num.div(&f_den, Self::PRECISION, Self::ROUNDING_MODE);
// -ln(1-f)
let one = BigFloat::from_u32(1, Self::PRECISION);
let one_minus_f = one.sub(&f, Self::PRECISION, Self::ROUNDING_MODE);
let ln_one_minus_f = one_minus_f.ln(Self::PRECISION, Self::ROUNDING_MODE, &mut cc);
let neg_ln = ln_one_minus_f.neg();
// t₀_constant = floor(p * (-ln(1-f)))
let t0_constant = Self::floor_bigfloat(
&p.mul(&neg_ln, Self::PRECISION, Self::ROUNDING_MODE),
&mut cc,
);
// ln²(1-f)
let ln_sq = ln_one_minus_f.mul(&ln_one_minus_f, Self::PRECISION, Self::ROUNDING_MODE);
// p * ln²(1-f)
let t1_float = p.mul(&ln_sq, Self::PRECISION, Self::ROUNDING_MODE);
// t₁_constant = floor(p * ln²(1-f) / 2)
let two = BigFloat::from_u32(2, Self::PRECISION);
let t1_constant = Self::floor_bigfloat(
&t1_float.div(&two, Self::PRECISION, Self::ROUNDING_MODE),
&mut cc,
);
Self {
t0_constant,
t1_constant,
}
}
/// Convert [`P`] (BN254 field order) to [`BigFloat`].
fn p_as_bigfloat(cc: &mut Consts) -> BigFloat {
// Use decimal to avoid any hex 'e' vs exponent ambiguity
BigFloat::parse(
P.to_str_radix(10).as_str(),
Radix::Dec,
Self::PRECISION,
Self::ROUNDING_MODE,
cc,
)
.expect("Constant should parse")
}
/// Floor a non-negative integer-valued [`BigFloat`] to [`BigUint`]
fn floor_bigfloat(val: &BigFloat, cc: &mut Consts) -> BigUint {
// Floor the value first
let floored = val.floor();
if floored.is_zero() {
return BigUint::ZERO;
}
// Since astro-float doesn't provide direct BigUint conversion,
// we extract hex digits and reconstruct the BigUint.
//
// - digits: mantissa as hex digits (0-15 each)
// - exp: num of digits before decimal points
let (sign, digits, exp) = floored
.convert_to_radix(Radix::Hex, RoundingMode::None, cc)
.expect("floored BigFloat should convert to hex");
assert_eq!(sign, Sign::Pos, "floored BigFloat should be positive");
// Negative exponent means value < 1, which floors to 0
let exp = exp.max(0) as usize;
// Build BigUint by processing each hex digit left-to-right.
// Each hex digit represents 4 bits, so we shift left by 4
// and add the digit value (or 0 if beyond mantissa length).
let mut result = BigUint::ZERO;
for i in 0..exp {
result <<= 4;
if i < digits.len() {
result += u64::from(digits[i]);
}
}
result
}
#[cfg(feature = "high-active-slot-coefficient")]
{
BigUint::from_str_radix(
"5193d04d01fb16d1b9c55677fc83950d1cf88207f4a5756431fde6db9ab1768",
16,
)
.expect("Constant should parse")
/// Computes the lottery values t₀ and t₁ for a given total stake.
pub fn compute_lottery_values(&self, total_stake: u64) -> (Fr, Fr) {
let total_stake = BigUint::from(total_stake);
let lottery_0 = &self.t0_constant / &total_stake;
let lottery_1 = P
.checked_sub(&(&self.t1_constant / &total_stake.pow(2)))
.expect("(T1 / (S^2)) must be less than P");
(lottery_0.into(), lottery_1.into())
}
});
}
#[cfg(test)]
mod tests {
use super::*;
/// From [Proof of Leadership spec](https://www.notion.so/nomos-tech/Proof-of-Leadership-Specification-21c261aa09df819ba5b6d95d0fe3066d?source=copy_link#256261aa09df800fbc88e5aae5ea7e06)
pub static T1_CONSTANT: LazyLock<BigUint> = LazyLock::new(|| {
#[cfg(not(feature = "high-active-slot-coefficient"))]
{
BigUint::from_str_radix(
"71e790b41991052e30c93934b5612412e7958837bac8b1c524c24d84cc7d0",
16,
)
.expect("Constant should parse")
#[test]
fn test_compute_lottery_constants() {
let constants = LotteryConstants::new(NonNegativeRatio::new(1, 30.try_into().unwrap()));
assert_eq!(
constants.t0_constant,
BigUint::from_str_radix(
"1a3fb997fd5838f2a1585ee090a95c88129ab25cc4d2e2d28f1a95f81d85465",
16,
)
.unwrap(),
);
assert_eq!(
constants.t1_constant,
BigUint::from_str_radix(
"71e790b4199113a9a00298d823c5716ddac764a110a45fe3b770bbb3e8a57",
16,
)
.unwrap()
);
let constants = LotteryConstants::new(NonNegativeRatio::new(1, 10.try_into().unwrap()));
assert_eq!(
constants.t0_constant,
BigUint::from_str_radix(
"5193d04d01fb16d1b9c55677fc83950d1cf88207f4a5756431fde6db9ab1768",
16,
)
.unwrap(),
);
assert_eq!(
constants.t1_constant,
BigUint::from_str_radix(
"44c2a290c72d4dc7d6e514a4b9683cc6e7c15e64f0f59482ec7d3f5906784a",
16,
)
.unwrap()
);
}
#[cfg(feature = "high-active-slot-coefficient")]
{
BigUint::from_str_radix(
"44c2a290c72d4dc7d6e514a4b9683cc6e7c15e64f0f59482ec7d3f5906784a",
16,
)
.expect("Constant should parse")
#[test]
fn test_compute_lottery_values() {
let constants = LotteryConstants::new(NonNegativeRatio::new(1, 30.try_into().unwrap()));
let (lottery_0, lottery_1) = constants.compute_lottery_values(1000);
assert_eq!(
lottery_0,
BigUint::from_str_radix(
"6b83fe55f9383508b9bbe2d335e8e78d9c133ce0554b4f251b0ca3b6be8c",
16,
)
.unwrap()
.into(),
);
assert_eq!(
lottery_1,
BigUint::from_str_radix(
"30644e7269c19af80558c2b75767747a6fa9f2beb0e87df2e51121184e5e6c17",
16,
)
.unwrap()
.into(),
);
}
});
pub fn compute_lottery_values(total_stake: u64) -> (BigUint, BigUint) {
let total_stake = BigUint::from(total_stake);
#[test]
fn floor_bigfloat() {
let mut cc = Consts::new().unwrap();
let lottery_0 = T0_CONSTANT.deref().div(total_stake.clone());
let lottery_1 = P
.checked_sub(&T1_CONSTANT.deref().div(total_stake.pow(2)))
.expect("(T1 / (S^2)) must be less than P");
(lottery_0, lottery_1)
assert_eq!(
LotteryConstants::floor_bigfloat(
&BigFloat::from_u32(0, LotteryConstants::PRECISION),
&mut cc
),
BigUint::ZERO
);
assert_eq!(
LotteryConstants::floor_bigfloat(
&BigFloat::from_u32(42, LotteryConstants::PRECISION),
&mut cc
),
BigUint::from(42u32)
);
assert_eq!(
LotteryConstants::floor_bigfloat(
&make_bigfloat("123456789012345678901234567890", &mut cc),
&mut cc
),
BigUint::parse_bytes(b"123456789012345678901234567890", 10).unwrap()
);
assert_eq!(
LotteryConstants::floor_bigfloat(&make_bigfloat("99.999", &mut cc), &mut cc),
BigUint::from(99u32)
);
assert_eq!(
LotteryConstants::floor_bigfloat(&make_bigfloat("0.999", &mut cc), &mut cc),
BigUint::ZERO
);
}
fn make_bigfloat(val: &str, cc: &mut Consts) -> BigFloat {
BigFloat::parse(
val,
Radix::Dec,
LotteryConstants::PRECISION,
RoundingMode::None,
cc,
)
}
}
+3
View File
@@ -21,6 +21,9 @@ serde_json = { version = "1.0" }
thiserror = "2.0"
tracing = { default-features = false, workspace = true }
[dev-dependencies]
lb-utils = { workspace = true }
[build-dependencies]
lb-circuits-utils = { workspace = true }
+7 -8
View File
@@ -1,5 +1,5 @@
use lb_groth16::{Fr, Groth16Input, Groth16InputDeser};
use lb_pol::{P, compute_lottery_values};
use lb_pol::P;
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -20,7 +20,8 @@ pub struct PoQChainInputsData {
pub core_root: Fr,
pub pol_ledger_aged: Fr,
pub pol_epoch_nonce: Fr,
pub total_stake: u64,
pub lottery_0: Fr,
pub lottery_1: Fr,
}
#[derive(Deserialize, Serialize)]
@@ -98,23 +99,21 @@ impl TryFrom<PoQChainInputsData> for PoQChainInputs {
core_root,
pol_ledger_aged,
pol_epoch_nonce,
total_stake,
lottery_0,
lottery_1,
}: PoQChainInputsData,
) -> Result<Self, Self::Error> {
let session = BigUint::from(session);
if session > *P {
return Err(PoQInputsFromDataError::SessionGreaterThanP);
}
let (lottery_0, lottery_1) = compute_lottery_values(total_stake);
Ok(Self {
session: Groth16Input::new(session.into()),
core_root: core_root.into(),
pol_ledger_aged: pol_ledger_aged.into(),
pol_epoch_nonce: pol_epoch_nonce.into(),
pol_t0: Groth16Input::new(lottery_0.into()),
pol_t1: Groth16Input::new(lottery_1.into()),
pol_t0: Groth16Input::new(lottery_0),
pol_t1: Groth16Input::new(lottery_1),
})
}
}
+4 -6
View File
@@ -1,5 +1,4 @@
use lb_groth16::{Field as _, Fr, Groth16Input, Groth16InputDeser};
use lb_pol::compute_lottery_values;
use serde::{Deserialize, Serialize};
use crate::{
@@ -106,7 +105,8 @@ pub struct PoQVerifierInputData {
pub k_part_one: Fr,
pub k_part_two: Fr,
pub pol_epoch_nonce: Fr,
pub total_stake: u64,
pub lottery_0: Fr,
pub lottery_1: Fr,
pub pol_ledger_aged: Fr,
}
@@ -164,8 +164,6 @@ impl PoQVerifierInput {
impl From<PoQVerifierInputData> for PoQVerifierInput {
fn from(value: PoQVerifierInputData) -> Self {
let (lottery_0, lottery_1) = compute_lottery_values(value.total_stake);
Self {
core_quota: Groth16Input::new(value.core_quota.into()),
core_root: value.core_root.into(),
@@ -175,8 +173,8 @@ impl From<PoQVerifierInputData> for PoQVerifierInput {
leader_quota: Groth16Input::new(value.leader_quota.into()),
pol_epoch_nonce: value.pol_epoch_nonce.into(),
pol_ledger_aged: value.pol_ledger_aged.into(),
pol_t0: Groth16Input::new(lottery_0.into()),
pol_t1: Groth16Input::new(lottery_1.into()),
pol_t0: Groth16Input::new(value.lottery_0),
pol_t1: Groth16Input::new(value.lottery_1),
session: Groth16Input::new(value.session.into()),
}
}
+16 -4
View File
@@ -120,6 +120,8 @@ pub fn verify(proof: &PoQProof, public_inputs: PoQVerifierInput) -> Result<bool,
mod tests {
use std::str::FromStr as _;
use lb_pol::LotteryConstants;
use lb_utils::math::NonNegativeRatio;
use num_bigint::BigUint;
use super::*;
@@ -217,6 +219,9 @@ mod tests {
]
.map(|(value, selector)| (BigUint::from_str(value).unwrap().into(), selector)),
};
let (lottery_0, lottery_1) =
LotteryConstants::new(NonNegativeRatio::new(1, 10.try_into().unwrap()))
.compute_lottery_values(5000);
let chain_data = PoQChainInputsData {
session: 150,
core_root: BigUint::from_str(
@@ -234,7 +239,8 @@ mod tests {
)
.unwrap()
.into(),
total_stake: 5000,
lottery_0,
lottery_1,
};
let common_data = PoQCommonInputsData {
core_quota: 15,
@@ -266,7 +272,8 @@ mod tests {
pol_epoch_nonce: chain_data.pol_epoch_nonce,
pol_ledger_aged: chain_data.pol_ledger_aged,
session: chain_data.session,
total_stake: chain_data.total_stake,
lottery_0: chain_data.lottery_0,
lottery_1: chain_data.lottery_1,
};
assert!(verify(&proof, recomputed_verify_inputs.into()).unwrap());
}
@@ -274,6 +281,9 @@ mod tests {
#[expect(clippy::too_many_lines, reason = "For the sake of the test let it be")]
#[test]
fn test_leader_full_flow() {
let (lottery_0, lottery_1) =
LotteryConstants::new(NonNegativeRatio::new(1, 10.try_into().unwrap()))
.compute_lottery_values(5000);
let chain_data = PoQChainInputsData {
session: 150,
core_root: BigUint::from_str(
@@ -291,7 +301,8 @@ mod tests {
)
.unwrap()
.into(),
total_stake: 5000,
lottery_0,
lottery_1,
};
let common_data = PoQCommonInputsData {
core_quota: 15,
@@ -469,7 +480,8 @@ mod tests {
pol_epoch_nonce: chain_data.pol_epoch_nonce,
pol_ledger_aged: chain_data.pol_ledger_aged,
session: chain_data.session,
total_stake: chain_data.total_stake,
lottery_0: chain_data.lottery_0,
lottery_1: chain_data.lottery_1,
};
assert!(verify(&proof, recomputed_verify_inputs.into()).unwrap());
}