mirror of
https://github.com/d0zingcat/ore.git
synced 2026-05-13 15:09:57 +00:00
Variable difficulty
This commit is contained in:
3395
Cargo.lock
generated
3395
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "ore"
|
||||
version = "0.1.0"
|
||||
description = "Ore is a hard, fast, borderless, permissionless, auditable, digital currency"
|
||||
description = "Ore is a cryptocurrency everyone can mine"
|
||||
edition = "2021"
|
||||
license = "Apache 2.0"
|
||||
homepage = ""
|
||||
@@ -24,7 +24,9 @@ default = []
|
||||
[dependencies]
|
||||
anchor-lang = "0.29.0"
|
||||
anchor-spl = { version = "0.29.0", features = ["token"] }
|
||||
bincode = "1.3.3"
|
||||
static_assertions = "1.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8.5"
|
||||
solana-program-test = "1.17"
|
||||
# rand = "0.8.5"
|
||||
|
||||
@@ -4,6 +4,7 @@ use anchor_lang::{
|
||||
prelude::*,
|
||||
solana_program::{
|
||||
hash::{hashv, Hash},
|
||||
slot_hashes::SlotHash,
|
||||
system_program, sysvar,
|
||||
},
|
||||
};
|
||||
@@ -12,40 +13,49 @@ use anchor_spl::{
|
||||
token::{self, Mint, MintTo, TokenAccount},
|
||||
};
|
||||
|
||||
// TODO Upgrade to token22
|
||||
// TODO Use the confidential transfers extension.
|
||||
// TODO Use the memo extension?
|
||||
|
||||
declare_id!("CeJShZEAzBLwtcLQvbZc7UT38e4nUTn63Za5UFyYYDTS");
|
||||
|
||||
// TODO Set this to a reasonable value.
|
||||
pub const INITIAL_DIFFICULTY: Hash = Hash::new_from_array([
|
||||
0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
]);
|
||||
|
||||
// TODO Set this before deployment
|
||||
/// 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;
|
||||
|
||||
/// One ORE token, denominated in indivisible units of nanoORE.
|
||||
pub const ONE_ORE: u64 = 10u64.pow(TOKEN_DECIMALS as u32);
|
||||
|
||||
/// The quantity of ORE expected to be minted per epoch, in units of indivisible nanoORE.
|
||||
/// Inflation rate = 1 ORE / epoch
|
||||
pub const EXPECTED_EPOCH_REWARDS: u64 = ONE_ORE; // 10u64.pow(TOKEN_DECIMALS as u32);
|
||||
|
||||
/// 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 = 2;
|
||||
|
||||
/// The decimal precision of the ORE token.
|
||||
/// Using SI prefixes, the smallest indivisible unit of ORE is a nanoORE.
|
||||
/// 1 nanoORE = 0.000000001 ORE = one billionth of an ORE
|
||||
pub const TOKEN_DECIMALS: u8 = 9;
|
||||
|
||||
/// The number of available bus accounts, for parallelizing mine operations.
|
||||
/// One ORE token, denominated in units of nanoORE.
|
||||
pub const ONE_ORE: u64 = 10u64.pow(TOKEN_DECIMALS as u32);
|
||||
|
||||
/// The duration of an epoch, in units of seconds.
|
||||
pub const EPOCH_DURATION: i64 = 60;
|
||||
|
||||
/// The target quantity of ORE to be mined per epoch, in units of nanoORE.
|
||||
/// Inflation rate ≈ 1 ORE / epoch (min 0, max 2)
|
||||
pub const TARGET_EPOCH_REWARDS: u64 = ONE_ORE;
|
||||
|
||||
/// 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 = 2;
|
||||
|
||||
/// The number of bus accounts, for parallelizing mine operations.
|
||||
pub const BUS_COUNT: u8 = 8;
|
||||
|
||||
/// The quantity of ORE each bus will be topped up with at the beginning of each epoch.
|
||||
pub const BUS_BALANCE: u64 = TARGET_EPOCH_REWARDS
|
||||
.saturating_mul(SMOOTHING_FACTOR)
|
||||
.saturating_div(BUS_COUNT as u64);
|
||||
|
||||
/// The initial hashing difficulty. The admin authority can update this in the future, if needed.
|
||||
pub const INITIAL_DIFFICULTY: Hash = Hash::new_from_array([
|
||||
0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
]);
|
||||
|
||||
// TODO Set this before deployment
|
||||
/// The unix timestamp after which mining is allowed.
|
||||
pub const START_AT: i64 = 0;
|
||||
|
||||
// Assert ONE_ORE is evenly divisible by BUS_COUNT
|
||||
static_assertions::const_assert!((ONE_ORE / BUS_COUNT as u64) * BUS_COUNT as u64 == ONE_ORE);
|
||||
|
||||
@@ -89,7 +99,7 @@ mod ore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO Rename to `initialize_proof`?
|
||||
// TODO Rename to `initialize_proof` for naming consistency?
|
||||
/// Initializes a proof account for a new miner.
|
||||
pub fn register(ctx: Context<Register>) -> Result<()> {
|
||||
let proof = &mut ctx.accounts.proof;
|
||||
@@ -99,12 +109,6 @@ mod ore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the difficulty to a new value. Can only be invoked by the admin authority.
|
||||
pub fn update_difficulty(ctx: Context<UpdateDifficulty>, new_difficulty: Hash) -> Result<()> {
|
||||
ctx.accounts.metadata.difficulty = new_difficulty;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the reward rate and starts the new epoch.
|
||||
pub fn reset_epoch(ctx: Context<ResetEpoch>) -> Result<()> {
|
||||
// Validate epoch has ended.
|
||||
@@ -203,7 +207,7 @@ mod ore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Distributes Ore tokens to signers who submit a valid hash.
|
||||
/// Distributes Ore tokens to the signer if a valid hash is provided.
|
||||
pub fn mine(ctx: Context<Mine>, hash: Hash, nonce: u64) -> Result<()> {
|
||||
// Validate epoch is active.
|
||||
let clock = Clock::get().unwrap();
|
||||
@@ -229,38 +233,15 @@ mod ore {
|
||||
bus.hashes = bus.hashes.saturating_add(1);
|
||||
bus.rewards = bus.rewards.saturating_add(metadata.reward_rate);
|
||||
|
||||
// Hash a recent slot hash into the next hash to prevent pre-mining attacks.
|
||||
// TODO is this the right bit slice to use?
|
||||
let slot_hash_bytes = &ctx.accounts.slot_hashes.data.borrow()[0..(64 + 256)];
|
||||
// Hash a recent slot hash into the next hash to prevent pre-mining attacks.
|
||||
// let slot_hash_bytes = &ctx.accounts.slot_hashes.data.borrow()[0..(64 + 256)];
|
||||
let slot_hash_bytes = &ctx.accounts.slot_hashes.data.borrow()[0..size_of::<SlotHash>()];
|
||||
let x: SlotHash =
|
||||
bincode::deserialize(slot_hash_bytes).expect("Failed to deserialize slot hash");
|
||||
msg!("Slot hash: {:?}", x);
|
||||
proof.hash = hashv(&[hash.as_ref(), slot_hash_bytes]);
|
||||
|
||||
// TODO Give an admin authority can update the difficulty, can this check be removed?
|
||||
// As long as difficulty is high enough, this should never happen. Even if this does
|
||||
// happen, with the check removed, the most that can be distributed in an epoch is what's
|
||||
// in the busses.
|
||||
//
|
||||
// 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!(
|
||||
bus.hashes
|
||||
.le(&EXPECTED_EPOCH_REWARDS.saturating_div(BUS_COUNT as u64)),
|
||||
ProgramError::BusQuotaFilled
|
||||
);
|
||||
|
||||
// Distribute tokens from bus to beneficiary.
|
||||
let bus_tokens = &ctx.accounts.bus_tokens;
|
||||
require!(
|
||||
@@ -282,6 +263,30 @@ mod ore {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the admin to a new value. Can only be invoked by the admin authority.
|
||||
pub fn update_admin(ctx: Context<UpdateDifficulty>, new_admin: Pubkey) -> Result<()> {
|
||||
ctx.accounts.metadata.admin = new_admin;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the difficulty to a new value. Can only be invoked by the admin authority.
|
||||
///
|
||||
/// Ore subdivides into 1B units of indivisible nanoORE. If global hashpower increases to the
|
||||
/// point where >1B valid hashes are being submitted per epoch, the Ore inflation rate could
|
||||
/// be pushed steadily above 1 ORE/epoch. The protocol guarantees inflation can never exceed
|
||||
/// 2 ORE/epoch, but it is the responsibility of the admin to adjust the mining difficulty
|
||||
/// as needed to maintain the 1 ORE/epoch average.
|
||||
///
|
||||
/// It is worth noting that Solana today processes well below 1M real TPS or
|
||||
/// (60 * 1,000,000) = 60,000,000 hashes per epoch. Even if every transaction on the network
|
||||
/// were mine operation, this is still two orders of magnitude below the threshold where the
|
||||
/// Ore inflation rate would be challenged. So in practice, Solana is likely to reach its
|
||||
/// network saturation point long before the Ore inflation hits its boundary condition.
|
||||
pub fn update_difficulty(ctx: Context<UpdateDifficulty>, new_difficulty: Hash) -> Result<()> {
|
||||
ctx.accounts.metadata.difficulty = new_difficulty;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_hash(
|
||||
@@ -313,7 +318,7 @@ fn calculate_new_reward_rate(current_rate: u64, epoch_rewards: u64) -> u64 {
|
||||
|
||||
// Calculate new reward rate.
|
||||
let new_rate = (current_rate as u128)
|
||||
.saturating_mul(EXPECTED_EPOCH_REWARDS as u128)
|
||||
.saturating_mul(TARGET_EPOCH_REWARDS as u128)
|
||||
.saturating_div(epoch_rewards as u128) as u64;
|
||||
|
||||
// Smooth reward rate to not change by more than a constant factor from one epoch to the next.
|
||||
@@ -336,8 +341,8 @@ fn reset_bus<'info>(
|
||||
bus.hashes = 0;
|
||||
bus.rewards = 0;
|
||||
|
||||
// Top up bus account with 1 ORE.
|
||||
let amount = ONE_ORE.saturating_sub(bus_tokens.amount);
|
||||
// Top up bus account.
|
||||
let amount = BUS_BALANCE.saturating_sub(bus_tokens.amount);
|
||||
if amount.gt(&0) {
|
||||
token::mint_to(
|
||||
CpiContext::new_with_signer(
|
||||
@@ -516,7 +521,7 @@ pub struct InitializeBusTokens<'info> {
|
||||
pub mint: Account<'info, Mint>,
|
||||
|
||||
/// The bus account.
|
||||
#[account()]
|
||||
#[account(constraint = bus.id.lt(&BUS_COUNT) @ ProgramError::BusInvalid)]
|
||||
pub bus: Account<'info, Bus>,
|
||||
|
||||
/// The bus token account.
|
||||
@@ -556,18 +561,6 @@ pub struct Register<'info> {
|
||||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(new_difficulty: Hash)]
|
||||
pub struct UpdateDifficulty<'info> {
|
||||
/// The signer of the transaction (i.e. the miner).
|
||||
#[account(mut)]
|
||||
pub signer: Signer<'info>,
|
||||
|
||||
/// The metadata account.
|
||||
#[account(seeds = [METADATA], bump = metadata.bump, constraint = metadata.admin.eq(&signer.key()) @ ProgramError::NotAuthorized)]
|
||||
pub metadata: Account<'info, Metadata>,
|
||||
}
|
||||
|
||||
// ResetEpoch adjusts the reward rate based on global hashpower and begins the new epoch.
|
||||
#[derive(Accounts)]
|
||||
pub struct ResetEpoch<'info> {
|
||||
@@ -656,6 +649,7 @@ pub struct ResetEpoch<'info> {
|
||||
pub associated_token_program: Program<'info, associated_token::AssociatedToken>,
|
||||
}
|
||||
|
||||
/// Mine distributes Ore to the beneficiary if the signer provides a valid hash.
|
||||
#[derive(Accounts)]
|
||||
#[instruction(hash: Hash, nonce: u64)]
|
||||
pub struct Mine<'info> {
|
||||
@@ -668,7 +662,7 @@ pub struct Mine<'info> {
|
||||
pub beneficiary: Account<'info, TokenAccount>,
|
||||
|
||||
/// A bus account.
|
||||
#[account(mut)]
|
||||
#[account(mut, constraint = bus.id.lt(&BUS_COUNT) @ ProgramError::BusInvalid)]
|
||||
pub bus: Account<'info, Bus>,
|
||||
|
||||
/// The bus' token account.
|
||||
@@ -697,6 +691,32 @@ pub struct Mine<'info> {
|
||||
pub slot_hashes: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
/// UpdateAdmin allows the admin to reassign the admin authority.
|
||||
#[derive(Accounts)]
|
||||
#[instruction(new_admin: Pubkey)]
|
||||
pub struct UpdateAdmin<'info> {
|
||||
/// The signer of the transaction (i.e. the admin).
|
||||
#[account(mut)]
|
||||
pub signer: Signer<'info>,
|
||||
|
||||
/// The metadata account.
|
||||
#[account(seeds = [METADATA], bump = metadata.bump, constraint = metadata.admin.eq(&signer.key()) @ ProgramError::NotAuthorized)]
|
||||
pub metadata: Account<'info, Metadata>,
|
||||
}
|
||||
|
||||
/// UpdateDifficulty allows the admin to update the mining difficulty.
|
||||
#[derive(Accounts)]
|
||||
#[instruction(new_difficulty: Hash)]
|
||||
pub struct UpdateDifficulty<'info> {
|
||||
/// The signer of the transaction (i.e. the admin).
|
||||
#[account(mut)]
|
||||
pub signer: Signer<'info>,
|
||||
|
||||
/// The metadata account.
|
||||
#[account(seeds = [METADATA], bump = metadata.bump, constraint = metadata.admin.eq(&signer.key()) @ ProgramError::NotAuthorized)]
|
||||
pub metadata: Account<'info, Metadata>,
|
||||
}
|
||||
|
||||
/// MineEvent logs revelant data about a successful Ore mining transaction.
|
||||
#[event]
|
||||
#[derive(Debug)]
|
||||
@@ -750,9 +770,7 @@ mod tests {
|
||||
solana_program::hash::{hashv, Hash},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
calculate_new_reward_rate, validate_hash, EXPECTED_EPOCH_REWARDS, SMOOTHING_FACTOR,
|
||||
};
|
||||
use crate::{calculate_new_reward_rate, validate_hash, SMOOTHING_FACTOR, TARGET_EPOCH_REWARDS};
|
||||
|
||||
#[test]
|
||||
fn test_validate_hash_pass() {
|
||||
@@ -798,7 +816,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_calculate_new_reward_rate_stable() {
|
||||
let current_rate = 1000;
|
||||
let new_rate = calculate_new_reward_rate(current_rate, EXPECTED_EPOCH_REWARDS);
|
||||
let new_rate = calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS);
|
||||
assert!(new_rate.eq(¤t_rate));
|
||||
}
|
||||
|
||||
@@ -812,20 +830,16 @@ mod tests {
|
||||
#[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),
|
||||
);
|
||||
let new_rate =
|
||||
calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS.saturating_add(1_000_000));
|
||||
assert!(new_rate.lt(¤t_rate));
|
||||
}
|
||||
|
||||
#[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),
|
||||
);
|
||||
let new_rate =
|
||||
calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS.saturating_sub(1_000_000));
|
||||
assert!(new_rate.gt(¤t_rate));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user