Files
salvium-rs/src/consensus.js
T
Matt Hess aa716113f5 Add STAKE transaction creation and fix CLSAG verification
STAKE transactions:
  - Add buildStakeTransaction() for creating stake transactions
  - Add createStakeTransaction() wallet API for staking SAL/SAL1
  - Extend serializeTxPrefix() with Salvium-specific fields (txType,
    amount_burnt, asset types, return address, protocol_tx_data)
  - Support STAKE_LOCK_PERIOD from network config (21600 mainnet, 20 testnet)

  CLSAG fixes:
  - Fix challenge hash to include full ring data matching C++ implementation
  - Update both signing and verification to use consistent hash format
2026-01-22 03:10:40 +00:00

812 lines
25 KiB
JavaScript

/**
* Salvium Consensus Rules and Constants
*
* This module contains all consensus-critical constants and validation functions
* needed to implement a full validating node.
*
* Reference: salvium/src/cryptonote_config.h, cryptonote_basic_impl.cpp, difficulty.cpp
*/
// =============================================================================
// CORE CONSTANTS
// =============================================================================
// Money supply and emission
export const MONEY_SUPPLY = 18440000000000000n; // 184.4M coins * 10^8
export const EMISSION_SPEED_FACTOR_PER_MINUTE = 21;
export const FINAL_SUBSIDY_PER_MINUTE = 30000000n; // 3 * 10^7 (tail emission)
export const COIN = 100000000n; // 10^8 atomic units per coin
export const CRYPTONOTE_DISPLAY_DECIMAL_POINT = 8;
// Premine
export const PREMINE_AMOUNT = 2210000000000000n; // 12% of MONEY_SUPPLY
export const PREMINE_AMOUNT_UPFRONT = 650000000000000n; // 3.4% of MONEY_SUPPLY
export const PREMINE_AMOUNT_MONTHLY = 65000000000000n; // 8.6%/24 of MONEY_SUPPLY
// Treasury SAL1 minting
export const TREASURY_SAL1_MINT_AMOUNT = 130000000000000n; // 1.3M
export const TREASURY_SAL1_MINT_COUNT = 8;
// Block timing
export const DIFFICULTY_TARGET_V1 = 60; // seconds (before first fork)
export const DIFFICULTY_TARGET_V2 = 120; // seconds (current)
export const CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT = 60 * 60 * 2; // 2 hours
export const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW = 60;
// Difficulty adjustment
export const DIFFICULTY_WINDOW = 720;
export const DIFFICULTY_WINDOW_V2 = 70;
export const DIFFICULTY_LAG = 15;
export const DIFFICULTY_CUT = 60;
export const DIFFICULTY_BLOCKS_COUNT = DIFFICULTY_WINDOW + DIFFICULTY_LAG;
export const DIFFICULTY_BLOCKS_COUNT_V2 = DIFFICULTY_WINDOW_V2;
// Block size/weight
export const CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1 = 20000;
export const CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 = 60000;
export const CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 = 300000;
export const CRYPTONOTE_LONG_TERM_BLOCK_WEIGHT_WINDOW_SIZE = 100000;
export const CRYPTONOTE_SHORT_TERM_BLOCK_WEIGHT_SURGE_FACTOR = 50;
// Transaction limits
export const CRYPTONOTE_MAX_TX_SIZE = 1000000;
export const CRYPTONOTE_MAX_TX_PER_BLOCK = 0x10000000;
export const MAX_TX_EXTRA_SIZE = 1060;
export const BULLETPROOF_MAX_OUTPUTS = 16;
export const BULLETPROOF_PLUS_MAX_OUTPUTS = 16;
// Maturity and unlock
export const CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW = 60;
export const CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE = 10;
export const CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS = 1;
// Transaction versions
export const CURRENT_TRANSACTION_VERSION = 4;
export const TRANSACTION_VERSION_2_OUTS = 2;
export const TRANSACTION_VERSION_N_OUTS = 3;
export const TRANSACTION_VERSION_CARROT = 4;
// Fees
export const FEE_PER_KB = 200000n; // 2 * 10^5
export const FEE_PER_BYTE = 30n;
export const DYNAMIC_FEE_PER_KB_BASE_FEE = 200000n;
export const DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD = 1000000000n; // 10 * 10^8
export const DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT = 3000n;
export const PER_KB_FEE_QUANTIZATION_DECIMALS = 8;
export const DEFAULT_DUST_THRESHOLD = 2000000000n; // 2 * 10^9
export const BASE_REWARD_CLAMP_THRESHOLD = 100000000n; // 10^8
// Mempool
export const CRYPTONOTE_MEMPOOL_TX_LIVETIME = 86400 * 3; // 3 days
export const CRYPTONOTE_MEMPOOL_TX_FROM_ALT_BLOCK_LIVETIME = 604800; // 1 week
export const DEFAULT_TXPOOL_MAX_WEIGHT = 648000000n; // 3 days at 300000
// Ring size (Salvium uses 16)
export const DEFAULT_RING_SIZE = 16;
// Pricing record
export const PRICING_RECORD_VALID_BLOCKS = 10;
export const PRICING_RECORD_VALID_TIME_DIFF_FROM_BLOCK = 120;
// Lock periods
export const BURN_LOCK_PERIOD = 0;
export const CONVERT_LOCK_PERIOD = 0;
// =============================================================================
// HARD FORK VERSIONS
// =============================================================================
export const HF_VERSION = {
// Version 1 features
DYNAMIC_FEE: 1,
PER_BYTE_FEE: 1,
ENFORCE_MIN_AGE: 1,
EXACT_COINBASE: 1,
CLSAG: 1,
DETERMINISTIC_UNLOCK_TIME: 1,
SMALLER_BP: 1,
MIN_V2_COINBASE_TX: 1,
REJECT_SIGS_IN_COINBASE: 1,
BULLETPROOF_PLUS: 1,
ENABLE_RETURN: 1,
VIEW_TAGS: 1,
// Version 2 features
LONG_TERM_BLOCK_WEIGHT: 2,
SCALING_2021: 2,
ENABLE_N_OUTS: 2,
// Version 3+
FULL_PROOFS: 3,
ENFORCE_FULL_PROOFS: 4,
SHUTDOWN_USER_TXS: 5,
AUDIT1: 6,
SALVIUM_ONE_PROOFS: 6,
AUDIT1_PAUSE: 7,
AUDIT2: 8,
AUDIT2_PAUSE: 9,
CARROT: 10,
// Future (v255 placeholder)
REQUIRE_VIEW_TAGS: 255,
ENABLE_CONVERT: 255,
ENABLE_ORACLE: 255,
SLIPPAGE_YIELD: 255,
};
// =============================================================================
// NETWORK CONFIGURATIONS
// =============================================================================
export const NETWORK_ID = {
MAINNET: 0,
TESTNET: 1,
STAGENET: 2,
FAKECHAIN: 3,
};
export const MAINNET_CONFIG = {
ADDRESS_PREFIX: 0x3ef318n, // SaLv
INTEGRATED_ADDRESS_PREFIX: 0x55ef318n, // SaLvi
SUBADDRESS_PREFIX: 0xf5ef318n, // SaLvs
CARROT_ADDRESS_PREFIX: 0x180c96n, // SC1
CARROT_INTEGRATED_PREFIX: 0x2ccc96n, // SC1i
CARROT_SUBADDRESS_PREFIX: 0x314c96n, // SC1s
P2P_PORT: 19080,
RPC_PORT: 19081,
ZMQ_PORT: 19082,
GENESIS_NONCE: 10000,
GENESIS_TX: '020001ff000180c0d0c7bbbff603031c7d3e2240c8ddbc2966c9dcbf703c3aa99624d34b82fbfebd71dcfa001c59800353414c3cb42101d7be8f8312cdd54e1ae390e86d6733c3d8f1ef7be27f75f5acbf0dc57aa8e60d010000',
STAKE_LOCK_PERIOD: 30 * 24 * 30, // blocks
TREASURY_SAL1_MINT_PERIOD: 30 * 24 * 30,
TREASURY_ADDRESS: 'SaLvdZR6w1A21sf2Wh6jYEh1wzY4GSbT7RX6FjyPsnLsffWLrzFQeXUXJcmBLRWDzZC2YXeYe5t7qKsnrg9FpmxmEcxPHsEYfqA',
};
export const TESTNET_CONFIG = {
ADDRESS_PREFIX: 0x15beb318n, // SaLvT
INTEGRATED_ADDRESS_PREFIX: 0xd055eb318n, // SaLvTi
SUBADDRESS_PREFIX: 0xa59eb318n, // SaLvTs
CARROT_ADDRESS_PREFIX: 0x254c96n, // SC1T
CARROT_INTEGRATED_PREFIX: 0x1ac50c96n, // SC1Ti
CARROT_SUBADDRESS_PREFIX: 0x3c54c96n, // SC1Ts
P2P_PORT: 29080,
RPC_PORT: 29081,
ZMQ_PORT: 29082,
GENESIS_NONCE: 10001,
GENESIS_TX: '020001ff000180c0d0c7bbbff60302838f76f69b70bb0d0f1961a12f6082a033d22285c07d4f12ec93c28197ae2a600353414c3c2101009e8b0abce686c417a1b1344eb7337176bdca90cc928b0facec8a9516190645010000',
STAKE_LOCK_PERIOD: 20,
TREASURY_SAL1_MINT_PERIOD: 20,
TREASURY_ADDRESS: 'SaLvTyLFta9BiAXeUfFkKvViBkFt4ay5nEUBpWyDKewYggtsoxBbtCUVqaBjtcCDyY1euun8Giv7LLEgvztuurLo5a6Km1zskZn36',
};
export const STAGENET_CONFIG = {
ADDRESS_PREFIX: 0x149eb318n, // SaLvS
INTEGRATED_ADDRESS_PREFIX: 0xf343eb318n, // SaLvSi
SUBADDRESS_PREFIX: 0x2d47eb318n, // SaLvSs
CARROT_ADDRESS_PREFIX: 0x24cc96n, // SC1S
CARROT_INTEGRATED_PREFIX: 0x1a848c96n, // SC1Si
CARROT_SUBADDRESS_PREFIX: 0x384cc96n, // SC1Ss
P2P_PORT: 39080,
RPC_PORT: 39081,
ZMQ_PORT: 39082,
GENESIS_NONCE: 10002,
GENESIS_TX: '013c01ff0001ffffffffffff0302df5d56da0c7d643ddd1ce61901c7bdc5fb1738bfe39fbe69c28a3a7032729c0f2101168d0c4ca86fb55a4cf6a36d31431be1c53a3bd7411bb24e8832410289fa6f3b',
STAKE_LOCK_PERIOD: 20,
TREASURY_SAL1_MINT_PERIOD: 20,
TREASURY_ADDRESS: 'fuLMowH85abK8nz9BBMEem7MAfUbQu4aSHHUV9j5Z86o6Go9Lv2U5ZQiJCWPY9R9HA8p5idburazjAhCqDngLo7fYPCD9ciM9ee1A',
};
/**
* Get network configuration
* @param {number} network - Network type (MAINNET, TESTNET, STAGENET)
* @returns {Object} Network configuration
*/
export function getNetworkConfig(network) {
switch (network) {
case NETWORK_ID.MAINNET:
case NETWORK_ID.FAKECHAIN:
return MAINNET_CONFIG;
case NETWORK_ID.TESTNET:
return TESTNET_CONFIG;
case NETWORK_ID.STAGENET:
return STAGENET_CONFIG;
default:
throw new Error(`Invalid network type: ${network}`);
}
}
// =============================================================================
// BLOCK REWARD CALCULATION
// =============================================================================
/**
* Get minimum block weight for full reward
* @param {number} version - Hard fork version
* @returns {number} Minimum block weight in bytes
*/
export function getMinBlockWeight(version) {
if (version < 2) {
return CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1;
}
return CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5;
}
/**
* Calculate block reward
*
* Formula: base_reward = (MONEY_SUPPLY - already_generated) >> emission_speed_factor
* With penalty for blocks larger than median weight
*
* @param {number} medianWeight - Median block weight
* @param {number} currentBlockWeight - Current block weight
* @param {bigint} alreadyGeneratedCoins - Total coins generated so far
* @param {number} version - Hard fork version
* @returns {{ success: boolean, reward: bigint }} Block reward result
*/
export function getBlockReward(medianWeight, currentBlockWeight, alreadyGeneratedCoins, version = 1) {
const target = DIFFICULTY_TARGET_V2;
const targetMinutes = target / 60;
const emissionSpeedFactor = EMISSION_SPEED_FACTOR_PER_MINUTE - (targetMinutes - 1);
// Calculate base reward
let baseReward = (MONEY_SUPPLY - alreadyGeneratedCoins) >> BigInt(emissionSpeedFactor);
// Apply tail emission (minimum subsidy)
const minSubsidy = FINAL_SUBSIDY_PER_MINUTE * BigInt(targetMinutes);
if (baseReward < minSubsidy) {
baseReward = minSubsidy;
}
// Genesis block (premine)
if (alreadyGeneratedCoins === 0n) {
return { success: true, reward: PREMINE_AMOUNT };
}
// Get full reward zone
let fullRewardZone = BigInt(getMinBlockWeight(version));
if (BigInt(medianWeight) < fullRewardZone) {
medianWeight = Number(fullRewardZone);
}
// No penalty if block is small
if (currentBlockWeight <= medianWeight) {
return { success: true, reward: baseReward };
}
// Block too large
if (currentBlockWeight > 2 * medianWeight) {
return { success: false, reward: 0n };
}
// Calculate penalty: reward * (2*M - W) * W / M^2
// Where M = median weight, W = current weight
const multiplicand = BigInt(2 * medianWeight - currentBlockWeight) * BigInt(currentBlockWeight);
const reward = (baseReward * multiplicand) / BigInt(medianWeight) / BigInt(medianWeight);
return { success: true, reward };
}
/**
* Calculate total emission at a given height (approximate)
* @param {number} height - Block height
* @returns {bigint} Approximate total emission
*/
export function getApproximateEmission(height) {
if (height === 0) return PREMINE_AMOUNT;
// Simplified calculation - actual emission is cumulative
// This is an approximation; real emission requires summing all block rewards
const target = DIFFICULTY_TARGET_V2;
const targetMinutes = target / 60;
const emissionSpeedFactor = EMISSION_SPEED_FACTOR_PER_MINUTE - (targetMinutes - 1);
// Geometric series approximation
// Sum ≈ MONEY_SUPPLY * (1 - 0.5^(height/halvingPeriod))
// Where halvingPeriod = 2^emissionSpeedFactor blocks
let emission = PREMINE_AMOUNT;
let remaining = MONEY_SUPPLY - PREMINE_AMOUNT;
for (let h = 1; h <= height && remaining > 0n; h++) {
const reward = remaining >> BigInt(emissionSpeedFactor);
const minReward = FINAL_SUBSIDY_PER_MINUTE * BigInt(targetMinutes);
const actualReward = reward < minReward ? minReward : reward;
emission += actualReward;
remaining -= actualReward;
}
return emission;
}
// =============================================================================
// DIFFICULTY CALCULATION
// =============================================================================
/**
* Calculate next difficulty using original algorithm
*
* @param {number[]} timestamps - Block timestamps (newest first)
* @param {bigint[]} cumulativeDifficulties - Cumulative difficulties
* @param {number} targetSeconds - Target block time
* @returns {bigint} Next difficulty
*/
export function nextDifficulty(timestamps, cumulativeDifficulties, targetSeconds = DIFFICULTY_TARGET_V2) {
// Trim to window size
if (timestamps.length > DIFFICULTY_WINDOW) {
timestamps = timestamps.slice(0, DIFFICULTY_WINDOW);
cumulativeDifficulties = cumulativeDifficulties.slice(0, DIFFICULTY_WINDOW);
}
const length = timestamps.length;
if (length !== cumulativeDifficulties.length) {
throw new Error('Timestamps and difficulties must have same length');
}
if (length <= 1) {
return 1n;
}
// Sort timestamps
timestamps = [...timestamps].sort((a, b) => a - b);
// Calculate cut points
let cutBegin, cutEnd;
if (length <= DIFFICULTY_WINDOW - 2 * DIFFICULTY_CUT) {
cutBegin = 0;
cutEnd = length;
} else {
cutBegin = Math.floor((length - (DIFFICULTY_WINDOW - 2 * DIFFICULTY_CUT) + 1) / 2);
cutEnd = cutBegin + (DIFFICULTY_WINDOW - 2 * DIFFICULTY_CUT);
}
// Calculate time span and work
let timeSpan = BigInt(timestamps[cutEnd - 1] - timestamps[cutBegin]);
if (timeSpan === 0n) timeSpan = 1n;
const totalWork = cumulativeDifficulties[cutEnd - 1] - cumulativeDifficulties[cutBegin];
// difficulty = work * target / timeSpan
return (totalWork * BigInt(targetSeconds) + timeSpan - 1n) / timeSpan;
}
/**
* Calculate next difficulty using LWMA (Linearly Weighted Moving Average) v2
*
* LWMA algorithm by Zawy
* https://github.com/zawy12/difficulty-algorithms/issues/3
*
* @param {number[]} timestamps - Block timestamps (oldest first, length N+1)
* @param {bigint[]} cumulativeDifficulties - Cumulative difficulties (length N+1)
* @param {number} targetSeconds - Target block time
* @returns {bigint} Next difficulty
*/
export function nextDifficultyV2(timestamps, cumulativeDifficulties, targetSeconds = DIFFICULTY_TARGET_V2) {
const T = targetSeconds;
let N = DIFFICULTY_WINDOW_V2;
// Trim to window
if (timestamps.length > N + 1) {
timestamps = timestamps.slice(0, N + 1);
cumulativeDifficulties = cumulativeDifficulties.slice(0, N + 1);
}
const n = timestamps.length;
if (n !== cumulativeDifficulties.length) {
throw new Error('Timestamps and difficulties must have same length');
}
// First 5 blocks: return difficulty 1
if (n < 6) return 1n;
// If height < N+1, adjust N
if (n < N + 1) N = n - 1;
// Adjustment factor for average solvetime accuracy
const adjust = 0.998;
// Normalization divisor
const k = N * (N + 1) / 2;
let LWMA = 0;
let sumInverseD = 0;
// Loop through N most recent blocks
for (let i = 1; i <= N; i++) {
let solveTime = Number(timestamps[i]) - Number(timestamps[i - 1]);
// Clamp solve time to [-7T, 7T]
solveTime = Math.min(T * 7, Math.max(solveTime, -7 * T));
const difficulty = Number(cumulativeDifficulties[i] - cumulativeDifficulties[i - 1]);
LWMA += (solveTime * i) / k;
sumInverseD += 1 / difficulty;
}
// Sanity check
if (LWMA < T / 20) LWMA = T / 20;
// Calculate harmonic mean of difficulties
const harmonicMeanD = N / sumInverseD * adjust;
// Next difficulty
const nextDiff = harmonicMeanD * T / LWMA;
return BigInt(Math.floor(nextDiff));
}
/**
* Check if a hash meets difficulty target
*
* hash * difficulty <= 2^256
*
* @param {Uint8Array} hash - 32-byte hash
* @param {bigint} difficulty - Difficulty target
* @returns {boolean} True if hash meets difficulty
*/
export function checkHash(hash, difficulty) {
if (hash.length !== 32) {
throw new Error('Hash must be 32 bytes');
}
// Convert hash to big-endian bigint
let hashVal = 0n;
for (let i = 0; i < 32; i++) {
hashVal = (hashVal << 8n) | BigInt(hash[i]);
}
// Check: hash * difficulty <= 2^256
const max256 = (1n << 256n) - 1n;
return hashVal * difficulty <= max256;
}
// =============================================================================
// TIMESTAMP VALIDATION
// =============================================================================
/**
* Get median timestamp from recent blocks
* @param {number[]} timestamps - Recent block timestamps (most recent first)
* @returns {number} Median timestamp
*/
export function getMedianTimestamp(timestamps) {
if (timestamps.length === 0) return 0;
const sorted = [...timestamps].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
return Math.floor((sorted[mid - 1] + sorted[mid]) / 2);
}
return sorted[mid];
}
/**
* Validate block timestamp
*
* Timestamp must be:
* 1. Greater than median of last BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW blocks
* 2. Not more than CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT in the future
*
* @param {number} timestamp - Block timestamp
* @param {number[]} recentTimestamps - Recent block timestamps
* @param {number} currentTime - Current Unix timestamp
* @returns {{ valid: boolean, error?: string }}
*/
export function validateBlockTimestamp(timestamp, recentTimestamps, currentTime) {
// Check future limit
if (timestamp > currentTime + CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT) {
return {
valid: false,
error: `Timestamp too far in future: ${timestamp} > ${currentTime + CRYPTONOTE_BLOCK_FUTURE_TIME_LIMIT}`
};
}
// Check median time rule
if (recentTimestamps.length >= BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW) {
const medianTime = getMedianTimestamp(recentTimestamps.slice(0, BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW));
if (timestamp <= medianTime) {
return {
valid: false,
error: `Timestamp not greater than median: ${timestamp} <= ${medianTime}`
};
}
}
return { valid: true };
}
// =============================================================================
// UNLOCK TIME VALIDATION
// =============================================================================
/**
* Check if an output is unlocked (spendable)
*
* @param {bigint} unlockTime - Unlock time (0 = no lock, <500M = block height, >=500M = unix timestamp)
* @param {number} currentHeight - Current blockchain height
* @param {number} currentTime - Current Unix timestamp
* @param {number} version - Hard fork version
* @returns {boolean} True if unlocked
*/
export function isOutputUnlocked(unlockTime, currentHeight, currentTime, version = 1) {
// No lock
if (unlockTime === 0n) return true;
const unlockTimeNum = Number(unlockTime);
// Threshold: 500,000,000 - below = height, above = timestamp
const UNLOCK_TIME_THRESHOLD = 500000000;
if (unlockTimeNum < UNLOCK_TIME_THRESHOLD) {
// Block height based unlock
const allowedDelta = version >= 2
? CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS
: CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS;
return currentHeight + allowedDelta >= unlockTimeNum;
} else {
// Timestamp based unlock
const allowedDelta = version >= 2
? DIFFICULTY_TARGET_V2 * CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS
: DIFFICULTY_TARGET_V1 * CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS;
return currentTime + allowedDelta >= unlockTimeNum;
}
}
/**
* Check if coinbase output is mature (spendable)
*
* @param {number} outputHeight - Height where output was created
* @param {number} currentHeight - Current blockchain height
* @returns {boolean} True if mature
*/
export function isCoinbaseMature(outputHeight, currentHeight) {
return currentHeight >= outputHeight + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW;
}
/**
* Check if output meets minimum age requirement
*
* @param {number} outputHeight - Height where output was created
* @param {number} currentHeight - Current blockchain height
* @returns {boolean} True if old enough
*/
export function meetsMinimumAge(outputHeight, currentHeight) {
return currentHeight >= outputHeight + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE;
}
// =============================================================================
// FEE VALIDATION
// =============================================================================
/**
* Calculate minimum required fee
*
* @param {number} txWeight - Transaction weight in bytes
* @param {bigint} baseReward - Current block reward
* @param {number} version - Hard fork version
* @returns {bigint} Minimum fee
*/
export function getMinimumFee(txWeight, baseReward, version = 1) {
if (version >= HF_VERSION.PER_BYTE_FEE) {
// Per-byte fee
return BigInt(txWeight) * FEE_PER_BYTE;
}
// Legacy per-KB fee
const kbSize = BigInt(Math.ceil(txWeight / 1024));
return kbSize * FEE_PER_KB;
}
/**
* Calculate dynamic fee based on block reward
*
* @param {bigint} baseReward - Current block reward
* @param {number} txWeight - Transaction weight
* @param {number} version - Hard fork version
* @returns {bigint} Dynamic fee
*/
export function getDynamicFee(baseReward, txWeight, version = 1) {
const fee = DYNAMIC_FEE_PER_KB_BASE_FEE * BigInt(txWeight) / 1024n;
// Scale by reward ratio
if (baseReward > 0n) {
const scaledFee = fee * DYNAMIC_FEE_PER_KB_BASE_BLOCK_REWARD / baseReward;
return scaledFee > fee ? scaledFee : fee;
}
return fee;
}
/**
* Quantize fee (round to reduce fingerprinting)
*
* @param {bigint} fee - Raw fee
* @returns {bigint} Quantized fee
*/
export function quantizeFee(fee) {
const mask = (10n ** BigInt(PER_KB_FEE_QUANTIZATION_DECIMALS)) - 1n;
return ((fee + mask) / (mask + 1n)) * (mask + 1n);
}
// =============================================================================
// BLOCK VALIDATION
// =============================================================================
/**
* Validate block header chain linkage
*
* @param {Object} currentHeader - Current block header
* @param {Object} previousHeader - Previous block header
* @returns {{ valid: boolean, error?: string }}
*/
export function validateBlockLinkage(currentHeader, previousHeader) {
// Check previous hash matches
const prevHashHex = typeof currentHeader.prevId === 'string'
? currentHeader.prevId
: Buffer.from(currentHeader.prevId).toString('hex');
const expectedPrevHash = typeof previousHeader.hash === 'string'
? previousHeader.hash
: Buffer.from(previousHeader.hash).toString('hex');
if (prevHashHex !== expectedPrevHash) {
return {
valid: false,
error: `Previous hash mismatch: ${prevHashHex} != ${expectedPrevHash}`
};
}
// Check height is sequential
if (currentHeader.height !== previousHeader.height + 1) {
return {
valid: false,
error: `Height not sequential: ${currentHeader.height} != ${previousHeader.height + 1}`
};
}
return { valid: true };
}
/**
* Validate block size/weight
*
* @param {number} blockWeight - Block weight in bytes
* @param {number} medianWeight - Median block weight
* @param {number} version - Hard fork version
* @returns {{ valid: boolean, error?: string }}
*/
export function validateBlockWeight(blockWeight, medianWeight, version = 1) {
const maxWeight = 2 * Math.max(medianWeight, getMinBlockWeight(version));
if (blockWeight > maxWeight) {
return {
valid: false,
error: `Block weight ${blockWeight} exceeds max ${maxWeight}`
};
}
return { valid: true };
}
// =============================================================================
// TRANSACTION VALIDATION
// =============================================================================
/**
* Validate transaction size
*
* @param {number} txSize - Transaction size in bytes
* @returns {{ valid: boolean, error?: string }}
*/
export function validateTxSize(txSize) {
if (txSize > CRYPTONOTE_MAX_TX_SIZE) {
return {
valid: false,
error: `Transaction size ${txSize} exceeds max ${CRYPTONOTE_MAX_TX_SIZE}`
};
}
return { valid: true };
}
/**
* Validate transaction extra field size
*
* @param {number} extraSize - Extra field size in bytes
* @returns {{ valid: boolean, error?: string }}
*/
export function validateTxExtraSize(extraSize) {
if (extraSize > MAX_TX_EXTRA_SIZE) {
return {
valid: false,
error: `Transaction extra size ${extraSize} exceeds max ${MAX_TX_EXTRA_SIZE}`
};
}
return { valid: true };
}
/**
* Validate transaction output count
*
* @param {number} outputCount - Number of outputs
* @param {number} version - Hard fork version
* @returns {{ valid: boolean, error?: string }}
*/
export function validateOutputCount(outputCount, version = 1) {
if (outputCount === 0) {
return { valid: false, error: 'Transaction must have at least one output' };
}
if (outputCount > BULLETPROOF_PLUS_MAX_OUTPUTS) {
return {
valid: false,
error: `Output count ${outputCount} exceeds max ${BULLETPROOF_PLUS_MAX_OUTPUTS}`
};
}
return { valid: true };
}
/**
* Validate ring size
*
* @param {number} ringSize - Ring size (number of decoys + 1)
* @param {number} version - Hard fork version
* @returns {{ valid: boolean, error?: string }}
*/
export function validateRingSize(ringSize, version = 1) {
if (ringSize < 1) {
return { valid: false, error: 'Ring size must be at least 1' };
}
// Salvium requires ring size of 16
if (ringSize !== DEFAULT_RING_SIZE) {
return {
valid: false,
error: `Ring size must be ${DEFAULT_RING_SIZE}, got ${ringSize}`
};
}
return { valid: true };
}
// =============================================================================
// EXPORTS
// =============================================================================
export default {
// Constants
MONEY_SUPPLY,
EMISSION_SPEED_FACTOR_PER_MINUTE,
FINAL_SUBSIDY_PER_MINUTE,
COIN,
PREMINE_AMOUNT,
DIFFICULTY_TARGET_V1,
DIFFICULTY_TARGET_V2,
DIFFICULTY_WINDOW,
DIFFICULTY_WINDOW_V2,
CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW,
CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE,
CRYPTONOTE_MAX_TX_SIZE,
DEFAULT_RING_SIZE,
FEE_PER_KB,
FEE_PER_BYTE,
HF_VERSION,
NETWORK_ID,
// Functions
getNetworkConfig,
getBlockReward,
nextDifficulty,
nextDifficultyV2,
checkHash,
getMedianTimestamp,
validateBlockTimestamp,
isOutputUnlocked,
isCoinbaseMature,
meetsMinimumAge,
getMinimumFee,
getDynamicFee,
validateBlockLinkage,
validateBlockWeight,
validateTxSize,
validateRingSize,
};