diff --git a/src/contracts/Random.h b/src/contracts/Random.h index e62340d70..8ed3967a3 100644 --- a/src/contracts/Random.h +++ b/src/contracts/Random.h @@ -1,12 +1,112 @@ using namespace QPI; -struct RANDOM2 +// Random contract: collects entropy reveals (commit-reveal), maintains an entropy pool, +// lets buyers purchase bytes of entropy, and pays miners/shareholders. + +// Key sizes and limits: +constexpr uint32_t RANDOM_MAX_RECENT_MINERS = 512; // 2^9 +constexpr uint32_t RANDOM_MAX_COMMITMENTS = 1024; // 2^10 +constexpr uint32_t RANDOM_ENTROPY_HISTORY_LEN = 4; // 2^2, even if 3 would suffice +constexpr uint32_t RANDOM_VALID_DEPOSIT_AMOUNTS = 16; +constexpr uint32_t RANDOM_MAX_USER_COMMITMENTS = 32; +constexpr uint32_t RANDOM_RANDOMBYTES_LEN = 32; + +struct RANDOM2 {}; + +// Recent miner info (LRU-ish tracking used to reward miners) +struct RANDOM_RecentMiner { + id minerId; + uint64 deposit; + uint64 lastEntropyVersion; + uint32 lastRevealTick; }; +// Stored commitment created by miners (commit-reveal scheme) +struct RANDOM_EntropyCommitment +{ + id digest; // K12(revealedBits) stored at commit time + id invocatorId; // who committed + uint64 amount; // security deposit + uint32 commitTick; + uint32 revealDeadlineTick; + bool hasRevealed; +}; + +// Contract state and logic struct RANDOM : public ContractBase { +private: + // --- QPI contract state --- + + // Circular history of recent entropy pools (m256i) + Array entropyHistory; + Array entropyPoolVersionHistory; + uint32 entropyHistoryHead; + + // current 256-bit entropy pool and its version + m256i currentEntropyPool; + uint64 entropyPoolVersion; + + // Metrics and bookkeeping + uint64 totalCommits; + uint64 totalReveals; + uint64 totalSecurityDepositsLocked; + + // Configurable parameters + uint64 minimumSecurityDeposit; + uint32 revealTimeoutTicks; + + // Revenue accounting + uint64 totalRevenue; + uint64 pendingShareholderDistribution; + uint64 lostDepositsRevenue; + uint64 minerEarningsPool; + uint64 shareholderEarningsPool; + + // Pricing + uint64 pricePerByte; + uint64 priceDepositDivisor; + + // Recent miners (LRU-like), used to split miner earnings + Array recentMiners; + uint32 recentMinerCount; + + // Allowed deposit amounts (valid security deposits) + Array validDepositAmounts; + + // Active commitments (commitments array + count) + Array commitments; + uint32 commitmentCount; + + // --- QPI-compliant helpers --- + + // Simple helpers that avoid forbidden constructs in contracts. + + static inline bool validDepositAmountAt(const RANDOM& stateRef, uint64 amount, uint32 idx) + { + return amount == stateRef.validDepositAmounts.get(idx); + } + + static inline bool isEqualIdCheck(const id& a, const id& b) + { + return a == b; + } + + static inline bool isZeroIdCheck(const id& value) + { + return isZero(value); + } + + static inline uint64 calculatePrice(const RANDOM& state, uint32 numberOfBytes, uint64 minMinerDeposit) + { + return state.pricePerByte * numberOfBytes * + (div(minMinerDeposit, state.priceDepositDivisor) + 1ULL); + } + public: + // --- Inputs / outputs for user-facing procedures and functions --- + struct RevealAndCommit_input { bit_4096 revealedBits; @@ -14,27 +114,670 @@ struct RANDOM : public ContractBase }; struct RevealAndCommit_output { + Array randomBytes; + uint64 entropyVersion; + bool revealSuccessful; + bool commitSuccessful; + uint64 depositReturned; }; -private: - uint64 _earnedAmount; - uint64 _distributedAmount; - uint64 _burnedAmount; + struct GetContractInfo_input {}; + struct GetContractInfo_output + { + uint64 totalCommits; + uint64 totalReveals; + uint64 totalSecurityDepositsLocked; + uint64 minimumSecurityDeposit; + uint32 revealTimeoutTicks; + uint32 activeCommitments; + Array validDepositAmounts; + uint32 currentTick; + uint64 entropyPoolVersion; + uint64 totalRevenue; + uint64 pendingShareholderDistribution; + uint64 lostDepositsRevenue; + uint64 minerEarningsPool; + uint64 shareholderEarningsPool; + uint32 recentMinerCount; + }; + + struct GetUserCommitments_input + { + id userId; + }; + struct GetUserCommitments_output + { + struct UserCommitment + { + id digest; + uint64 amount; + uint32 commitTick; + uint32 revealDeadlineTick; + bool hasRevealed; + }; + Array commitments; + uint32 commitmentCount; + }; + + struct BuyEntropy_input + { + uint32 numberOfBytes; + uint64 minMinerDeposit; + }; + struct BuyEntropy_output + { + bool success; + Array randomBytes; + uint64 entropyVersion; + uint64 usedMinerDeposit; + uint64 usedPoolVersion; + }; + + struct QueryPrice_input { uint32 numberOfBytes; uint64 minMinerDeposit; }; + struct QueryPrice_output { uint64 price; }; + + //---- Locals storage for procedures --- + + struct RevealAndCommit_locals + { + uint32 currentTick; + bool hasRevealData; + bool hasNewCommit; + bool isStoppingMining; + sint32 existingIndex; + uint32 i; + uint32 rm; + uint32 lowestIx; + bool hashMatches; + + // locals for random-bytes generation (no stack locals) + uint32 histIdx; + uint32 rb_i; + + // precompute K12 digest of revealedBits once per call + id revealedDigest; + + // per-iteration commitment + RANDOM_EntropyCommitment cmt; + + // per-iteration temporaries (moved into locals for compliance) + uint64 lostDeposit; + RANDOM_RecentMiner recentMinerA; + RANDOM_RecentMiner recentMinerB; + + // deposit validity flag moved into locals + bool depositValid; + + // temporary used to create new commitments (moved out of stack) + RANDOM_EntropyCommitment ncmt; + }; + struct BuyEntropy_locals + { + uint32 currentTick; + bool eligible; + uint64 usedMinerDeposit; + uint32 i; + uint64 minPrice; + uint64 buyerFee; + uint32 histIdx; + uint64 half; - uint32 _bitFee; // Amount of qus + RANDOM_EntropyCommitment cmt; - PUBLIC_PROCEDURE(RevealAndCommit) + // per-iteration temporaries + uint64 lostDeposit; + RANDOM_RecentMiner recentMinerTemp; + }; + struct END_EPOCH_locals + { + uint32 currentTick; + uint32 i; + uint64 payout; + + RANDOM_EntropyCommitment cmt; + + // per-iteration temporaries + uint64 lostDeposit; + RANDOM_RecentMiner recentMinerTemp; + }; + struct GetUserCommitments_locals + { + uint32 userCommitmentCount; + uint32 i; + + RANDOM_EntropyCommitment cmt; + GetUserCommitments_output::UserCommitment ucmt; + }; + struct GetContractInfo_locals + { + uint32 currentTick; + uint32 activeCount; + uint32 i; + }; + struct INITIALIZE_locals + { + uint32 i; + uint32 j; + uint64 val; + }; + + // -------------------------------------------------- + // RevealAndCommit procedure: + // - Removes expired commitments + // - Optionally processes a reveal (preimage) and returns deposit if valid + // - Optionally accepts a new commitment (invocation reward as deposit) + + PUBLIC_PROCEDURE_WITH_LOCALS(RevealAndCommit) + { + locals.currentTick = qpi.tick(); + + // Remove expired commitments (sweep) -- reclaim lost deposits into revenue pools + for (locals.i = 0; locals.i < state.commitmentCount;) + { + locals.cmt = state.commitments.get(locals.i); + if (!locals.cmt.hasRevealed && locals.currentTick > locals.cmt.revealDeadlineTick) + { + // Move deposit into lost revenue and remove commitment (swap-with-last) + locals.lostDeposit = locals.cmt.amount; + state.lostDepositsRevenue += locals.lostDeposit; + state.totalRevenue += locals.lostDeposit; + state.pendingShareholderDistribution += locals.lostDeposit; + state.totalSecurityDepositsLocked -= locals.lostDeposit; + if (locals.i != state.commitmentCount - 1) + { + locals.cmt = state.commitments.get(state.commitmentCount - 1); + state.commitments.set(locals.i, locals.cmt); + } + state.commitmentCount--; + } + else + { + locals.i++; + } + } + + // Special-case early epoch: forcibly return deposits that expire exactly this tick + if (qpi.numberOfTickTransactions() == -1) + { + for (locals.i = 0; locals.i < state.commitmentCount;) + { + locals.cmt = state.commitments.get(locals.i); + if (!locals.cmt.hasRevealed && locals.cmt.revealDeadlineTick == qpi.tick()) + { + // refund directly in early epoch mode + qpi.transfer(locals.cmt.invocatorId, locals.cmt.amount); + state.totalSecurityDepositsLocked -= locals.cmt.amount; + if (locals.i != state.commitmentCount - 1) + { + locals.cmt = state.commitments.get(state.commitmentCount - 1); + state.commitments.set(locals.i, locals.cmt); + } + state.commitmentCount--; + } + else + { + locals.i++; + } + } + return; + } + + // Precompute digest of revealedBits once to avoid forbidden casts into bit_4096 + locals.revealedDigest = qpi.K12(input.revealedBits); + + // Presence of reveal is treated as an attempt to match stored digests + locals.hasRevealData = true; + locals.hasNewCommit = !isZeroIdCheck(input.committedDigest); + locals.isStoppingMining = (qpi.invocationReward() == 0); + + // If reveal provided: search for matching commitment(s) by this invocator + for (locals.i = 0; locals.i < state.commitmentCount;) + { + locals.cmt = state.commitments.get(locals.i); + if (!locals.cmt.hasRevealed && isEqualIdCheck(locals.cmt.invocatorId, qpi.invocator())) + { + // Compare stored digest to precomputed K12(revealedBits) + locals.hashMatches = (locals.cmt.digest == locals.revealedDigest); + + if (locals.hashMatches) + { + // If reveal too late, deposit is forfeited; otherwise update entropy pool and refund. + if (locals.currentTick > locals.cmt.revealDeadlineTick) + { + locals.lostDeposit = locals.cmt.amount; + state.lostDepositsRevenue += locals.lostDeposit; + state.totalRevenue += locals.lostDeposit; + state.pendingShareholderDistribution += locals.lostDeposit; + output.revealSuccessful = false; + } + else + { + // Apply the 256-bit digest to the pool by XORing the 4 x 64-bit lanes. + // This avoids inspecting bit_4096 internals and keeps everything QPI-compliant. + state.currentEntropyPool.u64._0 ^= locals.revealedDigest.u64._0; + state.currentEntropyPool.u64._1 ^= locals.revealedDigest.u64._1; + state.currentEntropyPool.u64._2 ^= locals.revealedDigest.u64._2; + state.currentEntropyPool.u64._3 ^= locals.revealedDigest.u64._3; + + // Advance circular history with copy of new pool and bump version. + state.entropyHistoryHead = (state.entropyHistoryHead + 1) & (RANDOM_ENTROPY_HISTORY_LEN - 1); + state.entropyHistory.set(state.entropyHistoryHead, state.currentEntropyPool); + + state.entropyPoolVersion++; + state.entropyPoolVersionHistory.set(state.entropyHistoryHead, state.entropyPoolVersion); + + // Refund deposit to invocator and update stats. + qpi.transfer(qpi.invocator(), locals.cmt.amount); + output.revealSuccessful = true; + output.depositReturned = locals.cmt.amount; + state.totalReveals++; + state.totalSecurityDepositsLocked -= locals.cmt.amount; + + // Maintain recentMiners LRU: update existing entry, append if space, or replace lowest. + locals.existingIndex = -1; + for (locals.rm = 0; locals.rm < state.recentMinerCount; ++locals.rm) + { + locals.recentMinerA = state.recentMiners.get(locals.rm); + if (isEqualIdCheck(locals.recentMinerA.minerId, qpi.invocator())) + { + locals.existingIndex = locals.rm; + break; + } + } + if (locals.existingIndex >= 0) + { + // update stored recent miner entry + locals.recentMinerA = state.recentMiners.get(locals.existingIndex); + if (locals.recentMinerA.deposit < locals.cmt.amount) + { + locals.recentMinerA.deposit = locals.cmt.amount; + locals.recentMinerA.lastEntropyVersion = state.entropyPoolVersion; + } + locals.recentMinerA.lastRevealTick = locals.currentTick; + state.recentMiners.set(locals.existingIndex, locals.recentMinerA); + } + else if (state.recentMinerCount < RANDOM_MAX_RECENT_MINERS) + { + // append new recent miner + locals.recentMinerA.minerId = qpi.invocator(); + locals.recentMinerA.deposit = locals.cmt.amount; + locals.recentMinerA.lastEntropyVersion = state.entropyPoolVersion; + locals.recentMinerA.lastRevealTick = locals.currentTick; + state.recentMiners.set(state.recentMinerCount, locals.recentMinerA); + state.recentMinerCount++; + } + else + { + // Find lowest-ranked miner and replace if current qualifies + locals.lowestIx = 0; + for (locals.rm = 1; locals.rm < RANDOM_MAX_RECENT_MINERS; ++locals.rm) + { + locals.recentMinerA = state.recentMiners.get(locals.rm); + locals.recentMinerB = state.recentMiners.get(locals.lowestIx); + if (locals.recentMinerA.deposit < locals.recentMinerB.deposit || + (locals.recentMinerA.deposit == locals.recentMinerB.deposit && locals.recentMinerA.lastEntropyVersion < locals.recentMinerB.lastEntropyVersion)) + { + locals.lowestIx = locals.rm; + } + } + locals.recentMinerA = state.recentMiners.get(locals.lowestIx); + if ( + locals.cmt.amount > locals.recentMinerA.deposit || + (locals.cmt.amount == locals.recentMinerA.deposit && state.entropyPoolVersion > locals.recentMinerA.lastEntropyVersion) + ) + { + locals.recentMinerA.minerId = qpi.invocator(); + locals.recentMinerA.deposit = locals.cmt.amount; + locals.recentMinerA.lastEntropyVersion = state.entropyPoolVersion; + locals.recentMinerA.lastRevealTick = locals.currentTick; + state.recentMiners.set(locals.lowestIx, locals.recentMinerA); + } + } + } + + // Remove commitment (swap with last) and continue scanning without incrementing i. + state.totalSecurityDepositsLocked -= locals.cmt.amount; + if (locals.i != state.commitmentCount - 1) + { + locals.cmt = state.commitments.get(state.commitmentCount - 1); + state.commitments.set(locals.i, locals.cmt); + } + state.commitmentCount--; + continue; + } + } + locals.i++; + } + + // If caller provided a new commitment (invocationReward used as deposit) and not stopping mining, + // accept it if deposit is valid and meets minimum. + if (locals.hasNewCommit && !locals.isStoppingMining) + { + // Inline deposit validity check using allowed-values array + locals.depositValid = false; + for (locals.i = 0; locals.i < RANDOM_VALID_DEPOSIT_AMOUNTS; ++locals.i) + { + if (validDepositAmountAt(state, qpi.invocationReward(), locals.i)) + { + locals.depositValid = true; + break; + } + } + + if (locals.depositValid && qpi.invocationReward() >= state.minimumSecurityDeposit) + { + if (state.commitmentCount < RANDOM_MAX_COMMITMENTS) + { + // Use locals.ncmt (approved locals) as temporary to avoid stack-local. + locals.ncmt.digest = input.committedDigest; + locals.ncmt.invocatorId = qpi.invocator(); + locals.ncmt.amount = qpi.invocationReward(); + locals.ncmt.commitTick = locals.currentTick; + locals.ncmt.revealDeadlineTick = locals.currentTick + state.revealTimeoutTicks; + locals.ncmt.hasRevealed = false; + state.commitments.set(state.commitmentCount, locals.ncmt); + state.commitmentCount++; + state.totalCommits++; + state.totalSecurityDepositsLocked += qpi.invocationReward(); + output.commitSuccessful = true; + } + } + } + + // Produce 32 random-like bytes from latest entropy history and current tick: + // - take most recent history entry (histIdx) and extract bytes from its 64-bit lanes, + // - XOR first 8 bytes with tick-derived bytes to add per-tick variation. + locals.histIdx = (state.entropyHistoryHead + RANDOM_ENTROPY_HISTORY_LEN - 0) & (RANDOM_ENTROPY_HISTORY_LEN - 1); + for (locals.rb_i = 0; locals.rb_i < RANDOM_RANDOMBYTES_LEN; ++locals.rb_i) + { + // Extract the correct 64-bit lane and then the requested byte without using plain []. + output.randomBytes.set( + locals.rb_i, + static_cast( + ( + ( + (locals.rb_i < 8) ? state.entropyHistory.get(locals.histIdx).u64._0 : + (locals.rb_i < 16) ? state.entropyHistory.get(locals.histIdx).u64._1 : + (locals.rb_i < 24) ? state.entropyHistory.get(locals.histIdx).u64._2 : + state.entropyHistory.get(locals.histIdx).u64._3 + ) >> (8 * (locals.rb_i & 7)) + ) & 0xFF + ) ^ + (locals.rb_i < 8 ? static_cast((static_cast(locals.currentTick) >> (8 * locals.rb_i)) & 0xFF) : 0) + ); + } + + output.entropyVersion = state.entropyPoolVersion; + } + + // BuyEntropy procedure: + // - Removes expired commitments (same sweep) + // - Checks buyer fee and miner eligibility + // - Charges buyer and returns requested bytes from slightly older pool version + PUBLIC_PROCEDURE_WITH_LOCALS(BuyEntropy) + { + locals.currentTick = qpi.tick(); + + // Sweep expired commitments + for (locals.i = 0; locals.i < state.commitmentCount;) + { + locals.cmt = state.commitments.get(locals.i); + if (!locals.cmt.hasRevealed && locals.currentTick > locals.cmt.revealDeadlineTick) + { + locals.lostDeposit = locals.cmt.amount; + state.lostDepositsRevenue += locals.lostDeposit; + state.totalRevenue += locals.lostDeposit; + state.pendingShareholderDistribution += locals.lostDeposit; + state.totalSecurityDepositsLocked -= locals.lostDeposit; + if (locals.i != state.commitmentCount - 1) + { + locals.cmt = state.commitments.get(state.commitmentCount - 1); + state.commitments.set(locals.i, locals.cmt); + } + state.commitmentCount--; + } + else + { + locals.i++; + } + } + + // Disallow in early-epoch mode -- refund buyer + if (qpi.numberOfTickTransactions() == -1) + { + output.success = false; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); // <-- refund buyer + return; + } + + output.success = false; + locals.buyerFee = qpi.invocationReward(); + locals.eligible = false; + locals.usedMinerDeposit = 0; + + // Find an eligible recent miner whose deposit >= minMinerDeposit and who revealed recently + for (locals.i = 0; locals.i < state.recentMinerCount; ++locals.i) + { + locals.recentMinerTemp = state.recentMiners.get(locals.i); + if (locals.recentMinerTemp.deposit >= input.minMinerDeposit && + (locals.currentTick - locals.recentMinerTemp.lastRevealTick) <= state.revealTimeoutTicks) + { + locals.eligible = true; + locals.usedMinerDeposit = locals.recentMinerTemp.deposit; + break; + } + } + + if (!locals.eligible) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); // <-- refund buyer (no entropy available) + return; + } + + // Compute minimum price and check buyer fee + locals.minPrice = calculatePrice(state, input.numberOfBytes, input.minMinerDeposit); + + if (locals.buyerFee < locals.minPrice) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); // <-- refund buyer (not enough fee) + return; + } + + // Use the previous-but-one history entry for purchased entropy (to avoid last-second reveals) + locals.histIdx = (state.entropyHistoryHead + RANDOM_ENTROPY_HISTORY_LEN - 2) & (RANDOM_ENTROPY_HISTORY_LEN - 1); + + // Produce requested bytes (bounded by RANDOM_RANDOMBYTES_LEN) + for (locals.i = 0; locals.i < ((input.numberOfBytes > RANDOM_RANDOMBYTES_LEN) ? RANDOM_RANDOMBYTES_LEN : input.numberOfBytes); ++locals.i) + { + output.randomBytes.set( + locals.i, + static_cast( + ( + ( + (locals.i < 8) ? state.entropyHistory.get(locals.histIdx).u64._0 : + (locals.i < 16) ? state.entropyHistory.get(locals.histIdx).u64._1 : + (locals.i < 24) ? state.entropyHistory.get(locals.histIdx).u64._2 : + state.entropyHistory.get(locals.histIdx).u64._3 + ) >> (8 * (locals.i & 7)) + ) & 0xFF + ) ^ + (locals.i < 8 ? static_cast((static_cast(locals.currentTick) >> (8 * locals.i)) & 0xFF) : 0) + ); + } + + // Return entropy pool/version info and signal success + output.entropyVersion = state.entropyPoolVersionHistory.get(locals.histIdx); + output.usedMinerDeposit = locals.usedMinerDeposit; + output.usedPoolVersion = state.entropyPoolVersionHistory.get(locals.histIdx); + output.success = true; + + // Split fee: half to miners pool, half to shareholders + locals.half = div(locals.buyerFee, 2ULL); + state.minerEarningsPool += locals.half; + state.shareholderEarningsPool += (locals.buyerFee - locals.half); + } + + // GetContractInfo: return public state summary + PUBLIC_FUNCTION_WITH_LOCALS(GetContractInfo) + { + locals.currentTick = qpi.tick(); + locals.activeCount = 0; + + output.totalCommits = state.totalCommits; + output.totalReveals = state.totalReveals; + output.totalSecurityDepositsLocked = state.totalSecurityDepositsLocked; + output.minimumSecurityDeposit = state.minimumSecurityDeposit; + output.revealTimeoutTicks = state.revealTimeoutTicks; + output.currentTick = locals.currentTick; + output.entropyPoolVersion = state.entropyPoolVersion; + + output.totalRevenue = state.totalRevenue; + output.pendingShareholderDistribution = state.pendingShareholderDistribution; + output.lostDepositsRevenue = state.lostDepositsRevenue; + output.minerEarningsPool = state.minerEarningsPool; + output.shareholderEarningsPool = state.shareholderEarningsPool; + output.recentMinerCount = state.recentMinerCount; + + // Copy valid deposit amounts + copyMemory(output.validDepositAmounts, state.validDepositAmounts); + + // Count active commitments + for (locals.i = 0; locals.i < state.commitmentCount; ++locals.i) + { + if (!state.commitments.get(locals.i).hasRevealed) + { + locals.activeCount++; + } + } + output.activeCommitments = locals.activeCount; + } + + // GetUserCommitments: list commitments for a user (bounded) + PUBLIC_FUNCTION_WITH_LOCALS(GetUserCommitments) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + locals.userCommitmentCount = 0; + for (locals.i = 0; locals.i < state.commitmentCount && locals.userCommitmentCount < RANDOM_MAX_USER_COMMITMENTS; ++locals.i) + { + locals.cmt = state.commitments.get(locals.i); + if (isEqualIdCheck(locals.cmt.invocatorId, input.userId)) + { + // copy to output buffer + locals.ucmt.digest = locals.cmt.digest; + locals.ucmt.amount = locals.cmt.amount; + locals.ucmt.commitTick = locals.cmt.commitTick; + locals.ucmt.revealDeadlineTick = locals.cmt.revealDeadlineTick; + locals.ucmt.hasRevealed = locals.cmt.hasRevealed; + output.commitments.set(locals.userCommitmentCount, locals.ucmt); + locals.userCommitmentCount++; + } + } + output.commitmentCount = locals.userCommitmentCount; } + // QueryPrice: compute price for a buyer based on requested bytes and min miner deposit + PUBLIC_FUNCTION(QueryPrice) + { + output.price = calculatePrice(state, input.numberOfBytes, input.minMinerDeposit); + } + + // END_EPOCH: sweep expired commitments and distribute earnings to recent miners and shareholders + END_EPOCH_WITH_LOCALS() + { + locals.currentTick = qpi.tick(); + + // Sweep expired commitments (same logic) + for (locals.i = 0; locals.i < state.commitmentCount;) + { + locals.cmt = state.commitments.get(locals.i); + if (!locals.cmt.hasRevealed && locals.currentTick > locals.cmt.revealDeadlineTick) + { + locals.lostDeposit = locals.cmt.amount; + state.lostDepositsRevenue += locals.lostDeposit; + state.totalRevenue += locals.lostDeposit; + state.pendingShareholderDistribution += locals.lostDeposit; + state.totalSecurityDepositsLocked -= locals.lostDeposit; + + if (locals.i != state.commitmentCount - 1) + { + locals.cmt = state.commitments.get(state.commitmentCount - 1); + state.commitments.set(locals.i, locals.cmt); + } + state.commitmentCount--; + } + else + { + locals.i++; + } + } + + // Pay miners equally from minerEarningsPool + if (state.minerEarningsPool > 0 && state.recentMinerCount > 0) + { + locals.payout = div(state.minerEarningsPool, (uint64)state.recentMinerCount); + for (locals.i = 0; locals.i < state.recentMinerCount; ++locals.i) + { + locals.recentMinerTemp = state.recentMiners.get(locals.i); + if (!isZeroIdCheck(locals.recentMinerTemp.minerId)) + { + qpi.transfer(locals.recentMinerTemp.minerId, locals.payout); + } + } + // reset miner pool and recentMiners + state.minerEarningsPool = 0; + for (locals.i = 0; locals.i < RANDOM_MAX_RECENT_MINERS; ++locals.i) + { + locals.recentMinerTemp.minerId = id::zero(); + locals.recentMinerTemp.deposit = 0; + locals.recentMinerTemp.lastEntropyVersion = 0; + locals.recentMinerTemp.lastRevealTick = 0; + state.recentMiners.set(locals.i, locals.recentMinerTemp); + } + state.recentMinerCount = 0; + } + + // Distribute any pending shareholder distribution (from earnings and/or lost deposits) + uint64 totalShareholderPayout = state.shareholderEarningsPool + state.pendingShareholderDistribution; + if (totalShareholderPayout > 0) + { + qpi.distributeDividends(div(totalShareholderPayout, (uint64)NUMBER_OF_COMPUTORS)); + state.shareholderEarningsPool = 0; + state.pendingShareholderDistribution = 0; + } + } + + // Register functions and procedures (standard QPI boilerplate) REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { + REGISTER_USER_FUNCTION(GetContractInfo, 1); + REGISTER_USER_FUNCTION(GetUserCommitments, 2); + REGISTER_USER_FUNCTION(QueryPrice, 3); + REGISTER_USER_PROCEDURE(RevealAndCommit, 1); + REGISTER_USER_PROCEDURE(BuyEntropy, 2); } - INITIALIZE() + // INITIALIZE: set defaults and fill valid deposit amounts array (powers of 10) + INITIALIZE_WITH_LOCALS() { - state._bitFee = 1000; + locals.i = 0; + locals.j = 0; + locals.val = 0; + + state.entropyHistoryHead = 0; + state.minimumSecurityDeposit = 1; + state.revealTimeoutTicks = 9; + state.pricePerByte = 10; + state.priceDepositDivisor = 1000; + + // validDepositAmounts: 1, 10, 100, 1000, ... + for (locals.i = 0; locals.i < RANDOM_VALID_DEPOSIT_AMOUNTS; ++locals.i) + { + locals.val = 1ULL; + for (locals.j = 0; locals.j < locals.i; ++locals.j) + { + locals.val *= 10; + } + state.validDepositAmounts.set(locals.i, locals.val); + } } }; diff --git a/test/contract_random.cpp b/test/contract_random.cpp new file mode 100644 index 000000000..a48b0affa --- /dev/null +++ b/test/contract_random.cpp @@ -0,0 +1,511 @@ +#define NO_UEFI + +#include +#include "contract_testing.h" + +class ContractTestingRandom : public ContractTesting +{ +public: + ContractTestingRandom() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(RANDOM); + callSystemProcedure(0, INITIALIZE); + } + + // Commit+reveal convenience + void commit(const id& miner, const bit_4096& commitBits, uint64_t deposit) + { + increaseEnergy(miner, deposit *2); // ensure enough QU + RANDOM::RevealAndCommit_input inp{}; + inp.committedDigest = k12Digest(commitBits); // Use real K12 digest for test + RANDOM::RevealAndCommit_output out{}; + invokeUserProcedure(0,1, inp, out, miner, deposit); + } + + void revealAndCommit(const id& miner, const bit_4096& revealBits, const bit_4096& newCommitBits, uint64_t deposit) + { + RANDOM::RevealAndCommit_input inp{}; + inp.revealedBits = revealBits; + inp.committedDigest = k12Digest(newCommitBits); + RANDOM::RevealAndCommit_output out{}; + invokeUserProcedure(0,1, inp, out, miner, deposit); + } + + void stopMining(const id& miner, const bit_4096& revealBits) + { + RANDOM::RevealAndCommit_input inp{}; + inp.revealedBits = revealBits; + inp.committedDigest = id::zero(); + RANDOM::RevealAndCommit_output out{}; + invokeUserProcedure(0,1, inp, out, miner,0); + } + + bool buyEntropy(const id& buyer, uint32_t numBytes, uint64_t minMinerDeposit, uint64_t suggestedFee, bool expectSuccess) + { + increaseEnergy(buyer, suggestedFee +10000); + RANDOM::BuyEntropy_input inp{}; + inp.numberOfBytes = numBytes; + inp.minMinerDeposit = minMinerDeposit; + RANDOM::BuyEntropy_output out{}; + invokeUserProcedure(0,2, inp, out, buyer, suggestedFee); + if (expectSuccess) + EXPECT_TRUE(out.success); + else + EXPECT_FALSE(out.success); + return out.success; + } + + // Direct call to get price + uint64_t queryPrice(uint32_t numBytes, uint64_t minMinerDeposit) + { + RANDOM::QueryPrice_input q{}; + q.numberOfBytes = numBytes; + q.minMinerDeposit = minMinerDeposit; + RANDOM::QueryPrice_output o{}; + callFunction(0,3, q, o); + return o.price; + } + + // Helper entropy/id for test readability + static bit_4096 testBits(uint64_t v) { + bit_4096 b{}; + uint64_t* ptr = reinterpret_cast(&b); + for (int i =0; i <64; ++i) ptr[i] = v ^ (0xDEADBEEF12340000ULL | i); + return b; + } + static id testId(uint64_t base) { + id d = id::zero(); + for (int i =0; i <32; ++i) d.m256i_u8[i] = uint8_t((base >> (i %8)) + i); + return d; + } + static id k12Digest(const bit_4096& b) { + id digest = id::zero(); + KangarooTwelve(&b, sizeof(b), &digest, sizeof(digest)); + return digest; + } +}; + +//------------------------------ +// TEST CASES +//------------------------------ + +// Helper macros for tick/time simulation +#define SET_TICK(val) (system.tick = (val)) +#define GET_TICK() (system.tick) +// To simulate an empty tick, set numberTickTransactions to -1 +#define SET_TICK_IS_EMPTY(val) (numberTickTransactions = ((val) ? -1 :0)) + +TEST(ContractRandom, BasicCommitRevealStop) +{ + ContractTestingRandom random; + id miner = ContractTestingRandom::testId(10); + bit_4096 E1 = ContractTestingRandom::testBits(101); + bit_4096 E2 = ContractTestingRandom::testBits(202); + + random.commit(miner, E1, 1000); + random.revealAndCommit(miner, E1, E2, 1000); + random.stopMining(miner, E2); + + RANDOM::GetContractInfo_input ci{}; + RANDOM::GetContractInfo_output co{}; + random.callFunction(0, 1, ci, co); + EXPECT_EQ(co.activeCommitments, 0); +} + +TEST(ContractRandom, TimeoutsAndRefunds) +{ + ContractTestingRandom random; + id miner = ContractTestingRandom::testId(11); + bit_4096 bits = ContractTestingRandom::testBits(303); + + random.commit(miner, bits, 2000); + + // Timeout: Advance tick past deadline + RANDOM::GetContractInfo_input ci0{}; + RANDOM::GetContractInfo_output co0{}; + random.callFunction(0, 1, ci0, co0); + int timeoutTick = GET_TICK() + co0.revealTimeoutTicks + 1; + SET_TICK(timeoutTick); + + // Trigger timeout (choose any call, including another commit or dummy reveal) + RANDOM::RevealAndCommit_input dummy = {}; + RANDOM::RevealAndCommit_output out{}; + random.invokeUserProcedure(0, 1, dummy, out, miner, 0); + + RANDOM::GetContractInfo_input ci{}; + RANDOM::GetContractInfo_output co{}; + random.callFunction(0, 1, ci, co); + EXPECT_EQ(co.activeCommitments, 0); + EXPECT_EQ(co.lostDepositsRevenue, 2000); +} + +TEST(ContractRandom, EmptyTickRefund) +{ + ContractTestingRandom random; + id miner = ContractTestingRandom::testId(12); + bit_4096 bits = ContractTestingRandom::testBits(404); + + random.commit(miner, bits, 3000); + + // Use GetContractInfo to get revealTimeoutTicks + RANDOM::GetContractInfo_input ci0{}; + RANDOM::GetContractInfo_output co0{}; + random.callFunction(0, 1, ci0, co0); + int refundTick = system.tick + co0.revealTimeoutTicks; + system.tick = refundTick; + numberTickTransactions = -1; + + // All deadlines expire on an empty tick: refund + RANDOM::RevealAndCommit_input dummy = {}; + RANDOM::RevealAndCommit_output out{}; + random.invokeUserProcedure(0, 1, dummy, out, miner, 0); + RANDOM::GetContractInfo_input ci{}; + RANDOM::GetContractInfo_output co{}; + random.callFunction(0, 1, ci, co); + EXPECT_EQ(co.activeCommitments, 0); +} + +TEST(ContractRandom, BuyEntropyEligibility) +{ + ContractTestingRandom random; + id miner = ContractTestingRandom::testId(13); + id buyer = ContractTestingRandom::testId(14); + bit_4096 bits = ContractTestingRandom::testBits(321); + + // No miners: should fail + EXPECT_FALSE(random.buyEntropy(buyer, 8, 1000, 8000, false)); + + // Commit/reveal + random.commit(miner, bits, 1000); + random.revealAndCommit(miner, bits, ContractTestingRandom::testBits(333), 1000); + + // Should now succeed + EXPECT_TRUE(random.buyEntropy(buyer, 16, 1000, 16000, true)); + + // Advance past freshness window, should fail again + RANDOM::GetContractInfo_input ci0{}; + RANDOM::GetContractInfo_output co0{}; + random.callFunction(0, 1, ci0, co0); + system.tick = system.tick + co0.revealTimeoutTicks + 1; + EXPECT_FALSE(random.buyEntropy(buyer, 16, 1000, 16000, false)); +} + +TEST(ContractRandom, QueryPriceLogic) +{ + ContractTestingRandom random; + RANDOM::QueryPrice_input q{}; + q.numberOfBytes = 16; + q.minMinerDeposit = 1000; + RANDOM::QueryPrice_output o{}; + random.callFunction(0, 3, q, o); + uint64_t price = random.queryPrice(16, 1000); + EXPECT_EQ(price, o.price); +} + +TEST(ContractRandom, CompactionBehavior) +{ + ContractTestingRandom random; + for (int i = 0; i < 10; ++i) { + id miner = ContractTestingRandom::testId(100 + i); + bit_4096 bits = ContractTestingRandom::testBits(1001 + i); + random.commit(miner, bits, 5000); + random.revealAndCommit(miner, bits, ContractTestingRandom::testBits(2001 + i), 5000); + random.stopMining(miner, ContractTestingRandom::testBits(2001 + i)); + } +} + +TEST(ContractRandom, MultipleMinersAndBuyers) +{ + ContractTestingRandom random; + id minerA = ContractTestingRandom::testId(1001); + id minerB = ContractTestingRandom::testId(1002); + id buyer1 = ContractTestingRandom::testId(1003); + id buyer2 = ContractTestingRandom::testId(1004); + bit_4096 entropyA = ContractTestingRandom::testBits(5678); + bit_4096 entropyB = ContractTestingRandom::testBits(6789); + + // Both miners commit, same deposit + random.commit(minerA, entropyA, 10000); + random.commit(minerB, entropyB, 10000); + random.revealAndCommit(minerA, entropyA, ContractTestingRandom::testBits(8888), 10000); + random.revealAndCommit(minerB, entropyB, ContractTestingRandom::testBits(9999), 10000); + + // Buyer1 can purchase with either miner as eligible + EXPECT_TRUE(random.buyEntropy(buyer1, 8, 10000, 20000, true)); + // Buyer2 requires more security than available + EXPECT_FALSE(random.buyEntropy(buyer2, 16, 20000, 35000, false)); +} + +TEST(ContractRandom, MaxCommitmentsAndEviction) +{ + ContractTestingRandom random; + // Fill the commitments array + const int N = 32; + std::vector miners; + for (int i = 0; i < N; ++i) { + miners.push_back(ContractTestingRandom::testId(300 + i)); + random.commit(miners.back(), ContractTestingRandom::testBits(1234 + i), 5555); + } + + // Reveal all out-of-order, ensure compaction + for (int i = N - 1; i >= 0; --i) { + random.revealAndCommit(miners[i], ContractTestingRandom::testBits(1234 + i), ContractTestingRandom::testBits(2000 + i), 5555); + random.stopMining(miners[i], ContractTestingRandom::testBits(2000 + i)); + } +} + +TEST(ContractRandom, EndEpochDistribution) +{ + ContractTestingRandom random; + id miner1 = ContractTestingRandom::testId(99); + id miner2 = ContractTestingRandom::testId(98); + bit_4096 e1 = ContractTestingRandom::testBits(501); + bit_4096 e2 = ContractTestingRandom::testBits(502); + + random.commit(miner1, e1, 10000); + random.revealAndCommit(miner1, e1, ContractTestingRandom::testBits(601), 10000); + random.commit(miner2, e2, 10000); + random.revealAndCommit(miner2, e2, ContractTestingRandom::testBits(602), 10000); + + id buyer = ContractTestingRandom::testId(90); + uint64_t price = random.queryPrice(16, 10000); + random.buyEntropy(buyer, 16, 10000, price, true); + + // Simulate EndEpoch + random.callSystemProcedure(0, END_EPOCH); + + // After epoch, earnings pools should be zeroed and recentMinerCount cleared + RANDOM::GetContractInfo_input ci{}; + RANDOM::GetContractInfo_output co{}; + random.callFunction(0, 1, ci, co); + EXPECT_EQ(co.minerEarningsPool, 0); + EXPECT_EQ(co.shareholderEarningsPool, 0); + EXPECT_EQ(co.recentMinerCount, 0); +} + +TEST(ContractRandom, RecentMinerEvictionPolicy) +{ + ContractTestingRandom random; + const int maxMiners = RANDOM_MAX_RECENT_MINERS; + std::vector miners; + auto baseDeposit = 1000; + + // Fill up to RANDOM_MAX_RECENT_MINERS, all with same deposit + for (int i = 0; i < maxMiners; ++i) { + auto miner = ContractTestingRandom::testId(5000 + i); + miners.push_back(miner); + random.commit(miner, random.testBits(7000 + i), baseDeposit); + random.revealAndCommit(miner, random.testBits(7000 + i), random.testBits(8000 + i), baseDeposit); + } + RANDOM::GetContractInfo_input ci0{}; + RANDOM::GetContractInfo_output co0{}; + random.callFunction(0, 1, ci0, co0); + EXPECT_EQ(co0.recentMinerCount, maxMiners); + + // Add new miner with higher deposit, should evict one of the previous (lowest deposit) + id highMiner = ContractTestingRandom::testId(99999); + random.commit(highMiner, random.testBits(55555), baseDeposit * 10); + random.revealAndCommit(highMiner, random.testBits(55555), random.testBits(55566), baseDeposit * 10); + + RANDOM::GetContractInfo_input ci1{}; + RANDOM::GetContractInfo_output co1{}; + random.callFunction(0, 1, ci1, co1); + EXPECT_EQ(co1.recentMinerCount, maxMiners); + + // All lower deposit miners except one likely evicted, highMiner should be present with max deposit + int foundHigh = 0; + RANDOM::GetUserCommitments_input inp{}; + RANDOM::GetUserCommitments_output out{}; + for (uint32_t i = 0; i < maxMiners; ++i) { + inp.userId = highMiner; + random.callFunction(0, 2, inp, out); + if (out.commitmentCount > 0) foundHigh++; + } + EXPECT_EQ(foundHigh, 1); +} + +TEST(ContractRandom, BuyerPickinessHighRequirement) +{ + ContractTestingRandom random; + id miner = random.testId(721); + id buyer = random.testId(722); + uint64_t lowDeposit = 1000, highDeposit = 100000; + + random.commit(miner, random.testBits(100), lowDeposit); + random.revealAndCommit(miner, random.testBits(100), random.testBits(101), lowDeposit); + + // As buyer, require higher min deposit than any available miner supplied + EXPECT_FALSE(random.buyEntropy(buyer, 8, highDeposit, 10000, false)); +} + +TEST(ContractRandom, MixedDepositLevels) +{ + ContractTestingRandom random; + id lowMiner = random.testId(1001); + id highMiner = random.testId(1002); + id buyer = random.testId(1003); + + random.commit(lowMiner, random.testBits(88), 1000); + random.commit(highMiner, random.testBits(89), 100000); + random.revealAndCommit(lowMiner, random.testBits(88), random.testBits(188), 1000); + random.revealAndCommit(highMiner, random.testBits(89), random.testBits(189), 100000); + + EXPECT_TRUE(random.buyEntropy(buyer, 8, 1000, 10000, true)); + EXPECT_TRUE(random.buyEntropy(buyer, 8, 100000, 100000, true)); + EXPECT_FALSE(random.buyEntropy(buyer, 8, 100001, 100000, false)); +} + +TEST(ContractRandom, EmptyTickRefund_MultiMiners) +{ + ContractTestingRandom random; + id m1 = random.testId(931); + id m2 = random.testId(932); + random.commit(m1, random.testBits(401), 5000); + random.commit(m2, random.testBits(402), 7000); + + RANDOM::GetContractInfo_input ci0{}; + RANDOM::GetContractInfo_output co0{}; + random.callFunction(0, 1, ci0, co0); + int tick = system.tick + co0.revealTimeoutTicks; + system.tick = tick; + numberTickTransactions = -1; + + RANDOM::RevealAndCommit_input dummy = {}; + RANDOM::RevealAndCommit_output out{}; + random.invokeUserProcedure(0, 1, dummy, out, m1, 0); + RANDOM::GetContractInfo_input ci{}; + RANDOM::GetContractInfo_output co{}; + random.callFunction(0, 1, ci, co); + EXPECT_EQ(co.activeCommitments, 0); + // test both miners' balances for refund if desired +} + +TEST(ContractRandom, Timeout_MultiMiners) +{ + ContractTestingRandom random; + id m1 = random.testId(7777); + id m2 = random.testId(8888); + random.commit(m1, random.testBits(111), 2000); + random.commit(m2, random.testBits(112), 4000); + RANDOM::GetContractInfo_input ci0{}; + RANDOM::GetContractInfo_output co0{}; + random.callFunction(0, 1, ci0, co0); + int afterTimeout = system.tick + co0.revealTimeoutTicks + 1; + system.tick = afterTimeout; + + RANDOM::RevealAndCommit_input dummy = {}; + RANDOM::RevealAndCommit_output out{}; + random.invokeUserProcedure(0, 1, dummy, out, m2, 0); + + RANDOM::GetContractInfo_input ci{}; + RANDOM::GetContractInfo_output co{}; + random.callFunction(0, 1, ci, co); + EXPECT_EQ(co.activeCommitments, 0); + EXPECT_EQ(co.lostDepositsRevenue, 6000); +} + +TEST(ContractRandom, MultipleBuyersEpochReset) +{ + ContractTestingRandom random; + id miner = random.testId(1201); + id buyer1 = random.testId(1301); + id buyer2 = random.testId(1401); + + random.commit(miner, random.testBits(900), 8000); + random.revealAndCommit(miner, random.testBits(900), random.testBits(901), 8000); + + EXPECT_TRUE(random.buyEntropy(buyer1, 8, 8000, 20000, true)); + EXPECT_TRUE(random.buyEntropy(buyer2, 16, 8000, 50000, true)); + + random.callSystemProcedure(0, END_EPOCH); + + RANDOM::GetContractInfo_input ci{}; + RANDOM::GetContractInfo_output co{}; + random.callFunction(0, 1, ci, co); + EXPECT_EQ(co.minerEarningsPool, 0); + EXPECT_EQ(co.shareholderEarningsPool, 0); + EXPECT_EQ(co.recentMinerCount, 0); +} + +TEST(ContractRandom, QueryUserCommitmentsInfo) +{ + ContractTestingRandom random; + id miner = random.testId(2001); + + random.commit(miner, random.testBits(1234), 10000); + + // Call GetUserCommitments for miner + RANDOM::GetUserCommitments_input inp{}; + inp.userId = miner; + RANDOM::GetUserCommitments_output out{}; + random.callFunction(0, 2, inp, out); + EXPECT_GE(out.commitmentCount, 1); + + // Call GetContractInfo for global stats + RANDOM::GetContractInfo_input ci{}; + RANDOM::GetContractInfo_output co{}; + random.callFunction(0, 1, ci, co); + EXPECT_GE(co.totalCommits, 1); +} + +TEST(ContractRandom, RejectInvalidDeposits) +{ + ContractTestingRandom random; + id miner = random.testId(2012); + + // Try to commit invalid deposit (not a power of ten) + RANDOM::RevealAndCommit_input inp{}; + inp.committedDigest = ContractTestingRandom::k12Digest(ContractTestingRandom::testBits(66)); + RANDOM::RevealAndCommit_output out{}; + // Use7777 which is not a power of ten, should not register a commitment + random.invokeUserProcedure(0, 1, inp, out, miner, 7777); + + RANDOM::GetContractInfo_input ci{}; + RANDOM::GetContractInfo_output co{}; + random.callFunction(0, 1, ci, co); + EXPECT_EQ(co.activeCommitments, 0); +} + +TEST(ContractRandom, BuyEntropyEdgeNumBytes) +{ + ContractTestingRandom random; + id miner = random.testId(3031); + id buyer = random.testId(3032); + + random.commit(miner, random.testBits(8888), 8000); + random.revealAndCommit(miner, random.testBits(8888), random.testBits(8899), 8000); + + //1 byte (minimum) + EXPECT_TRUE(random.buyEntropy(buyer, 1, 8000, 10000, true)); + //32 bytes (maximum) + EXPECT_TRUE(random.buyEntropy(buyer, 32, 8000, 40000, true)); + //33 bytes (over contract max, should clamp or fail) + EXPECT_FALSE(random.buyEntropy(buyer, 33, 8000, 50000, false)); +} + +TEST(ContractRandom, OutOfOrderRevealAndCompaction) +{ + ContractTestingRandom random; + std::vector miners; + for (int i = 0; i < 8; ++i) { + miners.push_back(random.testId(5400 + i)); + random.commit(miners.back(), random.testBits(8500 + i), 6000); + } + // Reveal/stop in random order + random.revealAndCommit(miners[3], random.testBits(8500 + 3), random.testBits(9500 + 3), 6000); + random.revealAndCommit(miners[1], random.testBits(8500 + 1), random.testBits(9500 + 1), 6000); + random.stopMining(miners[3], random.testBits(9500 + 3)); + random.stopMining(miners[1], random.testBits(9500 + 1)); + // Now reveal/stop remainder + for (int i = 0; i < 8; ++i) { + if (i == 1 || i == 3) continue; + random.revealAndCommit(miners[i], random.testBits(8500 + i), random.testBits(9500 + i), 6000); + random.stopMining(miners[i], random.testBits(9500 + i)); + } + RANDOM::GetContractInfo_input ci{}; + RANDOM::GetContractInfo_output co{}; + random.callFunction(0, 1, ci, co); + EXPECT_EQ(co.activeCommitments, 0); +} diff --git a/test/test.vcxproj b/test/test.vcxproj index 5979b640b..b4d9c844b 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -122,6 +122,7 @@ + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 05f9b9622..eef1ad2d8 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -44,6 +44,7 @@ +