From a81cced5cc94d2ee3612fb2b6843a23d87afd51f Mon Sep 17 00:00:00 2001 From: Hardhat Chad Date: Wed, 10 Jan 2024 00:42:56 +0000 Subject: [PATCH] Add bus accounts and readme --- README.md | 10 ++ programs/ore/src/lib.rs | 378 +++++++++++++++++++--------------------- 2 files changed, 193 insertions(+), 195 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef53ea9 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Ore – A cryptocurrency everybody can mine. + +In its ideal form, mining is a fun, effective, and egalitarian method of token distribution. Mining allows anyone with access to a computer to earn tokens by solving mathematical puzzles. Unfortunately, most people who try to mine cryptocurrency today find they are unable to win any rewards at all due to the prohibitively high computing requirements. This occurs because as more hashpower comes online to mine a particular token, the smaller players win puzzles less frequently, and eventually getting pushed out entirely. This process consolidates the remaining rewards into a handful of large professional firms. We refer this problem as "miner starvation". + +In computer networking, starvation is a failure mode that can occur when a process monopolizes a particular resource and prevents other processes from accessing it. This is what happens in crypto mining today when smaller players are starved out from being able to earn rewards by large professional mining firms. Ultimately, starvation prevents new users from accessing the token via the same direct mechanism that larger, more establish players have the priviledge of using. + +Ore solves the miner starvation problem using a new, non-exclusive mining protocol. Rather than positioning every miner in a competition to solve the same puzzle as traditional minable cryptocurrencies do, Ore gives each miner their own individual puzzle to work on. As long as a miner provides a valid solution to their personal puzzle, the protocol guarantees they will earn a reward. + +To enforce a stable and predictable token inflation rate, Ore automatically adjusts the earned reward amount on a periodic basis. Larger miners may win a majority of the rewards, but they can't exclude other miners from winning as well. + diff --git a/programs/ore/src/lib.rs b/programs/ore/src/lib.rs index 48b973f..97314ec 100644 --- a/programs/ore/src/lib.rs +++ b/programs/ore/src/lib.rs @@ -18,37 +18,33 @@ pub const DIFFICULTY: Hash = Hash::new_from_array([ ]); // TODO Set this before deployment -/// The time after which mining can happen. +/// The start time for which mining can begin. pub const START_AT: i64 = 0; +/// The duration of an epoch, in units of seconds. pub const EPOCH_DURATION: i64 = 60; -pub const EXPECTED_EPOCH_REWARDS: u64 = 10u64.pow(TOKEN_DECIMALS as u32); // 1 ORE / epoch +/// The quantity of ORE expected to be minted per epoch, in units of non-divisible nanoORE. +/// Inflation rate = 1 ORE / epoch +pub const EXPECTED_EPOCH_REWARDS: u64 = 10u64.pow(TOKEN_DECIMALS as u32); -pub const SMOOTHING_FACTOR: u64 = 256; +/// The smoothing factor for reward rate changes. The reward rate cannot change by more or less +/// than factor of this constant from one epoch to the next. +pub const SMOOTHING_FACTOR: u64 = 16; -/// The decimal precision of the Ore token. -// If we use a decimal precision of 16, we can fit 10_000_000_000_000_000 (10 quadrillion) hashes in each minute. -// This is sufficiently far beyond what Solana is capable of processing. -// Max supply would still be very large and take millions of years to reach at a rate of 1 ORE / minute. -// We will not have to implement a variable difficulty to maintain the 1 ORE / min average. -// If token decimals were only 8, we likely would need a variable difficulty at some point. +/// The decimal precision of the ORE token. +/// Using SI prefixes, the smallest non-divisible unit of ORE is a nanoORE. +/// 1 nanoORE = 0.000000001 ORE = one billionth of an ORE pub const TOKEN_DECIMALS: u8 = 9; -pub const BUS_COUNT: u64 = 8; - -// TODO Use 8,9,or 10 decimals. Test and run math to see which one makes the most sense. -// TODO Track number of successful hashes per bus and add limit to # hashes/bus exceeding EXPECTED_EPOCH_REWARDS/NUMBER_OF_BUSSES. -// Rewards per epoch can exceed target limit. But hashes cannot exceed theoretical maximum. -// Eg. scenario where reward_rate = 1, cannot go lower, and the network is submitting enough hashes to push issuance rate above 1 ORE / epoch. -// In this case, each valid hash is earning the smallest reward possible. -// By hard limitting the number of hashes per bus per epoch, we prevent the 1 ORE/epoch limit from being fundamentally broken. +/// The number of available bus accounts, for parallelizing mine operations. +pub const BUS_COUNT: u8 = 8; #[program] mod ore { use super::*; - /// Initializes the program. Can only be executed once. + /// Initializes the metadata account. Can only be invoked once. pub fn initialize_metadata(ctx: Context) -> Result<()> { ctx.accounts.metadata.bump = ctx.bumps.metadata; ctx.accounts.metadata.reward_rate = 10u64.pow(TOKEN_DECIMALS.saturating_div(2) as u32); @@ -56,7 +52,7 @@ mod ore { Ok(()) } - /// Initializes the program. Can only be executed once. + /// Initializes the bus accounts. Can only be invoked once. pub fn initialize_busses(ctx: Context) -> Result<()> { ctx.accounts.bus_0.bump = ctx.bumps.bus_0; ctx.accounts.bus_0.id = 0; @@ -77,15 +73,17 @@ mod ore { Ok(()) } - pub fn register_miner(ctx: Context) -> Result<()> { - let miner = &mut ctx.accounts.miner; - miner.authority = ctx.accounts.signer.key(); - miner.bump = ctx.bumps.miner; - miner.hash = hashv(&[&ctx.accounts.signer.key().to_bytes()]); + /// Initializes a proof account for a new miner. + pub fn register(ctx: Context) -> Result<()> { + let proof = &mut ctx.accounts.proof; + proof.authority = ctx.accounts.signer.key(); + proof.bump = ctx.bumps.proof; + proof.hash = hashv(&[&ctx.accounts.signer.key().to_bytes()]); Ok(()) } - pub fn start_epoch(ctx: Context) -> Result<()> { + /// Updates the reward rate and starts the new epoch. + pub fn reset_epoch(ctx: Context) -> Result<()> { // Validate epoch has ended. let clock = Clock::get().unwrap(); let metadata = &mut ctx.accounts.metadata; @@ -117,7 +115,7 @@ mod ore { // Update the reward amount for the next epoch. metadata.reward_rate = calculate_new_reward_rate(metadata.reward_rate, total_epoch_rewards); - // Reset state for new epoch. + // Reset bus accounts. bus_0.hashes = 0; bus_1.hashes = 0; bus_2.hashes = 0; @@ -126,7 +124,7 @@ mod ore { bus_5.hashes = 0; bus_6.hashes = 0; bus_7.hashes = 0; - bus_0.hashes = 0; + bus_0.rewards = 0; bus_1.rewards = 0; bus_2.rewards = 0; bus_3.rewards = 0; @@ -134,10 +132,14 @@ mod ore { bus_5.rewards = 0; bus_6.rewards = 0; bus_7.rewards = 0; + + // Record the new epoch start time. metadata.epoch_start_at = clock.unix_timestamp; + Ok(()) } + /// Mints new Ore tokens to miners who submit valid hashes. pub fn mine(ctx: Context, hash: Hash, nonce: u64) -> Result<()> { // Validate epoch is active. let clock = Clock::get().unwrap(); @@ -145,13 +147,13 @@ mod ore { let epoch_end_at = metadata.epoch_start_at.saturating_add(EPOCH_DURATION); require!( clock.unix_timestamp.lt(&epoch_end_at), - ProgramError::EpochNotActive + ProgramError::EpochNeedsReset ); // Validate hash. - let miner = &mut ctx.accounts.miner; + let proof = &mut ctx.accounts.proof; validate_hash( - miner.hash.clone(), + proof.hash.clone(), hash.clone(), ctx.accounts.signer.key(), nonce, @@ -159,25 +161,36 @@ mod ore { )?; // Update state. - ctx.accounts.bus.hashes = ctx.accounts.bus.hashes.saturating_add(1); - ctx.accounts.bus.rewards = ctx - .accounts - .bus - .rewards - .saturating_add(metadata.reward_rate); - miner.hash = hash.clone(); + let bus = &mut ctx.accounts.bus; + bus.hashes = bus.hashes.saturating_add(1); + bus.rewards = bus.rewards.saturating_add(metadata.reward_rate); + proof.hash = hash.clone(); - // Error if this bus has already processed its quota of hashes for the epoch. + // Error if this bus has reached its hash quota for the epoch. + // + // This constraint places an upper bound on the total number of hashes that can be + // processed during any given epoch. This limit is necessary to maintain a constant + // 1 ORE/epoch inflation rate in the potential future scenario where global hashpower + // is so great that the reward rate is pushed to its minimum non-divisible value + // of 1 (one nanoORE) and miners are submitting >10^9 mine ops per epoch. + // + // In this extreme scenario, we err to the side of hardcapping inflation at 1 ORE/epoch. + // Even if all busses are saturated, miners can still avoid starvation by waiting until + // the next epoch and submitting transactions with a market rate priority fee. + // + // Even if Solana achieves 1M real TPS and all transactions are mine ops, the network + // would still only be able to process (60 * 1,000,000) = 60,000,000 hashes per epoch. + // That is, Solana should reach its network saturation point long before this quota + // is enforced here. require!( ctx.accounts .bus .hashes - .le(&EXPECTED_EPOCH_REWARDS.saturating_div(BUS_COUNT)), - // TODO Needs a dedicated error - ProgramError::HashInvalid + .le(&EXPECTED_EPOCH_REWARDS.saturating_div(BUS_COUNT as u64)), + ProgramError::BusQuotaFilled ); - // Mint reward to beneficiary. + // Mint tokens to beneficiary. token::mint_to( CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), @@ -224,83 +237,81 @@ fn calculate_new_reward_rate(current_rate: u64, epoch_rewards: u64) -> u64 { return current_rate; } - // Calculate new rate. - msg!("Current rate: {}", current_rate); - msg!("Epoch rewards: {}", epoch_rewards); - msg!("Expected rewards: {}", EXPECTED_EPOCH_REWARDS); + // Calculate new reward rate. let new_rate = (current_rate as u128) .saturating_mul(EXPECTED_EPOCH_REWARDS as u128) .saturating_div(epoch_rewards as u128) as u64; - msg!("New rate: {}", new_rate); // Smooth reward rate to not change by more than a constant factor from one epoch to the next. let new_rate_min = current_rate.saturating_div(SMOOTHING_FACTOR); let new_rate_max = current_rate.saturating_mul(SMOOTHING_FACTOR); let new_rate_smoothed = new_rate_min.max(new_rate_max.min(new_rate)); - msg!("New rate min: {}", new_rate_min); - msg!("New rate max: {}", new_rate_max); - msg!("New rate smoothed: {}", new_rate_smoothed); - // Prevent new reward from reaching 0 and return. + // Prevent reward rate from reaching 0 and return. new_rate_smoothed.max(1) } -/// The seed of the Bus account PDA. +/// The seed of the bus account PDA. pub const BUS: &[u8] = b"bus"; -/// The seed of the Metadata account PDA. +/// The seed of the metadata account PDA. pub const METADATA: &[u8] = b"metadata"; -/// The seed of the Miner account PDA. -pub const MINER: &[u8] = b"miner"; +/// The seed of the proof account PDA. +pub const PROOF: &[u8] = b"proof"; +/// Bus is an account type used to track the number of processed hashes and issued rewards +/// during an epoch. There are 8 bus accounts to provide sufficient parallelism for mine ops +/// and reduce write lock contention. +#[account] +#[derive(Debug)] +pub struct Bus { + /// The bump of the bus account PDA. + pub bump: u8, + + /// The ID of the bus account. + pub id: u8, + + /// The count of valid hashes submited to this bus in the current epoch. + pub hashes: u64, + + /// The count of rewards issued by this bus in the current epoch. + pub rewards: u64, +} + +/// Metadata is an account type used to track global program variables. #[account] #[derive(Debug)] pub struct Metadata { - /// The bump of the metadata PDA. + /// The bump of the metadata account PDA. pub bump: u8, - /// The mint address of the Ore token. + /// The mint address of the ORE token. pub mint: Pubkey, /// The timestamp of the start of the current epoch. pub epoch_start_at: i64, - /// Reweard + /// The reward rate to payout to miners for submiting valid hashes. pub reward_rate: u64, } +// TODO Track lifetime rewards? +// TODO Track lifetime hashes? +/// Proof is an account type used to track a miner's hash chain. #[account] #[derive(Debug)] -pub struct Miner { - /// The bump of the miner PDA. +pub struct Proof { + /// The bump of the proof account PDA. pub bump: u8, - /// The account authorized to hash this chain. + /// The account (i.e. miner) authorized to use this proof. pub authority: Pubkey, - /// The miner's current hash. + /// The proof's current hash. pub hash: Hash, } -/// Bus is an account used to track rewards issued during an epoch. -/// There are 8 bus accounts to provide parallelism and reduce write lock contention. -#[account] -#[derive(Debug)] -pub struct Bus { - /// The bump of the counter PDA. - pub bump: u8, - - /// The ID of this counter account. - pub id: u8, - - /// The count of rewards issued this epoch. - pub rewards: u64, - - /// The count of valid hashes that have been submitted on this bus this epoch. - pub hashes: u64, -} - #[derive(Accounts)] pub struct InitializeMetadata<'info> { /// The signer of the transaction. @@ -371,29 +382,30 @@ pub struct InitializeBusses<'info> { pub system_program: Program<'info, System>, } -/// RegisterMiner registers a new miner with the Ore program and starts a new hash chain for them to mine. +/// Register registers a new miner and initialize a proof account for them. #[derive(Accounts)] -pub struct RegisterMiner<'info> { +pub struct Register<'info> { /// The signer of the transaction. #[account(mut)] pub signer: Signer<'info>, - /// The miner account. - #[account(init, seeds = [MINER, signer.key().as_ref()], bump, payer = signer, space = 8 + size_of::())] - pub miner: Account<'info, Miner>, + /// The proof account. + #[account(init, seeds = [PROOF, signer.key().as_ref()], bump, payer = signer, space = 8 + size_of::())] + pub proof: Account<'info, Proof>, /// The Solana system program. #[account(address = system_program::ID)] pub system_program: Program<'info, System>, } +// ResetEpoch adjusts the reward rate based on global hashpower and begins the new epoch. #[derive(Accounts)] -pub struct StartEpoch<'info> { +pub struct ResetEpoch<'info> { /// The signer of the transaction. #[account(mut)] pub signer: Signer<'info>, - /// Counter account 0. + /// Bus account 0. #[account(mut, seeds = [BUS, &[0]], bump)] pub bus_0: Account<'info, Bus>, @@ -430,7 +442,6 @@ pub struct StartEpoch<'info> { pub metadata: Account<'info, Metadata>, } -// TODO Bytes, not strings #[derive(Accounts)] #[instruction(hash: Hash, nonce: u64)] pub struct Mine<'info> { @@ -442,7 +453,7 @@ pub struct Mine<'info> { #[account(mut, token::mint = mint)] pub beneficiary: Account<'info, TokenAccount>, - /// A bus account for tracking epoch rewards. + /// One of the bus accounts. #[account(mut)] pub bus: Account<'info, Bus>, @@ -450,9 +461,9 @@ pub struct Mine<'info> { #[account(seeds = [METADATA], bump = metadata.bump, has_one = mint)] pub metadata: Account<'info, Metadata>, - /// The metadata account. - #[account(mut, seeds = [MINER, signer.key().as_ref()], bump = miner.bump, constraint = signer.key().eq(&miner.authority))] - pub miner: Account<'info, Miner>, + /// The proof account. + #[account(mut, seeds = [PROOF, signer.key().as_ref()], bump = proof.bump, constraint = signer.key().eq(&proof.authority))] + pub proof: Account<'info, Proof>, /// The Ore token mint account. #[account(mut)] @@ -467,7 +478,7 @@ pub struct Mine<'info> { #[event] #[derive(Debug)] pub struct MineEvent { - /// The signer of the transaction (i.e. the miner). + /// The signer of the transaction (i.e. the proof). pub signer: Pubkey, /// The beneficiary token account to which rewards were minted. @@ -498,131 +509,108 @@ pub enum ProgramError { #[msg("Mining has not started yet")] NotStarted, #[msg("The epoch has ended and needs to be reset")] - EpochNotActive, + EpochNeedsReset, + #[msg("This bus hash reached its hash quota for this epoch")] + BusQuotaFilled, } #[cfg(test)] mod tests { - // use anchor_lang::prelude::Pubkey; - // use bnum::types::U256; - // use rand::prelude::*; + use anchor_lang::{ + prelude::Pubkey, + solana_program::hash::{hashv, Hash}, + }; - // use crate::validate_hash; + use crate::{ + calculate_new_reward_rate, validate_hash, EXPECTED_EPOCH_REWARDS, SMOOTHING_FACTOR, + }; #[test] fn test_validate_hash_pass() { - // let h1 = sha256::digest("Seed"); - // let signer = Pubkey::new_unique(); - // let nonce = 10; - // let h2 = sha256::digest(format!("{}-{}-{}", h1, signer, nonce)); - // let res = validate_hash(h1, h2, signer, nonce, U256::MAX); - // assert!(res.is_ok()); + let h1 = Hash::new_from_array([1; 32]); + let signer = Pubkey::new_unique(); + let nonce = 10u64; + let difficulty = Hash::new_from_array([255; 32]); + let h2 = hashv(&[ + h1.to_bytes().as_slice(), + signer.to_bytes().as_slice(), + nonce.to_be_bytes().as_slice(), + ]); + let res = validate_hash(h1, h2, signer, nonce, difficulty); + assert!(res.is_ok()); } #[test] fn test_validate_hash_fail() { - // let h1 = sha256::digest("Seed"); - // let signer = Pubkey::new_unique(); - // let nonce = 10; - // let h2 = String::from("Invalid hash"); - // let res = validate_hash(h1, h2, signer, nonce, U256::MAX); - // assert!(res.is_err()); + let h1 = Hash::new_from_array([1; 32]); + let signer = Pubkey::new_unique(); + let nonce = 10u64; + let difficulty = Hash::new_from_array([255; 32]); + let h2 = Hash::new_from_array([2; 32]); + let res = validate_hash(h1, h2, signer, nonce, difficulty); + assert!(res.is_err()); } #[test] fn test_validate_hash_fail_difficulty() { - // let h1 = sha256::digest("Seed"); - // let signer = Pubkey::new_unique(); - // let nonce = 10; - // let h2 = sha256::digest(format!("{}-{}-{}", h1, signer, nonce)); - // let res = validate_hash(h1, h2, signer, nonce, U256::MIN); - // assert!(res.is_err()); + let h1 = Hash::new_from_array([1; 32]); + let signer = Pubkey::new_unique(); + let nonce = 10u64; + let difficulty = Hash::new_from_array([0; 32]); + let h2 = hashv(&[ + h1.to_bytes().as_slice(), + signer.to_bytes().as_slice(), + nonce.to_be_bytes().as_slice(), + ]); + let res = validate_hash(h1, h2, signer, nonce, difficulty); + assert!(res.is_err()); } #[test] - fn test_validate_hash_fuzz() { - // let h1 = sha256::digest("Seed"); - // let signer = Pubkey::new_unique(); - // let mut rng = rand::thread_rng(); - // for i in 0..10_000 { - // let nonce = rng.gen::(); - // let h2 = sha256::digest(i.to_string()); - // let res = validate_hash(h1.clone(), h2, signer, nonce, U256::MAX); - // assert!(res.is_err()); - // } + fn test_calculate_new_reward_rate_stable() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, EXPECTED_EPOCH_REWARDS); + assert!(new_rate.eq(¤t_rate)); } - // #[test] - // fn test_calculate_new_difficulty_stable() { - // let t1 = 0i64; - // let t2 = 86_400i64; - // let difficulty = U256::from_digit(100); - // let new_difficulty = calculate_new_difficulty(t1, t2, difficulty); - // assert!(new_difficulty.is_ok()); - // let x = new_difficulty.unwrap(); - // assert!(x.eq(&U256::from_digit(100))); - // } + #[test] + fn test_calculate_new_reward_rate_no_chage() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, 0); + assert!(new_rate.eq(¤t_rate)); + } - // #[test] - // fn test_calculate_new_difficulty_higher() { - // let t1 = 0i64; - // let t2 = 172_800i64; - // let difficulty = U256::from_digit(100); - // let new_difficulty = calculate_new_difficulty(t1, t2, difficulty); - // assert!(new_difficulty.is_ok()); - // let x = new_difficulty.unwrap(); - // assert!(x.eq(&U256::from_digit(200))); - // } + #[test] + fn test_calculate_new_reward_rate_lower() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate( + current_rate, + EXPECTED_EPOCH_REWARDS.saturating_add(1_000_000), + ); + assert!(new_rate.lt(¤t_rate)); + } - // #[test] - // fn test_calculate_new_difficulty_lower() { - // let t1 = 0i64; - // let t2 = 43_200i64; - // let difficulty = U256::from_digit(100); - // let new_difficulty = calculate_new_difficulty(t1, t2, difficulty); - // assert!(new_difficulty.is_ok()); - // let x = new_difficulty.unwrap(); - // assert!(x.eq(&U256::from_digit(50))); - // } + #[test] + fn test_calculate_new_reward_rate_higher() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate( + current_rate, + EXPECTED_EPOCH_REWARDS.saturating_sub(1_000_000), + ); + assert!(new_rate.gt(¤t_rate)); + } - // #[test] - // fn test_calculate_new_difficulty_max() { - // let t1 = 0i64; - // let t2 = 1_000_000i64; - // let difficulty = U256::from_digit(100); - // let new_difficulty = calculate_new_difficulty(t1, t2, difficulty); - // assert!(new_difficulty.is_ok()); - // let x = new_difficulty.unwrap(); - // assert!(x.eq(&U256::from_digit(400))); - // } + #[test] + fn test_calculate_new_reward_rate_max_smooth() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, 1); + assert!(new_rate.eq(¤t_rate.saturating_mul(SMOOTHING_FACTOR))); + } - // #[test] - // fn test_calculate_new_difficulty_min() { - // let t1 = 0i64; - // let t2 = 1i64; - // let difficulty = U256::from_digit(100); - // let new_difficulty = calculate_new_difficulty(t1, t2, difficulty); - // assert!(new_difficulty.is_ok()); - // let x = new_difficulty.unwrap(); - // assert!(x.eq(&U256::from_digit(25))); - // } - - // #[test] - // fn test_calculate_new_difficulty_err() { - // let t1 = 10i64; - // let t2 = 5i64; - // let difficulty = U256::from_digit(100); - // let new_difficulty = calculate_new_difficulty(t1, t2, difficulty); - // assert!(new_difficulty.is_err()); - // } - - // #[test] - // fn test_calculate_supply() { - // let s1 = calculate_supply(1); - // let s10 = calculate_supply(10); - // let s100 = calculate_supply(100); - // assert!(s1.eq(&4_848_400_000_000)); - // assert!(s10.eq(&20_118_631_803_084)); - // assert!(s100.eq(&83_483_075_989_621)); - // } + #[test] + fn test_calculate_new_reward_rate_min_smooth() { + let current_rate = 1000; + let new_rate = calculate_new_reward_rate(current_rate, u64::MAX); + assert!(new_rate.eq(¤t_rate.saturating_div(SMOOTHING_FACTOR))); + } }