Skip to content

Commit 3f90eb0

Browse files
committed
lockupvault
1 parent a9c42fc commit 3f90eb0

5 files changed

Lines changed: 367 additions & 148 deletions

File tree

ex/build.Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
FROM ubuntu:24.04
44
ENV DEBIAN_FRONTEND noninteractive
55

6-
ENV SSL_VERSION=3.6.2
7-
ENV OTP_VERSION=OTP-28.5
8-
ENV ELIXIR_VERSION=v1.19.5
6+
ENV SSL_VERSION=3.6.3
7+
ENV OTP_VERSION=OTP-29.0.3
8+
ENV ELIXIR_VERSION=v1.20.2
99

1010
RUN apt-get update && apt-get install -y vim git curl locate wget apt-transport-https apt-utils locales
1111
ENV LANGUAGE en_US.UTF-8

ex/native/rdb/src/consensus/bic/epoch.rs

Lines changed: 191 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::consensus::aggsig::DST_MOTION;
22
use crate::{bcat, consensus};
33
use num_bigint::BigUint;
4-
use std::collections::HashSet;
4+
use std::collections::{BTreeMap, HashSet};
55
use std::panic::panic_any;
66

77
use crate::consensus::consensus_apply::ApplyEnv;
@@ -10,6 +10,18 @@ use crate::consensus::consensus_kv::{kv_delete, kv_exists, kv_get, kv_get_next,
1010
pub const EPOCH_EMISSION_BASE: i128 = 1_000_000_000_000_000;
1111
pub const EPOCH_INTERVAL: i128 = 100_000;
1212

13+
pub const NETWORK_TAX_BPS: i128 = 2_500; //25%
14+
15+
pub const SOLVER_PARTICIPATION_TARGET: i128 = 100;
16+
17+
pub const PARTICIPATION_VAULT_EPOCH: u64 = 1150;
18+
19+
//per-epoch emission not disbursed carries over in these pools: the vault pool feeds
20+
//back into future vault APY budgets (and funds the network tax); the solver pool
21+
//just accumulates for now (disposition TBD).
22+
pub const VAULT_ACCRUED_POOL_KEY: &[u8] = b"bic:epoch:vault_accrued_pool";
23+
pub const SOLVER_ACCRUED_POOL_KEY: &[u8] = b"bic:epoch:solver_accrued_pool";
24+
1325
pub const TREASURY_DONATION_ADDRESS: &[u8; 48] = &[
1426
140, 71, 6, 83, 31, 185, 171, 240, 47, 5, 14, 246, 98, 23, 105, 24, 183, 118, 193, 92, 66, 82, 64, 5, 239, 255, 254, 87, 139, 252, 148, 176, 113, 6, 207,
1527
153, 51, 25, 202, 45, 48, 153, 223, 248, 219, 210, 80, 254,
@@ -611,11 +623,138 @@ pub fn next(env: &mut ApplyEnv) {
611623
clear_epoch_data(env);
612624
}
613625

614-
fn distribute_emissions_to_trainers(env: &mut ApplyEnv, trainers_to_recv: &Vec<(Vec<u8>, i128)>, total_emission: i128, total_sols: i128) {
626+
//epoch boundary from VAULT_ACTIVATION_EPOCH on (and always on testnet): the vault
627+
//engine plus the reworked emission model. same backbone as next() — collect
628+
//leaders, set next epoch's validators, adjust difficulty, clear epoch data — with:
629+
// * queued vault validator changes post once, first thing (the only promotion site)
630+
// * emission split in half: vault APY vs solvers
631+
// * vault APY paid from (this half + carried pool); leftover carries on
632+
// * a 25% treasury tax on all payouts, FUNDED from the vault leftover (not minted
633+
// on top) so issuance stays within the curve
634+
// * emission curbed by participation (phash): below SOLVER_PARTICIPATION_TARGET only
635+
// phash% pays, the rest accrues. always applies to the solver half; applies to
636+
// vault APY too from PARTICIPATION_VAULT_EPOCH on
637+
// * validator set = peddlebike67 + every >=1m-stake vault validator + top 33 solvers
638+
// * no community-fund payout (peddlebike67 only enter the validator set)
639+
pub fn next2(env: &mut ApplyEnv) {
640+
let epoch_cur = env.caller_env.entry_epoch;
641+
let epoch_next = env.caller_env.entry_epoch + 1;
642+
643+
consensus::bic::lockup_vault::promote_pending_validators(env, epoch_next);
644+
645+
let peddlebike67_map: HashSet<Vec<u8>> = if env.testnet {
646+
env.testnet_peddlebikes.iter().map(|pk| pk.to_vec()).collect()
647+
} else {
648+
PEDDLEBIKE67.iter().map(|pk| pk.to_vec()).collect()
649+
};
650+
651+
let trainers = kv_get_trainers(env, env.caller_env.entry_height);
652+
let trainers_map: HashSet<Vec<u8>> = trainers.into_iter().collect();
653+
let trainers_removed = kv_get_trainers_removed(env);
654+
let trainers_removed_map: HashSet<Vec<u8>> = trainers_removed.into_iter().collect();
655+
656+
//total_score_all sums EVERY sol submitter (matching the stats `score()` used by
657+
//pflops); leaders excludes slashed (removed) trainers, as in next().
658+
let mut leaders: Vec<(Vec<u8>, i128)> = Vec::new();
659+
let mut total_score_all: i128 = 0;
660+
let mut cursor: Vec<u8> = Vec::new();
661+
while let Some((pk, val)) = kv_get_next(env, b"bic:epoch:solutions_count:", &cursor) {
662+
let count = std::str::from_utf8(&val).ok().and_then(|s| s.parse::<i128>().ok()).unwrap_or_else(|| panic_any("invalid_solutions_count"));
663+
total_score_all = total_score_all.checked_add(count).unwrap_or_else(|| panic_any("invalid_solutions_count"));
664+
if !trainers_removed_map.contains(&pk) {
665+
leaders.push((pk.clone(), count));
666+
}
667+
cursor = pk;
668+
}
669+
// sort descending; highest sol count first; tiebreak on PK
670+
leaders.sort_unstable_by(|(ka, ca), (kb, cb)| match cb.cmp(ca) {
671+
std::cmp::Ordering::Equal => kb.cmp(ka),
672+
other => other,
673+
});
674+
675+
//every sol-submitting solver in the set (not a peddlebike) is eligible — no cap
676+
let solvers: Vec<(Vec<u8>, i128)> =
677+
leaders.iter().cloned().filter(|(pk, _)| trainers_map.contains(pk) && !peddlebike67_map.contains(pk)).collect();
678+
let total_sols: i128 = solvers.iter().map(|(_, count)| count).sum();
679+
680+
let epoch_total_emission = epoch_emission_active(epoch_cur);
681+
let vault_half = epoch_total_emission / 2;
682+
let solver_half = epoch_total_emission - vault_half;
683+
684+
//--- participation (phash) — computed up front so it can also curb vault APY ---
685+
//at phash >= SOLVER_PARTICIPATION_TARGET (100 PFLOPS) full emission pays; below that
686+
//only phash% pays and the shortfall accrues, so a low-participation (bear) period
687+
//can't take the lion's share of easy emissions.
688+
let height_in_epoch = (env.caller_env.entry_height % 100_000) as i128;
689+
let phash = net_phash(env, total_score_all, height_in_epoch);
690+
let participation = phash.clamp(0, SOLVER_PARTICIPATION_TARGET); //0..=100
691+
692+
//participation curbs the solver half always; it curbs vault APY only from
693+
//PARTICIPATION_VAULT_EPOCH on. before that, vaults always pay full and only solvers
694+
//are curbed; after it, low solver participation heavily reduces vault APY too.
695+
let vault_reduction_pct: u64 = if epoch_cur >= PARTICIPATION_VAULT_EPOCH { participation as u64 } else { 100 };
696+
697+
//--- vault APY ---
698+
let vault_pool = kv_get(env, VAULT_ACCRUED_POOL_KEY).and_then(|v| std::str::from_utf8(&v).ok().and_then(|s| s.parse::<i128>().ok())).unwrap_or(0);
699+
let vault_budget = vault_half.checked_add(vault_pool).unwrap_or_else(|| panic_any("vault_budget_overflow"));
700+
let vault_paid = consensus::bic::lockup_vault::pay_epoch_yield(env, &trainers_map, vault_reduction_pct, vault_budget);
701+
let vault_leftover = vault_budget.checked_sub(vault_paid).unwrap_or_else(|| panic_any("vault_pool_underflow"));
702+
let vault_stakes = consensus::bic::lockup_vault::validator_stakes(env);
703+
704+
//--- solver emission, curbed by participation ---
705+
let solver_budget = solver_half.checked_mul(participation).unwrap_or_else(|| panic_any("emission_overflow")) / SOLVER_PARTICIPATION_TARGET;
706+
let solver_paid = distribute_emissions_to_trainers(env, &solvers, solver_budget, total_sols);
707+
//the part of the solver half not paid (participation shortfall + rounding) accrues
708+
let solver_accrued = solver_half.checked_sub(solver_paid).unwrap_or_else(|| panic_any("emission_overflow"));
709+
if solver_accrued > 0 {
710+
let _ = kv_increment(env, SOLVER_ACCRUED_POOL_KEY, solver_accrued);
711+
}
712+
713+
//--- 25% network tax, charged AFTER all participation reductions so it tracks what
714+
//was actually paid (vault_paid and solver_paid are already reduced). funded from the
715+
//vault leftover so it never exceeds emission; capped at the leftover — payouts are
716+
//never cut, the tax yields first. remaining leftover rolls into the vault pool. ---
717+
let taxable = vault_paid.checked_add(solver_paid).unwrap_or_else(|| panic_any("emission_overflow"));
718+
let nominal_tax = taxable.checked_mul(NETWORK_TAX_BPS).unwrap_or_else(|| panic_any("emission_overflow")) / 10_000;
719+
let tax = nominal_tax.min(vault_leftover);
720+
if tax > 0 {
721+
let _ = kv_increment(env, &bcat(&[b"account:", TREASURY_DONATION_ADDRESS.as_slice(), b":balance:AMA"]), tax);
722+
}
723+
let new_vault_pool = vault_leftover - tax;
724+
kv_put(env, VAULT_ACCRUED_POOL_KEY, new_vault_pool.to_string().as_bytes());
725+
726+
//--- validators for next epoch: peddlebike67 + >=1m vault validators + top 33 solvers ---
727+
let new_validators = build_and_shuffle_new_validators2(env, &leaders, &vault_stakes);
728+
let new_validators = consensus::bic::list_of_binaries_to_vecpak(new_validators);
729+
let height_next = format!("{:012}", env.caller_env.entry_height.saturating_add(1)).into_bytes();
730+
let _ = kv_put(env, &bcat(&[b"bic:epoch:validators:height:", &height_next]), &new_validators);
731+
732+
update_difficulty_and_log_sols(env, epoch_cur, epoch_next, total_sols);
733+
clear_epoch_data(env);
734+
}
735+
736+
fn net_phash(env: &mut ApplyEnv, total_sols: i128, height_in_epoch: i128) -> i128 {
737+
const OPS: i128 = 16 * 16 * 50_240 * 2; //25_722_880 (MACs x2, per pflops)
738+
739+
let diff_bits = kv_get(env, b"bic:epoch:diff_bits")
740+
.and_then(|v| std::str::from_utf8(&v).ok().and_then(|s| s.parse::<u32>().ok()))
741+
.unwrap_or(24);
742+
let diff_multiplier: i128 = if diff_bits >= 127 { i128::MAX } else { 1i128 << diff_bits };
743+
744+
let total_calcs = total_sols.checked_mul(diff_multiplier).unwrap_or(i128::MAX);
745+
let numer = total_calcs.checked_mul(OPS).unwrap_or(i128::MAX);
746+
let denom = (height_in_epoch + 2).checked_mul(500_000_000_000_000).unwrap_or_else(|| panic_any("phash_denom_overflow"));
747+
numer / denom
748+
}
749+
750+
//distributes `total_emission` to solvers pro rata by sol count (paid in full);
751+
//returns the total actually paid, which the caller uses for tax accounting.
752+
fn distribute_emissions_to_trainers(env: &mut ApplyEnv, trainers_to_recv: &Vec<(Vec<u8>, i128)>, total_emission: i128, total_sols: i128) -> i128 {
615753
if total_sols == 0 {
616-
return;
754+
return 0;
617755
}
618756

757+
let mut paid: i128 = 0;
619758
for (trainer, trainer_sols) in trainers_to_recv {
620759
let coins = trainer_sols
621760
.checked_mul(total_emission)
@@ -627,9 +766,13 @@ fn distribute_emissions_to_trainers(env: &mut ApplyEnv, trainers_to_recv: &Vec<(
627766
if let Some(addr) = emission_address { bcat(&[b"account:", &addr, b":balance:AMA"]) } else { bcat(&[b"account:", trainer, b":balance:AMA"]) };
628767

629768
let _ = kv_increment(env, &balance_key, coins);
769+
paid = paid.checked_add(coins).unwrap_or_else(|| panic_any("emission_overflow"));
630770
}
771+
paid
631772
}
632773

774+
//legacy (pre-VAULT_ACTIVATION_EPOCH) community-fund split. from the fork on,
775+
//peddlebike67 receive no emission payout — they only enter the validator set.
633776
fn distribute_peddlebike67_community_fund(env: &mut ApplyEnv, total_emission: i128) {
634777
let n_count = PEDDLEBIKE67.len() as i128;
635778
let q = total_emission / n_count;
@@ -646,6 +789,10 @@ fn distribute_peddlebike67_community_fund(env: &mut ApplyEnv, total_emission: i1
646789
}
647790
}
648791

792+
//number of top solvers admitted to the validator set from the fork on
793+
const SOLVER_VALIDATOR_SLOTS: usize = 33;
794+
795+
//legacy (pre-fork) validator set: peddlebike67 + all leaders, capped at 99.
649796
fn build_and_shuffle_new_validators(env: &ApplyEnv, leaders: &Vec<(Vec<u8>, i128)>) -> Vec<Vec<u8>> {
650797
let PEDDLEBIKE_LOCAL: Vec<[u8; 48]> = if env.testnet {
651798
env.testnet_peddlebikes.iter().map(|pk| pk.as_slice().try_into().expect("Testnet key was not 48 bytes long")).collect()
@@ -660,12 +807,50 @@ fn build_and_shuffle_new_validators(env: &ApplyEnv, leaders: &Vec<(Vec<u8>, i128
660807
new_validators.extend(filtered_leaders);
661808
new_validators.truncate(99);
662809

810+
shuffle_validators(env, &mut new_validators);
811+
new_validators
812+
}
813+
814+
//fork validator set: peddlebike67 + every >=1m-stake vault validator + top 33 solvers,
815+
//deduped, no fixed size cap.
816+
fn build_and_shuffle_new_validators2(env: &ApplyEnv, leaders: &Vec<(Vec<u8>, i128)>, vault_stakes: &BTreeMap<Vec<u8>, i128>) -> Vec<Vec<u8>> {
817+
let PEDDLEBIKE_LOCAL: Vec<[u8; 48]> = if env.testnet {
818+
env.testnet_peddlebikes.iter().map(|pk| pk.as_slice().try_into().expect("Testnet key was not 48 bytes long")).collect()
819+
} else {
820+
PEDDLEBIKE67.to_vec()
821+
};
822+
823+
let mut new_validators: Vec<Vec<u8>> = PEDDLEBIKE_LOCAL.iter().map(|p| p.to_vec()).collect();
824+
825+
//vault-backed validators with at least VALIDATOR_MIN_STAKE (1m AMA amount+accrued)
826+
for (validator, stake) in vault_stakes {
827+
if *stake >= consensus::bic::lockup_vault::VALIDATOR_MIN_STAKE {
828+
new_validators.push(validator.clone());
829+
}
830+
}
831+
832+
//top SOLVER_VALIDATOR_SLOTS solvers (leaders are pre-sorted by sol count desc,
833+
//so a low-sol solver only enters if fewer than 33 out-sol it)
834+
let top_solvers = leaders
835+
.iter()
836+
.map(|(pk, _)| pk.clone())
837+
.filter(|pk| !PEDDLEBIKE_LOCAL.iter().any(|p| p.as_slice() == pk.as_slice()))
838+
.take(SOLVER_VALIDATOR_SLOTS);
839+
new_validators.extend(top_solvers);
840+
841+
//a vault validator may also be a peddlebike or a top solver; keep first occurrence
842+
let mut seen: HashSet<Vec<u8>> = HashSet::new();
843+
new_validators.retain(|pk| seen.insert(pk.clone()));
844+
845+
shuffle_validators(env, &mut new_validators);
846+
new_validators
847+
}
848+
849+
fn shuffle_validators(env: &ApplyEnv, validators: &mut Vec<Vec<u8>>) {
663850
let seed_bytes = &env.caller_env.seed;
664851
let seed_array: [u8; 32] = seed_bytes.get(..32).and_then(|s| s.try_into().ok()).unwrap_or([0u8; 32]);
665852
let mut rng = crate::consensus::bic::exsss::Exsss::from_seed(&seed_array);
666-
rng.shuffle(&mut new_validators);
667-
668-
new_validators
853+
rng.shuffle(validators);
669854
}
670855

671856
fn update_difficulty_and_log_sols(env: &mut ApplyEnv, epoch_cur: u64, epoch_next: u64, total_sols: i128) {

0 commit comments

Comments
 (0)