diff --git a/CMakeLists.txt b/CMakeLists.txt index f640e996..b89b96ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ option(BUILD_BENCHMARK "Build the EFI benchmark application" OFF) option(BUILD_BINARY "Build the EFI application" ON) option(USE_SANITIZER "Build test with sanitizer support (clang only)" ON) option(LITE_WASM_SC "Enable testnet-lite-RAM Wasm smart contracts" OFF) +option(ANT_WALKER "Build the ant-colony walker sidecar and its node client" ON) set(QUBIC_STANDALONE_RELEASE ON CACHE INTERNAL "Standalone builds are the only supported configuration" FORCE) set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build static dependencies" FORCE) set(OPENSSL_USE_STATIC_LIBS TRUE CACHE BOOL "Use static OpenSSL" FORCE) diff --git a/data/bpp9000.task b/data/bpp9000.task index 8ffa5e17..38ce2351 100644 Binary files a/data/bpp9000.task and b/data/bpp9000.task differ diff --git a/doc/ant_colony_mining.md b/doc/ant_colony_mining.md index 881d6501..5e694af4 100644 --- a/doc/ant_colony_mining.md +++ b/doc/ant_colony_mining.md @@ -45,9 +45,10 @@ Standalone mining searches alone: every attempt starts from scratch. Ant-colony - Every **mining identity** (a computor or candidate public key) owns its **own tree** - the colony is a per-identity forest. A pool's workers extend the tree of the computor they mine for. -- Each tree starts from a **virtual root**: a starting solution derived from that identity's public key - and the epoch's spectrum digest. It is fixed for the epoch, identical every time you derive it, and - is never stored or submitted. +- Every tree starts from the same **virtual root**: one starting solution per epoch, derived from the + epoch's spectrum digest alone - identical for all identities, identical every time you derive it, + and never stored or submitted. All identities search from one shared origin; the trees branching + from it stay per-identity. - To mine, you pick a **parent** (the root, or any node already in your tree), **inherit** it, vary it under your nonce, and score the result. - If the result **strictly beats the parent** and clears the epoch **threshold**, you **submit** it. On @@ -58,7 +59,7 @@ miner starts from there instead of from scratch. The goal of the epoch is the si found anywhere in the forest. ``` - virtual root (per identity, not stored) + virtual root (shared per epoch, not stored) | +----+----+ | | @@ -73,20 +74,20 @@ Concretely, error gates every attachment: it only falls down a branch (a child m and a *start* - a depth-1 child of the root - must clear the threshold. ``` - error = error count, lower is better threshold = 3838 + error = error count, lower is better threshold = 4000 - root ~4044 raw a fresh root sits above 3838; a start must mutate below it + root ~4200 raw the epoch root (same for everyone) sits above 4000; a start must mutate below it | - +-- A 3790 <= threshold ACCEPT (depth-1 start) + +-- A 3900 <= threshold ACCEPT (depth-1 start) | | - | +-- B 3540 < 3790, beats A ACCEPT + | +-- B 3540 < 3900, beats A ACCEPT | | | | | +-- D 3120 < 3540, beats B ACCEPT | | +-- E 3560 not < 3540 REJECT (must beat parent) | | - | +-- C 3700 < 3790, beats A ACCEPT + | +-- C 3700 < 3900, beats A ACCEPT | - +-- X 3900 > threshold REJECT (over threshold) + +-- X 4100 > threshold REJECT (over threshold) Error only falls as you go deeper. The epoch winner is the single lowest-error node found in any identity's forest. @@ -96,7 +97,7 @@ At epoch end the node ranks every identity by its **single best** score and **ha (the number of computors). **Anti-spam deposit.** Each solution a computor publishes on-chain carries a **refundable -1,000,000 QU deposit**, funded by the computor - not the worker. It is returned when the solution is +1000000 QU deposit**, funded by the computor - not the worker. It is returned when the solution is accepted **and** its claimed score matches the node's recompute; otherwise it is kept. So a computor only publishes solutions it has already validated, and an honest, correct one costs nothing. @@ -105,7 +106,7 @@ only publishes solutions it has already validated, and an honest, correct one co ## Part 2 - Miner / pool integration guide **In short.** A miner works one identity's tree. It reads the epoch context, takes a **parent** (the -identity's virtual root, or a node already in the tree), picks a canonical **nonce**, inherits the +epoch's shared virtual root, or a node already in the tree), picks a canonical **nonce**, inherits the parent's network, and **mutates and scores** it - reproducing the node's score exactly. If the result **beats its parent** and **clears the threshold**, it hands the solution to the **computor**, which re-checks it and **publishes it on-chain**; every node then recomputes the score, folds it into @@ -117,8 +118,8 @@ scorer** - the tree, gates, deposit, and queries are the wrapper around it. 1. **Epoch context** - `REQUEST_ANT_EPOCH_CONTEXT` (public). Read the threshold, freshness window, epoch spectrum digest, and child cap for this epoch, and **verify your task file** against the returned `topologyHash` / `dataHash` (section 2.7a) before doing any work. -2. **Get a starting point** - derive your identity's virtual root, or fetch an existing node you want - to extend (`REQUEST_ANT_PARENT_ANN`). +2. **Get a starting point** - derive the epoch's shared virtual root (from the spectrum digest), or + fetch an existing node you want to extend (`REQUEST_ANT_PARENT_ANN`). 3. **Pick a parent** - the root, or any node in your own tree. 4. **Search** - choose a nonce (section 2.2), inherit the parent LUT, run the mutation walk, score (section 2.3). @@ -153,17 +154,21 @@ knobs are not two solutions. ### 2.3 Scoring - bpp9000 (must be bit-exact) Throughout, `publicKey` is the **mining identity you are extending** - the computor you mine for, which -becomes the transaction's `sourcePublicKey`. Derive the root and the mutation seed from **that** key, -not your worker key, or the node's recompute will not match yours. +becomes the transaction's `sourcePublicKey`. The **mutation seed** derives from **that** key, not your +worker key, or the node's recompute will not match yours. The **root** derives from no key at all - +see below. -**Root.** `deriveRootANN(publicKey, epochPool)`: `K12(publicKey)` seeds a per-neuron LUT from the -epoch's random pool (the pool comes from the epoch-start spectrum digest). No mutation walk. Never -stored. The same every time for the epoch. +**Root.** `deriveRootANN(spectrumDigest, epochPool)`: `K12(spectrumDigest)` - the epoch-start +spectrum digest from the epoch context - seeds a per-neuron LUT from the epoch's random pool (the +pool itself also comes from that digest). No mutation walk. Never stored. **One root per epoch, +identical for every identity**; per-identity variation enters only through the mutation seeds. **Child.** `computeScoreFromParent(parentLUT, publicKey, nonce, anchorTickDigest)`: 1. Inherit `parentLUT`. -2. `mutationSeed = K12(publicKey || nonce[3..31] || anchorTickDigest)` (`nonce[0..2]` zeroed). +2. `mutationSeed = K12(publicKey || nonce || anchorTickDigest)` with `nonce[0..2]` zeroed in place + (the full 32-byte nonce is hashed, its first 3 bytes set to 0, not dropped) - still keyed by the + mining identity, so different identities walk differently from the shared root. 3. Walk `numberOfMutations = 100` steps. Each step rewrites `L` LUT entries. For the first `K` steps accept a worse-or-equal result (**explore**); after that accept only better-or-equal (**exploit**); one-step rollback on reject. Keep and return the **best** score seen. The best is seeded with the @@ -181,8 +186,9 @@ Score is an error count in `[0, 8088]`; lower is better. ### 2.4 Accept rules -A submission is accepted (`Valid` or `ValidNotStored`) only if **all** of these hold. The node checks -in this order; the first failure is the reject reason: +A submission is accepted (`Valid` or `ValidNotStored`) only if **all** of these hold. On failure the +reject reason names the rule the node found violated (the exact evaluation order is an internal +detail and can change): | Check | Reject reason if it fails | |-------|---------------------------| @@ -201,14 +207,15 @@ recorded but the **deposit is kept** and the miner is **not ranked**. **Starting a tree.** The root's record score is the worst possible value, so a first (depth-1) child passes the "beats parent" check trivially - the **threshold is the only score gate** for starting a -tree. A random root scores far above the threshold, so a start still requires real mutation. +tree. The shared epoch root scores far above the threshold, so a start still requires real mutation - +and every identity starts from the same score, so ranking differences reflect search effort only. **`ValidNotStored`.** Accepted, refunded, and ranked exactly like `Valid`, but the per-epoch store was full so the node was not persisted for others to extend. Ranking and refund are unaffected. ### 2.5 The deposit -Every on-chain `AntColonyMiningSolutionTransaction` carries a **1,000,000 QU** deposit +Every on-chain `AntColonyMiningSolutionTransaction` carries a **1000000 QU** deposit (`SOLUTION_SECURITY_DEPOSIT`), funded by the **computor** that publishes it - not the miner (see 2.6). It is refunded **iff** the solution is accepted (`Valid` / `ValidNotStored`) **and** the claimed score equals the node's recompute; otherwise it is kept. So a computor risks its own deposit and therefore @@ -232,8 +239,8 @@ BroadcastMessage { // 96-byte envelope } // then the payload: AntSolutionBroadcastPayload { // 48 bytes - unsigned int parentTick; // ABSOLUTE tick of the parent node (0 with the index below = root) - unsigned int parentSolutionIndexInTick; + unsigned int parentTick; // ABSOLUTE tick of the parent node; the virtual root is (0, 0xFFFFFFFF) + unsigned int parentSolutionIndexInTick; // parent's index within its tick; 0xFFFFFFFF with parentTick 0 = root unsigned int anchorTick; // ABSOLUTE tick number unsigned int claimedScore; m256i nonce; // the 32-byte nonce from 2.2 @@ -254,7 +261,7 @@ AntColonyMiningSolutionTransaction : Transaction { // 80-byte header + 48-byte // --- Transaction header --- m256i sourcePublicKey; // the COMPUTOR (tree owner); signs the tx and funds the deposit m256i destinationPublicKey; // zero (NULL_ID) - long long amount; // SOLUTION_SECURITY_DEPOSIT = 1,000,000 QU + long long amount; // SOLUTION_SECURITY_DEPOSIT = 1000000 QU unsigned int tick; // publish tick unsigned short inputType; // ANT_COLONY_MINING_SOLUTION_INPUT_TYPE = 12 unsigned short inputSize; // 48 @@ -281,6 +288,37 @@ becomes `sourcePublicKey` of the transaction. Workers hold no tree and post no o hand solutions to the pool's computor, which pre-validates them and risks its own deposit only on solutions it expects to be accepted and refunded. Fund the computor identity, not the workers. +### 2.6a Submitting a root (depth-1) solution + +A root solution starts a tree: its parent is the epoch's shared virtual root, which is derived rather +than stored, so you never fetch it. This is the first solution every identity submits, and it differs +from extending an existing node only in how the parent is named and scored. + +1. **Parent is the root.** Set `parentRef = (parentTick = 0, parentSolutionIndexInTick = 0xFFFFFFFF)`. + Both fields are load-bearing: `(0, 0xFFFFFFFF)` is the only value the node reads as root; a `0` + tick with any other index is treated as a normal parent, found nowhere, and rejected with + `RejectParentNotRegistered`. +2. **Derive the parent LUT yourself.** `deriveRootANN(spectrumDigest, epochPool)` from the epoch + context (section 2.3) - do **not** call `REQUEST_ANT_PARENT_ANN` for the root; it answers + `status = IS_ROOT` with no ANN payload precisely so you derive it locally. The root is identical + for every identity. +3. **Search.** Pick a canonical nonce (section 2.2), inherit the derived root LUT, run the walk, and + take the best score - exactly as for any parent. +4. **The only score gate is the threshold.** The root's record score is the worst possible value, so + the "strictly beats the parent" rule passes trivially; a root child is accepted on score iff its + score is `<=` the epoch threshold. The shared root scores far above the threshold, so a valid + start still requires real mutation - and since every identity starts from the same root score, + ranking reflects search effort alone. +5. **Anchor and submit.** Choose a non-empty anchor tick within `freshnessWindow` of the publish tick + (section 2.3), fill the payload with the root `parentRef` above, and hand it to your computor + (section 2.6, stage 1). The computor publishes it as the usual `AntColonyMiningSolutionTransaction`. + +On the node, a root submission is recognized by `parentRef.isRoot()`: the parent lookup returns a +null record (root is not a stored solution), the node derives the shared root itself to recompute +your `claimedScore`, and root children are de-duplicated per miner because the root is shared by all +identities. Once accepted, the node becomes a normal parent - extend it by copying its `selfTick` / +`selfSolutionIndexInTick` from the identity-tree query (section 2.7b) into a child's `parentRef`. + ### 2.7 Read queries Three request/response pairs. **Identity tree** and **parent ANN** are **operator-signed**; **epoch @@ -301,7 +339,7 @@ signature = sign(operatorSubseed, operatorPublicKey, digest) // 64 bytes, appe Request: empty. Response `RespondAntEpochContext` (120 bytes, packed): ``` -m256i spectrumDigest; // epoch-start spectrum digest (seeds every root) +m256i spectrumDigest; // epoch-start spectrum digest; IS the root seed (and seeds the pool) m256i topologyHash; // canonical task topology-block hash (BPP9000_TOPOLOGY_HASH) m256i dataHash; // canonical task data-block hash (BPP9000_DATA_HASH) unsigned int threshold; // per-epoch accept bound @@ -364,11 +402,11 @@ byte, the exact form the scorer consumes - no unpacking needed): unsigned int parentRefTick; unsigned int parentRefSolutionIndexInTick; unsigned int annSizeBytes; // ANN LUT size when status is OK, else 0 -unsigned char status; // 0 = OK, 1 = NOT_FOUND, 2 = IS_ROOT (derive your own root instead) +unsigned char status; // 0 = OK, 1 = NOT_FOUND, 2 = IS_ROOT (derive the epoch root instead) unsigned char padding[3]; ``` -**Canonical ANN layout.** The same byte form is used everywhere ANN bytes leave the node: this response, the snapshot pool, and the epoch export. Under the current bpp9000 parameters it is 1'728 bytes = 64 rows of 27: +**Canonical ANN layout.** The same byte form is used everywhere ANN bytes leave the node: this response, the snapshot pool, and the epoch export. Under the current bpp9000 parameters it is 1728 bytes = 64 rows of 27: ``` row k, k = 0..45 LUT of neuron updatedNeuronIndices[k]: the k-th NON-INPUT neuron in diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eb4cb27c..7cf11416 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -395,6 +395,10 @@ if(IS_CLANG) endif() target_link_libraries(qubic-cli PRIVATE m) +if(ANT_WALKER) + target_compile_definitions(Qubic PRIVATE ANT_WALKER) +endif() + # Configure linker settings based on compiler if(IS_MSVC) if(NOT LITE_WASM_SC) diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 911cae3e..3db1b943 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -26,7 +26,6 @@ - diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index b222cf7d..6bfbe322 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -242,9 +242,6 @@ contracts - - contracts - platform diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 074a6dd6..91e2f122 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -145,11 +145,7 @@ #define CONTRACT_INDEX QSWAP_CONTRACT_INDEX #define CONTRACT_STATE_TYPE QSWAP #define CONTRACT_STATE2_TYPE QSWAP2 -#ifdef OLD_QSWAP -#include "contracts/Qswap_old.h" -#else #include "contracts/Qswap.h" -#endif #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE diff --git a/src/contracts/QIP.h b/src/contracts/QIP.h index bec11e1e..de4483d2 100644 --- a/src/contracts/QIP.h +++ b/src/contracts/QIP.h @@ -29,6 +29,7 @@ enum QIPLogInfo { QIP_invalidIssuer = 15, QIP_qxAskOrderFound = 16, QIP_icoAlreadyExists = 17, + QIP_returnFundsError = 18, }; struct QIPLogger @@ -715,9 +716,16 @@ struct QIP : public ContractBase } } - if (locals.log._type == QIP_success && locals.buyerInfo.isReturned) + if (locals.log._type == QIP_success) { - locals.log._type = QIPLogInfo::QIP_fundsAlreadyReturned; + if (qpi.epoch() <= locals.ico.startEpoch + 2) + { + locals.log._type = QIPLogInfo::QIP_returnFundsError; + } + else if (locals.buyerInfo.isReturned) + { + locals.log._type = QIPLogInfo::QIP_fundsAlreadyReturned; + } } if (locals.log._type != QIP_success) diff --git a/src/contracts/Qswap_old.h b/src/contracts/Qswap_old.h deleted file mode 100644 index ecf63457..00000000 --- a/src/contracts/Qswap_old.h +++ /dev/null @@ -1,2436 +0,0 @@ -using namespace QPI; - -// Log types enum for QSWAP contract -enum QSWAPLogInfo { - QSWAPAddLiquidity = 4, - QSWAPRemoveLiquidity = 5, - QSWAPSwapExactQuForAsset = 6, - QSWAPSwapQuForExactAsset = 7, - QSWAPSwapExactAssetForQu = 8, - QSWAPSwapAssetForExactQu = 9, - QSWAPFailedDistribution = 10, -}; - -// FIXED CONSTANTS -constexpr uint64 QSWAP_INITIAL_MAX_POOL = 16384; -constexpr uint64 QSWAP_MAX_POOL = QSWAP_INITIAL_MAX_POOL * X_MULTIPLIER; -constexpr uint64 QSWAP_MAX_USER_PER_POOL = 256; -constexpr sint64 QSWAP_MIN_LIQUIDITY = 1000; -constexpr uint32 QSWAP_SWAP_FEE_BASE = 10000; -constexpr uint32 QSWAP_FEE_BASE_100 = 100; - -struct QSWAP2 -{ -}; - -struct QSWAP : public ContractBase -{ -public: - // Logging message structures - struct AddLiquidityMessage - { - uint32 _contractIndex; - uint32 _type; - id assetIssuer; - uint64 assetName; - sint64 userIncreaseLiquidity; - sint64 quAmount; - sint64 assetAmount; - sint8 _terminator; - }; - - struct RemoveLiquidityMessage - { - uint32 _contractIndex; - uint32 _type; - sint64 quAmount; - sint64 assetAmount; - sint8 _terminator; - }; - - struct SwapMessage - { - uint32 _contractIndex; - uint32 _type; - id assetIssuer; - uint64 assetName; - sint64 assetAmountIn; - sint64 assetAmountOut; - sint8 _terminator; - }; - - struct FailedDistributionMessage - { - uint32 _contractIndex; - uint32 _type; - id dst; - uint64 amount; - sint8 _terminator; - }; - - // Types used by state fields - struct PoolBasicState - { - id poolID; - sint64 reservedQuAmount; - sint64 reservedAssetAmount; - sint64 totalLiquidity; - }; - - struct LiquidityInfo - { - id entity; - sint64 liquidity; - }; - - struct StateData - { - uint32 swapFeeRate; // e.g. 30: 0.3% (base: 10_000) - uint32 investRewardsFeeRate;// 3: 3% of swap fees to Invest & Rewards (base: 100) - uint32 shareholderFeeRate; // 27: 27% of swap fees to SC shareholders (base: 100) - uint32 poolCreationFeeRate; // e.g. 10: 10% (base: 100) - - id investRewardsId; - uint64 investRewardsEarnedFee; - uint64 investRewardsDistributedAmount; - - uint64 shareholderEarnedFee; - uint64 shareholderDistributedAmount; - - Array mPoolBasicStates; - Collection mLiquidities; - - uint32 qxFeeRate; // 5: 5% of swap fees to QX (base: 100) - uint32 burnFeeRate; // 1: 1% of swap fees burned (base: 100) - - uint64 qxEarnedFee; - uint64 qxDistributedAmount; - - uint64 burnEarnedFee; // Total burn fees collected (to be burned in END_TICK) - uint64 burnedAmount; // Total amount actually burned - - uint32 cachedIssuanceFee; - uint32 cachedTransferFee; - }; - - struct Fees_input - { - }; - struct Fees_output - { - uint32 assetIssuanceFee; // Amount of qus - uint32 poolCreationFee; // Amount of qus - uint32 transferFee; // Amount of qus - - uint32 swapFee; // 30 -> 0.3% - uint32 shareholderFee; // 27 -> 27% of swap fee, for SC shareholders - uint32 investRewardsFee; // 3 -> 3% of swap fee, for Invest & Rewards - uint32 qxFee; // 5 -> 5% of swap fee, for QX - uint32 burnFee; // 1 -> 1% of swap fee, burned - }; - - struct InvestRewardsInfo_input - { - }; - struct InvestRewardsInfo_output - { - uint32 investRewardsFee; // 3 -> 3% of swap fee - id investRewardsId; - }; - - struct SetInvestRewardsInfo_input - { - id newInvestRewardsId; - }; - struct SetInvestRewardsInfo_output - { - bit success; - }; - - struct GetPoolBasicState_input - { - id assetIssuer; - uint64 assetName; - }; - struct GetPoolBasicState_output - { - sint64 poolExists; - sint64 reservedQuAmount; - sint64 reservedAssetAmount; - sint64 totalLiquidity; - }; - - struct GetLiquidityOf_input - { - id assetIssuer; - uint64 assetName; - id account; - }; - struct GetLiquidityOf_output - { - sint64 liquidity; - }; - - struct QuoteExactQuInput_input - { - id assetIssuer; - uint64 assetName; - sint64 quAmountIn; - }; - struct QuoteExactQuInput_output - { - sint64 assetAmountOut; - }; - - struct QuoteExactQuOutput_input{ - id assetIssuer; - uint64 assetName; - sint64 quAmountOut; - }; - struct QuoteExactQuOutput_output - { - sint64 assetAmountIn; - }; - - struct QuoteExactAssetInput_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountIn; - }; - struct QuoteExactAssetInput_output - { - sint64 quAmountOut; - }; - - struct QuoteExactAssetOutput_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountOut; - }; - struct QuoteExactAssetOutput_output - { - sint64 quAmountIn; - }; - - struct IssueAsset_input - { - uint64 assetName; - sint64 numberOfShares; - uint64 unitOfMeasurement; - sint8 numberOfDecimalPlaces; - }; - struct IssueAsset_output - { - sint64 issuedNumberOfShares; - }; - - struct CreatePool_input - { - id assetIssuer; - uint64 assetName; - }; - struct CreatePool_output - { - bit success; - }; - - struct TransferShareOwnershipAndPossession_input - { - id assetIssuer; - uint64 assetName; - id newOwnerAndPossessor; - sint64 amount; - }; - struct TransferShareOwnershipAndPossession_output - { - sint64 transferredAmount; - }; - - /** - * @param quAmountADesired The amount of tokenA to add as liquidity if the B/A price is <= amountBDesired/amountADesired (A depreciates). - * @param assetAmountBDesired The amount of tokenB to add as liquidity if the A/B price is <= amountADesired/amountBDesired (B depreciates). - * @param quAmountMin Bounds the extent to which the B/A price can go up before the transaction reverts. Must be <= amountADesired. - * @param assetAmountMin Bounds the extent to which the A/B price can go up before the transaction reverts. Must be <= amountBDesired. - */ - struct AddLiquidity_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountDesired; - sint64 quAmountMin; - sint64 assetAmountMin; - }; - struct AddLiquidity_output - { - sint64 userIncreaseLiquidity; - sint64 quAmount; - sint64 assetAmount; - }; - - struct RemoveLiquidity_input - { - id assetIssuer; - uint64 assetName; - sint64 burnLiquidity; - sint64 quAmountMin; - sint64 assetAmountMin; - }; - - struct RemoveLiquidity_output - { - sint64 quAmount; - sint64 assetAmount; - }; - - struct SwapExactQuForAsset_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountOutMin; - }; - struct SwapExactQuForAsset_output - { - sint64 assetAmountOut; - }; - - struct SwapQuForExactAsset_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountOut; - }; - struct SwapQuForExactAsset_output - { - sint64 quAmountIn; - }; - - struct SwapExactAssetForQu_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountIn; - sint64 quAmountOutMin; - }; - struct SwapExactAssetForQu_output - { - sint64 quAmountOut; - }; - - struct SwapAssetForExactQu_input - { - id assetIssuer; - uint64 assetName; - sint64 assetAmountInMax; - sint64 quAmountOut; - }; - struct SwapAssetForExactQu_output - { - sint64 assetAmountIn; - }; - - struct TransferShareManagementRights_input - { - Asset asset; - sint64 numberOfShares; - uint32 newManagingContractIndex; - }; - struct TransferShareManagementRights_output - { - sint64 transferredNumberOfShares; - }; - -protected: - - - inline static sint64 min(sint64 a, sint64 b) - { - return (a < b) ? a : b; - } - - // find the sqrt of a*b - inline static sint64 sqrt(sint64& a, sint64& b, uint128& prod, uint128& y, uint128& z) - { - if (a == b) - { - return a; - } - - prod = uint128(a) * uint128(b); - - y = uint128(0); - - z = uint128(1) << uint128(0, 126); - while (z > prod) - { - z >>= uint128(0, 2); - } - - while (z) - { - if (prod >= y + z) - { - prod -= y + z; - y = (y >> uint128(0, 1)) + z; - } - else - { - y >>= uint128(0, 1); - } - z >>= uint128(0, 2); - } - - return sint64(y.low); - } - - inline static sint64 quoteEquivalentAmountB(sint64& amountADesired, sint64& reserveA, sint64& reserveB, uint128& tmpRes) - { - // amountDesired * reserveB / reserveA - tmpRes = div(uint128(amountADesired) * uint128(reserveB), uint128(reserveA)); - - if ((tmpRes.high != 0)|| (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) - { - return -1; - } - else - { - return sint64(tmpRes.low); - } - } - - // reserveIn * reserveOut = (reserveIn + amountIn * (1-fee)) * (reserveOut - x) - // x = reserveOut * amountIn * (1-fee) / (reserveIn + amountIn * (1-fee)) - inline static sint64 getAmountOutTakeFeeFromInToken( - sint64& amountIn, - sint64& reserveIn, - sint64& reserveOut, - uint32 fee, - uint128& amountInWithFee, - uint128& numerator, - uint128& denominator, - uint128& tmpRes - ) - { - if (amountIn >= MAX_AMOUNT) return -1; - - amountInWithFee = uint128(amountIn) * uint128(QSWAP_SWAP_FEE_BASE - fee); - numerator = uint128(reserveOut) * amountInWithFee; - denominator = uint128(reserveIn) * uint128(QSWAP_SWAP_FEE_BASE) + amountInWithFee; - - // numerator / denominator - tmpRes = div(numerator, denominator); - if ((tmpRes.high != 0) || (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) - { - return -1; - } - else - { - return sint64(tmpRes.low); - } - } - - // reserveIn * reserveOut = (reserveIn + x * (1-fee)) * (reserveOut - amountOut) - // x = (reserveIn * amountOut * 10000) / ((reserveOut - amountOut) * (10000 - fee)) - inline static sint64 getAmountInTakeFeeFromInToken(sint64& amountOut, sint64& reserveIn, sint64& reserveOut, uint32 fee, uint128& numerator, uint128& denominator, uint128& tmpRes) - { - if (amountOut >= MAX_AMOUNT) return -1; - - // Calculate full numerator first to avoid premature truncation - numerator = uint128(reserveIn) * uint128(amountOut) * uint128(QSWAP_SWAP_FEE_BASE); - denominator = uint128(reserveOut - amountOut) * uint128(QSWAP_SWAP_FEE_BASE - fee); - - // Perform single division at the end - // Use floor + 1 to ensure user pays at least enough (protects LPs) - tmpRes = div(numerator, denominator) + uint128(1); - - if ((tmpRes.high != 0) || (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) - { - return -1; - } - else - { - return sint64(tmpRes.low); - } - } - - // (reserveIn + amountIn) * (reserveOut - x) = reserveIn * reserveOut - // x = reserveOut * amountIn / (reserveIn + amountIn) - // NOTE: Despite the name, this returns the GROSS output (before fee deduction). - // The fee parameter is unused here because fee is applied separately by the caller. - // This is intentional: the caller needs the gross value for fee distribution calculation. - inline static sint64 getAmountOutTakeFeeFromOutToken(sint64& amountIn, sint64& reserveIn, sint64& reserveOut, uint32 fee, uint128& numerator, uint128& denominator, uint128& tmpRes) - { - if (amountIn >= MAX_AMOUNT) return -1; - - numerator = uint128(reserveOut) * uint128(amountIn); - denominator = uint128(reserveIn + amountIn); - - tmpRes = div(numerator, denominator); - if ((tmpRes.high != 0)|| (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) - { - return -1; - } - else - { - return sint64(tmpRes.low); - } - } - - // (reserveIn + x) * (reserveOut - amountOut/(1 - fee)) = reserveIn * reserveOut - // x = (reserveIn * amountOut * 10000) / (reserveOut * (10000-fee) - amountOut * 10000) - inline static sint64 getAmountInTakeFeeFromOutToken(sint64& amountOut, sint64& reserveIn, sint64& reserveOut, uint32 fee, uint128& numerator, uint128& denominator, uint128& tmpRes) - { - if (amountOut >= MAX_AMOUNT) return -1; - - // Calculate full numerator to avoid premature truncation - numerator = uint128(reserveIn) * uint128(amountOut) * uint128(QSWAP_SWAP_FEE_BASE); - - // Check: reserveOut * (1-fee) must be greater than amountOut - // Scale reserveOut by (10000-fee) and amountOut by 10000 for comparison - // Use tmpRes and denominator temporarily for the comparison - tmpRes = uint128(reserveOut) * uint128(QSWAP_SWAP_FEE_BASE - fee); - denominator = uint128(amountOut) * uint128(QSWAP_SWAP_FEE_BASE); - - if (tmpRes <= denominator) - { - return -1; - } - - denominator = tmpRes - denominator; - - // Use floor + 1 to ensure user pays at least enough (protects LPs) - tmpRes = div(numerator, denominator) + uint128(1); - - if ((tmpRes.high != 0) || (tmpRes.low > 0x7FFFFFFFFFFFFFFF)) - { - return -1; - } - else - { - return sint64(tmpRes.low); - } - } - - PUBLIC_FUNCTION(Fees) - { - output.assetIssuanceFee = state.get().cachedIssuanceFee; - output.poolCreationFee = uint32(div(uint64(state.get().cachedIssuanceFee) * uint64(state.get().poolCreationFeeRate), uint64(QSWAP_FEE_BASE_100))); - output.transferFee = state.get().cachedTransferFee; - output.swapFee = state.get().swapFeeRate; - output.shareholderFee = state.get().shareholderFeeRate; - output.investRewardsFee = state.get().investRewardsFeeRate; - output.qxFee = state.get().qxFeeRate; - output.burnFee = state.get().burnFeeRate; - } - - struct GetPoolBasicState_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - uint32 i0; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(GetPoolBasicState) - { - output.poolExists = 0; - output.totalLiquidity = -1; - output.reservedAssetAmount = -1; - output.reservedQuAmount = -1; - - // asset not issued - if (!qpi.isAssetIssued(input.assetIssuer, input.assetName)) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = NULL_INDEX; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == NULL_INDEX) - { - return; - } - - output.poolExists = 1; - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - output.reservedQuAmount = locals.poolBasicState.reservedQuAmount; - output.reservedAssetAmount = locals.poolBasicState.reservedAssetAmount; - output.totalLiquidity = locals.poolBasicState.totalLiquidity; - } - - struct GetLiquidityOf_locals - { - id poolID; - sint64 liqElementIndex; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(GetLiquidityOf) - { - output.liquidity = 0; - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.liqElementIndex = state.get().mLiquidities.headIndex(locals.poolID, 0); - - while (locals.liqElementIndex != NULL_INDEX) - { - if (state.get().mLiquidities.element(locals.liqElementIndex).entity == input.account) - { - output.liquidity = state.get().mLiquidities.element(locals.liqElementIndex).liquidity; - return; - } - locals.liqElementIndex = state.get().mLiquidities.nextElementIndex(locals.liqElementIndex); - } - } - - struct QuoteExactQuInput_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - - uint32 i0; - uint128 i1, i2, i3, i4; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(QuoteExactQuInput) - { - output.assetAmountOut = -1; - - if (input.quAmountIn <= 0) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - // no available slot for new pool - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // no liquidity in the pool - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - output.assetAmountOut = getAmountOutTakeFeeFromInToken( - input.quAmountIn, - locals.poolBasicState.reservedQuAmount, - locals.poolBasicState.reservedAssetAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3, - locals.i4 - ); - } - - struct QuoteExactQuOutput_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - - uint32 i0; - uint128 i1, i2, i3; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(QuoteExactQuOutput) - { - output.assetAmountIn = -1; - - if (input.quAmountOut <= 0) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - // no available slot for new pool - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // no liquidity in the pool - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - if (input.quAmountOut >= locals.poolBasicState.reservedQuAmount) - { - return; - } - - output.assetAmountIn = getAmountInTakeFeeFromOutToken( - input.quAmountOut, - locals.poolBasicState.reservedAssetAmount, - locals.poolBasicState.reservedQuAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - } - - struct QuoteExactAssetInput_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - sint64 quAmountOutWithFee; - - uint32 i0; - uint128 i1, i2, i3; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(QuoteExactAssetInput) - { - output.quAmountOut = -1; - - if (input.assetAmountIn <= 0) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - // no available slot for new pool - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // no liquidity in the pool - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - locals.quAmountOutWithFee = getAmountOutTakeFeeFromOutToken( - input.assetAmountIn, - locals.poolBasicState.reservedAssetAmount, - locals.poolBasicState.reservedQuAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - - // above call overflow - if (locals.quAmountOutWithFee == -1) - { - return; - } - - // amount * (1-fee), no overflow risk - output.quAmountOut = sint64(div( - uint128(locals.quAmountOutWithFee) * uint128(QSWAP_SWAP_FEE_BASE - state.get().swapFeeRate), - uint128(QSWAP_SWAP_FEE_BASE) - ).low); - } - - struct QuoteExactAssetOutput_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - - uint32 i0; - uint128 i1, i2, i3; - }; - - PUBLIC_FUNCTION_WITH_LOCALS(QuoteExactAssetOutput) - { - output.quAmountIn = -1; - - if (input.assetAmountOut <= 0) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - // no available slot for new pool - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // no liquidity in the pool - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - if (input.assetAmountOut >= locals.poolBasicState.reservedAssetAmount) - { - return; - } - - output.quAmountIn = getAmountInTakeFeeFromInToken( - input.assetAmountOut, - locals.poolBasicState.reservedQuAmount, - locals.poolBasicState.reservedAssetAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - } - - PUBLIC_FUNCTION(InvestRewardsInfo) - { - output.investRewardsFee = state.get().investRewardsFeeRate; - output.investRewardsId = state.get().investRewardsId; - } - -// -// procedure -// - PUBLIC_PROCEDURE(IssueAsset) - { - output.issuedNumberOfShares = 0; - if ((qpi.invocationReward() < state.get().cachedIssuanceFee)) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - // check the validity of input - if ((input.numberOfShares <= 0) || (input.numberOfDecimalPlaces < 0)) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - // asset already issued - if (qpi.isAssetIssued(qpi.invocator(), input.assetName)) - { - if (qpi.invocationReward() > 0 ) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - output.issuedNumberOfShares = qpi.issueAsset( - input.assetName, - qpi.invocator(), - input.numberOfDecimalPlaces, - input.numberOfShares, - input.unitOfMeasurement - ); - - if (output.issuedNumberOfShares == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - else - { - if (qpi.invocationReward() > state.get().cachedIssuanceFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.get().cachedIssuanceFee); - } - state.mut().shareholderEarnedFee += state.get().cachedIssuanceFee; - } - } - - struct CreatePool_locals - { - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - uint32 poolCreationFee; - - uint32 i0, i1; - }; - - // create uniswap like pool - // TODO: reject if there is no shares avaliabe shares in current contract, e.g. asset is issue in contract qx - PUBLIC_PROCEDURE_WITH_LOCALS(CreatePool) - { - output.success = false; - - locals.poolCreationFee = uint32(div(uint64(state.get().cachedIssuanceFee) * uint64(state.get().poolCreationFeeRate), uint64(QSWAP_FEE_BASE_100))); - - // fee check - if (qpi.invocationReward() < locals.poolCreationFee) - { - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - // asset no exist - if (!qpi.isAssetIssued(input.assetIssuer, input.assetName)) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - // check if pool already exist - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL ; locals.i0 ++ ) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - } - - // find an vacant pool slot - locals.poolSlot = -1; - for (locals.i1 = 0; locals.i1 < QSWAP_MAX_POOL; locals.i1 ++) - { - if (state.get().mPoolBasicStates.get(locals.i1).poolID == id(0,0,0,0)) - { - locals.poolSlot = locals.i1; - break; - } - } - - // no available slot for new pool - if (locals.poolSlot == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolBasicState.poolID = locals.poolID; - locals.poolBasicState.reservedAssetAmount = 0; - locals.poolBasicState.reservedQuAmount = 0; - locals.poolBasicState.totalLiquidity = 0; - - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - if(qpi.invocationReward() > locals.poolCreationFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.poolCreationFee ); - } - state.mut().shareholderEarnedFee += locals.poolCreationFee; - - output.success = true; - } - - - struct AddLiquidity_locals - { - AddLiquidityMessage addLiquidityMessage; - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - LiquidityInfo tmpLiquidity; - - sint64 userLiquidityElementIndex; - sint64 quAmountDesired; - - sint64 quTransferAmount; - sint64 assetTransferAmount; - sint64 quOptimalAmount; - sint64 assetOptimalAmount; - sint64 increaseLiquidity; - sint64 reservedAssetAmountBefore; - sint64 reservedAssetAmountAfter; - - uint128 tmpIncLiq0; - uint128 tmpIncLiq1; - - uint32 i0; - uint128 i1, i2, i3; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(AddLiquidity) - { - output.userIncreaseLiquidity = 0; - output.assetAmount = 0; - output.quAmount = 0; - - // add liquidity must stake both qu and asset - if (qpi.invocationReward() <= 0) - { - return; - } - - locals.quAmountDesired = qpi.invocationReward(); - - // check the vadility of input params - if ((input.assetAmountDesired <= 0) || - (input.quAmountMin < 0) || - (input.assetAmountMin < 0)) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - // check the pool existance - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // check if pool state meet the input condition before desposit - // and confirm the final qu and asset amount to stake - if (locals.poolBasicState.totalLiquidity == 0) - { - locals.quTransferAmount = locals.quAmountDesired; - locals.assetTransferAmount = input.assetAmountDesired; - } - else - { - locals.assetOptimalAmount = quoteEquivalentAmountB( - locals.quAmountDesired, - locals.poolBasicState.reservedQuAmount, - locals.poolBasicState.reservedAssetAmount, - locals.i1 - ); - // overflow - if (locals.assetOptimalAmount == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return ; - } - - if (locals.assetOptimalAmount <= input.assetAmountDesired ) - { - if (locals.assetOptimalAmount < input.assetAmountMin) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return ; - } - locals.quTransferAmount = locals.quAmountDesired; - locals.assetTransferAmount = locals.assetOptimalAmount; - } - else - { - locals.quOptimalAmount = quoteEquivalentAmountB( - input.assetAmountDesired, - locals.poolBasicState.reservedAssetAmount, - locals.poolBasicState.reservedQuAmount, - locals.i1 - ); - // overflow - if (locals.quOptimalAmount == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return ; - } - if (locals.quOptimalAmount > locals.quAmountDesired) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return ; - } - if (locals.quOptimalAmount < input.quAmountMin) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return ; - } - locals.quTransferAmount = locals.quOptimalAmount; - locals.assetTransferAmount = input.assetAmountDesired; - } - } - - // check if the qu is enough - if (qpi.invocationReward() < locals.quTransferAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // check if the asset is enough - if (qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - SELF_INDEX, - SELF_INDEX - ) < locals.assetTransferAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // for pool's initial mint - if (locals.poolBasicState.totalLiquidity == 0) - { - locals.increaseLiquidity = sqrt(locals.quTransferAmount, locals.assetTransferAmount, locals.i1, locals.i2, locals.i3); - - if (locals.increaseLiquidity < QSWAP_MIN_LIQUIDITY ) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.reservedAssetAmountBefore = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - locals.assetTransferAmount, - SELF - ); - locals.reservedAssetAmountAfter = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - - if (locals.reservedAssetAmountAfter - locals.reservedAssetAmountBefore < locals.assetTransferAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // permanently lock the first MINIMUM_LIQUIDITY tokens - locals.tmpLiquidity.entity = SELF; - locals.tmpLiquidity.liquidity = QSWAP_MIN_LIQUIDITY; - state.mut().mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); - - locals.tmpLiquidity.entity = qpi.invocator(); - locals.tmpLiquidity.liquidity = locals.increaseLiquidity - QSWAP_MIN_LIQUIDITY; - state.mut().mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); - - output.quAmount = locals.quTransferAmount; - output.assetAmount = locals.assetTransferAmount; - output.userIncreaseLiquidity = locals.increaseLiquidity - QSWAP_MIN_LIQUIDITY; - } - else - { - locals.tmpIncLiq0 = div( - uint128(locals.quTransferAmount) * uint128(locals.poolBasicState.totalLiquidity), - uint128(locals.poolBasicState.reservedQuAmount) - ); - if (locals.tmpIncLiq0.high != 0 || locals.tmpIncLiq0.low > 0x7FFFFFFFFFFFFFFF) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - locals.tmpIncLiq1 = div( - uint128(locals.assetTransferAmount) * uint128(locals.poolBasicState.totalLiquidity), - uint128(locals.poolBasicState.reservedAssetAmount) - ); - if (locals.tmpIncLiq1.high != 0 || locals.tmpIncLiq1.low > 0x7FFFFFFFFFFFFFFF) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // increaseLiquity = min( - // quTransferAmount * totalLiquity / reserveQuAmount, - // assetTransferAmount * totalLiquity / reserveAssetAmount - // ); - locals.increaseLiquidity = min(sint64(locals.tmpIncLiq0.low), sint64(locals.tmpIncLiq1.low)); - - // maybe too little input - if (locals.increaseLiquidity == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // find user liquidity index - locals.userLiquidityElementIndex = state.get().mLiquidities.headIndex(locals.poolID, 0); - while (locals.userLiquidityElementIndex != NULL_INDEX) - { - if(state.get().mLiquidities.element(locals.userLiquidityElementIndex).entity == qpi.invocator()) - { - break; - } - - locals.userLiquidityElementIndex = state.get().mLiquidities.nextElementIndex(locals.userLiquidityElementIndex); - } - - // no more space for new liquidity item - if ((locals.userLiquidityElementIndex == NULL_INDEX) && ( state.get().mLiquidities.population() == state.get().mLiquidities.capacity())) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // transfer the asset from invocator to contract - locals.reservedAssetAmountBefore = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - locals.assetTransferAmount, - SELF - ); - locals.reservedAssetAmountAfter = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - - // only trust the amount in the contract - if (locals.reservedAssetAmountAfter - locals.reservedAssetAmountBefore < locals.assetTransferAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - if (locals.userLiquidityElementIndex == NULL_INDEX) - { - locals.tmpLiquidity.entity = qpi.invocator(); - locals.tmpLiquidity.liquidity = locals.increaseLiquidity; - state.mut().mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); - } - else - { - locals.tmpLiquidity = state.get().mLiquidities.element(locals.userLiquidityElementIndex); - locals.tmpLiquidity.liquidity += locals.increaseLiquidity; - state.mut().mLiquidities.replace(locals.userLiquidityElementIndex, locals.tmpLiquidity); - } - - output.quAmount = locals.quTransferAmount; - output.assetAmount = locals.assetTransferAmount; - output.userIncreaseLiquidity = locals.increaseLiquidity; - } - - locals.poolBasicState.reservedQuAmount += locals.quTransferAmount; - locals.poolBasicState.reservedAssetAmount += locals.assetTransferAmount; - locals.poolBasicState.totalLiquidity += locals.increaseLiquidity; - - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log AddLiquidity procedure - locals.addLiquidityMessage._contractIndex = SELF_INDEX; - locals.addLiquidityMessage._type = QSWAPAddLiquidity; - locals.addLiquidityMessage.assetIssuer = input.assetIssuer; - locals.addLiquidityMessage.assetName = input.assetName; - locals.addLiquidityMessage.userIncreaseLiquidity = output.userIncreaseLiquidity; - locals.addLiquidityMessage.quAmount = output.quAmount; - locals.addLiquidityMessage.assetAmount = output.assetAmount; - LOG_INFO(locals.addLiquidityMessage); - - if (qpi.invocationReward() > locals.quTransferAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.quTransferAmount); - } - } - - struct RemoveLiquidity_locals - { - RemoveLiquidityMessage removeLiquidityMessage; - id poolID; - PoolBasicState poolBasicState; - sint64 userLiquidityElementIndex; - sint64 poolSlot; - LiquidityInfo userLiquidity; - sint64 burnQuAmount; - sint64 burnAssetAmount; - - uint32 i0; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(RemoveLiquidity) - { - output.quAmount = 0; - output.assetAmount = 0; - - if (qpi.invocationReward() > 0 ) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - // check the vadility of input params - if (input.quAmountMin < 0 || input.assetAmountMin < 0 || input.burnLiquidity <= 0) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - // get the pool's basic state - locals.poolSlot = -1; - - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - // the pool does not exsit - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - locals.userLiquidityElementIndex = state.get().mLiquidities.headIndex(locals.poolID, 0); - while (locals.userLiquidityElementIndex != NULL_INDEX) - { - if(state.get().mLiquidities.element(locals.userLiquidityElementIndex).entity == qpi.invocator()) - { - break; - } - - locals.userLiquidityElementIndex = state.get().mLiquidities.nextElementIndex(locals.userLiquidityElementIndex); - } - - if (locals.userLiquidityElementIndex == NULL_INDEX) - { - return; - } - - locals.userLiquidity = state.get().mLiquidities.element(locals.userLiquidityElementIndex); - - // not enough liquidity for burning - if (locals.userLiquidity.liquidity < input.burnLiquidity ) - { - return; - } - - if (locals.poolBasicState.totalLiquidity < input.burnLiquidity ) - { - return; - } - - // since burnLiquidity < totalLiquidity, so there will be no overflow risk - locals.burnQuAmount = sint64(div( - uint128(input.burnLiquidity) * uint128(locals.poolBasicState.reservedQuAmount), - uint128(locals.poolBasicState.totalLiquidity) - ).low); - - // since burnLiquidity < totalLiquidity, so there will be no overflow risk - locals.burnAssetAmount = sint64(div( - uint128(input.burnLiquidity) * uint128(locals.poolBasicState.reservedAssetAmount), - uint128(locals.poolBasicState.totalLiquidity) - ).low); - - - if ((locals.burnQuAmount < input.quAmountMin) || (locals.burnAssetAmount < input.assetAmountMin)) - { - return; - } - - // return qu and asset to invocator - qpi.transfer(qpi.invocator(), locals.burnQuAmount); - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - SELF, - SELF, - locals.burnAssetAmount, - qpi.invocator() - ); - - output.quAmount = locals.burnQuAmount; - output.assetAmount = locals.burnAssetAmount; - - // modify invocator's liquidity info - locals.userLiquidity.liquidity -= input.burnLiquidity; - if (locals.userLiquidity.liquidity == 0) - { - state.mut().mLiquidities.remove(locals.userLiquidityElementIndex); - } - else - { - state.mut().mLiquidities.replace(locals.userLiquidityElementIndex, locals.userLiquidity); - } - - // modify the pool's liquidity info - locals.poolBasicState.totalLiquidity -= input.burnLiquidity; - locals.poolBasicState.reservedQuAmount -= locals.burnQuAmount; - locals.poolBasicState.reservedAssetAmount -= locals.burnAssetAmount; - - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log RemoveLiquidity procedure - locals.removeLiquidityMessage._contractIndex = SELF_INDEX; - locals.removeLiquidityMessage._type = QSWAPRemoveLiquidity; - locals.removeLiquidityMessage.quAmount = output.quAmount; - locals.removeLiquidityMessage.assetAmount = output.assetAmount; - LOG_INFO(locals.removeLiquidityMessage); - } - - struct SwapExactQuForAsset_locals - { - SwapMessage swapMessage; - id poolID; - sint64 poolSlot; - sint64 quAmountIn; - PoolBasicState poolBasicState; - sint64 assetAmountOut; - - uint32 i0; - uint128 i1, i2, i3, i4; - uint128 swapFee; - uint128 feeToInvestRewards; - uint128 feeToShareholders; - - uint128 feeToQx; - uint128 feeToBurn; - - sint64 totalFee; - }; - - // given an input qu amountIn, only execute swap in case (amountOut >= amountOutMin) - // https://docs.uniswap.org/contracts/v2/reference/smart-contracts/router-02#swapexacttokensfortokens - PUBLIC_PROCEDURE_WITH_LOCALS(SwapExactQuForAsset) - { - output.assetAmountOut = 0; - - // require input qu > 0 - if (qpi.invocationReward() <= 0) - { - return; - } - - if (input.assetAmountOutMin < 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.quAmountIn = qpi.invocationReward(); - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // check the liquidity validity - if (locals.poolBasicState.totalLiquidity == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.assetAmountOut = getAmountOutTakeFeeFromInToken( - locals.quAmountIn, - locals.poolBasicState.reservedQuAmount, - locals.poolBasicState.reservedAssetAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3, - locals.i4 - ); - - // overflow - if (locals.assetAmountOut == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // not meet user's amountOut requirement - if (locals.assetAmountOut < input.assetAmountOutMin) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // swapFee = quAmountIn * 0.3% (swapFeeRate/10000) - // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP - locals.swapFee = div(uint128(locals.quAmountIn) * uint128(state.get().swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); - if (locals.swapFee == uint128_t(0)) - { - locals.swapFee = QSWAP_FEE_BASE_100; - } - locals.feeToShareholders = div(locals.swapFee * uint128(state.get().shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToQx = div(locals.swapFee * uint128(state.get().qxFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToInvestRewards = div(locals.swapFee * uint128(state.get().investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToBurn = div(locals.swapFee * uint128(state.get().burnFeeRate), uint128(QSWAP_FEE_BASE_100)); - - locals.totalFee = sint64(locals.feeToShareholders.low) + sint64(locals.feeToQx.low) + sint64(locals.feeToInvestRewards.low) + sint64(locals.feeToBurn.low); - - // Overflow protection: ensure all fees fit in uint64 - if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 - || locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0 - || locals.quAmountIn < locals.totalFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // transfer the asset from pool to qpi.invocator() - output.assetAmountOut = qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - SELF, - SELF, - locals.assetAmountOut, - qpi.invocator() - ) < 0 ? 0: locals.assetAmountOut; - - // in case asset transfer failed - if (output.assetAmountOut == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // update fee state after successful transfer - state.mut().shareholderEarnedFee += locals.feeToShareholders.low; - state.mut().qxEarnedFee += locals.feeToQx.low; - state.mut().investRewardsEarnedFee += locals.feeToInvestRewards.low; - state.mut().burnEarnedFee += locals.feeToBurn.low; - - locals.poolBasicState.reservedQuAmount += locals.quAmountIn - locals.totalFee; - locals.poolBasicState.reservedAssetAmount -= locals.assetAmountOut; - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log SwapExactQuForAsset procedure - locals.swapMessage._contractIndex = SELF_INDEX; - locals.swapMessage._type = QSWAPSwapExactQuForAsset; - locals.swapMessage.assetIssuer = input.assetIssuer; - locals.swapMessage.assetName = input.assetName; - locals.swapMessage.assetAmountIn = locals.quAmountIn; - locals.swapMessage.assetAmountOut = output.assetAmountOut; - LOG_INFO(locals.swapMessage); - } - - struct SwapQuForExactAsset_locals - { - SwapMessage swapMessage; - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - sint64 quAmountIn; - sint64 transferredAssetAmount; - - uint32 i0; - uint128 i1, i2, i3; - uint128 swapFee; - uint128 feeToInvestRewards; - uint128 feeToShareholders; - uint128 feeToQx; - uint128 feeToBurn; - - sint64 totalFee; - }; - - // https://docs.uniswap.org/contracts/v2/reference/smart-contracts/router-02#swaptokensforexacttokens - PUBLIC_PROCEDURE_WITH_LOCALS(SwapQuForExactAsset) - { - output.quAmountIn = 0; - - // require input qu amount > 0 - if (qpi.invocationReward() <= 0) - { - return; - } - - // check input param validity - if (input.assetAmountOut <= 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // check if there is liquidity in the poool - if (locals.poolBasicState.totalLiquidity == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // check if reserved asset is enough - if (input.assetAmountOut >= locals.poolBasicState.reservedAssetAmount) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - locals.quAmountIn = getAmountInTakeFeeFromInToken( - input.assetAmountOut, - locals.poolBasicState.reservedQuAmount, - locals.poolBasicState.reservedAssetAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - - // above call overflow - if (locals.quAmountIn == -1 || locals.quAmountIn > qpi.invocationReward()) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // swapFee = quAmountIn * 0.3% (swapFeeRate/10000) - // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP - locals.swapFee = div(uint128(locals.quAmountIn) * uint128(state.get().swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); - if (locals.swapFee == uint128_t(0)) - { - locals.swapFee = QSWAP_FEE_BASE_100; - } - locals.feeToShareholders = div(locals.swapFee * uint128(state.get().shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToQx = div(locals.swapFee * uint128(state.get().qxFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToInvestRewards = div(locals.swapFee * uint128(state.get().investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToBurn = div(locals.swapFee * uint128(state.get().burnFeeRate), uint128(QSWAP_FEE_BASE_100)); - - locals.totalFee = sint64(locals.feeToShareholders.low) + sint64(locals.feeToQx.low) + sint64(locals.feeToInvestRewards.low) + sint64(locals.feeToBurn.low); - if (locals.quAmountIn < locals.totalFee) - { - qpi.transfer(qpi.invocator(), locals.quAmountIn); - return; - } - - // Overflow protection: ensure all fees fit in uint64 - if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 || - locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // transfer the asset from pool to qpi.invocator() - locals.transferredAssetAmount = qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - SELF, - SELF, - input.assetAmountOut, - qpi.invocator() - ) < 0 ? 0: input.assetAmountOut; - - // asset transfer failed - if (locals.transferredAssetAmount == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - output.quAmountIn = locals.quAmountIn; - if (qpi.invocationReward() > locals.quAmountIn) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.quAmountIn); - } - - // update fee state after successful transfer - state.mut().shareholderEarnedFee += locals.feeToShareholders.low; - state.mut().qxEarnedFee += locals.feeToQx.low; - state.mut().investRewardsEarnedFee += locals.feeToInvestRewards.low; - state.mut().burnEarnedFee += locals.feeToBurn.low; - - locals.poolBasicState.reservedQuAmount += locals.quAmountIn - locals.totalFee; - locals.poolBasicState.reservedAssetAmount -= input.assetAmountOut; - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log SwapQuForExactAsset procedure - locals.swapMessage._contractIndex = SELF_INDEX; - locals.swapMessage._type = QSWAPSwapQuForExactAsset; - locals.swapMessage.assetIssuer = input.assetIssuer; - locals.swapMessage.assetName = input.assetName; - locals.swapMessage.assetAmountIn = output.quAmountIn; - locals.swapMessage.assetAmountOut = input.assetAmountOut; - LOG_INFO(locals.swapMessage); - } - - struct SwapExactAssetForQu_locals - { - SwapMessage swapMessage; - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - sint64 quAmountOut; - sint64 quAmountOutWithFee; - sint64 transferredAssetAmountBefore; - sint64 transferredAssetAmountAfter; - - uint32 i0; - uint128 i1, i2, i3; - uint128 swapFee; - uint128 feeToInvestRewards; - uint128 feeToShareholders; - uint128 feeToQx; - uint128 feeToBurn; - - sint64 totalFee; - }; - - // given an amount of asset swap in, only execute swaping if quAmountOut >= input.amountOutMin - PUBLIC_PROCEDURE_WITH_LOCALS(SwapExactAssetForQu) - { - output.quAmountOut = 0; - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - // check input param validity - if ((input.assetAmountIn <= 0 )||(input.quAmountOutMin < 0)) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // check the liquidity validity - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - // invocator's asset not enough - if (qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - SELF_INDEX, - SELF_INDEX - ) < input.assetAmountIn ) - { - return; - } - - locals.quAmountOutWithFee = getAmountOutTakeFeeFromOutToken( - input.assetAmountIn, - locals.poolBasicState.reservedAssetAmount, - locals.poolBasicState.reservedQuAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - - // above call overflow - if (locals.quAmountOutWithFee == -1) - { - return; - } - - // no overflow risk - // locals.quAmountOutWithFee * (QSWAP_SWAP_FEE_BASE - state.swapFeeRate) / QSWAP_SWAP_FEE_BASE - locals.quAmountOut = sint64(div( - uint128(locals.quAmountOutWithFee) * uint128(QSWAP_SWAP_FEE_BASE - state.get().swapFeeRate), - uint128(QSWAP_SWAP_FEE_BASE) - ).low); - - // not meet user min amountOut requirement - if (locals.quAmountOut < input.quAmountOutMin) - { - return; - } - - // swapFee = quAmountOutWithFee * 0.3% (swapFeeRate/10000) - // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP - locals.swapFee = div(uint128(locals.quAmountOutWithFee) * uint128(state.get().swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE)); - if (locals.swapFee == uint128_t(0)) - { - locals.swapFee = QSWAP_FEE_BASE_100; - } - locals.feeToShareholders = div(locals.swapFee * uint128(state.get().shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToQx = div(locals.swapFee * uint128(state.get().qxFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToInvestRewards = div(locals.swapFee * uint128(state.get().investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToBurn = div(locals.swapFee * uint128(state.get().burnFeeRate), uint128(QSWAP_FEE_BASE_100)); - - // Overflow protection: ensure all fees fit in uint64 - if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 || - locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0) - { - return; - } - - // transfer assets from user to pool - locals.transferredAssetAmountBefore = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - input.assetAmountIn, - SELF - ); - locals.transferredAssetAmountAfter = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - - // pool does not receive enough asset, rollback any received shares - if (locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore < input.assetAmountIn) - { - // return any shares that were transferred - if (locals.transferredAssetAmountAfter > locals.transferredAssetAmountBefore) - { - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - SELF, - SELF, - locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore, - qpi.invocator() - ); - } - return; - } - - qpi.transfer(qpi.invocator(), locals.quAmountOut); - output.quAmountOut = locals.quAmountOut; - - // update fee state after successful transfers - state.mut().shareholderEarnedFee += locals.feeToShareholders.low; - state.mut().qxEarnedFee += locals.feeToQx.low; - state.mut().investRewardsEarnedFee += locals.feeToInvestRewards.low; - state.mut().burnEarnedFee += locals.feeToBurn.low; - - // update pool states - locals.poolBasicState.reservedAssetAmount += input.assetAmountIn; - locals.totalFee = locals.quAmountOut + sint64(locals.feeToShareholders.low) + sint64(locals.feeToQx.low) + sint64(locals.feeToInvestRewards.low) + sint64(locals.feeToBurn.low); - if (locals.poolBasicState.reservedQuAmount < locals.totalFee) - { - locals.poolBasicState.reservedQuAmount = 0; - } - else - { - locals.poolBasicState.reservedQuAmount -= locals.totalFee; - } - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log SwapExactAssetForQu procedure - locals.swapMessage._contractIndex = SELF_INDEX; - locals.swapMessage._type = QSWAPSwapExactAssetForQu; - locals.swapMessage.assetIssuer = input.assetIssuer; - locals.swapMessage.assetName = input.assetName; - locals.swapMessage.assetAmountIn = input.assetAmountIn; - locals.swapMessage.assetAmountOut = output.quAmountOut; - LOG_INFO(locals.swapMessage); - } - - struct SwapAssetForExactQu_locals - { - SwapMessage swapMessage; - id poolID; - sint64 poolSlot; - PoolBasicState poolBasicState; - sint64 assetAmountIn; - sint64 transferredAssetAmountBefore; - sint64 transferredAssetAmountAfter; - - uint32 i0; - uint128 i1, i2, i3; - uint128 swapFee; - uint128 feeToInvestRewards; - uint128 feeToShareholders; - uint128 feeToQx; - uint128 feeToBurn; - - sint64 totalFee; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(SwapAssetForExactQu) - { - output.assetAmountIn = 0; - if (qpi.invocationReward() > 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - - if ((input.assetAmountInMax <= 0 )||(input.quAmountOut <= 0)) - { - return; - } - - locals.poolID = input.assetIssuer; - locals.poolID.u64._3 = input.assetName; - - locals.poolSlot = -1; - for (locals.i0 = 0; locals.i0 < QSWAP_MAX_POOL; locals.i0 ++) - { - if (state.get().mPoolBasicStates.get(locals.i0).poolID == locals.poolID) - { - locals.poolSlot = locals.i0; - break; - } - } - - if (locals.poolSlot == -1) - { - return; - } - - locals.poolBasicState = state.get().mPoolBasicStates.get(locals.poolSlot); - - // check the liquidity validity - if (locals.poolBasicState.totalLiquidity == 0) - { - return; - } - - // pool does not hold enough asset - if (input.quAmountOut >= locals.poolBasicState.reservedQuAmount) - { - return; - } - - locals.assetAmountIn = getAmountInTakeFeeFromOutToken( - input.quAmountOut, - locals.poolBasicState.reservedAssetAmount, - locals.poolBasicState.reservedQuAmount, - state.get().swapFeeRate, - locals.i1, - locals.i2, - locals.i3 - ); - - // invalid input, assetAmountIn overflow - if (locals.assetAmountIn == -1) - { - return; - } - - // user does not hold enough asset - if (qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - SELF_INDEX, - SELF_INDEX - ) < locals.assetAmountIn ) - { - return; - } - - // not meet user amountIn reqiurement - if (locals.assetAmountIn > input.assetAmountInMax) - { - return; - } - - // swapFee = quAmountOut * 30/(10_000 - 30) - // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP - locals.swapFee = div(uint128(input.quAmountOut) * uint128(state.get().swapFeeRate), uint128(QSWAP_SWAP_FEE_BASE - state.get().swapFeeRate)); - if (locals.swapFee == uint128_t(0)) - { - locals.swapFee = QSWAP_FEE_BASE_100; - } - locals.feeToShareholders = div(locals.swapFee * uint128(state.get().shareholderFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToQx = div(locals.swapFee * uint128(state.get().qxFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToInvestRewards = div(locals.swapFee * uint128(state.get().investRewardsFeeRate), uint128(QSWAP_FEE_BASE_100)); - locals.feeToBurn = div(locals.swapFee * uint128(state.get().burnFeeRate), uint128(QSWAP_FEE_BASE_100)); - - // Overflow protection: ensure all fees fit in uint64 - if (locals.feeToShareholders.high != 0 || locals.feeToQx.high != 0 || - locals.feeToInvestRewards.high != 0 || locals.feeToBurn.high != 0) - { - return; - } - - locals.transferredAssetAmountBefore = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - locals.assetAmountIn, - SELF - ); - locals.transferredAssetAmountAfter = qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - SELF, - SELF, - SELF_INDEX, - SELF_INDEX - ); - - // pool does not receive enough asset, rollback any received shares - if (locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore < locals.assetAmountIn) - { - // return any shares that were transferred - if (locals.transferredAssetAmountAfter > locals.transferredAssetAmountBefore) - { - qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - SELF, - SELF, - locals.transferredAssetAmountAfter - locals.transferredAssetAmountBefore, - qpi.invocator() - ); - } - return; - } - - qpi.transfer(qpi.invocator(), input.quAmountOut); - output.assetAmountIn = locals.assetAmountIn; - - // update fee state after successful transfers - state.mut().shareholderEarnedFee += locals.feeToShareholders.low; - state.mut().qxEarnedFee += locals.feeToQx.low; - state.mut().investRewardsEarnedFee += locals.feeToInvestRewards.low; - state.mut().burnEarnedFee += locals.feeToBurn.low; - - // update pool states - locals.poolBasicState.reservedAssetAmount += locals.assetAmountIn; - locals.totalFee = input.quAmountOut + sint64(locals.feeToShareholders.low) + sint64(locals.feeToQx.low) + sint64(locals.feeToInvestRewards.low) + sint64(locals.feeToBurn.low); - if (locals.poolBasicState.reservedQuAmount < locals.totalFee) - { - locals.poolBasicState.reservedQuAmount = 0; - } - else - { - locals.poolBasicState.reservedQuAmount -= locals.totalFee; - } - state.mut().mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); - - // Log SwapAssetForExactQu procedure - locals.swapMessage._contractIndex = SELF_INDEX; - locals.swapMessage._type = QSWAPSwapAssetForExactQu; - locals.swapMessage.assetIssuer = input.assetIssuer; - locals.swapMessage.assetName = input.assetName; - locals.swapMessage.assetAmountIn = output.assetAmountIn; - locals.swapMessage.assetAmountOut = input.quAmountOut; - LOG_INFO(locals.swapMessage); - } - - PUBLIC_PROCEDURE(TransferShareOwnershipAndPossession) - { - output.transferredAmount = 0; - - if (qpi.invocationReward() < state.get().cachedTransferFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - if (input.amount <= 0) - { - if (qpi.invocationReward() > 0 ) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - if (qpi.numberOfPossessedShares( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - SELF_INDEX, - SELF_INDEX - ) < input.amount) - { - - if (qpi.invocationReward() > 0 ) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - return; - } - - output.transferredAmount = qpi.transferShareOwnershipAndPossession( - input.assetName, - input.assetIssuer, - qpi.invocator(), - qpi.invocator(), - input.amount, - input.newOwnerAndPossessor - ) < 0 ? 0 : input.amount; - - if (output.transferredAmount == 0) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - } - else - { - if (qpi.invocationReward() > state.get().cachedTransferFee) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.get().cachedTransferFee); - } - state.mut().shareholderEarnedFee += state.get().cachedTransferFee; - } - } - - PUBLIC_PROCEDURE(SetInvestRewardsInfo) - { - output.success = false; - if (qpi.invocator() != state.get().investRewardsId) - { - return; - } - - state.mut().investRewardsId = input.newInvestRewardsId; - output.success = true; - } - - struct TransferShareManagementRights_locals - { - sint64 result; - sint64 reward; - sint64 refundAmount; - sint64 requiredFee; - bit success; - }; - - PUBLIC_PROCEDURE_WITH_LOCALS(TransferShareManagementRights) - { - locals.reward = qpi.invocationReward(); - locals.refundAmount = locals.reward; - - output.transferredNumberOfShares = 0; - - locals.success = false; - - if (qpi.numberOfPossessedShares( - input.asset.assetName, - input.asset.issuer, - qpi.invocator(), - qpi.invocator(), - SELF_INDEX, - SELF_INDEX) >= input.numberOfShares) - { - locals.result = qpi.releaseShares( - input.asset, - qpi.invocator(), - qpi.invocator(), - input.numberOfShares, - input.newManagingContractIndex, - input.newManagingContractIndex, - locals.reward - ); - - if (locals.result != INVALID_AMOUNT && locals.result >= 0) - { - locals.success = true; - locals.refundAmount = locals.reward - locals.result; - } - } - - if (locals.success) - { - output.transferredNumberOfShares = input.numberOfShares; - } - - if (locals.refundAmount > 0) - { - qpi.transfer(qpi.invocator(), locals.refundAmount); - } - } - - REGISTER_USER_FUNCTIONS_AND_PROCEDURES() - { - // functions - REGISTER_USER_FUNCTION(Fees, 1); - REGISTER_USER_FUNCTION(GetPoolBasicState, 2); - REGISTER_USER_FUNCTION(GetLiquidityOf, 3); - REGISTER_USER_FUNCTION(QuoteExactQuInput, 4); - REGISTER_USER_FUNCTION(QuoteExactQuOutput, 5); - REGISTER_USER_FUNCTION(QuoteExactAssetInput, 6); - REGISTER_USER_FUNCTION(QuoteExactAssetOutput, 7); - REGISTER_USER_FUNCTION(InvestRewardsInfo, 8); - - // procedure - REGISTER_USER_PROCEDURE(IssueAsset, 1); - REGISTER_USER_PROCEDURE(TransferShareOwnershipAndPossession, 2); - REGISTER_USER_PROCEDURE(CreatePool, 3); - REGISTER_USER_PROCEDURE(AddLiquidity, 4); - REGISTER_USER_PROCEDURE(RemoveLiquidity, 5); - REGISTER_USER_PROCEDURE(SwapExactQuForAsset, 6); - REGISTER_USER_PROCEDURE(SwapQuForExactAsset, 7); - REGISTER_USER_PROCEDURE(SwapExactAssetForQu, 8); - REGISTER_USER_PROCEDURE(SwapAssetForExactQu, 9); - REGISTER_USER_PROCEDURE(SetInvestRewardsInfo, 10); - REGISTER_USER_PROCEDURE(TransferShareManagementRights, 11); - } - - INITIALIZE() - { - state.mut().swapFeeRate = 30; // 0.3%, must be less than 10000 - state.mut().poolCreationFeeRate = 20; // 20%, must be less than 100 - - // swapFee distribution: 27% shareholders, 5% QX, 3% invest&rewards, 1% burn, 64% LP providers - state.mut().shareholderFeeRate = 27; // 27% of swap fees to SC shareholders - state.mut().investRewardsFeeRate = 3; // 3% of swap fees to Invest & Rewards - state.mut().qxFeeRate = 5; // 5% of swap fees to QX - state.mut().burnFeeRate = 1; // 1% of swap fees burned - - ASSERT(state.get().swapFeeRate < QSWAP_SWAP_FEE_BASE); - ASSERT(state.get().shareholderFeeRate + state.get().investRewardsFeeRate + state.get().qxFeeRate + state.get().burnFeeRate <= 100); - // - state.mut().investRewardsId = ID(_V, _J, _G, _R, _U, _F, _W, _J, _C, _U, _S, _N, _H, _C, _Q, _J, _R, _W, _R, _R, _Y, _X, _A, _U, _E, _J, _F, _C, _V, _H, _Y, _P, _X, _W, _K, _T, _D, _L, _Y, _K, _U, _A, _C, _P, _V, _V, _Y, _B, _G, _O, _L, _V, _C, _J, _S, _F); - } - - struct BEGIN_EPOCH_locals - { - QX::Fees_input feesInput; - QX::Fees_output feesOutput; - }; - - BEGIN_EPOCH_WITH_LOCALS() - { - CALL_OTHER_CONTRACT_FUNCTION(QX, Fees, locals.feesInput, locals.feesOutput); - - if (interContractCallError == NoCallError) - { - state.mut().cachedIssuanceFee = locals.feesOutput.assetIssuanceFee; - state.mut().cachedTransferFee = locals.feesOutput.transferFee; - } - } - - struct END_TICK_locals - { - uint64 toDistribute; - uint64 toBurn; - uint64 dividendPerComputor; - sint64 transferredAmount; - FailedDistributionMessage logMsg; - }; - - END_TICK_WITH_LOCALS() - { - // Distribute Invest & Rewards fees - if (state.get().investRewardsEarnedFee > state.get().investRewardsDistributedAmount) - { - locals.toDistribute = state.get().investRewardsEarnedFee - state.get().investRewardsDistributedAmount; - locals.transferredAmount = qpi.transfer(state.get().investRewardsId, locals.toDistribute); - if (locals.transferredAmount < 0) - { - locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSWAPFailedDistribution; - locals.logMsg.dst = state.get().investRewardsId; - locals.logMsg.amount = locals.toDistribute; - LOG_INFO(locals.logMsg); - } - else - state.mut().investRewardsDistributedAmount += locals.toDistribute; - } - - // Distribute QX fees as donation - if (state.get().qxEarnedFee > state.get().qxDistributedAmount) - { - locals.toDistribute = state.get().qxEarnedFee - state.get().qxDistributedAmount; - locals.transferredAmount = qpi.transfer(id(QX_CONTRACT_INDEX, 0, 0, 0), locals.toDistribute); - if (locals.transferredAmount < 0) - { - locals.logMsg._contractIndex = SELF_INDEX; - locals.logMsg._type = QSWAPFailedDistribution; - locals.logMsg.dst = id(QX_CONTRACT_INDEX, 0, 0, 0); - locals.logMsg.amount = locals.toDistribute; - LOG_INFO(locals.logMsg); - } - else - state.mut().qxDistributedAmount += locals.toDistribute; - } - - // Distribute shareholder fees (to IPO shareholders via dividends) - if (state.get().shareholderEarnedFee > state.get().shareholderDistributedAmount) - { - locals.dividendPerComputor = div((state.get().shareholderEarnedFee - state.get().shareholderDistributedAmount), 676ULL); - if (locals.dividendPerComputor > 0 && qpi.distributeDividends(locals.dividendPerComputor)) - { - state.mut().shareholderDistributedAmount += locals.dividendPerComputor * NUMBER_OF_COMPUTORS; - } - } - - // Burn fees (adds to contract execution fee reserve) - if (state.get().burnEarnedFee > state.get().burnedAmount) - { - locals.toBurn = state.get().burnEarnedFee - state.get().burnedAmount; - qpi.burn(locals.toBurn); - state.mut().burnedAmount += locals.toBurn; - } - } - - PRE_ACQUIRE_SHARES() - { - output.allowTransfer = true; - } -}; diff --git a/src/extensions/ant_colony_maintenance.h b/src/extensions/ant_colony_maintenance.h new file mode 100644 index 00000000..e2073bfb --- /dev/null +++ b/src/extensions/ant_colony_maintenance.h @@ -0,0 +1,43 @@ +#pragma once + +// Colony upkeep outside consensus. Pure functions of a colony, so they are testable without a node. + +namespace AntColonyMaintenance +{ +// fork() clones only the calling thread, so a promoted child can inherit a claim with no owner and +// ensureAntRecordAnn's waiter would spin on it forever. +inline unsigned int releaseInheritedClaims(AntColonyBpp9000T& colony) +{ + unsigned int released = 0; + const unsigned int recordCount = colony.solutionCount(); + for (unsigned int index = 0; index < recordCount; index++) + { + if (colony.isAnnClaimHeld(index)) + { + colony.releaseAnnClaim(index); + released++; + } + } + return released; +} + +// A rebuild starts from the parent's network, so a record whose parent has none cannot be taken. +inline bool isRebuildableNow(AntColonyBpp9000T& colony, unsigned int index) +{ + if (colony.isAnnMaterialised(index) || colony.isAnnClaimHeld(index)) + { + return false; + } + const AntSolutionRecord* record = colony.recordAt(index); + if (record == nullptr) + { + return false; + } + if (record->parentRef.isRoot()) + { + return true; + } + const long long parentIndex = colony.findIndexBySolutionRef(record->parentRef); + return parentIndex != ANT_INVALID_INDEX && colony.isAnnMaterialised((unsigned int)parentIndex); +} +} diff --git a/src/extensions/ant_walker_client.h b/src/extensions/ant_walker_client.h new file mode 100644 index 00000000..b4dd2248 --- /dev/null +++ b/src/extensions/ant_walker_client.h @@ -0,0 +1,1000 @@ +#pragma once + +// Node side of the ant walker: picks records committed without a network, sends the walk to a +// separate process, publishes the result after re-verifying it against the record. + +#if defined(ANT_WALKER) && !defined(_WIN32) + +#include "ant_walker_proto.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace AntWalker +{ +enum class LinkState +{ + Disabled, + Disconnected, + Connecting, + Handshaking, + Ready, +}; + +// A wrong walker fails every job, and marking each would poison good records permanently, so a +// streak is read as a broken walker instead. +static constexpr unsigned int DISAGREEMENT_STREAK_LIMIT = 3; +static constexpr unsigned int DEADLINE_STREAK_LIMIT = 3; +// "Gone", not "slow": a walk runs for minutes and the walker answers PING throughout. +static constexpr long long MIN_JOB_DEADLINE_MS = 600'000; +static constexpr long long PING_INTERVAL_MS = 10'000; +static constexpr long long BACKOFF_MIN_MS = 100; +static constexpr long long BACKOFF_MAX_MS = 30'000; +static constexpr int POLL_SLICE_MS = 100; +static constexpr long long HEARTBEAT_INTERVAL_MS = 60'000; +// Covers a legitimate 512 MB pool derive without hiding a wedged walker. +static constexpr long long HANDSHAKE_DEADLINE_MS = 120'000; +// A record the tick is blocked on is handed back, but only once the job is old enough to be stuck: +// yanking a healthy walk throws its progress away and the node pays the whole walk again. +static constexpr long long PREEMPT_MIN_AGE_MS = 180'000; +static constexpr long long PREEMPT_ACK_WAIT_MS = 500; +static constexpr unsigned int PREEMPT_NONE = 0xFFFFFFFFu; + +struct InFlight +{ + unsigned long long jobId; + unsigned int recordIndex; + long long sentAtMs; + // Kept for the replay key: re-resolving the anchor later could pick a different digest. + m256i anchorDigest; +}; + +struct State +{ + std::string socketPath; + unsigned int threadCount = 0; + bool debug = false; + + std::atomic link{ (int)LinkState::Disabled }; + std::atomic fd{ -1 }; + std::atomic stopping{ false }; + std::atomic quiesceRequested{ false }; + std::atomic quiesceAcknowledged{ true }; + std::atomic seedGeneration{ 0 }; + std::atomic walkerPid{ -1 }; + std::atomic suspectWalkerPid{ -1 }; + std::atomic preemptRequest{ PREEMPT_NONE }; + std::atomic handedBack{ PREEMPT_NONE }; + + std::atomic jobsSent{ 0 }; + std::atomic materialised{ 0 }; + std::atomic memoHits{ 0 }; + std::atomic preempted{ 0 }; + std::atomic disagreements{ 0 }; + std::atomic deadlineExpiries{ 0 }; + std::atomic staleDropped{ 0 }; + std::atomic reconnects{ 0 }; + std::atomic failedCount{ 0 }; + std::atomic walkMsEma{ 0 }; + std::atomic lastResultAtMs{ 0 }; + std::atomic inFlightCount{ 0 }; + std::atomic backlog{ 0 }; + + std::vector failedBits; + std::vector rolledBackCandidates; + std::vector inFlight; + unsigned long long nextJobId = 1; + unsigned int cursor = 0; + unsigned int disagreementStreak = 0; + unsigned int deadlineStreak = 0; + unsigned int helloGeneration = 0; + long long handshakeStartedAtMs = 0; + + std::thread* dispatcher = nullptr; +}; + +inline State gState; + +inline const char* linkName(LinkState state) +{ + switch (state) + { + case LinkState::Disabled: return "disabled"; + case LinkState::Disconnected: return "disconnected"; + case LinkState::Connecting: return "connecting"; + case LinkState::Handshaking: return "handshaking"; + case LinkState::Ready: return "ready"; + } + return "?"; +} + +inline long long nowMs() +{ + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +inline void logLine(const char* format, ...) +{ + va_list arguments; + va_start(arguments, format); + fprintf(stderr, "[ant-walk] "); + vfprintf(stderr, format, arguments); + fprintf(stderr, "\n"); + va_end(arguments); + fflush(stderr); +} + +inline bool isEnabled() +{ + return gState.threadCount > 0 && !gState.socketPath.empty(); +} + +// ── failed-record bitmap ──────────────────────────────────────────────────────────────────────── +// Scheduling only: the on-demand rebuild path ignores this and still walks. + +inline void ensureFailedBits() +{ + const size_t needed = (size_t)ANT_MAX_NODES_PER_EPOCH / 8; + if (gState.failedBits.size() != needed) + { + gState.failedBits.assign(needed, 0); + } +} + +inline bool isFailed(unsigned int index) +{ + ensureFailedBits(); + return (gState.failedBits[index >> 3] >> (index & 7)) & 1; +} + +inline void markFailed(unsigned int index) +{ + ensureFailedBits(); + if (!isFailed(index)) + { + gState.failedCount.fetch_add(1, std::memory_order_relaxed); + } + gState.failedBits[index >> 3] |= (unsigned char)(1u << (index & 7)); +} + +inline void clearFailed(unsigned int index) +{ + ensureFailedBits(); + if (isFailed(index)) + { + gState.failedCount.fetch_sub(1, std::memory_order_relaxed); + } + gState.failedBits[index >> 3] &= (unsigned char)~(1u << (index & 7)); +} + +// ── framing ───────────────────────────────────────────────────────────────────────────────────── + +inline bool writeFully(int fd, const void* buffer, size_t size) +{ + const unsigned char* in = (const unsigned char*)buffer; + size_t done = 0; + while (done < size) + { + const ssize_t put = write(fd, in + done, size - done); + if (put > 0) + { + done += (size_t)put; + continue; + } + if (put < 0 && errno == EINTR) + { + continue; + } + return false; + } + return true; +} + +inline bool readFully(int fd, void* buffer, size_t size) +{ + unsigned char* out = (unsigned char*)buffer; + size_t done = 0; + while (done < size) + { + const ssize_t got = read(fd, out + done, size - done); + if (got > 0) + { + done += (size_t)got; + continue; + } + if (got < 0 && errno == EINTR) + { + continue; + } + return false; + } + return true; +} + +inline bool sendFrame(int fd, unsigned int type, const void* payload, unsigned int payloadSize) +{ + AntWalkProto::FrameHeader header; + header.magic = AntWalkProto::MAGIC; + header.type = type; + header.payloadSize = payloadSize; + header.reserved = 0; + if (!writeFully(fd, &header, sizeof(header))) + { + return false; + } + return payloadSize == 0 || writeFully(fd, payload, payloadSize); +} + +// ── link ──────────────────────────────────────────────────────────────────────────────────────── + +// Releases every in-flight claim: the on-demand path would otherwise wait on a dead connection. +inline void dropLink(const char* reason) +{ + const int fd = gState.fd.exchange(-1); + if (fd >= 0) + { + close(fd); + } + unsigned int released = 0; + for (const InFlight& job : gState.inFlight) + { + gAntColony.releaseAnnClaim(job.recordIndex); + released++; + } + gState.inFlight.clear(); + gState.inFlightCount.store(0, std::memory_order_release); + if (gState.link.load(std::memory_order_acquire) != (int)LinkState::Disconnected) + { + logLine("sidecar lost (%s), %u claims released", reason, released); + } + gState.link.store((int)LinkState::Disconnected, std::memory_order_release); +} + +inline bool sendHello(int fd) +{ + AntWalkProto::HelloPayload hello; + memset(&hello, 0, sizeof(hello)); + hello.version = AntWalkProto::VERSION; + hello.epochId = gState.seedGeneration.load(std::memory_order_acquire); + hello.annBytes = AntWalkProto::ANN_BYTES; + hello.configHash = AntWalkProto::CONFIG_HASH; + gState.helloGeneration = hello.epochId; + copyMem(hello.miningSeed, score->currentRandomSeed.m256i_u8, 32); + copyMem(hello.topologyHash, BPP9000_TOPOLOGY_HASH, 32); + copyMem(hello.dataHash, BPP9000_DATA_HASH, 32); + return sendFrame(fd, AntWalkProto::MsgHello, &hello, (unsigned int)sizeof(hello)); +} + +inline bool tryConnect() +{ + gState.link.store((int)LinkState::Connecting, std::memory_order_release); + const int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + { + return false; + } + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + snprintf(address.sun_path, sizeof(address.sun_path), "%s", gState.socketPath.c_str()); + if (connect(fd, (struct sockaddr*)&address, sizeof(address)) != 0) + { + close(fd); + gState.link.store((int)LinkState::Disconnected, std::memory_order_release); + return false; + } + + gState.fd.store(fd, std::memory_order_release); + gState.link.store((int)LinkState::Handshaking, std::memory_order_release); + gState.handshakeStartedAtMs = nowMs(); + if (!sendHello(fd)) + { + dropLink("hello write failed"); + return false; + } + return true; +} + +// ── selection ─────────────────────────────────────────────────────────────────────────────────── + +// Commit order is topological, so this walks each lineage bottom-up without repeating a level. +inline bool selectNextRecord(unsigned int& outIndex) +{ + const unsigned int recordCount = gAntColony.solutionCount(); + if (recordCount == 0) + { + return false; + } + + for (unsigned int scanned = 0; scanned < recordCount; scanned++) + { + const unsigned int index = (gState.cursor + scanned) % recordCount; + if (index == gState.handedBack.load(std::memory_order_acquire)) + { + // Held until the tick has actually taken it, so the walker cannot re-take what it gave up. + if (gAntColony.isAnnClaimHeld(index) || gAntColony.isAnnMaterialised(index)) + { + gState.handedBack.store(PREEMPT_NONE, std::memory_order_release); + } + continue; + } + if (isFailed(index) || !AntColonyMaintenance::isRebuildableNow(gAntColony, index)) + { + continue; + } + gState.cursor = (index + 1) % recordCount; + outIndex = index; + return true; + } + return false; +} + +// A full record scan (2^23 on mainnet), so it runs on the heartbeat rather than per dispatch. +inline void refreshBacklog() +{ + const unsigned int recordCount = gAntColony.solutionCount(); + unsigned int backlogCount = 0; + for (unsigned int index = 0; index < recordCount; index++) + { + if (!gAntColony.isAnnMaterialised(index)) + { + backlogCount++; + } + } + gState.backlog.store(backlogCount, std::memory_order_release); +} + +inline bool dispatchOne() +{ + unsigned int index; + if (!selectNextRecord(index)) + { + return false; + } + if (gAntColony.tryClaimAnn(index) != AntColonyBpp9000T::AnnClaimOwned) + { + return false; + } + const AntSolutionRecord* record = gAntColony.recordAt(index); + if (record == nullptr) + { + gAntColony.releaseAnnClaim(index); + return false; + } + + AntWalkProto::JobPayload job; + memset(&job, 0, sizeof(job)); + job.jobId = gState.nextJobId++; + job.epochId = gState.seedGeneration.load(std::memory_order_acquire); + job.isRoot = record->parentRef.isRoot() ? 1u : 0u; + copyMem(job.pubkey, record->pubkey.m256i_u8, 32); + copyMem(job.nonce, record->nonce.m256i_u8, 32); + + m256i anchorDigest; + if (!getAntAnchorDigestForRebuild(record->anchorTick, anchorDigest)) + { + gAntColony.releaseAnnClaim(index); + markFailed(index); + return false; + } + copyMem(job.anchorDigest, anchorDigest.m256i_u8, 32); + + // Every other scoring path memoises its walk, so a score already held here costs no job at all. + const AntColonyBpp9000T::ReplayKey replayKey = makeAntReplayKey(record->pubkey, record->nonce, record->parentRef, anchorDigest); + AntColonyBpp9000T::Ann memoAnn; + unsigned int memoScore; + if (gAntColony.tryGetReplayScore(replayKey, memoScore, memoAnn)) + { + if (memoScore != record->score) + { + // The node's own cache disagreeing is the record's fault, not the walker's, so no streak. + gAntColony.releaseAnnClaim(index); + markFailed(index); + logLine("record %u memo %u != accepted %u, marked failed", index, memoScore, record->score); + return false; + } + unsigned int memoHash; + KangarooTwelve(&memoAnn, sizeof(memoAnn), &memoHash, sizeof(memoHash)); + gAntColony.publishAnn(index, memoAnn, memoHash); + gState.memoHits.fetch_add(1, std::memory_order_relaxed); + gState.materialised.fetch_add(1, std::memory_order_relaxed); + if (gState.debug) + { + logLine("record %u from the replay cache, no job", index); + } + return true; + } + + if (!job.isRoot) + { + const long long parentIndex = gAntColony.findIndexBySolutionRef(record->parentRef); + const AntSolutionRecord* parentRecord = (parentIndex == ANT_INVALID_INDEX) ? nullptr : gAntColony.recordAt(parentIndex); + AntColonyBpp9000T::Ann parentAnn; + if (parentRecord == nullptr || !gAntColony.annOfNonRoot(*parentRecord, parentAnn)) + { + gAntColony.releaseAnnClaim(index); + return false; + } + copyMem(job.parentAnn, &parentAnn, sizeof(parentAnn)); + } + + const int fd = gState.fd.load(std::memory_order_acquire); + if (fd < 0 || !sendFrame(fd, AntWalkProto::MsgJob, &job, (unsigned int)sizeof(job))) + { + gAntColony.releaseAnnClaim(index); + dropLink("job write failed"); + return false; + } + + gState.inFlight.push_back(InFlight{ job.jobId, index, nowMs(), anchorDigest }); + gState.inFlightCount.store((unsigned int)gState.inFlight.size(), std::memory_order_release); + gState.jobsSent.fetch_add(1, std::memory_order_relaxed); + if (gState.debug) + { + logLine("job %llu record %u depth %u sent", (unsigned long long)job.jobId, index, (unsigned)record->depth); + } + return true; +} + +// ── results ───────────────────────────────────────────────────────────────────────────────────── + +inline bool takeInFlight(unsigned long long jobId, InFlight& out) +{ + for (size_t i = 0; i < gState.inFlight.size(); i++) + { + if (gState.inFlight[i].jobId == jobId) + { + out = gState.inFlight[i]; + gState.inFlight.erase(gState.inFlight.begin() + (long)i); + gState.inFlightCount.store((unsigned int)gState.inFlight.size(), std::memory_order_release); + return true; + } + } + return false; +} + +inline void noteDisagreement(unsigned int index) +{ + markFailed(index); + gState.rolledBackCandidates.push_back(index); + gState.disagreements.fetch_add(1, std::memory_order_relaxed); + gState.disagreementStreak++; + if (gState.disagreementStreak < DISAGREEMENT_STREAK_LIMIT) + { + return; + } + for (unsigned int candidate : gState.rolledBackCandidates) + { + clearFailed(candidate); + } + logLine("%u consecutive disagreements - walker suspect, %u marks rolled back, disconnecting", + gState.disagreementStreak, (unsigned int)gState.rolledBackCandidates.size()); + gState.rolledBackCandidates.clear(); + gState.disagreementStreak = 0; + gState.suspectWalkerPid.store(gState.walkerPid.load(std::memory_order_acquire), std::memory_order_release); + dropLink("walker disagrees on every job"); +} + +inline void noteSuccess() +{ + gState.disagreementStreak = 0; + gState.deadlineStreak = 0; + gState.rolledBackCandidates.clear(); +} + +inline void applyResult(const AntWalkProto::ResultPayload& result) +{ + InFlight job; + if (!takeInFlight(result.jobId, job)) + { + return; + } + + const long long walkMs = nowMs() - job.sentAtMs; + const unsigned long long previousEma = gState.walkMsEma.load(std::memory_order_acquire); + gState.walkMsEma.store(previousEma ? (previousEma * 3 + (unsigned long long)walkMs) / 4 : (unsigned long long)walkMs, std::memory_order_release); + gState.lastResultAtMs.store((unsigned long long)nowMs(), std::memory_order_release); + + if (result.epochId != gState.seedGeneration.load(std::memory_order_acquire) || result.status == AntWalkProto::ResultStaleEpoch) + { + gAntColony.releaseAnnClaim(job.recordIndex); + gState.staleDropped.fetch_add(1, std::memory_order_relaxed); + logLine("job %llu dropped, epoch %u is not %u", (unsigned long long)result.jobId, + result.epochId, gState.seedGeneration.load(std::memory_order_acquire)); + return; + } + + const AntSolutionRecord* record = gAntColony.recordAt(job.recordIndex); + if (record == nullptr) + { + gAntColony.releaseAnnClaim(job.recordIndex); + return; + } + + if (result.status != AntWalkProto::ResultOk || result.score != record->score) + { + gAntColony.releaseAnnClaim(job.recordIndex); + logLine("record %u walked %u != accepted %u, marked failed", job.recordIndex, result.score, record->score); + noteDisagreement(job.recordIndex); + return; + } + + AntColonyBpp9000T::Ann childAnn; + copyMem(&childAnn, result.childAnn, sizeof(childAnn)); + unsigned int annHash; + KangarooTwelve(&childAnn, sizeof(childAnn), &annHash, sizeof(annHash)); + + gAntColony.publishAnn(job.recordIndex, childAnn, annHash); + // The cache every scoring path consults, so a strict replay of this solution is a lookup. + const AntColonyBpp9000T::ReplayKey replayKey = makeAntReplayKey(record->pubkey, record->nonce, record->parentRef, job.anchorDigest); + gAntColony.putReplayScore(replayKey, result.score, childAnn); + gState.materialised.fetch_add(1, std::memory_order_relaxed); + noteSuccess(); + if (gState.debug) + { + logLine("job %llu record %u score %u in %lld ms", (unsigned long long)result.jobId, job.recordIndex, result.score, walkMs); + } +} + +inline void checkDeadlines() +{ + const unsigned long long ema = gState.walkMsEma.load(std::memory_order_acquire); + const long long deadlineMs = (long long)(ema * 3) > MIN_JOB_DEADLINE_MS + ? (long long)(ema * 3) : MIN_JOB_DEADLINE_MS; + const long long now = nowMs(); + + for (size_t i = 0; i < gState.inFlight.size();) + { + if (now - gState.inFlight[i].sentAtMs < deadlineMs) + { + i++; + continue; + } + const unsigned int index = gState.inFlight[i].recordIndex; + const unsigned long long jobId = gState.inFlight[i].jobId; + gState.inFlight.erase(gState.inFlight.begin() + (long)i); + gAntColony.releaseAnnClaim(index); + gState.deadlineExpiries.fetch_add(1, std::memory_order_relaxed); + // A missing result says nothing about the record, so the bitmap is left alone. + logLine("job %llu record %u no result in %lld ms, claim released, not marked", jobId, index, deadlineMs); + gState.deadlineStreak++; + } + gState.inFlightCount.store((unsigned int)gState.inFlight.size(), std::memory_order_release); + + if (gState.deadlineStreak >= DEADLINE_STREAK_LIMIT) + { + gState.deadlineStreak = 0; + dropLink("walker alive but not answering"); + } +} + +// ── dispatcher ────────────────────────────────────────────────────────────────────────────────── + +// Answers the on-demand path waiting on a record this link claimed. Only a job past the stale age is +// handed back: a healthy walk is worth waiting out, since taking it back discards all of its progress. +inline void servePreempt() +{ + const unsigned int index = gState.preemptRequest.load(std::memory_order_acquire); + if (index == PREEMPT_NONE) + { + return; + } + + const unsigned long long ema = gState.walkMsEma.load(std::memory_order_acquire); + const long long staleMs = (long long)(ema * 2) > PREEMPT_MIN_AGE_MS ? (long long)(ema * 2) : PREEMPT_MIN_AGE_MS; + for (size_t i = 0; i < gState.inFlight.size(); i++) + { + if (gState.inFlight[i].recordIndex != index || nowMs() - gState.inFlight[i].sentAtMs < staleMs) + { + continue; + } + const unsigned long long jobId = gState.inFlight[i].jobId; + const long long ageMs = nowMs() - gState.inFlight[i].sentAtMs; + gState.inFlight.erase(gState.inFlight.begin() + (long)i); + gState.inFlightCount.store((unsigned int)gState.inFlight.size(), std::memory_order_release); + gState.handedBack.store(index, std::memory_order_release); + gAntColony.releaseAnnClaim(index); + gState.preempted.fetch_add(1, std::memory_order_relaxed); + logLine("job %llu record %u handed back to the tick after %lld ms, its result will be dropped", jobId, index, ageMs); + break; + } + gState.preemptRequest.store(PREEMPT_NONE, std::memory_order_release); +} + +// Reseeding invalidates every index in flight, so claims and scan state go before it proceeds. +inline void serveQuiesce() +{ + for (const InFlight& job : gState.inFlight) + { + gAntColony.releaseAnnClaim(job.recordIndex); + } + gState.inFlight.clear(); + gState.inFlightCount.store(0, std::memory_order_release); + gState.preemptRequest.store(PREEMPT_NONE, std::memory_order_release); + gState.handedBack.store(PREEMPT_NONE, std::memory_order_release); + gState.cursor = 0; + gState.disagreementStreak = 0; + gState.deadlineStreak = 0; + gState.rolledBackCandidates.clear(); + gState.failedBits.assign((size_t)ANT_MAX_NODES_PER_EPOCH / 8, 0); + gState.failedCount.store(0, std::memory_order_release); + gState.quiesceAcknowledged.store(true, std::memory_order_release); + + while (gState.quiesceRequested.load(std::memory_order_acquire) && !gState.stopping.load(std::memory_order_acquire)) + { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + const int fd = gState.fd.load(std::memory_order_acquire); + if (fd >= 0 && !sendHello(fd)) + { + dropLink("hello write failed after epoch reset"); + } +} + +inline bool readOneFrame(int fd, unsigned char* payload) +{ + AntWalkProto::FrameHeader header; + if (!readFully(fd, &header, sizeof(header))) + { + dropLink("connection closed"); + return false; + } + if (header.magic != AntWalkProto::MAGIC || header.payloadSize > AntWalkProto::MAX_PAYLOAD_BYTES) + { + dropLink("bad frame"); + return false; + } + if (header.payloadSize && !readFully(fd, payload, header.payloadSize)) + { + dropLink("short frame"); + return false; + } + + if (header.type == AntWalkProto::MsgReady) + { + if (header.payloadSize != sizeof(AntWalkProto::ReadyPayload)) + { + dropLink("bad ready frame"); + return false; + } + AntWalkProto::ReadyPayload ready; + memcpy(&ready, payload, sizeof(ready)); + if ((int)ready.walkerPid == gState.suspectWalkerPid.load(std::memory_order_acquire)) + { + logLine("walker pid %u already disagreed on every job, not using it", ready.walkerPid); + dropLink("suspect walker"); + return false; + } + if (ready.status != AntWalkProto::ReadyOk) + { + logLine("handshake refused by the walker (status %u) - not retrying until it changes", ready.status); + dropLink("handshake refused"); + return false; + } + // Also answers the hello resent on a seed rotation, which is a re-seed, not a new link. + const bool wasReady = (LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready; + gState.threadCount = ready.threadCount; + gState.walkerPid.store((int)ready.walkerPid, std::memory_order_release); + gState.link.store((int)LinkState::Ready, std::memory_order_release); + if (wasReady) + { + logLine("sidecar re-seeded for epoch %u", ready.epochId); + } + else + { + gState.reconnects.fetch_add(1, std::memory_order_relaxed); + logLine("sidecar connected, pid %u, %u threads, epoch %u", ready.walkerPid, ready.threadCount, ready.epochId); + } + } + else if (header.type == AntWalkProto::MsgResult) + { + if (header.payloadSize != sizeof(AntWalkProto::ResultPayload)) + { + dropLink("bad result frame"); + return false; + } + AntWalkProto::ResultPayload result; + memcpy(&result, payload, sizeof(result)); + applyResult(result); + } + return true; +} + +inline void heartbeat() +{ + logLine("backlog %llu, done %llu (memo %llu), failed %llu, inflight %u/%u, walk avg %llu ms, link %s", + (unsigned long long)gState.backlog.load(std::memory_order_acquire), (unsigned long long)gState.materialised.load(std::memory_order_acquire), + (unsigned long long)gState.memoHits.load(std::memory_order_acquire), (unsigned long long)gState.failedCount.load(std::memory_order_acquire), + gState.inFlightCount.load(std::memory_order_acquire), gState.threadCount, (unsigned long long)gState.walkMsEma.load(std::memory_order_acquire), + linkName((LinkState)gState.link.load(std::memory_order_acquire))); +} + +inline void dispatcherLoop() +{ + long long backoffMs = BACKOFF_MIN_MS; + long long reconnectAtMs = 0; + long long nextPingAtMs = nowMs() + PING_INTERVAL_MS; + long long nextHeartbeatAtMs = nowMs() + HEARTBEAT_INTERVAL_MS; + std::vector payload(AntWalkProto::MAX_PAYLOAD_BYTES); + + while (!gState.stopping.load(std::memory_order_acquire)) + { + servePreempt(); + + if (nowMs() >= nextHeartbeatAtMs) + { + nextHeartbeatAtMs = nowMs() + HEARTBEAT_INTERVAL_MS; + refreshBacklog(); + heartbeat(); + } + + if (gState.quiesceRequested.load(std::memory_order_acquire)) + { + serveQuiesce(); + continue; + } + + // A walker seeded before the first mining seed exists would disagree on every record. + if (score == nullptr || isZero(score->currentRandomSeed)) + { + std::this_thread::sleep_for(std::chrono::milliseconds(POLL_SLICE_MS)); + continue; + } + + const LinkState link = (LinkState)gState.link.load(std::memory_order_acquire); + if (link != LinkState::Ready && link != LinkState::Handshaking) + { + const long long now = nowMs(); + if (now < reconnectAtMs) + { + std::this_thread::sleep_for(std::chrono::milliseconds(POLL_SLICE_MS)); + continue; + } + if (!tryConnect()) + { + backoffMs = (backoffMs * 2 < BACKOFF_MAX_MS) ? backoffMs * 2 : BACKOFF_MAX_MS; + reconnectAtMs = nowMs() + backoffMs; + continue; + } + backoffMs = BACKOFF_MIN_MS; + continue; + } + + if (link == LinkState::Handshaking && nowMs() - gState.handshakeStartedAtMs > HANDSHAKE_DEADLINE_MS) + { + dropLink("no ready frame within the handshake deadline"); + continue; + } + + const int fd = gState.fd.load(std::memory_order_acquire); + if (fd < 0) + { + continue; + } + + struct pollfd entry; + entry.fd = fd; + entry.events = POLLIN; + entry.revents = 0; + const int ready = poll(&entry, 1, POLL_SLICE_MS); + if (ready > 0 && !readOneFrame(fd, payload.data())) + { + continue; + } + if (ready < 0 && errno != EINTR) + { + dropLink("poll failed"); + continue; + } + + checkDeadlines(); + + // Deeper only lets a wedged walker hold claims the on-demand path may need. + if ((LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready) + { + while (gState.inFlight.size() < gState.threadCount && dispatchOne()) + { + } + } + + const unsigned int generation = gState.seedGeneration.load(std::memory_order_acquire); + if (generation != gState.helloGeneration && (LinkState)gState.link.load(std::memory_order_acquire) == LinkState::Ready) + { + const int helloFd = gState.fd.load(std::memory_order_acquire); + if (helloFd < 0 || !sendHello(helloFd)) + { + dropLink("hello write failed on seed change"); + continue; + } + } + + const long long now = nowMs(); + if (now >= nextPingAtMs) + { + nextPingAtMs = now + PING_INTERVAL_MS; + const int liveFd = gState.fd.load(std::memory_order_acquire); + if (liveFd >= 0 && !sendFrame(liveFd, AntWalkProto::MsgPing, nullptr, 0)) + { + dropLink("ping write failed"); + } + } + } + + dropLink("shutting down"); +} + +// ── node-facing api ───────────────────────────────────────────────────────────────────────────── + +inline void configure(const std::string& socketPath, unsigned int threadCount, bool debug) +{ + gState.socketPath = socketPath; + gState.threadCount = threadCount; + gState.debug = debug; +} + +inline void start() +{ + if (!isEnabled() || gState.dispatcher != nullptr) + { + return; + } + gState.stopping.store(false, std::memory_order_release); + gState.link.store((int)LinkState::Disconnected, std::memory_order_release); + gState.dispatcher = new std::thread(dispatcherLoop); +} + +inline void stop() +{ + if (gState.dispatcher == nullptr) + { + return; + } + gState.stopping.store(true, std::memory_order_release); + gState.dispatcher->join(); + delete gState.dispatcher; + gState.dispatcher = nullptr; +} + +// The pool seed changed, so results still in flight were computed against the old one. +inline void onEpochBegin() +{ + gState.seedGeneration.fetch_add(1, std::memory_order_acq_rel); +} + +inline void quiesceBegin() +{ + if (gState.dispatcher == nullptr) + { + return; + } + gState.quiesceAcknowledged.store(false, std::memory_order_release); + gState.quiesceRequested.store(true, std::memory_order_release); + + const long long startedAtMs = nowMs(); + while (!gState.quiesceAcknowledged.load(std::memory_order_acquire)) + { + if (nowMs() - startedAtMs > 5'000) + { + logLine("quiesce not acknowledged in 5000 ms, continuing"); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + logLine("quiesce for epoch reset, waited %lld ms", nowMs() - startedAtMs); +} + +inline void quiesceEnd() +{ + gState.quiesceRequested.store(false, std::memory_order_release); +} + +// Asks the dispatcher to hand back a record the on-demand path is stuck behind. The caller re-checks +// the claim afterwards: a claim held by another node processor is not the walker's to release. +inline void preemptClaim(unsigned int index) +{ + if (!isEnabled() || gState.inFlightCount.load(std::memory_order_acquire) == 0) + { + return; + } + unsigned int slot = PREEMPT_NONE; + if (!gState.preemptRequest.compare_exchange_strong(slot, index, std::memory_order_acq_rel)) + { + return; + } + + const long long deadlineMs = nowMs() + PREEMPT_ACK_WAIT_MS; + while (gState.preemptRequest.load(std::memory_order_acquire) == index && nowMs() < deadlineMs) + { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + // A dispatcher that never answered must not leave the slot taken for every later request. + slot = index; + gState.preemptRequest.compare_exchange_strong(slot, PREEMPT_NONE, std::memory_order_acq_rel); +} + +// The fork child inherits the descriptor; two readers on one socket interleave results. +inline void closeInheritedSocket() +{ + const int fd = gState.fd.exchange(-1); + if (fd >= 0) + { + close(fd); + } + gState.link.store((int)LinkState::Disconnected, std::memory_order_release); +} + +// The promote path swept the claims, so only the client's own view is rebuilt here. +inline void restartAfterPromote() +{ + closeInheritedSocket(); + // Only the handle came through fork(), so it is abandoned rather than joined or destroyed. + gState.dispatcher = nullptr; + gState.inFlight.clear(); + gState.inFlightCount.store(0, std::memory_order_release); + gState.preemptRequest.store(PREEMPT_NONE, std::memory_order_release); + gState.handedBack.store(PREEMPT_NONE, std::memory_order_release); + gState.quiesceRequested.store(false, std::memory_order_release); + gState.quiesceAcknowledged.store(true, std::memory_order_release); + gState.cursor = 0; + gState.disagreementStreak = 0; + gState.deadlineStreak = 0; + gState.rolledBackCandidates.clear(); + start(); + if (isEnabled()) + { + logLine("dispatcher restarted after promote"); + } +} + +inline std::string statsJson() +{ + char buffer[896]; + snprintf(buffer, sizeof(buffer), + "{\"enabled\":%s,\"state\":\"%s\",\"socket\":\"%s\",\"threads\":%u," + "\"inflight\":%u,\"backlog\":%llu,\"materialised\":%llu,\"failed\":%llu," + "\"jobsSent\":%llu,\"disagreements\":%llu,\"deadlineExpiries\":%llu," + "\"staleDropped\":%llu,\"reconnects\":%llu,\"walkAvgMs\":%llu," + "\"memoHits\":%llu,\"preempted\":%llu,\"epochId\":%u}", + isEnabled() ? "true" : "false", linkName((LinkState)gState.link.load(std::memory_order_acquire)), gState.socketPath.c_str(), gState.threadCount, + gState.inFlightCount.load(std::memory_order_acquire), (unsigned long long)gState.backlog.load(std::memory_order_acquire), + (unsigned long long)gState.materialised.load(std::memory_order_acquire), (unsigned long long)gState.failedCount.load(std::memory_order_acquire), + (unsigned long long)gState.jobsSent.load(std::memory_order_acquire), (unsigned long long)gState.disagreements.load(std::memory_order_acquire), + (unsigned long long)gState.deadlineExpiries.load(std::memory_order_acquire), (unsigned long long)gState.staleDropped.load(std::memory_order_acquire), + (unsigned long long)gState.reconnects.load(std::memory_order_acquire), (unsigned long long)gState.walkMsEma.load(std::memory_order_acquire), + (unsigned long long)gState.memoHits.load(std::memory_order_acquire), (unsigned long long)gState.preempted.load(std::memory_order_acquire), + gState.seedGeneration.load(std::memory_order_acquire)); + return std::string(buffer); +} +} + +#else + +#include + +namespace AntWalker +{ +inline void configure(const std::string&, unsigned int, bool) {} +inline void start() {} +inline void stop() {} +inline void onEpochBegin() {} +inline void quiesceBegin() {} +inline void quiesceEnd() {} +inline void preemptClaim(unsigned int) {} +inline void closeInheritedSocket() {} +inline void restartAfterPromote() {} +inline std::string statsJson() { return std::string("{\"enabled\":false}"); } +} + +#endif // ANT_WALKER && !_WIN32 diff --git a/src/extensions/ant_walker_proto.h b/src/extensions/ant_walker_proto.h new file mode 100644 index 00000000..7f8f5e80 --- /dev/null +++ b/src/extensions/ant_walker_proto.h @@ -0,0 +1,128 @@ +#pragma once + +// Wire format shared by the walker client and the walker process. Fixed-size payloads only, so a +// frame is one read of a known length. + +#include "score.h" +#include "public_settings.h" + +namespace AntWalkProto +{ +static constexpr unsigned int MAGIC = 0x57544E41u; // "ANTW" +static constexpr unsigned int VERSION = 1; + +static constexpr unsigned int ANN_BYTES = (unsigned int)sizeof(score_engine::ScoreBpp9000T::ANN); + +// Both sides compile the same scorer, so only a build disagreeing on these params scores +// differently. Compared at the handshake, before a job runs. +constexpr unsigned int mixConfig(unsigned int accumulated, unsigned long long value) +{ + for (unsigned int byteIndex = 0; byteIndex < 8; byteIndex++) + { + accumulated = (accumulated ^ (unsigned int)((value >> (byteIndex * 8)) & 0xFF)) * 16777619u; + } + return accumulated; +} + +constexpr unsigned int CONFIG_HASH = + mixConfig(mixConfig(mixConfig(mixConfig(mixConfig(mixConfig(mixConfig(mixConfig(mixConfig(mixConfig( + 2166136261u, + BPP9000_NUMBER_OF_INPUT_NEURONS), + BPP9000_NUMBER_OF_OUTPUT_NEURONS), + BPP9000_SEQUENCE_LENGTH), + BPP9000_WINDOW_WIDTH), + BPP9000_MAX_NUMBER_OF_TICKS), + BPP9000_NUMBER_OF_NEIGHBORS), + BPP9000_POPULATION_THRESHOLD), + BPP9000_NUMBER_OF_MUTATIONS), + BPP9000_SOLUTION_THRESHOLD_DEFAULT), + (unsigned long long)ANN_BYTES); + +enum MessageType : unsigned int +{ + MsgHello = 1, // node -> walker + MsgReady = 2, // walker -> node + MsgJob = 3, // node -> walker + MsgResult = 4, // walker -> node + MsgPing = 5, // node -> walker + MsgPong = 6, // walker -> node +}; + +enum ResultStatus : unsigned int +{ + ResultOk = 0, + ResultUnscorable = 1, // the engine returned INVALID_SCORE_VALUE, childAnn is not written + ResultStaleEpoch = 2, // the job named an epoch the walker is no longer seeded for +}; + +enum ReadyStatus : unsigned int +{ + ReadyOk = 0, + ReadyVersionMismatch = 1, + ReadyTaskMismatch = 2, + ReadySeedFailed = 3, + ReadyConfigMismatch = 4, +}; + +struct FrameHeader +{ + unsigned int magic; + unsigned int type; + unsigned int payloadSize; + unsigned int reserved; +}; + +struct HelloPayload +{ + unsigned int version; + unsigned int epochId; + unsigned int annBytes; + unsigned int configHash; + unsigned char miningSeed[32]; + // The pinned task identity both binaries were compiled against; a mismatch means the two would + // score the same nonce differently, which is worth refusing before a single job runs. + unsigned char topologyHash[32]; + unsigned char dataHash[32]; +}; + +struct ReadyPayload +{ + unsigned int version; + unsigned int status; + unsigned int threadCount; + unsigned int annBytes; + unsigned int epochId; + unsigned int walkerPid; // lets the node refuse a walker it has already found to be wrong +}; + +struct JobPayload +{ + unsigned long long jobId; + unsigned int epochId; + unsigned int isRoot; // parentAnn is unused when set; the walker derives the miner's root + unsigned char pubkey[32]; + unsigned char nonce[32]; + unsigned char anchorDigest[32]; + unsigned char parentAnn[ANN_BYTES]; +}; + +struct ResultPayload +{ + unsigned long long jobId; + unsigned int epochId; + unsigned int status; + unsigned int score; + unsigned int reserved; + unsigned char childAnn[ANN_BYTES]; +}; + +static_assert(sizeof(FrameHeader) == 16, "FrameHeader must stay 16 bytes on the wire"); +static_assert(sizeof(HelloPayload) == 112, "HelloPayload layout changed - bump VERSION"); +static_assert(sizeof(ReadyPayload) == 24, "ReadyPayload layout changed - bump VERSION"); +static_assert(sizeof(JobPayload) == 16 + 96 + ANN_BYTES, "JobPayload layout changed - bump VERSION"); +static_assert(sizeof(ResultPayload) == 24 + ANN_BYTES, "ResultPayload layout changed - bump VERSION"); + +// The largest frame either side ever reads, so both can size one static buffer. +static constexpr unsigned int MAX_PAYLOAD_BYTES = + (sizeof(JobPayload) > sizeof(ResultPayload)) ? (unsigned int)sizeof(JobPayload) : (unsigned int)sizeof(ResultPayload); +} diff --git a/src/extensions/ant_walker_worker.h b/src/extensions/ant_walker_worker.h new file mode 100644 index 00000000..a191ef4d --- /dev/null +++ b/src/extensions/ant_walker_worker.h @@ -0,0 +1,581 @@ +#pragma once + +// The ant score walk, in a process re-exec'd from this binary (--ant-walk-worker): on a node thread +// it would hold the score-engine lock across nearly every fork point. Stateless, holds no node state. + +#if defined(ANT_WALKER) && !defined(_WIN32) + +#include "ant_walker_proto.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#if defined(__linux__) +#include +#endif + +namespace AntWalkerWorker +{ +using Ann = score_engine::ScoreBpp9000T::ANN; + +constexpr int NO_TRAFFIC_TIMEOUT_MS = 60'000; +constexpr int POLL_SLICE_MS = 1'000; + +struct Options +{ + std::string socketPath; + unsigned int threadCount = 4; +}; + +// ── socket helpers ────────────────────────────────────────────────────────────────────────────── + +bool readFully(int fd, void* buffer, size_t size) +{ + unsigned char* out = (unsigned char*)buffer; + size_t done = 0; + while (done < size) + { + const ssize_t got = read(fd, out + done, size - done); + if (got > 0) + { + done += (size_t)got; + continue; + } + if (got < 0 && errno == EINTR) + { + continue; + } + return false; + } + return true; +} + +bool writeFully(int fd, const void* buffer, size_t size) +{ + const unsigned char* in = (const unsigned char*)buffer; + size_t done = 0; + while (done < size) + { + const ssize_t put = write(fd, in + done, size - done); + if (put > 0) + { + done += (size_t)put; + continue; + } + if (put < 0 && errno == EINTR) + { + continue; + } + return false; + } + return true; +} + +// Bounded, so a peer that vanished without an EOF cannot park this process forever. +bool waitReadable(int fd, int timeoutMs, const std::atomic& stop) +{ + int waitedMs = 0; + while (waitedMs < timeoutMs) + { + if (stop.load(std::memory_order_acquire)) + { + return false; + } + struct pollfd entry; + entry.fd = fd; + entry.events = POLLIN; + entry.revents = 0; + const int slice = (timeoutMs - waitedMs < POLL_SLICE_MS) ? (timeoutMs - waitedMs) : POLL_SLICE_MS; + const int ready = poll(&entry, 1, slice); + if (ready > 0) + { + return true; + } + if (ready < 0 && errno != EINTR) + { + return false; + } + waitedMs += slice; + } + return false; +} + +// ── engine ────────────────────────────────────────────────────────────────────────────────────── + +unsigned char* gPool = nullptr; +unsigned char gPoolSeed[32] = {}; +bool gPoolSeeded = false; + +bool ensurePool(const unsigned char* miningSeed) +{ + if (gPool == nullptr) + { + gPool = (unsigned char*)malloc((size_t)score_engine::POOL_VEC_PADDING_SIZE); + if (gPool == nullptr) + { + return false; + } + } + if (gPoolSeeded && memcmp(gPoolSeed, miningSeed, 32) == 0) + { + return true; + } + unsigned char state[score_engine::STATE_SIZE]; + score_engine::generateRandom2Pool(miningSeed, state, gPool); + memcpy(gPoolSeed, miningSeed, 32); + gPoolSeeded = true; + return true; +} + +bool loadEmbeddedTask(score_engine::ScoreBpp9000T& engine) +{ + const unsigned int inputTrits = (unsigned int)BPP9000_NUMBER_OF_INPUT_NEURONS; + const unsigned int outputTrits = (unsigned int)BPP9000_NUMBER_OF_OUTPUT_NEURONS; + const unsigned int population = (unsigned int)BPP9000_POPULATION_THRESHOLD; + const unsigned int neighbors = (unsigned int)BPP9000_NUMBER_OF_NEIGHBORS; + + const unsigned long long topologyBytes = score_task_file::topologyBytes(inputTrits, outputTrits, population, neighbors); + const unsigned char* topologyBlock = BPP9000_TASK_BYTES + sizeof(score_task_file::TaskFileHeader); + const unsigned char* dataBlock = topologyBlock + topologyBytes; + + engine.initMemory(); + return engine.loadTaskFromMemory(topologyBlock, dataBlock); +} + +// ── worker pool ───────────────────────────────────────────────────────────────────────────────── + +struct WorkerPool +{ + std::mutex queueMutex; // SMARTMUTEX-EXEMPT: separate process, holds no node state and never forks + std::condition_variable queueSignal; + std::deque queue; + + std::mutex writeMutex; // SMARTMUTEX-EXEMPT: separate process, holds no node state and never forks + std::atomic stopping{ false }; + std::atomic connected{ false }; + std::atomic epochId{ 0 }; + std::atomic connectionFd{ -1 }; + std::atomic busy{ 0 }; + + std::vector threads; +}; + +WorkerPool gPoolOfWorkers; + +void runWorker(unsigned int workerIndex) +{ + score_engine::ScoreBpp9000T* engine = (score_engine::ScoreBpp9000T*)aligned_alloc(64, (sizeof(score_engine::ScoreBpp9000T) + 63) / 64 * 64); + if (engine == nullptr || !loadEmbeddedTask(*engine)) + { + fprintf(stderr, "[ant-walker] worker %u could not load the embedded task\n", workerIndex); + fflush(stderr); + return; + } + Ann rootAnn; + Ann childAnn; + + for (;;) + { + AntWalkProto::JobPayload job; + { + std::unique_lock lock(gPoolOfWorkers.queueMutex); + gPoolOfWorkers.queueSignal.wait(lock, [] { + return gPoolOfWorkers.stopping.load(std::memory_order_acquire) + || !gPoolOfWorkers.queue.empty(); + }); + if (gPoolOfWorkers.stopping.load(std::memory_order_acquire) && gPoolOfWorkers.queue.empty()) + { + break; + } + job = gPoolOfWorkers.queue.front(); + gPoolOfWorkers.queue.pop_front(); + gPoolOfWorkers.busy.fetch_add(1, std::memory_order_acq_rel); + } + + struct BusyGuard + { + ~BusyGuard() { gPoolOfWorkers.busy.fetch_sub(1, std::memory_order_acq_rel); } + } busyGuard; + + AntWalkProto::ResultPayload result; + memset(&result, 0, sizeof(result)); + result.jobId = job.jobId; + result.epochId = job.epochId; + + if (job.epochId != gPoolOfWorkers.epochId.load(std::memory_order_acquire)) + { + result.status = AntWalkProto::ResultStaleEpoch; + } + else + { + const Ann* parentAnn; + if (job.isRoot) + { + engine->deriveRootANN(job.pubkey, gPool, rootAnn); + parentAnn = &rootAnn; + } + else + { + parentAnn = (const Ann*)job.parentAnn; + } + + const unsigned int score = engine->computeScoreFromParent(*parentAnn, job.pubkey, job.nonce, job.anchorDigest, gPool); + if (score == score_engine::INVALID_SCORE_VALUE) + { + result.status = AntWalkProto::ResultUnscorable; + } + else + { + engine->getBestANN(childAnn); + result.status = AntWalkProto::ResultOk; + result.score = score; + memcpy(result.childAnn, &childAnn, sizeof(childAnn)); + } + } + + // A walk cannot be aborted, so one that outlived its connection finishes and is dropped. + if (!gPoolOfWorkers.connected.load(std::memory_order_acquire)) + { + continue; + } + std::lock_guard writeGuard(gPoolOfWorkers.writeMutex); + const int fd = gPoolOfWorkers.connectionFd.load(std::memory_order_acquire); + if (fd < 0) + { + continue; + } + AntWalkProto::FrameHeader header; + header.magic = AntWalkProto::MAGIC; + header.type = AntWalkProto::MsgResult; + header.payloadSize = (unsigned int)sizeof(result); + header.reserved = 0; + if (!writeFully(fd, &header, sizeof(header)) || !writeFully(fd, &result, sizeof(result))) + { + gPoolOfWorkers.connected.store(false, std::memory_order_release); + } + } + + free(engine); +} + +// ── connection ────────────────────────────────────────────────────────────────────────────────── + +bool sendReady(int fd, unsigned int status, unsigned int threadCount, unsigned int epochId) +{ + AntWalkProto::ReadyPayload ready; + memset(&ready, 0, sizeof(ready)); + ready.version = AntWalkProto::VERSION; + ready.status = status; + ready.threadCount = threadCount; + ready.annBytes = AntWalkProto::ANN_BYTES; + ready.epochId = epochId; + ready.walkerPid = (unsigned int)getpid(); + + AntWalkProto::FrameHeader header; + header.magic = AntWalkProto::MAGIC; + header.type = AntWalkProto::MsgReady; + header.payloadSize = (unsigned int)sizeof(ready); + header.reserved = 0; + + std::lock_guard writeGuard(gPoolOfWorkers.writeMutex); + return writeFully(fd, &header, sizeof(header)) && writeFully(fd, &ready, sizeof(ready)); +} + +unsigned int validateHello(const AntWalkProto::HelloPayload& hello) +{ + if (hello.version != AntWalkProto::VERSION || hello.annBytes != AntWalkProto::ANN_BYTES) + { + return AntWalkProto::ReadyVersionMismatch; + } + if (memcmp(hello.topologyHash, BPP9000_TOPOLOGY_HASH, 32) != 0 || memcmp(hello.dataHash, BPP9000_DATA_HASH, 32) != 0) + { + return AntWalkProto::ReadyTaskMismatch; + } + if (hello.configHash != AntWalkProto::CONFIG_HASH) + { + fprintf(stderr, "[ant-walker] scorer config %08x does not match the node's %08x\n", AntWalkProto::CONFIG_HASH, hello.configHash); + fflush(stderr); + return AntWalkProto::ReadyConfigMismatch; + } + return AntWalkProto::ReadyOk; +} + +bool hasWorkOutstanding() +{ + if (gPoolOfWorkers.busy.load(std::memory_order_acquire) > 0) + { + return true; + } + std::lock_guard queueGuard(gPoolOfWorkers.queueMutex); + return !gPoolOfWorkers.queue.empty(); +} + +void serveConnection(int fd, unsigned int threadCount) +{ + gPoolOfWorkers.connectionFd.store(fd, std::memory_order_release); + gPoolOfWorkers.connected.store(true, std::memory_order_release); + + unsigned char payload[AntWalkProto::MAX_PAYLOAD_BYTES]; + bool handshaken = false; + + while (gPoolOfWorkers.connected.load(std::memory_order_acquire)) + { + if (!waitReadable(fd, NO_TRAFFIC_TIMEOUT_MS, gPoolOfWorkers.stopping)) + { + // A walk outlasts this timeout, so silence only means a dead peer when nothing is queued. + if (hasWorkOutstanding()) + { + continue; + } + fprintf(stderr, "[ant-walker] no traffic for %d ms and nothing in flight, dropping the connection\n", NO_TRAFFIC_TIMEOUT_MS); + fflush(stderr); + break; + } + + AntWalkProto::FrameHeader header; + if (!readFully(fd, &header, sizeof(header))) + { + break; + } + if (header.magic != AntWalkProto::MAGIC || header.payloadSize > sizeof(payload)) + { + fprintf(stderr, "[ant-walker] bad frame (magic %08x size %u), dropping the connection\n", header.magic, header.payloadSize); + fflush(stderr); + break; + } + if (header.payloadSize && !readFully(fd, payload, header.payloadSize)) + { + break; + } + + if (header.type == AntWalkProto::MsgHello) + { + if (header.payloadSize != sizeof(AntWalkProto::HelloPayload)) + { + break; + } + AntWalkProto::HelloPayload hello; + memcpy(&hello, payload, sizeof(hello)); + + unsigned int status = validateHello(hello); + if (status == AntWalkProto::ReadyOk && !ensurePool(hello.miningSeed)) + { + status = AntWalkProto::ReadySeedFailed; + } + if (status == AntWalkProto::ReadyOk) + { + gPoolOfWorkers.epochId.store(hello.epochId, std::memory_order_release); + handshaken = true; + } + if (!sendReady(fd, status, threadCount, hello.epochId) || status != AntWalkProto::ReadyOk) + { + fprintf(stderr, "[ant-walker] handshake refused (status %u)\n", status); + fflush(stderr); + break; + } + fprintf(stderr, "[ant-walker] ready, %u threads, epoch %u\n", threadCount, hello.epochId); + fflush(stderr); + } + else if (header.type == AntWalkProto::MsgJob) + { + if (!handshaken || header.payloadSize != sizeof(AntWalkProto::JobPayload)) + { + break; + } + std::lock_guard queueGuard(gPoolOfWorkers.queueMutex); + gPoolOfWorkers.queue.emplace_back(); + memcpy(&gPoolOfWorkers.queue.back(), payload, sizeof(AntWalkProto::JobPayload)); + gPoolOfWorkers.queueSignal.notify_one(); + } + else if (header.type == AntWalkProto::MsgPing) + { + AntWalkProto::FrameHeader pong; + pong.magic = AntWalkProto::MAGIC; + pong.type = AntWalkProto::MsgPong; + pong.payloadSize = 0; + pong.reserved = 0; + std::lock_guard writeGuard(gPoolOfWorkers.writeMutex); + if (!writeFully(fd, &pong, sizeof(pong))) + { + break; + } + } + } + + gPoolOfWorkers.connected.store(false, std::memory_order_release); + { + // Queued jobs belong to a connection that is gone; only the walks already running are paid for. + std::lock_guard queueGuard(gPoolOfWorkers.queueMutex); + gPoolOfWorkers.queue.clear(); + } + { + // Held until no worker can still be inside writeFully on this descriptor. + std::lock_guard writeGuard(gPoolOfWorkers.writeMutex); + gPoolOfWorkers.connectionFd.store(-1, std::memory_order_release); + } + close(fd); +} + +// ── listener ──────────────────────────────────────────────────────────────────────────────────── + +// Competing with a live server is how an orphaned sidecar ends up answering for a running one. +bool socketPathIsServed(const char* path) +{ + const int probeFd = socket(AF_UNIX, SOCK_STREAM, 0); + if (probeFd < 0) + { + return false; + } + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + snprintf(address.sun_path, sizeof(address.sun_path), "%s", path); + const bool served = connect(probeFd, (struct sockaddr*)&address, sizeof(address)) == 0; + close(probeFd); + return served; +} + +int openListener(const char* path) +{ + if (socketPathIsServed(path)) + { + fprintf(stderr, "[ant-walker] %s is already served, refusing to compete\n", path); + fflush(stderr); + return -1; + } + unlink(path); + + const int listenFd = socket(AF_UNIX, SOCK_STREAM, 0); + if (listenFd < 0) + { + perror("[ant-walker] socket"); + return -1; + } + struct sockaddr_un address; + memset(&address, 0, sizeof(address)); + address.sun_family = AF_UNIX; + snprintf(address.sun_path, sizeof(address.sun_path), "%s", path); + if (bind(listenFd, (struct sockaddr*)&address, sizeof(address)) != 0 || listen(listenFd, 4) != 0) + { + perror("[ant-walker] bind/listen"); + close(listenFd); + return -1; + } + return listenFd; +} + +bool parseOptions(int argc, const char* argv[], Options& options) +{ + for (int i = 1; i + 1 < argc; i++) + { + const std::string argument = argv[i]; + if (argument == "--socket") + { + options.socketPath = argv[i + 1]; + } + else if (argument == "--threads") + { + options.threadCount = (unsigned int)strtoul(argv[i + 1], nullptr, 10); + } + } + return !options.socketPath.empty() && options.threadCount > 0; +} + +// Checked before any node setup: the node half of main() must not run in the walker. +inline bool requested(int argc, const char* argv[]) +{ + for (int i = 1; i < argc; i++) + { + if (std::string(argv[i]) == "--ant-walk-worker") + { + return true; + } + } + return false; +} + +inline int run(int argc, const char* argv[]) +{ + Options options; + if (!parseOptions(argc, argv, options)) + { + fprintf(stderr, "[ant-walker] --ant-walk-worker needs --socket --threads \n"); + return 2; + } + + signal(SIGPIPE, SIG_IGN); +#if defined(__linux__) + // The node is the only reason this process exists; outliving it would just squat the socket. + prctl(PR_SET_PDEATHSIG, SIGTERM); + if (getppid() == 1) + { + return 0; + } +#endif + + const int listenFd = openListener(options.socketPath.c_str()); + if (listenFd < 0) + { + return 3; + } + + for (unsigned int i = 0; i < options.threadCount; i++) + { + gPoolOfWorkers.threads.emplace_back(runWorker, i); + } + fprintf(stderr, "[ant-walker] listening on %s, %u threads, pid %d\n", options.socketPath.c_str(), options.threadCount, (int)getpid()); + fflush(stderr); + + while (!gPoolOfWorkers.stopping.load(std::memory_order_acquire)) + { + struct pollfd entry; + entry.fd = listenFd; + entry.events = POLLIN; + entry.revents = 0; + const int ready = poll(&entry, 1, POLL_SLICE_MS); +#if defined(__linux__) + if (getppid() == 1) + { + break; + } +#endif + if (ready <= 0) + { + continue; + } + const int connectionFd = accept(listenFd, nullptr, nullptr); + if (connectionFd < 0) + { + continue; + } + serveConnection(connectionFd, options.threadCount); + } + + gPoolOfWorkers.stopping.store(true, std::memory_order_release); + gPoolOfWorkers.queueSignal.notify_all(); + for (std::thread& worker : gPoolOfWorkers.threads) + { + worker.join(); + } + close(listenFd); + unlink(options.socketPath.c_str()); + return 0; +} +} + +#else + +namespace AntWalkerWorker +{ +inline bool requested(int, const char*[]) { return false; } +inline int run(int, const char*[]) { return 0; } +} + +#endif // ANT_WALKER && !_WIN32 diff --git a/src/extensions/http/controller/rpc_live_controller.h b/src/extensions/http/controller/rpc_live_controller.h index 4f8be75b..d1133db5 100644 --- a/src/extensions/http/controller/rpc_live_controller.h +++ b/src/extensions/http/controller/rpc_live_controller.h @@ -704,6 +704,19 @@ RPC_ROUTE("GET", "/live/v1/debug-trace") logs.append(logEntry); } entry["logs"] = logs; + + Json::Value cheats(Json::arrayValue); + for (const auto& cheat : trace.cheats) + { + Json::Value cheatEntry; + cheatEntry["id"] = cheat.id; + cheatEntry["part"] = (unsigned int)cheat.part; + cheatEntry["size"] = cheat.size; + cheatEntry["value"] = (Json::UInt64)cheat.value; + cheatEntry["hex"] = cheat.hex; + cheats.append(cheatEntry); + } + entry["cheats"] = cheats; entries.append(entry); } json["entries"] = entries; diff --git a/src/extensions/http/controller/rpc_stats_controller.h b/src/extensions/http/controller/rpc_stats_controller.h index fe1927ae..d73dffbe 100644 --- a/src/extensions/http/controller/rpc_stats_controller.h +++ b/src/extensions/http/controller/rpc_stats_controller.h @@ -140,6 +140,15 @@ RPC_ROUTE("GET", "/v1/fork-stats") return r; } +// Ant walker health: separates a walker chewing through work from one up but delivering nothing. +RPC_ROUTE("GET", "/v1/ant-walker") +{ + (void)req; + RpcResp r; + r.body = AntWalker::statsJson(); + return r; +} + // The full durable record of every unforkable tick (not a recent ring) — one line per skipped fork. RPC_ROUTE("GET", "/v1/unforkable-ticks") { diff --git a/src/extensions/supervisor_shim.h b/src/extensions/supervisor_shim.h index e8015433..b22c27a2 100644 --- a/src/extensions/supervisor_shim.h +++ b/src/extensions/supervisor_shim.h @@ -16,6 +16,7 @@ #include inline char gSidecarPort[16] = "41841"; // node http port -> sidecar listen + unix-socket key +inline char gAntWalkerThreads[16] = "0"; // matches the node default; 0 keeps the walker unspawned // Forward a stop signal to the children so the container/service stops promptly. static void shimForwardSignal(int sig) @@ -49,8 +50,38 @@ static pid_t shimForkSidecar() #endif } +// Re-exec self as the ant walker, a sibling of the node. Spawned here, not by the node, so it +// outlives a rollback promotion. +static pid_t shimForkAntWalker() +{ + if (std::atoi(gAntWalkerThreads) <= 0) + return -1; + + const pid_t supervisorPid = getpid(); + pid_t walkerPid = fork(); + if (walkerPid != 0) + return walkerPid; + prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); + if (getppid() != supervisorPid) + _exit(0); + + char self[512]; + ssize_t pathLength = readlink("/proc/self/exe", self, sizeof(self) - 1); + if (pathLength <= 0) + _exit(127); + self[pathLength] = 0; + + char socketPath[128]; + snprintf(socketPath, sizeof(socketPath), "/tmp/qubic-antwalk-%s.sock", gSidecarPort); + execl(self, "qubic-ant-walker", "--ant-walk-worker", "--socket", socketPath, "--threads", gAntWalkerThreads, (char*)nullptr); + // Not fatal for the node: without a walker the backlog is simply paid on demand as before. + fprintf(stderr, "[shim] could not exec the ant walker (%s), running without it\n", strerror(errno)); + fflush(stderr); + _exit(127); +} + // True while any child other than the sidecar exists (i.e. the node lineage is still alive). -static bool shimHasNodeChild(pid_t sidecar) +static bool shimHasNodeChild(pid_t sidecar, pid_t antWalker) { char path[64]; snprintf(path, sizeof(path), "/proc/self/task/%d/children", (int)getpid()); @@ -61,7 +92,7 @@ static bool shimHasNodeChild(pid_t sidecar) bool hasNodeChild = false; while (fscanf(childrenFile, "%d", &childPid) == 1) { - if (childPid != (int)sidecar) + if (childPid != (int)sidecar && childPid != (int)antWalker) { hasNodeChild = true; break; @@ -85,12 +116,19 @@ static inline void runUnderSupervisor(int argc, const char** argv) std::strncpy(gSidecarPort, argv[i + 1], sizeof(gSidecarPort) - 1); gSidecarPort[sizeof(gSidecarPort) - 1] = 0; } + if (std::string(argv[i]) == "--ant-walker-threads" && i + 1 < argc) + { + std::strncpy(gAntWalkerThreads, argv[i + 1], sizeof(gAntWalkerThreads) - 1); + gAntWalkerThreads[sizeof(gAntWalkerThreads) - 1] = 0; + } } if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) != 0) // can't subreap: run node inline return; pid_t sidecar = shimForkSidecar(); + pid_t antWalker = shimForkAntWalker(); + int antWalkerRestarts = 0; pid_t node = fork(); if (node < 0) @@ -121,14 +159,30 @@ static inline void runUnderSupervisor(int argc, const char** argv) sidecar = shimForkSidecar(); // RPC must not stay down: restart it continue; } + if (antWalker > 0 && reapedPid == antWalker) + { + sleep(1); // a squatted socket would hot-loop the respawn + // An unrunnable walker would otherwise respawn once a second forever. + if (++antWalkerRestarts > 5) + { + fprintf(stderr, "[shim] ant walker died %d times, leaving it down\n", antWalkerRestarts); + fflush(stderr); + antWalker = -1; + continue; + } + antWalker = shimForkAntWalker(); + continue; + } lastStatus = status; if (WIFSIGNALED(status)) sawSignal = true; - if (!shimHasNodeChild(sidecar)) + if (!shimHasNodeChild(sidecar, antWalker)) break; // node lineage drained -> shim exits } if (sidecar > 0) kill(sidecar, SIGTERM); + if (antWalker > 0) + kill(antWalker, SIGTERM); _exit(sawSignal ? 1 : (WIFEXITED(lastStatus) ? WEXITSTATUS(lastStatus) : 1)); } diff --git a/src/extensions/wasm/runtime/dispatch.h b/src/extensions/wasm/runtime/dispatch.h index d55e0674..db745947 100644 --- a/src/extensions/wasm/runtime/dispatch.h +++ b/src/extensions/wasm/runtime/dispatch.h @@ -79,6 +79,7 @@ static CallContext createCallContext(const void* context, uint32_t arenaStart, u callContext.arenaStart = arenaStart; callContext.arenaTop = arenaStart; callContext.arenaLimit = arenaLimit; + clearCheatWarp(); return callContext; } @@ -88,6 +89,7 @@ static void bindJournal(CallContext& callContext, const EngineSlot& slot) callContext.journalBaseOffset = slot.journalBaseOffset; callContext.stateOffset = slot.stateOffset; callContext.journalHeader = slot.journalBaseOffset ? &slot.journalHeader : nullptr; + callContext.guestContextOffset = slot.contextOffset; } static void bindEnvironment(wasm_exec_env_t execEnv, CallContext& callContext) diff --git a/src/extensions/wasm/runtime/host_services.h b/src/extensions/wasm/runtime/host_services.h index 083daf21..a0c2daef 100644 --- a/src/extensions/wasm/runtime/host_services.h +++ b/src/extensions/wasm/runtime/host_services.h @@ -28,6 +28,7 @@ static HostServices hostServices = .burn = &burn, .epoch = &epoch, .tick = &tick, + .initialTick = &initialTick, .numberOfTickTransactions = &numberOfTickTransactions, .getEntity = &getEntity, .queryFeeReserve = &queryFeeReserve, @@ -75,6 +76,7 @@ static HostServices hostServices = .liteInvokeProcedure = &invokeContractProcedure, .setShareholderProposal = &setShareholderProposal, .setShareholderVotes = &setShareholderVotes, + .cheat = &cheat, }; } // namespace Wasm::Runtime diff --git a/src/extensions/wasm/runtime/lhost_registry.h b/src/extensions/wasm/runtime/lhost_registry.h index 67d37fab..ee4e83dd 100644 --- a/src/extensions/wasm/runtime/lhost_registry.h +++ b/src/extensions/wasm/runtime/lhost_registry.h @@ -25,6 +25,8 @@ struct CallContext uint32_t journalBaseOffset = 0; uint32_t stateOffset = 0; const JournalHeader* journalHeader = nullptr; + // Where the guest's copy of QpiContext lives, so a prank can rewrite the caller it observes. + uint32_t guestContextOffset = 0; }; static inline CallContext* activeCallContext(wasm_exec_env_t execEnv) @@ -248,12 +250,54 @@ static void w_logBytes(wasm_exec_env_t execEnv, uint32_t contractIndex, uint32_t if (callContext && callContext->trace) { + // hostServices.logBytes stamps this word for the log store and zeroes it after; the trace must capture the stamped bytes. + *((unsigned int*)message) = contractIndex; recordLog((TraceEntry*)callContext->trace, (unsigned char)type, message, size); } hostServices.logBytes(contractIndex, (unsigned char)type, message, size); } +// CC_PRINT lands on the trace and nowhere else: no log id, no qLogger, no tick log range. Everything +// that touches node state goes on to hostServices.cheat, which is testnet-only and checks the context. +static int64_t w_cheat(wasm_exec_env_t execEnv, uint32_t op, uint64_t a, uint64_t b, uint32_t ptrOffset, uint32_t len) +{ + CallContext* callContext = activeCallContext(execEnv); + // The length says whether there is a payload, not the offset: offset 0 is an ordinary address, and + // contract state lives there, so testing the offset drops exactly the reads worth printing. + void* payload = len ? nativeAddress(execEnv, ptrOffset) : nullptr; + + if (op == CHEAT_OP_PRINT) + { + if (callContext && callContext->trace) + { + recordCheat((TraceEntry*)callContext->trace, (uint32_t)(a >> 8), (unsigned char)(a & 0xff), b, payload, len); + } + + return 0; + } + + if (!callContext) + { + return CHEAT_ERR_WRONG_CONTEXT; + } + + if (op == CHEAT_OP_PRANK || op == CHEAT_OP_UNPRANK) + { + void* guestContext = callContext->guestContextOffset ? nativeAddress(execEnv, callContext->guestContextOffset) : nullptr; + const bool prank = op == CHEAT_OP_PRANK; + + if (prank && (!payload || len != 32)) + { + return CHEAT_ERR_UNKNOWN_OP; + } + + return prankCheatCaller(callContext->ctx, guestContext, prank ? (const m256i*)payload : nullptr, (int64_t)a); + } + + return hostServices.cheat(callContext->ctx, op, a, b, payload, len); +} + static uint32_t w_getEntity(wasm_exec_env_t execEnv, uint32_t idOffset, uint32_t entityOffset) { CallContext* callContext = activeCallContext(execEnv); diff --git a/src/extensions/wasm/runtime/qpi_services.h b/src/extensions/wasm/runtime/qpi_services.h index 9838a933..60c6de1a 100644 --- a/src/extensions/wasm/runtime/qpi_services.h +++ b/src/extensions/wasm/runtime/qpi_services.h @@ -203,14 +203,29 @@ static long long burn(const void* context, long long amount, unsigned int contra return procedureContext(context)->burn(amount, contractIndex); } +// CC_WARP shifts only what the contract observes; the node still commits the real tick and epoch, so a +// warping contract cannot move consensus. Reset per dispatch, and constant-folded away off testnet. +#if defined(TESTNET) +static unsigned int cheatTickOffset = 0; +static unsigned short cheatEpochOffset = 0; +#else +static constexpr unsigned int cheatTickOffset = 0; +static constexpr unsigned short cheatEpochOffset = 0; +#endif + static unsigned short epoch(const void* context) { - return functionContext(context)->epoch(); + return (unsigned short)(functionContext(context)->epoch() + cheatEpochOffset); } static unsigned int tick(const void* context) { - return functionContext(context)->tick(); + return functionContext(context)->tick() + cheatTickOffset; +} + +static unsigned int initialTick(const void* context) +{ + return functionContext(context)->initialTick(); } static int numberOfTickTransactions(const void* context) @@ -424,6 +439,133 @@ static unsigned char distributeDividends(const void* context, long long amountPe return (unsigned char)procedureContext(context)->distributeDividends(amountPerShare); } +// Mirrors QpiContext's protected layout so a prank can rewrite the caller a contract observes. The +// size assert turns any drift in QpiContext into a compile error rather than a silent misread. +struct CheatContextImage +{ + unsigned int currentContractIndex; + int stackIndex; + m256i currentContractId; + m256i originator; + m256i invocator; + long long invocationReward; + unsigned char entryPoint; +}; +static_assert(sizeof(CheatContextImage) == sizeof(QPI::QpiContext), "CheatContextImage out of sync with QPI::QpiContext"); + +static void clearCheatWarp() +{ +#if defined(TESTNET) + cheatTickOffset = 0; + cheatEpochOffset = 0; +#endif +} + +#if defined(TESTNET) +// Sets a balance outright rather than transferring, which is the whole point of a deal. +static long long dealCheatBalance(const m256i& publicKey, long long amount) +{ + // The amount arrives as an unsigned word, so a value past the signed range lands here negative. + // A negative balance is meaningless, and letting it through would decrease against index -1. + if (amount < 0) + { + return CHEAT_ERR_UNKNOWN_OP; + } + + const int index = spectrumIndex(publicKey); + const long long current = index < 0 ? 0 : energy(index); + + if (current > amount) + { + return decreaseEnergy(index, current - amount) ? amount : CHEAT_ERR_WRONG_CONTEXT; + } + + if (current < amount) + { + increaseEnergy(publicKey, amount - current); + } + + return amount; +} + +#endif + +// Rewrites the guest's copy of the context. The host's own QpiContext is untouched, so the node still +// bills and attributes the real caller; only what the contract reads changes. Lives here rather than in +// the vtable because the guest address comes from the adapter, and the vtable signature must mirror the +// guest's exactly. +static long long prankCheatCaller(const void* context, void* guestContext, const m256i* caller, long long invocationReward) +{ +#if defined(TESTNET) + const CheatContextImage* hostImage = (const CheatContextImage*)context; + + if (!hostImage || hostImage->entryPoint == (unsigned char)DispatchKind::UserFunction) + { + return CHEAT_ERR_WRONG_CONTEXT; + } + + if (!guestContext) + { + return CHEAT_ERR_WRONG_CONTEXT; + } + + CheatContextImage* image = (CheatContextImage*)guestContext; + + // Unprank restores what the host handed the guest at dispatch, which is the host's own context. + image->originator = caller ? *caller : hostImage->originator; + image->invocator = caller ? *caller : hostImage->invocator; + image->invocationReward = caller ? invocationReward : hostImage->invocationReward; + return image->invocationReward; +#else + (void)context; + (void)guestContext; + (void)caller; + (void)invocationReward; + return CHEAT_ERR_DISABLED; +#endif +} + +// Development cheatcodes, opcode-dispatched behind one ABI row so a new one costs no import. Every +// state-touching opcode is testnet-only, and refusal is always a negative return, never a trap. +// CHEAT_OP_PRINT and the prank opcodes never reach here: the adapter owns the trace and guest memory. +static long long cheat(const void* context, unsigned int op, unsigned long long a, unsigned long long b, void* ptr, unsigned int len) +{ +#if defined(TESTNET) + const CheatContextImage* image = (const CheatContextImage*)context; + + // Every opcode below mutates something a read-only call must not touch. + if (!image || image->entryPoint == (unsigned char)DispatchKind::UserFunction) + { + return CHEAT_ERR_WRONG_CONTEXT; + } + + switch (op) + { + case CHEAT_OP_DEAL: + return (ptr && len == 32) ? dealCheatBalance(*(const m256i*)ptr, (long long)a) : CHEAT_ERR_UNKNOWN_OP; + + case CHEAT_OP_WARP_TICK: + cheatTickOffset += (unsigned int)a; + return (long long)cheatTickOffset; + + case CHEAT_OP_WARP_EPOCH: + cheatEpochOffset = (unsigned short)(cheatEpochOffset + a); + return (long long)cheatEpochOffset; + + default: + return CHEAT_ERR_UNKNOWN_OP; + } +#else + (void)context; + (void)op; + (void)a; + (void)b; + (void)ptr; + (void)len; + return CHEAT_ERR_DISABLED; +#endif +} + } // namespace Wasm::Runtime #endif // LITE_WASM_SC diff --git a/src/extensions/wasm/runtime/trace.h b/src/extensions/wasm/runtime/trace.h index e27f4ad0..4049bd5b 100644 --- a/src/extensions/wasm/runtime/trace.h +++ b/src/extensions/wasm/runtime/trace.h @@ -45,6 +45,16 @@ struct LogTrace std::string hex; }; +// One CC_PRINT argument. size == 0 means the value came through by register instead of by pointer. +struct CheatEntry +{ + unsigned int id = 0; + unsigned char part = 0; + unsigned int size = 0; + unsigned long long value = 0; + std::string hex; +}; + struct TraceEntry { unsigned long long sequence = 0; @@ -67,6 +77,7 @@ struct TraceEntry std::vector stateDiff; std::vector hostCalls; std::vector logs; + std::vector cheats; }; // On by default so a debugger attached after the fact still finds the calls that mattered. This header @@ -219,6 +230,23 @@ static inline void recordLog(TraceEntry* entry, unsigned char type, const void* }); } +// The development print channel. Deliberately separate from logs: it consumes no log id, reaches no +// qLogger, and is stripped from the contract before submission. +static inline void recordCheat(TraceEntry* entry, unsigned int id, unsigned char part, unsigned long long value, const void* bytes, unsigned int size) +{ + if (!entry) + { + return; + } + + // A guest offset outside linear memory resolves to null; record the size but never read from it. + const bool readable = bytes && size; + + entry->cheats.push_back(CheatEntry{ + id, part, size, value, readable ? hex(bytes, size) : std::string(), + }); +} + } // namespace Wasm::Runtime #endif // LITE_WASM_SC diff --git a/src/extensions/wasm/sdk/intercontract_calls.h b/src/extensions/wasm/sdk/intercontract_calls.h index fab80bb6..6d15ec00 100644 --- a/src/extensions/wasm/sdk/intercontract_calls.h +++ b/src/extensions/wasm/sdk/intercontract_calls.h @@ -13,10 +13,13 @@ int invokeProcedure(const void* callerContext, unsigned int calleeIndex, unsigne } // namespace Wasm::Sdk -// Calls remain restricted to lower-index contracts. +// Calls remain restricted to lower-index contracts. The entry-kind and locals-size checks match +// qpi_macros.h, so a mistake fails to compile here instead of returning a call error at run time. #undef CALL_OTHER_CONTRACT_FUNCTION_E #define CALL_OTHER_CONTRACT_FUNCTION_E(contractStateType, function, input, output, errorVar) \ static_assert(contractStateType::__contract_index < CONTRACT_INDEX, "lite: can only call a lower-index contract"); \ + static_assert(sizeof(contractStateType::function##_locals) <= MAX_SIZE_OF_CONTRACT_LOCALS, #function "_locals size too large"); \ + static_assert(contractStateType::__is_function_##function, "CALL_OTHER_CONTRACT_FUNCTION_E() cannot be used to invoke procedures."); \ QPI::InterContractCallError errorVar = (QPI::InterContractCallError)Wasm::Sdk::callFunction( \ &qpi, contractStateType::__contract_index, contractStateType##_##function##_inputType, \ &(input), sizeof(input), &(output), sizeof(output)) @@ -28,6 +31,8 @@ int invokeProcedure(const void* callerContext, unsigned int calleeIndex, unsigne #undef INVOKE_OTHER_CONTRACT_PROCEDURE_E #define INVOKE_OTHER_CONTRACT_PROCEDURE_E(contractStateType, procedure, input, output, invocationReward, errorVar) \ static_assert(contractStateType::__contract_index < CONTRACT_INDEX, "lite: can only call a lower-index contract"); \ + static_assert(sizeof(contractStateType::procedure##_locals) <= MAX_SIZE_OF_CONTRACT_LOCALS, #procedure "_locals size too large"); \ + static_assert(!contractStateType::__is_function_##procedure, "INVOKE_OTHER_CONTRACT_PROCEDURE_E() cannot be used to call functions."); \ QPI::InterContractCallError errorVar = (QPI::InterContractCallError)Wasm::Sdk::invokeProcedure( \ &qpi, contractStateType::__contract_index, contractStateType##_##procedure##_inputType, \ &(input), sizeof(input), &(output), sizeof(output), (invocationReward)) diff --git a/src/extensions/wasm/sdk/lhost_imports.h b/src/extensions/wasm/sdk/lhost_imports.h index 7f3881b0..ef6cb5bc 100644 --- a/src/extensions/wasm/sdk/lhost_imports.h +++ b/src/extensions/wasm/sdk/lhost_imports.h @@ -44,6 +44,7 @@ LH_IMPORT(abort) void lh_abort(unsigned int errorCode); LH_IMPORT(burn) long long lh_burn(long long amount, unsigned int contractIndexBurnedFor); LH_IMPORT(epoch) unsigned int lh_epoch(); LH_IMPORT(tick) unsigned int lh_tick(); +LH_IMPORT(initialTick) unsigned int lh_initialTick(); LH_IMPORT(numberOfTickTransactions) int lh_numberOfTickTransactions(); LH_IMPORT(getEntity) unsigned int lh_getEntity(const void* id32, void* entityOut); LH_IMPORT(queryFeeReserve) long long lh_queryFeeReserve(unsigned int contractIndex); @@ -91,6 +92,7 @@ LH_IMPORT(liteCallFunction) int lh_liteCallFunction(unsigned int calleeIdx, unsi LH_IMPORT(liteInvokeProcedure) int lh_liteInvokeProcedure(unsigned int calleeIdx, unsigned int inputType, const void* in, unsigned int inSize, void* out, unsigned int outSize, long long invocationReward); LH_IMPORT(liteSetShareholderProposal) unsigned int lh_liteSetShareholderProposal(unsigned int calleeIdx, const void* proposal1024, long long invocationReward); LH_IMPORT(liteSetShareholderVotes) unsigned int lh_liteSetShareholderVotes(unsigned int calleeIdx, const void* voteData, unsigned int voteSize, long long invocationReward); +LH_IMPORT(cheat) long long lh_cheat(unsigned int op, unsigned long long a, unsigned long long b, void* ptr, unsigned int len); } // extern "C" namespace Wasm::Sdk diff --git a/src/extensions/wasm/sdk/qpi_forwarders.h b/src/extensions/wasm/sdk/qpi_forwarders.h index 06eae067..3f780c0f 100644 --- a/src/extensions/wasm/sdk/qpi_forwarders.h +++ b/src/extensions/wasm/sdk/qpi_forwarders.h @@ -45,6 +45,11 @@ unsigned int QPI::QpiContextFunctionCall::tick() const return lh_tick(); } +unsigned int QPI::QpiContextFunctionCall::initialTick() const +{ + return lh_initialTick(); +} + int QPI::QpiContextFunctionCall::numberOfTickTransactions() const { return lh_numberOfTickTransactions(); diff --git a/src/extensions/wasm/shared/abi_metadata.h b/src/extensions/wasm/shared/abi_metadata.h index d47e8089..7197151d 100644 --- a/src/extensions/wasm/shared/abi_metadata.h +++ b/src/extensions/wasm/shared/abi_metadata.h @@ -1,7 +1,7 @@ #pragma once // Canonical rows shared by WAMR registration and SDK metadata generation. -#define WASM_ABI_VERSION 5u +#define WASM_ABI_VERSION 6u // G/H selects generated or handwritten adapters; Q/I selects QPI-bound or infrastructure calls. @@ -35,6 +35,7 @@ HQ("burn", burn, w_burn, "(Ii)I") \ GQ("epoch", epoch, "()i") \ GQ("tick", tick, "()i") \ + GQ("initialTick", initialTick, "()i") \ GQ("numberOfTickTransactions", numberOfTickTransactions, "()i") \ HQ("getEntity", getEntity, w_getEntity, "(ii)i") \ GQ("queryFeeReserve", queryFeeReserve, "(i)I") \ @@ -81,4 +82,5 @@ HQ("liteCallFunction", liteCallFunction, w_liteCallFunction, "(iiiiii)i") \ HQ("liteInvokeProcedure", liteInvokeProcedure, w_liteInvokeProcedure, "(iiiiiiI)i") \ HQ("liteSetShareholderProposal", setShareholderProposal, w_liteSetShareholderProposal, "(iiI)i") \ - HQ("liteSetShareholderVotes", setShareholderVotes, w_liteSetShareholderVotes, "(iiiI)i") + HQ("liteSetShareholderVotes", setShareholderVotes, w_liteSetShareholderVotes, "(iiiI)i") \ + HQ("cheat", cheat, w_cheat, "(iIIii)I") diff --git a/src/extensions/wasm/shared/abi_types.h b/src/extensions/wasm/shared/abi_types.h index 14d3bea9..5bb6e458 100644 --- a/src/extensions/wasm/shared/abi_types.h +++ b/src/extensions/wasm/shared/abi_types.h @@ -64,6 +64,7 @@ struct HostServices long long (*burn)(const void* ctx, long long amount, unsigned int contractIndexBurnedFor); unsigned short (*epoch)(const void* ctx); unsigned int (*tick)(const void* ctx); + unsigned int (*initialTick)(const void* ctx); int (*numberOfTickTransactions)(const void* ctx); unsigned char (*getEntity)(const void* ctx, const void* id32, void* entityOut); long long (*queryFeeReserve)(const void* ctx, unsigned int contractIndex); @@ -125,8 +126,26 @@ struct HostServices unsigned short (*setShareholderProposal)(const void* callerCtx, unsigned int calleeIdx, const void* proposal1024, long long invocationReward); unsigned char (*setShareholderVotes)(const void* callerCtx, unsigned int calleeIdx, const void* voteData, unsigned int voteSize, long long invocationReward); + // Testnet development aid: opcode-dispatched, negative return on refusal. See CHEAT_OP_* below. + long long (*cheat)(const void* ctx, unsigned int op, unsigned long long a, unsigned long long b, void* ptr, unsigned int len); }; +// Opcodes for HostServices::cheat. Reserved numbers stay listed so an older node answers CHEAT_ERR_UNKNOWN_OP +// rather than silently accepting a newer client's call. +#define CHEAT_OP_PRINT 1u +#define CHEAT_OP_DEAL 2u +#define CHEAT_OP_WARP_TICK 3u +#define CHEAT_OP_WARP_EPOCH 4u +#define CHEAT_OP_PRANK 5u +#define CHEAT_OP_UNPRANK 6u +#define CHEAT_OP_WARP_TIME 7u // reserved: needs a calendar inverse the node does not have +#define CHEAT_OP_SNAPSHOT 8u // reserved: no world-snapshot primitive exists in either runtime +#define CHEAT_OP_REVERT 9u // reserved, pairs with CHEAT_OP_SNAPSHOT + +#define CHEAT_ERR_UNKNOWN_OP (-1LL) +#define CHEAT_ERR_DISABLED (-2LL) +#define CHEAT_ERR_WRONG_CONTEXT (-3LL) + #define WASM_MAX_USER_ENTRIES 1024 struct ContractDescriptor diff --git a/src/mining/ant_colony/ant_colony.h b/src/mining/ant_colony/ant_colony.h index 4f8af706..8c1b3f23 100644 --- a/src/mining/ant_colony/ant_colony.h +++ b/src/mining/ant_colony/ant_colony.h @@ -521,7 +521,7 @@ class AntColony // Constraint specific functions // Resolves a parent for scoring. outParentRec is null for ROOT_REF, the caller derives the - // per-identity root from the submitter's pubkey instead. + // shared epoch root from the root seed instead. ValidityResult tryGetParent(const SolutionRef& parentRef, const AntSolutionRecord** outParentRec) const; diff --git a/src/mining/bpp9000_task.generated.h b/src/mining/bpp9000_task.generated.h index e5fa0670..28aab47a 100644 --- a/src/mining/bpp9000_task.generated.h +++ b/src/mining/bpp9000_task.generated.h @@ -1,67 +1,67 @@ // AUTO-GENERATED from data/bpp9000.task by cmake/EmbedBinary.cmake. -// SHA-256: 0c5e9e42c6d86c320af62f4125ca85b2446f2b098893fd6521bcf66c22f7f00a +// SHA-256: aad604c8ba64325ee83dc12e963ff76b393481dd24ff60ef334551470d4b1cb4 #pragma once alignas(64) inline constexpr unsigned char BPP9000_TASK_BYTES[] = { 0x4c, 0x55, 0x54, 0x54, 0x01, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x38, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x13, 0xe9, 0x9d, 0x5b, 0x2f, 0xca, 0x56, 0xaa, 0x78, 0x9c, 0xb9, 0x59, 0x57, 0x5f, 0x48, 0x39, - 0x2f, 0x1a, 0x44, 0x90, 0x9a, 0x8e, 0xaf, 0x27, 0xf2, 0xde, 0x8f, 0x8d, 0x74, 0xb0, 0x7a, 0x6b, + 0x39, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x76, 0xa3, 0xf5, 0x10, 0x20, 0x05, 0x9b, 0xf5, 0x22, 0x7f, 0x30, 0x20, 0x13, 0x69, 0xcb, 0x0a, + 0x32, 0x3b, 0x93, 0xcd, 0xc2, 0x59, 0x8e, 0x1f, 0x1c, 0x59, 0x66, 0xbd, 0x9c, 0x0d, 0xf4, 0xef, 0x97, 0x9c, 0xdc, 0x22, 0x47, 0xd2, 0xca, 0x4e, 0xd3, 0xd6, 0x14, 0xbf, 0x27, 0x89, 0x63, 0x84, 0xcb, 0x1c, 0x9c, 0x3d, 0x80, 0x4a, 0xf6, 0xed, 0xe6, 0xb5, 0x9f, 0xc5, 0x2c, 0x0e, 0x3d, 0xfa, - 0x0b, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, - 0x15, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, - 0x21, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x10, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, - 0x3c, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, - 0x26, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, - 0x3b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, - 0x1b, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, - 0x15, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, - 0x22, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x3f, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, - 0x3d, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, - 0x3f, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, - 0x2a, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, - 0x14, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, - 0x29, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, - 0x15, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, - 0x26, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x1c, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x13, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, - 0x33, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x1c, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x1f, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, - 0x34, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, - 0x1c, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, - 0x10, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, - 0x20, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, - 0x25, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x2b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, - 0x14, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, - 0x2e, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, - 0x2f, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, - 0x28, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x36, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, - 0x34, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, - 0x0a, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, - 0x17, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x34, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x23, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, + 0x36, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, + 0x3d, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, + 0x12, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x0f, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x2d, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, + 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, + 0x35, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, + 0x34, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, + 0x37, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, + 0x1d, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, + 0x2f, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + 0x35, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, + 0x35, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x39, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x2f, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, + 0x1e, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x1f, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, + 0x3e, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x35, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, + 0x25, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x3c, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x22, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x3d, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x27, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x38, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x36, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, + 0x16, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x3b, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x1b, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, + 0x1b, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, + 0x3b, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x29, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, + 0x31, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x38, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x79, 0x0d, 0x0d, 0x0d, 0x00, 0x00, 0x51, 0x5e, 0x0c, 0x01, 0x79, 0x28, 0x79, 0x0c, 0x00, 0x79, 0x28, 0x0d, 0x01, 0x00, 0x00, 0x00, 0x5e, 0x04, 0x01, 0x79, 0x79, 0x70, 0x04, 0x00, 0x79, 0x79, 0x25, 0x04, 0x00, 0x79, 0x79, 0x00, 0x00, 0x00, 0x79, 0x79, 0x28, 0x09, 0x00, 0x79, 0x79, 0x5b, @@ -2799,7 +2799,7 @@ alignas(64) inline constexpr unsigned char BPP9000_TASK_BYTES[] = { 0x01, 0x79, 0x28, 0x00, 0x00, 0x00, 0x79, 0x5e, 0x28, 0x03, 0x00, 0x79, 0x28, 0x1f, 0x0d, 0x00, 0x79, 0x79, 0x70, 0x01, 0x00, 0x00, 0x00, 0x75, 0x0d, 0x01, 0x79, 0x79, 0x27, 0x03, 0x00, 0x00, 0x00, 0x78, 0x00, 0x01, 0x00, 0x00, 0x52, 0x03, 0x01, 0x79, 0x79, 0x76, 0x00, 0x00, 0x79, 0x79, - 0x25, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x09, 0x01, + 0x25, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x09, 0x01, 0x00, 0x00, 0x1c, 0x0d, 0x01, }; inline constexpr unsigned long long BPP9000_TASK_SIZE = sizeof(BPP9000_TASK_BYTES); -static_assert(BPP9000_TASK_SIZE == 44744ULL); +static_assert(BPP9000_TASK_SIZE == 44749ULL); diff --git a/src/mining/score_bpp9000.h b/src/mining/score_bpp9000.h index 40468c8c..039a98e6 100644 --- a/src/mining/score_bpp9000.h +++ b/src/mining/score_bpp9000.h @@ -11,16 +11,6 @@ namespace score_engine // Largest L (mutations per step) the scorer clamps nonce[1] to static constexpr unsigned int MAX_LUT_ENTRIES_PER_STEP = 10; -// A bpp9000 nonce is canonical iff its score-irrelevant knob bytes are canonical: -// nonce[0] = algo (enforced by routing), nonce[1] = L in [1, MAX_LUT_ENTRIES_PER_STEP], nonce[2] = K = 0 -static bool isCanonicalBpp9000Nonce(const unsigned char* nonce) -{ - return (getAlgoType(nonce) == AlgoType::Bpp9000) - && (nonce[1] >= 1) - && (nonce[1] <= MAX_LUT_ENTRIES_PER_STEP) - && (nonce[2] == 0); -} - template struct ScoreBpp9000 { @@ -45,6 +35,17 @@ struct ScoreBpp9000 static_assert(lutSize <= lutStride, "LUT rows must fit the padded stride"); + // A nonce is canonical if its score-irrelevant knob bytes are canonical: + // nonce[0] = algo (enforced by routing), nonce[1] = L in [1, MAX_LUT_ENTRIES_PER_STEP], + // nonce[2] = K. The standalone walk pins K to 0, so only 0 is canonical there. + static bool isCanonicalStandaloneNonce(const unsigned char* nonce) + { + return (getAlgoType(nonce) == AlgoType::Bpp9000) + && (nonce[1] >= 1) + && (nonce[1] <= MAX_LUT_ENTRIES_PER_STEP) + && (nonce[2] == 0); + } + // K is a real degree of freedom here: the walk restores K = nonce[2] as its explore-step count static bool isCanonicalAntNonce(const unsigned char* nonce) { @@ -1073,8 +1074,9 @@ struct ScoreBpp9000 compact(bestANN, out); } - // Seed the ANN: root LUT from the pubkey alone (each computor's fixed root); mutation seeds from - // pubkey+nonce (nonce[0..2] are the algo/L/K knobs, excluded from the RNG). Returns the start score. + // Standalone path only: root LUT from the pubkey alone; the ant path derives its shared epoch + // root via deriveRootANN(rootSeed) instead. Mutation seeds from pubkey+nonce (nonce[0..2] are + // the algo/L/K knobs, excluded from the RNG). Returns the start score. unsigned int initializeANN( const unsigned char* publicKey, const unsigned char* nonce, @@ -1168,14 +1170,16 @@ struct ScoreBpp9000 return computeScoreFromCurrent(L, K, cur); } - // Ant colony: the network every one of an identity's lineages starts from. Written to a buffer the - // caller owns, so two roots can be derived on one engine without the first silently becoming the - // second - a child scored against the wrong root would differ only in resourceTestingDigest. + // Ant colony: the shared per-epoch network every identity's tree starts from. rootSeed is the + // epoch-start spectrum digest, so all identities derive the identical root; only the mutation + // walks stay per-identity. Written to a buffer the caller owns, so two roots can be derived on + // one engine without the first silently becoming the second - a child scored against the wrong + // root would differ only in resourceTestingDigest. // Uses currentANN as its working buffer, so it destroys whatever the engine was holding. Callers // derive a root and then score from it, which overwrites currentANN anyway. - void deriveRootANN(const unsigned char* publicKey, const unsigned char* pRandom2Pool, ANN& out) + void deriveRootANN(const unsigned char* rootSeed, const unsigned char* pRandom2Pool, ANN& out) { - deriveRootLut(publicKey, pRandom2Pool); + deriveRootLut(rootSeed, pRandom2Pool); applyRootLut(currentANN); compact(currentANN, out); } diff --git a/src/mining/score_engine.h b/src/mining/score_engine.h index a95cb048..a512d5ad 100644 --- a/src/mining/score_engine.h +++ b/src/mining/score_engine.h @@ -12,6 +12,9 @@ struct ScoreEngine ScoreBpp9000 _bpp9000Score; unsigned char lastNonceByte0; + // The inheritable per-neuron LUT the ant colony branches on. + using AntAnn = typename ScoreBpp9000::ANN; + void initMemory() { setMem(&_bpp9000Score, sizeof(ScoreBpp9000), 0); @@ -19,12 +22,6 @@ struct ScoreEngine _bpp9000Score.initMemory(); } - // Unused function - void initMiningData(const unsigned char* randomPool) - { - - } - // Load the task blocks into the active bpp9000 leaf; returns false on invalid topology/data. bool loadTask(const unsigned char* topoBlock, const unsigned char* dataBlock) { @@ -39,8 +36,6 @@ struct ScoreEngine unsigned int computeBpp9000Score(const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* randomPool) { - // The score IS the error count - smaller is better. A timeout maps to the worst in-range value - // rather than INVALID_SCORE_VALUE, which the score cache cannot store (it reads back as a miss). const unsigned int failures = _bpp9000Score.computeScore(publicKey, nonce, randomPool); return (failures == ScoreBpp9000::INFINITE_ERROR) ? (unsigned int)ScoreBpp9000::numberOfWindows @@ -61,9 +56,19 @@ struct ScoreEngine } } - // Each engine owns its canonical ant-nonce rule; this switch is the algorithm seam, so ingress - // code stays algorithm-agnostic. Neuraxon is reserved and not ant-minable, so no nonce in its - // slot is canonical. + // Each engine owns its canonical standalone-nonce rule + static bool isCanonicalStandaloneNonce(const unsigned char* nonce) + { + switch (getAlgoType(nonce)) + { + case AlgoType::Bpp9000: + return ScoreBpp9000::isCanonicalStandaloneNonce(nonce); + default: + return false; + } + } + + // Each engine owns its canonical ant-nonce rule static bool isCanonicalAntNonce(const unsigned char* nonce) { switch (getAlgoType(nonce)) @@ -75,6 +80,33 @@ struct ScoreEngine } } + // Ant colony: the shared per-epoch network every identity's tree starts from; rootSeed is the + // epoch-start spectrum digest + void deriveAntRootANN(const unsigned char* rootSeed, const unsigned char* randomPool, AntAnn& out) + { + _bpp9000Score.deriveRootANN(rootSeed, randomPool, out); + } + + // Ant colony: score a child by inheriting the parent's network and walking it with the child's + // own seeds. Returns INVALID_SCORE_VALUE for a non-canonical nonce or an unsupported algorithm. + unsigned int computeAntScoreFromParent(const AntAnn& parent, const unsigned char* publicKey, + const unsigned char* nonce, const unsigned char* anchorDigest, const unsigned char* randomPool) + { + switch (getAlgoType(nonce)) + { + case AlgoType::Bpp9000: + return _bpp9000Score.computeScoreFromParent(parent, publicKey, nonce, anchorDigest, randomPool); + default: + return INVALID_SCORE_VALUE; + } + } + + // Ant colony: the network that produced the score the walk returned. + void getAntBestANN(AntAnn& out) + { + _bpp9000Score.getBestANN(out); + } + // returns last computed output neurons of the active bpp9000 slot m256i getLastOutput() { diff --git a/src/network_messages/ant_colony_message.h b/src/network_messages/ant_colony_message.h index 98b123eb..10c31ed2 100644 --- a/src/network_messages/ant_colony_message.h +++ b/src/network_messages/ant_colony_message.h @@ -95,7 +95,7 @@ static_assert(sizeof(AntIdentityTreeResponse) // RespondAntParentAnnHeader.status values. constexpr unsigned char ANT_PARENT_ANN_STATUS_OK = 0; // ANN bytes follow the header constexpr unsigned char ANT_PARENT_ANN_STATUS_NOT_FOUND = 1; // parentRef has no record -constexpr unsigned char ANT_PARENT_ANN_STATUS_IS_ROOT = 2; // ROOT_REF; no ANN payload - miner derives its own per-identity root +constexpr unsigned char ANT_PARENT_ANN_STATUS_IS_ROOT = 2; // ROOT_REF; no ANN payload - miner derives the shared epoch root // ONE tree node's stored network, named by parentRef - the ANN state a miner mutates to extend // that node. The tree itself is listed by the identity-tree query; this fetches the material for a diff --git a/src/platform/concurrency.h b/src/platform/concurrency.h index 41670cbe..8aa653be 100644 --- a/src/platform/concurrency.h +++ b/src/platform/concurrency.h @@ -40,6 +40,8 @@ namespace ForkCensus { std::atomic depth{ 0 }; std::atomic what{ nullptr }; + // Every LockGuard lock reports the same `what`, so only the address names the offender. + std::atomic where{ nullptr }; std::atomic live{ 0 }; // 0 = free/reusable, 1 = owned by a live thread }; inline Slot gSlots[MAX_THREADS]; @@ -99,13 +101,14 @@ namespace ForkCensus } } - inline void enter(const char* what) + inline void enter(const char* what, const volatile void* where = nullptr) { if (tlLockSlot < 0) claimSlot(); if (tlLockSlot < 0) return; // registry full (>MAX_THREADS live): best-effort, this thread uncounted gSlots[tlLockSlot].what.store(what, std::memory_order_relaxed); + gSlots[tlLockSlot].where.store(where, std::memory_order_relaxed); gSlots[tlLockSlot].depth.fetch_add(1, std::memory_order_relaxed); } inline void leave() @@ -131,6 +134,22 @@ namespace ForkCensus } return heldDepth < 0 ? 0 : heldDepth; } + inline const volatile void* offenderAddress() + { + const int selfSlot = tlLockSlot; + int slotCount = gCount.load(std::memory_order_acquire); + if (slotCount > MAX_THREADS) + slotCount = MAX_THREADS; + for (int slotIndex = 0; slotIndex < slotCount; slotIndex++) + { + if (slotIndex != selfSlot && gSlots[slotIndex].depth.load(std::memory_order_relaxed) > 0) + { + return gSlots[slotIndex].where.load(std::memory_order_relaxed); + } + } + return nullptr; + } + inline const char* offenderName() { if (gOverflow.load(std::memory_order_acquire)) @@ -157,6 +176,7 @@ namespace ForkCensus { gSlots[i].depth.store(0, std::memory_order_relaxed); gSlots[i].what.store(nullptr, std::memory_order_relaxed); + gSlots[i].where.store(nullptr, std::memory_order_relaxed); gSlots[i].live.store(0, std::memory_order_relaxed); } gCount.store(0, std::memory_order_release); @@ -165,9 +185,9 @@ namespace ForkCensus } } -inline void forkCensusEnter(const char* what) +inline void forkCensusEnter(const char* what, const volatile void* where = nullptr) { - ForkCensus::enter(what); + ForkCensus::enter(what, where); } inline void forkCensusLeave() @@ -185,6 +205,11 @@ inline const char* forkCensusOffender() return ForkCensus::offenderName(); } +inline const volatile void* forkCensusOffenderAddress() +{ + return ForkCensus::offenderAddress(); +} + inline void forkCensusResetForChildPromote() { ForkCensus::resetForChildPromote(); @@ -199,7 +224,7 @@ inline bool gForkCensus = true; do { \ while (_InterlockedCompareExchange8(&lock, 1, 0)) \ _mm_pause(); \ - forkCensusEnter(#lock " @ " __FILE__); \ + forkCensusEnter(#lock " @ " __FILE__, &lock); \ } while (0) #ifdef NDEBUG @@ -232,7 +257,7 @@ class BusyWaitingTracker while (_InterlockedCompareExchange8(&lock, 1, 0)) \ bwt.pause(); \ } \ - forkCensusEnter(#lock " @ " __FILE__); \ + forkCensusEnter(#lock " @ " __FILE__, &lock); \ } while (0) #endif @@ -240,7 +265,7 @@ class BusyWaitingTracker // Try to acquire lock and return if successful (without blocking) #define TRY_ACQUIRE(lock) \ (_InterlockedCompareExchange8(&lock, 1, 0) == 0 \ - ? (forkCensusEnter(#lock " @ " __FILE__), true) \ + ? (forkCensusEnter(#lock " @ " __FILE__, &lock), true) \ : false) // Release lock diff --git a/src/public_settings.h b/src/public_settings.h index b15e051f..a46161b9 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -115,12 +115,12 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE // Config options that should NOT be changed by operators #define VERSION_A 1 -#define VERSION_B 302 +#define VERSION_B 303 #define VERSION_C 1 // Epoch and initial tick for node startup -#define EPOCH 228 -#define TICK 76550000 +#define EPOCH 229 +#define TICK 77700000 #define TICK_IS_FIRST_TICK_OF_EPOCH 1 // Set to 0 if the network is restarted during the EPOCH with a new initial TICK #define ARBITRATOR "AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ" @@ -160,8 +160,8 @@ static constexpr unsigned int NEURAXON_SOLUTION_THRESHOLD_DEFAULT = 1; // and hash-verified at node init static wchar_t SCORE_BPP9000_TASK_FILE_NAME[] = L"bpp9000.task"; static constexpr unsigned char BPP9000_TOPOLOGY_HASH[32] = - { 0x13, 0xe9, 0x9d, 0x5b, 0x2f, 0xca, 0x56, 0xaa, 0x78, 0x9c, 0xb9, 0x59, 0x57, 0x5f, 0x48, 0x39, - 0x2f, 0x1a, 0x44, 0x90, 0x9a, 0x8e, 0xaf, 0x27, 0xf2, 0xde, 0x8f, 0x8d, 0x74, 0xb0, 0x7a, 0x6b }; + { 0x76, 0xa3, 0xf5, 0x10, 0x20, 0x05, 0x9b, 0xf5, 0x22, 0x7f, 0x30, 0x20, 0x13, 0x69, 0xcb, 0x0a, + 0x32, 0x3b, 0x93, 0xcd, 0xc2, 0x59, 0x8e, 0x1f, 0x1c, 0x59, 0x66, 0xbd, 0x9c, 0x0d, 0xf4, 0xef }; static constexpr unsigned char BPP9000_DATA_HASH[32] = { 0x97, 0x9c, 0xdc, 0x22, 0x47, 0xd2, 0xca, 0x4e, 0xd3, 0xd6, 0x14, 0xbf, 0x27, 0x89, 0x63, 0x84, 0xcb, 0x1c, 0x9c, 0x3d, 0x80, 0x4a, 0xf6, 0xed, 0xe6, 0xb5, 0x9f, 0xc5, 0x2c, 0x0e, 0x3d, 0xfa }; diff --git a/src/qpi/impl/qpi_system_impl.h b/src/qpi/impl/qpi_system_impl.h index 383dacad..ecf85e39 100644 --- a/src/qpi/impl/qpi_system_impl.h +++ b/src/qpi/impl/qpi_system_impl.h @@ -12,3 +12,8 @@ unsigned int QPI::QpiContextFunctionCall::tick() const { return system.tick; } + +unsigned int QPI::QpiContextFunctionCall::initialTick() const +{ + return system.initialTick; +} diff --git a/src/qpi/qpi_context.h b/src/qpi/qpi_context.h index f8a2a56f..005203ff 100644 --- a/src/qpi/qpi_context.h +++ b/src/qpi/qpi_context.h @@ -194,6 +194,9 @@ namespace QPI inline uint32 tick( ) const; // [0..999'999'999] + inline uint32 initialTick( + ) const; + inline uint8 year( ) const; // [0..99] (0 = 2000, 1 = 2001, ..., 99 = 2099) diff --git a/src/qubic.cpp b/src/qubic.cpp index ebbf1698..e054837e 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -713,27 +713,45 @@ static bool getAntAnchorDigestForRebuild(unsigned int tick, m256i& out) return recomputeAntAnchorDigest(tick, out); } +namespace AntWalker +{ +inline void preemptClaim(unsigned int index); +} + +// A background walker job holds its claim for a whole walk, so a rebuild the tick needs would queue +// behind it. Asking repeatedly is free: the walker only hands back a job old enough to be stuck. +static constexpr unsigned int ANT_ANN_CLAIM_PREEMPT_POLLS = 250; + +// Waiting costs what the walk costs; walking it here as well would pay that twice. +static AntColonyBpp9000T::AnnClaim waitForAnnClaim(unsigned int idx) +{ + for (unsigned int polls = 1; gAntColony.isAnnClaimHeld(idx); polls++) + { + sleepMilliseconds(20); + if (polls % ANT_ANN_CLAIM_PREEMPT_POLLS == 0) + { + AntWalker::preemptClaim(idx); + } + } + return gAntColony.tryClaimAnn(idx); +} + // Rebuilds one record's network. Its parent must already have one, or be the root. static bool materialiseOneAntRecord(unsigned long long processorNumber, unsigned int idx) { - const AntColonyBpp9000T::AnnClaim claim = gAntColony.tryClaimAnn(idx); + AntColonyBpp9000T::AnnClaim claim = gAntColony.tryClaimAnn(idx); + while (claim == AntColonyBpp9000T::AnnClaimBusy) + { + claim = waitForAnnClaim(idx); + } if (claim == AntColonyBpp9000T::AnnClaimReady) { return true; } - if (claim == AntColonyBpp9000T::AnnClaimInvalid) + if (claim != AntColonyBpp9000T::AnnClaimOwned) { return false; } - if (claim == AntColonyBpp9000T::AnnClaimBusy) - { - // Waiting costs what the walk costs; walking it here as well would pay that twice. - while (gAntColony.isAnnClaimHeld(idx)) - { - sleepMilliseconds(20); - } - return gAntColony.isAnnMaterialised(idx); - } // Owned from here on, so every exit below either publishes or releases the claim. const unsigned long long rebuildStart = __rdtsc(); @@ -766,9 +784,15 @@ static bool materialiseOneAntRecord(unsigned long long processorNumber, unsigned return false; } + // The cache every other scoring path consults, so a rebuild never re-walks a score we hold. AntColonyBpp9000T::Ann& childAnn = gAntRebuildChildScratch[processorNumber]; - const unsigned int rebuiltScore = score->computeAntChildScore(processorNumber, parentAnn, - rec->pubkey, rec->nonce, anchorDigest, childAnn); + const AntColonyBpp9000T::ReplayKey replayKey = makeAntReplayKey(rec->pubkey, rec->nonce, rec->parentRef, anchorDigest); + unsigned int rebuiltScore; + if (!gAntColony.tryGetReplayScore(replayKey, rebuiltScore, childAnn)) + { + rebuiltScore = score->computeAntChildScore(processorNumber, parentAnn, rec->pubkey, rec->nonce, anchorDigest, childAnn); + gAntColony.putReplayScore(replayKey, rebuiltScore, childAnn); + } // This walk is the computation the record was admitted without. A disagreement means the acceptance // was wrong, so publish nothing: a network built for a score no other node holds is worse than none. @@ -859,6 +883,10 @@ static bool ensureAntRecordAnn(unsigned long long processorNumber, unsigned int return (rec != nullptr) && gAntColony.annOfNonRoot(*rec, out); } +#include "extensions/ant_colony_maintenance.h" +#include "extensions/ant_walker_worker.h" +#include "extensions/ant_walker_client.h" + // A pool miner's solution, arriving over BroadcastMessage static void queueAntSolution(unsigned long long processorNumber, const m256i& computorPublicKey, const AntSolutionBroadcastPayload& payload) @@ -968,8 +996,11 @@ static void antColonyBeginEpoch() #ifndef NDEBUG gAntDebugPrintBudget = ANT_DEBUG_PRINTS_PER_EPOCH; #endif + AntWalker::quiesceBegin(); gAntPendingSolutions.reset(); gAntColony.beginEpoch(score->currentRandomSeed, system.initialTick); + AntWalker::onEpochBegin(); + AntWalker::quiesceEnd(); gAntColony.setErrorThreshold((unsigned int)getSolutionThreshold(score_engine::AlgoType::Bpp9000)); // Every identity's root derives from this one value, so a node that seeded differently builds a @@ -4021,7 +4052,7 @@ static void processTickTransactionAntColonySolution( } else { - // A null parent record means root, the scorer derives the submitter's own root, since roots + // A null parent record means root, the scorer derives the shared epoch root, since roots // are never stored and so cannot be handed in. const AntColonyBpp9000T::Ann* parentAnn = nullptr; if (parentRec != nullptr) @@ -8799,7 +8830,9 @@ static bool loadBpp9000Task() const unsigned long long headerBytes = sizeof(score_task_file::TaskFileHeader); const unsigned long long totalBytes = headerBytes + topoBytes + dataBytes; - if (totalBytes != BPP9000_TASK_SIZE + // The canonical task may carry more samples than the configured sequence length consumes, so the + // embedded blob only has to cover totalBytes; anything past that is never read. + if (totalBytes > BPP9000_TASK_SIZE || save(SCORE_BPP9000_TASK_FILE_NAME, BPP9000_TASK_SIZE, BPP9000_TASK_BYTES, NULL) != (long long)BPP9000_TASK_SIZE) { @@ -9483,6 +9516,7 @@ static void deinitialize() fastTxWindow.deinit(); gAntPendingSolutions.deinit(); + AntWalker::stop(); gAntColony.deinit(); if (score) @@ -10613,6 +10647,13 @@ static void tickForkChildPromote(unsigned int strictUntilTick) const bool shadowClean = gShadow.purgeOrphans(); ts.resetSwapPinsForChildPromote(); forkCensusResetForChildPromote(); + const unsigned int releasedAntClaims = AntColonyMaintenance::releaseInheritedClaims(gAntColony); + AntWalker::restartAfterPromote(); + if (releasedAntClaims) + { + fprintf(stderr, "[FORK] CHILD: released %u inherited ant network claims\n", releasedAntClaims); + fflush(stderr); + } if (!shadowClean) { forceVerifySolutions = true; @@ -10804,8 +10845,8 @@ static void bspForkPoint() { const char* offendingLock = forkCensusOffender(); ForkStats::onForkSkipped(ForkStats::CENSUS, (unsigned)system.tick, offendingLock ? offendingLock : "?"); - fprintf(stderr, "[FORK] census: non-BSP thread holds '%s' -> skip fork, run tick %u strict\n", offendingLock ? offendingLock : "?", - (unsigned)system.tick); + fprintf(stderr, "[FORK] census: non-BSP thread holds '%s' (%p) -> skip fork, run tick %u strict\n", offendingLock ? offendingLock : "?", + (const void*)forkCensusOffenderAddress(), (unsigned)system.tick); fflush(stderr); quiescence.release(); tickFork::gChildPid = -1; @@ -10834,6 +10875,7 @@ static void bspForkPoint() { // CHILD BSP: block until parent's verdict, then become the node. quiescence.abandonInChild(); + AntWalker::closeInheritedSocket(); close(tickFork::gPipe[1]); const auto childCommand = tickForkControl::readChildCommand(tickFork::gPipe[0], (unsigned)system.tick + tickFork::gForkWindowK); if (childCommand.action == tickForkControl::ChildAction::Retire) @@ -11754,6 +11796,8 @@ void processArgs(int argc, const char* argv[]) { ("fbas-warmup", "TEST: publish this many valid ant solutions before switching to the --fbas mode", cxxopts::value()->default_value("0")) ("fbas-gap", "TEST: minimum ticks between ant publishes; a gap wider than the fork window makes each window retire", cxxopts::value()->default_value("0")) ("ant-debug", "Trace ant-colony accepts, over-accepts and network rebuilds (budgeted per epoch)", cxxopts::value()) + ("ant-walker-threads", "Ant network walks handed to the walker sidecar (0=off)", cxxopts::value()->default_value("0")) + ("ant-walker-debug", "Trace every ant walker job and result", cxxopts::value()) #if defined(__linux__) && !defined(LITE_WASM_SC) ("verify-fork-rollback", "TEST: assert fork re-run reproduces quorum digest", cxxopts::value()) ("fork-force-fork", "TEST: fork every tick (exercise MATCH path)", cxxopts::value()) @@ -11975,6 +12019,18 @@ void processArgs(int argc, const char* argv[]) { + std::to_string(ANT_DEBUG_PRINTS_PER_EPOCH) + " lines per epoch"); } + { + const unsigned int antWalkerThreads = result["ant-walker-threads"].as(); + char antWalkerSocket[128]; + snprintf(antWalkerSocket, sizeof(antWalkerSocket), "/tmp/qubic-antwalk-%d.sock", httpPort); + AntWalker::configure(antWalkerSocket, antWalkerThreads, result.count("ant-walker-debug") > 0); + if (antWalkerThreads > 0) + { + logColorToScreen("INFO", "Ant walker sidecar enabled, " + std::to_string(antWalkerThreads) + + " threads, socket " + antWalkerSocket); + } + } + if (result.count("rebuild-tx-hashmap")) { rebuildTxHashmap = true; @@ -12379,6 +12435,11 @@ int main(int argc, const char* argv[]) return tickStorageScan::scan(); } #endif + // The walker, not a node. _exit so static destructors never run against uninitialised globals. + if (AntWalkerWorker::requested(argc, argv)) + { + _exit(AntWalkerWorker::run(argc, argv)); + } #if defined(__linux__) && !defined(NO_RPC) && !defined(LITE_WASM_SC) int rpcProxyExitCode = 0; if (runRpcProxyIfRequested(argc, argv, rpcProxyExitCode)) @@ -12395,6 +12456,7 @@ int main(int argc, const char* argv[]) #if defined(__linux__) && !defined(NO_RPC) && !defined(LITE_WASM_SC) startRpcServices(); #endif + AntWalker::start(); #if defined(LITE_WASM_SC) && !defined(NO_RPC) // Wasm testnet serves HTTP in-process; the unix-socket/sidecar stack is compiled out. QubicHttpServer::start(httpPort); diff --git a/src/score.h b/src/score.h index 480213e4..c2835447 100644 --- a/src/score.h +++ b/src/score.h @@ -208,7 +208,8 @@ struct ScoreFunction // Ant colony main score function // score a child by inheriting its parent's network and walking it with the child's own seeds. - // parentAnn == nullptr means the parent is the submitter's root, which is derived here from the pubkey + // parentAnn == nullptr means the parent is the epoch root, which is derived here from the + // epoch-start spectrum digest (currentRandomSeed) and is identical for every identity // Returns INVALID_SCORE_VALUE for a non-canonical nonce, in which case outChildAnn is not written // bestANN would still hold the previous call's network, and committing that would put one node's // stale bytes into childAnnHash. @@ -222,25 +223,25 @@ struct ScoreFunction { const int solutionBufIdx = (int)(processor_Number % solutionBufferCount); LockGuard guard(solutionEngineLock[solutionBufIdx]); - score_engine::ScoreBpp9000T& engine = _computeBuffer[solutionBufIdx]._bpp9000Score; + score_engine::ScoreEngineT& engine = _computeBuffer[solutionBufIdx]; // Derived into this slot's scratch rather than the engine's own buffer: deriveRootANN() uses // currentANN as working space const score_engine::ScoreBpp9000T::ANN* parent = parentAnn; - // Depth 1, the start node of every public key + // Depth 1, the shared epoch root every identity starts from if (parent == nullptr) { - engine.deriveRootANN(publicKey.m256i_u8, poolVec, _antRootScratch[solutionBufIdx]); + engine.deriveAntRootANN(currentRandomSeed.m256i_u8, poolVec, _antRootScratch[solutionBufIdx]); parent = &_antRootScratch[solutionBufIdx]; } - const unsigned int childScore = engine.computeScoreFromParent( + const unsigned int childScore = engine.computeAntScoreFromParent( *parent, publicKey.m256i_u8, nonce.m256i_u8, anchorDigest.m256i_u8, poolVec); if (childScore == score_engine::INVALID_SCORE_VALUE) { return childScore; } - engine.getBestANN(outChildAnn); + engine.getAntBestANN(outChildAnn); return childScore; } // main score function @@ -248,17 +249,9 @@ struct ScoreFunction { PROFILE_SCOPE(); - switch (score_engine::getAlgoType(nonce.m256i_u8)) + if (!score_engine::ScoreEngineT::isCanonicalStandaloneNonce(nonce.m256i_u8)) { - case score_engine::AlgoType::Bpp9000: - if (!score_engine::isCanonicalBpp9000Nonce(nonce.m256i_u8)) - { - return score_engine::INVALID_SCORE_VALUE; - } - break; - default: - // Unsupported algo - return score_engine::INVALID_SCORE_VALUE; + return score_engine::INVALID_SCORE_VALUE; } if (isZero(miningSeed) || miningSeed != currentRandomSeed) diff --git a/test/ant_colony.cpp b/test/ant_colony.cpp index 6a93959e..0db4fc19 100644 --- a/test/ant_colony.cpp +++ b/test/ant_colony.cpp @@ -6,6 +6,7 @@ // The bound colony, not the bare template: these tests check bpp9000's binding as well as the rules. #include "../src/mining/ant_colony/ant_colony_bpp9000.h" +#include "../src/extensions/ant_colony_maintenance.h" #include @@ -1146,3 +1147,102 @@ TEST(TestAntColonyExport, BeginEpochClearsIt) EXPECT_EQ(header.entryCount, 0u); EXPECT_EQ(header.solutionCount, 0u); } + +// A child of an existing node committed on trust: no network, the way an AUX node stores one. +static long long commitChildWithoutAnn(AntColonyBpp9000T* colony, const m256i& owner, const SolutionRef& parentRef, unsigned int score, + unsigned int txIdx, unsigned long long nonceSeed, unsigned int tick = 100000) +{ + const AntSolutionRecord* parentRec = nullptr; + if (colony->tryGetParent(parentRef, &parentRec) != ValidityResult::Valid) + { + return ANT_INVALID_INDEX; + } + + AntCommitInput in; + in.pubkey = owner; + in.nonce = makeKey(nonceSeed); + in.parentRef = parentRef; + in.selfRef.tick = tick; + in.selfRef.solutionIndexInTick = txIdx; + in.anchorTick = tick; + in.publishTick = tick; + + const long long landsAt = (long long)colony->solutionCount(); + if (colony->commit(in, parentRec, score, nullptr, 0, true) != ValidityResult::Valid) + { + return ANT_INVALID_INDEX; + } + return landsAt; +} + +// A claim held at fork time has no owner in the child, and the on-demand waiter would spin on it. +TEST(TestAntColonyMaintenance, PromoteReleasesAnInheritedClaim) +{ + AntColonyBpp9000T* colony = freshColony(); + ASSERT_NE(colony, nullptr) << "colony init failed; needs ~6.2 GB"; + + const m256i me = makeKey(41); + const long long idx = commitRootChildWithoutAnn(colony, me, 3800, 0, 901); + ASSERT_NE(idx, ANT_INVALID_INDEX); + const unsigned int slot = (unsigned int)idx; + + ASSERT_EQ(colony->tryClaimAnn(slot), AntColonyBpp9000T::AnnClaimOwned); + ASSERT_TRUE(colony->isAnnClaimHeld(slot)); + + EXPECT_EQ(AntColonyMaintenance::releaseInheritedClaims(*colony), 1u); + EXPECT_FALSE(colony->isAnnClaimHeld(slot)); + // Retryable, not merely unclaimed: a Busy here is the hang the sweep exists to prevent. + EXPECT_EQ(colony->tryClaimAnn(slot), AntColonyBpp9000T::AnnClaimOwned); + colony->releaseAnnClaim(slot); + + EXPECT_EQ(AntColonyMaintenance::releaseInheritedClaims(*colony), 0u); +} + +// A rebuild starts from the parent's network, so a record whose parent has none cannot be taken yet. +TEST(TestAntColonyMaintenance, RebuildableOnlyOnceTheParentHasItsNetwork) +{ + AntColonyBpp9000T* colony = freshColony(); + ASSERT_NE(colony, nullptr) << "colony init failed; needs ~6.2 GB"; + + const m256i me = makeKey(42); + const long long parentIdx = commitRootChildWithoutAnn(colony, me, 3900, 0, 902); + ASSERT_NE(parentIdx, ANT_INVALID_INDEX); + SolutionRef parentRef; + parentRef.tick = 100000; + parentRef.solutionIndexInTick = 0; + const long long childIdx = commitChildWithoutAnn(colony, me, parentRef, 3800, 1, 903); + ASSERT_NE(childIdx, ANT_INVALID_INDEX); + + // The root is closed-form, so the parent is takeable immediately and the child is not. + EXPECT_TRUE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)parentIdx)); + EXPECT_FALSE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)childIdx)); + + AntColonyBpp9000T::Ann ann; + setMem(&ann, sizeof(ann), 0); + unsigned int annHash; + KangarooTwelve(&ann, sizeof(ann), &annHash, sizeof(annHash)); + ASSERT_EQ(colony->tryClaimAnn((unsigned int)parentIdx), AntColonyBpp9000T::AnnClaimOwned); + colony->publishAnn((unsigned int)parentIdx, ann, annHash); + + // Materialised records are done, and the level below is now unblocked. + EXPECT_FALSE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)parentIdx)); + EXPECT_TRUE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)childIdx)); +} + +// Two rebuilders must not walk the same record: the claim is what keeps the second one moving on. +TEST(TestAntColonyMaintenance, AClaimedRecordIsNotOfferedAgain) +{ + AntColonyBpp9000T* colony = freshColony(); + ASSERT_NE(colony, nullptr) << "colony init failed; needs ~6.2 GB"; + + const m256i me = makeKey(43); + const long long idx = commitRootChildWithoutAnn(colony, me, 3800, 0, 904); + ASSERT_NE(idx, ANT_INVALID_INDEX); + + EXPECT_TRUE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)idx)); + ASSERT_EQ(colony->tryClaimAnn((unsigned int)idx), AntColonyBpp9000T::AnnClaimOwned); + EXPECT_FALSE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)idx)); + + colony->releaseAnnClaim((unsigned int)idx); + EXPECT_TRUE(AntColonyMaintenance::isRebuildableNow(*colony, (unsigned int)idx)); +} diff --git a/test/contract_qip.cpp b/test/contract_qip.cpp index 2ad7905b..02ce5b73 100644 --- a/test/contract_qip.cpp +++ b/test/contract_qip.cpp @@ -1141,7 +1141,6 @@ TEST(ContractQIP, returnFunds) QIP.beginEpoch(); QIP::buyToken_output buyOutput2 = QIP.buyToken(buyer2, 0, buyAmount2, requiredReward2); EXPECT_EQ(buyOutput2.returnCode, QIPLogInfo::QIP_success); - //QIP.endEpoch(); EXPECT_EQ(numberOfPossessedShares(assetName, issuer, buyer, buyer, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), 0); EXPECT_EQ(numberOfPossessedShares(assetName, issuer, buyer2, buyer2, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), 0); @@ -1149,6 +1148,9 @@ TEST(ContractQIP, returnFunds) EXPECT_EQ(getBalance(QIP_CONTRACT_ID), 24171804); EXPECT_EQ(getBalance(QIP_testAddress2), 0); + QIP::returnFunds_output returnFundsOutput = QIP.returnFunds(buyer, 0); + EXPECT_EQ(returnFundsOutput.returnCode, QIPLogInfo::QIP_returnFundsError); + QIP.endEpoch(); // 1 EXPECT_EQ(numberOfPossessedShares(assetName, issuer, buyer, buyer, QIP_CONTRACT_INDEX, QIP_CONTRACT_INDEX), 792); // buyAmount / vestingPeriod @@ -1177,7 +1179,7 @@ TEST(ContractQIP, returnFunds) ++system.epoch; // 4 QIP.beginEpoch(); EXPECT_EQ(getBalance(buyer), 0); - QIP::returnFunds_output returnFundsOutput = QIP.returnFunds(buyer, 0); + returnFundsOutput = QIP.returnFunds(buyer, 0); EXPECT_EQ(returnFundsOutput.returnCode, QIPLogInfo::QIP_success); EXPECT_EQ(getBalance(buyer), 4059429); QIP.endEpoch(); diff --git a/test/data/bpp9000.task b/test/data/bpp9000.task index 8ffa5e17..38ce2351 100644 Binary files a/test/data/bpp9000.task and b/test/data/bpp9000.task differ diff --git a/test/data/gt_ant_production.csv b/test/data/gt_ant_production.csv index 664c7574..48aa11ed 100644 --- a/test/data/gt_ant_production.csv +++ b/test/data/gt_ant_production.csv @@ -1,65 +1,65 @@ chain, depth, pubkey, nonce, anchor, seed, score -2, 0, 886ef7f14d42287f8a74aaa92a409eaf901e25b6f9cfbedb5f72e1297bcad551, 01084ed07b9200fe2bb401df72b52ba819fd5c09e436f5f185594f8b8afcbc66, 54ba8fded70f55d660977d169d6a2ab6d7711b11f2b49596fbab1d5bf968a961, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4576 -5, 0, 2df29076f8969739636dff083eb1f54bfe7e5d8fcac5b2946d0871d40ba495aa, 0106216c6da947657f74a629a59d5b81da7a4a6ecce8a3abb4b23079b5f1c56e, 21c6ec71261f8027ee7fe4629982e12c50facf8d0acc1f8f297264db6bc2dc2c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 6400 -11, 0, 83b379182516db53587b3c553468e1af0ca4c21f13b95150024e68ebb2bb9550, 01085ebfe74c8324c42214dde9c3c71f691e327cd8a764e4c396d7431deaae03, 12545454084db67b036475332b9a7ebd7c7ed73b13e64bc17d87ec2e758dcb22, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5308 -1, 0, 17620b9920ef596dfa46753b355247b1641eaeda1246fa7bc43884630cf61bf2, 010523bbe90fa21b263946830cc53e8744e270cf6e944b1cf255e85ac74a7887, eae546338da0b54a3b4e10f859b6293f1d285a0e51e4881f10d1379ff511f819, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 6295 -0, 0, 4657de22da43ad6402c8cfdd45ba54542213486481988bdd28c82182ec2714af, 01023378b9bf89fae25fdcd165c8739da9dbba04ff92982345a623031d23b0fb, 91a1d4fc06689515127b01f116fd64c4c2ca4c02a4692675c1b0dd4fc1bf3589, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4800 -8, 0, e0b3b0f9b66fe475eaf17c6be38a2759ff254debd5485c4d4a27bc45486fa0ee, 010733ccce2e806a4aea54c0dcc386ebfd4efc6f3027f63c1de301686508f403, a2050b44a20027fb98984b156632263a8bccefd34e4c1a06d516ac807ea19433, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4414 -6, 0, dc40c51265eff259556066d427dbeebb4109a7e79690d61d6349ef650b29dcb9, 010a33855fd20d0f1c910f0f7f7bc4eef7dfde506c7184d12bb6e50dc095b9de, 0509c75356fc65087c969b8eb8602a28c903a34d8f0648394e29dbeaa9cb4892, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4206 -10, 0, eda9ead70a463d6bf5858faf2a8caa3c3d20b3c3e0e994167e685200bc750276, 01080bf6551a06f4826c9dfb047814381042f281b322ab9452623183daab9dd3, a0651819278fad70d8d2e24db8fa595f305cb879b400fb0c270ecad1361cc25c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4352 -4, 0, e5e05648b1c70c4deea4c7d7aefccd49b24defadceb45dd033caf0f16407808b, 0105337415ffe5aae3f1e4816712ba7ca6e4df3db6b10276868d4e2861cb69e8, a18598ffcfd37339a922658ea450e94f2e4cd083c4e64d5013d002c4fcb17743, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4379 -6, 1, dc40c51265eff259556066d427dbeebb4109a7e79690d61d6349ef650b29dcb9, 010a54a6afb54d321417a986b3e8ce05e51823ea0aceef2681392fcda42010b0, bb849545500e6716bdbbf6fbbdcf6bfe1fd2b4cc20c1024cb419be77dae27df0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4206 -5, 1, 2df29076f8969739636dff083eb1f54bfe7e5d8fcac5b2946d0871d40ba495aa, 010354eaf1f371ea113434301d5e15b04d24b92fc11dd20f5e03fb6e6f497d30, e9bf8e92a20da221725f0897cf111f326c7754ead360f7aba074459ad99ec7f9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5998 -3, 0, 072fef44ac2f7667c366f067b374016bdd39581dbe8acb0f8aadf98502d13646, 010329371769a20be8033c7868d60327ad8b345b190f69f6a6b54f55c7acde8c, a3e0ad1361d551b2ce13025624602a4ebbcb0bc37c93348a88d23d394c155bd9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4569 -2, 1, 886ef7f14d42287f8a74aaa92a409eaf901e25b6f9cfbedb5f72e1297bcad551, 01081abdbc10a2b70d301fe0bb0afa298f90a5d9bdd0777d24949228abc63106, 02724836ecf8a9bd54e8f78867ea5b4e5c13dc6c41566094a6b41fa36e219f09, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4576 -8, 1, e0b3b0f9b66fe475eaf17c6be38a2759ff254debd5485c4d4a27bc45486fa0ee, 010229cda723c097587f19e30ee090700b7da9bfc9a24e2fb73d83d55b4ba107, c55bea5399c1ca80efe9b9b6c51991ce99b6cc34f804984957cc7a110a1e03cd, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4373 -0, 1, 4657de22da43ad6402c8cfdd45ba54542213486481988bdd28c82182ec2714af, 010615c66cb4c65a74382fce9000de43248a4425ced3bc79929139502e97c705, 4a564c2984b914fda7e68a1c80fd1bfdf6a75ac2443f348e98645d42fbaa052a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4450 -10, 1, eda9ead70a463d6bf5858faf2a8caa3c3d20b3c3e0e994167e685200bc750276, 010a5e540d6e35170556e51d0a25400b8e7dce3a21872d56e397633411fc7656, eb23ade51e8662688b5e069f59e64c9049c7896058ee4460bab704180f8be6e6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4352 -1, 1, 17620b9920ef596dfa46753b355247b1641eaeda1246fa7bc43884630cf61bf2, 01050a3c2127a028962e38ab80d9eb6848790106a3809637f8b1eb849c261a1b, 8dc4c241e049f0be5341d03b0d1023ae5340cd8cc506dd6f03a998743e2fed75, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4663 -2, 2, 886ef7f14d42287f8a74aaa92a409eaf901e25b6f9cfbedb5f72e1297bcad551, 010246c5bb2fa79a2bc5f2ed1fbb2877ba2de90011ee439383347c684cfb80a6, ef9bab5a83f63baadbd79d9e3680a36eeb65d8c9c1c52c6dce0e47f807ccb465, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4394 -11, 1, 83b379182516db53587b3c553468e1af0ca4c21f13b95150024e68ebb2bb9550, 01025ded1ea31c0763dd89595fce37c703c886203ef104ccea9979bf6041b511, 967e0547550da817764ba0a7b51e33556c2435e9a910e4b16d9300bd785a06e1, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5308 -4, 1, e5e05648b1c70c4deea4c7d7aefccd49b24defadceb45dd033caf0f16407808b, 0106282d2480193df99350d4ef0c3b31b9a4314b38cc3d0e86c3c9564b9fc77b, 9ba91514fec4c80a40201e7fbc9bf7d08ee446b8f3efaff16e32ae54c5afdc92, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4310 -6, 2, dc40c51265eff259556066d427dbeebb4109a7e79690d61d6349ef650b29dcb9, 01052e993e876de4beeed6b7e5f674acf08d15f44a41dc59e080b50b5e9ee9ee, a570a04a90af0b23bf9ea5b4be54314a853f517a060c39df1d983e9665f4c45b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4206 -3, 1, 072fef44ac2f7667c366f067b374016bdd39581dbe8acb0f8aadf98502d13646, 0109595f50ab0a4d9c0c2209dc2e020058a6a3fcaa4ab687f5338d46a4259031, b8a420a950c50a97f8876a589cc1a4f2822eed80efc2e7ef6bd44b2c780061c6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4569 -5, 2, 2df29076f8969739636dff083eb1f54bfe7e5d8fcac5b2946d0871d40ba495aa, 01030cc6084cbef2f92c8fc481db9b3b36ec91723770bb5c6aaea1dfb2104376, b3310ce38053f82c73d26eed58ad00897a8d0de995d60c4eeb8d4755afc518c9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4836 -10, 2, eda9ead70a463d6bf5858faf2a8caa3c3d20b3c3e0e994167e685200bc750276, 01040809aeb747d430f914d0ceb1970d3ff7e50b44ac1db1de8ac6a910795423, 5df3e71634f680908f5954ce538b65f470d65779dfcae051f1d42b43f1527abe, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4241 -10, 3, eda9ead70a463d6bf5858faf2a8caa3c3d20b3c3e0e994167e685200bc750276, 01075358c48d3c17682f8c69578d2db1710168ebad922b591b764b0937353be2, 190832364108015065a051e38badc3535be4faff42a319132ebfecea4bf03416, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4241 -8, 2, e0b3b0f9b66fe475eaf17c6be38a2759ff254debd5485c4d4a27bc45486fa0ee, 010307ae588e0d66befcfb11099713189200bfeab97cb37d4f18bfa0d877cd2a, 2032dcba5a6c53d1cb68ad800ae8c7b455aef9aa90e1c14cb5591bbe2c8e1927, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4307 -6, 3, dc40c51265eff259556066d427dbeebb4109a7e79690d61d6349ef650b29dcb9, 01014f1dc6bcc0311403fe50177fe2b69d702b1109db30aab04be2b6cd964375, 7f2fbd2dca697fee9169049bd06a5cadb6932db37cc8190b7f781e2859a13ddf, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4206 -0, 2, 4657de22da43ad6402c8cfdd45ba54542213486481988bdd28c82182ec2714af, 01011280b730b2317044f434b8e56ca881f7efeaf8dd4b29b12e877ea43fb7a1, d260aab327663b1197f602584a74c80dfdc3915d487440e21d59b6d795ba284c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4450 -2, 3, 886ef7f14d42287f8a74aaa92a409eaf901e25b6f9cfbedb5f72e1297bcad551, 0107159b5267736895605a21efe7bfa20f676707dd2bfdccabc6d67ed211d511, 32fa4cc87d7f7ba35dfcc9fad0256dd64bd1c3a340e1fb1ce92a2e3102641be8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4291 -11, 2, 83b379182516db53587b3c553468e1af0ca4c21f13b95150024e68ebb2bb9550, 01040f59f2e3d85b897c1997ae4cd6a1aa20f0397795106ec35742efa2f60207, 70efb4b8a77579ec350179a71f89e517c378ec87896a526d5bca985e45e6a2fc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4581 -1, 2, 17620b9920ef596dfa46753b355247b1641eaeda1246fa7bc43884630cf61bf2, 0103333ea8e0bc20c0f2cfb7cc93e9d3ae302f4b5e933e605ca8d5f2b101f7c8, 39cb97cbe391fff147ecffe6bc7b6cfcf920726974c9ad5394b54e7a86d3e752, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4663 -14, 0, 8c2cce72fa295e618dada27cd9dce2a8f24a4596d676edf11e096bcf6f5c078f, 0103553c86f3dcc0e0a8c63a9004bc5719b09b04348d77a9de594f5b8bdffaae, db7e8851e5e2b45c072bf7cd15040a9bc313b6d4e8e568f7a50a62810a181842, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5808 -5, 3, 2df29076f8969739636dff083eb1f54bfe7e5d8fcac5b2946d0871d40ba495aa, 01022aa2e9c6bf0d46e240165a772be18804f052b8f76111e0c22af538b332e6, ed0147d60ab884ab994eca1b25f319349437e4106b21068d0f8ae9c357102e14, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4779 -4, 2, e5e05648b1c70c4deea4c7d7aefccd49b24defadceb45dd033caf0f16407808b, 01080ccfc0cce11c4229f4dd023630330c17f3f495c3db73a30ed50107882a5e, eb7729568d616b19004a42dab7f8b237defd6eb3a0a5448037ecf4ec56044bd7, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4137 -3, 2, 072fef44ac2f7667c366f067b374016bdd39581dbe8acb0f8aadf98502d13646, 01013064dacd09cf7f1898ac0b669d25f4c0db3c2bcea9b3c9cd51f3fc8a7414, 5a107488a0caf086ae94acfefca9b36f6ae84691b11a001ef3999a673d7b160b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4394 -0, 3, 4657de22da43ad6402c8cfdd45ba54542213486481988bdd28c82182ec2714af, 0109151f9d11847474e333460ea73646a31663c84162313db3cea328a74efa82, c10184404849ef460dd734d49d22cad1b25aafa77bbd3f22073dc88885f644ec, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4450 -4, 3, e5e05648b1c70c4deea4c7d7aefccd49b24defadceb45dd033caf0f16407808b, 01044b182869be4ba5670092661abac68606a2f4b06130d101882d236e482358, b7fc5ca1cb869e33b3725a1f3a4c96e8545200d0f263d099351016b6baf46039, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4137 -14, 1, 8c2cce72fa295e618dada27cd9dce2a8f24a4596d676edf11e096bcf6f5c078f, 01076440117d3ff4d99c831ad310105cb1217c9bfe4bbd7a8d98df63fde9dac1, c01ee3f86edc2dbb38b5fcabf8dca2e5f63e80cfc7161b897783b71fb24eb07b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5808 -8, 3, e0b3b0f9b66fe475eaf17c6be38a2759ff254debd5485c4d4a27bc45486fa0ee, 01092583572dbd57774dc64402af3254c4e9cfc97a76b686d690e728f4946882, c1003cacbb633a682fa1587cc0a59535501e3d8511b896df7431c320da790724, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4294 -11, 3, 83b379182516db53587b3c553468e1af0ca4c21f13b95150024e68ebb2bb9550, 010522c21f13bb19c26a46e5798d2ce597bfd035261c4a3e9e2d38963910dba2, e692e800bb6e620f5108e70950e6f65c46997a3dd8aef402a30d44870ba94440, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4344 -12, 0, 2814b082c6683e9490338df3e42c8d89ca9e2f889735013dcbb7ddbdf7d846c4, 010618c41d7d80fdeb077f54165537de721e39ee32e5f8d11dd16e5edff70d91, 9c65303cf21df1f289cd5d7e1d431ddd79016d87c6e8ec57b2c23c15c69c119f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4862 -1, 3, 17620b9920ef596dfa46753b355247b1641eaeda1246fa7bc43884630cf61bf2, 0108598ac27cff46dd5635d43a3ba7b32bf7c0f844c95418fb359f4bb8b14a48, cdb0470fe9f772124a6d158869b4bed2451b4445293ffa2cc56f96917b108ebc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4663 -12, 1, 2814b082c6683e9490338df3e42c8d89ca9e2f889735013dcbb7ddbdf7d846c4, 010a33039aba1207b1d63d559fbe40220eaa30b56fd25e5d0767eed2538ed35c, 27ee5997236575efdb19c9845b330da3e969ab8b69af32b6cbace636afbca8cc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4862 -13, 0, e36507d27632e2d6cc70480f6322a4469d20773ff0d047f6f70285e9d44beb5b, 010a602d4d374fe77a551445a50be97aa8ff00a8e6d1bc87eb6a05a7ca9d5bac, 5e65aac82ee69c59e00bd8bc6b688b45c6a30fc14a97c50f631f6255eaaf7327, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4916 -14, 2, 8c2cce72fa295e618dada27cd9dce2a8f24a4596d676edf11e096bcf6f5c078f, 010203df7f6c6ec32fb907ddd1bfe342d0e638248cf885e02534d40ee5b0fdc7, 7f25490bb25c2a15a4ae4b09834af66b32f6c747cd410f205ebb5f26157628b3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5475 -3, 3, 072fef44ac2f7667c366f067b374016bdd39581dbe8acb0f8aadf98502d13646, 01011fba8c525ade36b44b91c10f763cc452b9f359138a17b7095defba3de81f, ddbcfaeaeefae22924db28ad1ac6dbab0878009449fa125d1e072edfd29bfe29, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4337 -12, 2, 2814b082c6683e9490338df3e42c8d89ca9e2f889735013dcbb7ddbdf7d846c4, 01081ae266672ca194549f553909f744cc0043134958ecc17e1cb23b8b321b81, eabf4471ea17eeb0e6b8ef2868bdf533e538e9117d89ee7be0506b5b87100c31, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4862 -13, 1, e36507d27632e2d6cc70480f6322a4469d20773ff0d047f6f70285e9d44beb5b, 01061e74c28eab539b083cc334b3d8f49d500d3a0efaf0ddacfeac3c3d7db59c, a1f90ec26c1e922437b5dce2cb7d5aa74a73dd6b2a633e0aa1a87e181ef4e493, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4183 -12, 3, 2814b082c6683e9490338df3e42c8d89ca9e2f889735013dcbb7ddbdf7d846c4, 0107093cee10017f16cccbff4c1bf264ad41d8416654ce13f71d06d40a068d45, 4728ca3016635dbb5fed672ad264689e97f39372d87fdc8751d24d305147e50f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4808 -15, 0, dac78edf70e526e26a94ac4982c2abded02fe049c5038196738d66d80849ca3d, 010a4622db8bdc6f2d2a53c4d093a7db2eed1511435c0c9d5f3ad99ccc89461f, 782f1d06c5a143065b84a674c8c957ec1f6c335b9b9d1818aa440358490f9d42, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5165 -15, 1, dac78edf70e526e26a94ac4982c2abded02fe049c5038196738d66d80849ca3d, 010758cd460961f2865307025c35b5c371a152aa887aa79a5ed5db1562554b51, d949da8e7b84d066ea4e0d44064dfc4d8c58b4c4f6580a817de564a74500ce2e, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5165 -14, 3, 8c2cce72fa295e618dada27cd9dce2a8f24a4596d676edf11e096bcf6f5c078f, 01051655e2f51a514371801ef25660a391477f0d6c492b613c5a83bb30f490f6, 66473bfc883091c7d6b1ddcb8753db715e7a9568aca4b2f870f155edf0b952b3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4839 -15, 2, dac78edf70e526e26a94ac4982c2abded02fe049c5038196738d66d80849ca3d, 01034a2930d5d739d27f0a4b65bee1bd19ee6dccc2da5612aa3e380d60e263a3, 61cda7fa7dd9cb6b817139a51b7d17f3fef1ddb31db2a506082cd49411d0df57, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4541 -13, 2, e36507d27632e2d6cc70480f6322a4469d20773ff0d047f6f70285e9d44beb5b, 0102482e077b5bbcf0c1f5da9a77803663ade42ccf6667a2d07366ec25c05373, 7cdb704cdc5df918699d7bd399008f249f79ab8b4ca08689f8d3010c5813de38, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4183 -15, 3, dac78edf70e526e26a94ac4982c2abded02fe049c5038196738d66d80849ca3d, 0104501feb2927fd13760f5c39dc277827c125b069804c6ac18e6021b1811868, 61078520e97db6e828ce6465404fe3456dc27deb16516cfdc3c34c8293c52fd0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4541 -13, 3, e36507d27632e2d6cc70480f6322a4469d20773ff0d047f6f70285e9d44beb5b, 01024f0896557a0084a1d9a2937130ccb2711a8cb810e512ec9e2c97c3c56db6, d4cbdaa2fb4e3c1e8ef809ed628f0abf16d3bf4a93f81ce03bf77b68e07fb516, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4183 -9, 0, a14d543d43592ce38ef38f600a2dba801557fd924638de966d2725dabc5e2472, 0102152a605e81991fe51e4c8964a98462f6a78ab03dfabfd30552d39e9d4eeb, c17c5c2242fd08302a00dddf39bfe32952a319f4816dbc540b991d8ee33180b4, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5099 -9, 1, a14d543d43592ce38ef38f600a2dba801557fd924638de966d2725dabc5e2472, 01012c56d755fe8e1c3c18ce6c13e4fb74a62f4dbf6b4d88a4d4d5f12a82111f, aec69cdd848a1b7ff0f89c9ea97feb759270ed6719ec8a675ceba9cc0877a1de, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5035 -9, 2, a14d543d43592ce38ef38f600a2dba801557fd924638de966d2725dabc5e2472, 010918e449d18c7ea382562025280bd33f3dea08e299eb92eaa7413d9b42c367, 1cb9b9ffb834abc7b8bbfeb01e006fd3cc412717f8b9456e5e4d663466417e99, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4281 -9, 3, a14d543d43592ce38ef38f600a2dba801557fd924638de966d2725dabc5e2472, 010a107f47f98b2357d358f56f2a9cb9759714f3bec8ae28dee41e9ce170a5db, c6d51cd991be84979a28aa4b4e51de3ef0f585aadbab7b66a0593805a5085546, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4132 -7, 0, 158b59e2f807c4b05160ad5dae50f96f079b79a7f7467e11d86c37a808515c1a, 010303ab82e286a8adbe84e8b7d72f1c627058d749eb7fee957227ae354736a9, e1b75477f812f034862401a8ecf7c9b7ffed63e904ebff99b4662bb3157d55f7, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4382 -7, 1, 158b59e2f807c4b05160ad5dae50f96f079b79a7f7467e11d86c37a808515c1a, 01061f0f60aa46bb3d3db810b004f0d63a0666f4304c7902c2d0de7116fd4c3a, a6453d96ed2963d2f8073faf29405223c3b9de7b9f0bf925ad5b5807f0df29ec, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4382 -7, 2, 158b59e2f807c4b05160ad5dae50f96f079b79a7f7467e11d86c37a808515c1a, 010a01e51b9910f599cf59ad2c563f0dedb61dbc3c0226bad673b48d1b538419, 1485b5332779be0d64bd08c69344580358b320cf01b9f38ce70badc069e94ef1, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4032 -7, 3, 158b59e2f807c4b05160ad5dae50f96f079b79a7f7467e11d86c37a808515c1a, 01050c4baf9613939235ebdaa17d71ffd8523d368247815fb47fa0fa6334e99b, 182c36a0398009d6b6e58301e13c805cd5acfc37412f55cadee180e90b27afdc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4032 +5, 0, 39328e676b63cdd3ba4473d0431402f37b8326f6bb071bf52cd760a128f19013, 01063d6e0eeb11f4b8ee105280e1aeb4d5e280d6ec825a971b0815aa1dffb1a1, 3b61511981cbe61b4c5e26863eb5407b06562960bb9c1491039c7d45504f4a7d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4320 +5, 1, 39328e676b63cdd3ba4473d0431402f37b8326f6bb071bf52cd760a128f19013, 01093cee5c710d8760b3fe828149c8726e2b9526d2ca5ca94e57c9e9e5074803, 5ffc51127e569a1f927fd10c6cc5e1f0eeee7540b922298b2f0bc61e37060008, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4320 +5, 2, 39328e676b63cdd3ba4473d0431402f37b8326f6bb071bf52cd760a128f19013, 010837ec9b30eaf304d315bd3bef0da838c0c9aa1f4c84521a565f79f093e54d, d40339a95e54d68ffc5574a246e7a5158af5c86ca17ff30d4da101819eeef7dc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4320 +5, 3, 39328e676b63cdd3ba4473d0431402f37b8326f6bb071bf52cd760a128f19013, 010361d6347e3c2b08bc36b5bea2a5c93424c56575934614770fbcf0026adc91, 924b7a9f8736d7550c8cc75dac1d00a91677f67e653d9b37603f5e89873e4e2a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4320 +10, 0, 538fc8ec58a371c654ae2369acf7ff0d8df7a4e3ff178c8d6960edc4bf7d8506, 01082aa824190e5d3c70448ad8bb4717bf35149597c81e4ef2afe3636ce69208, 71c696a563f40ba722062e49d4a44054f51a16039ce71d36b0dc4d7f2fa21616, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5863 +10, 1, 538fc8ec58a371c654ae2369acf7ff0d8df7a4e3ff178c8d6960edc4bf7d8506, 01022fab9f5bdccd0b959ed890ece6faba183333a3cc575641024319877396a7, cb4c9af70d7b2e076091c1a36f16933dacd55fd3be92403b5384e1300e3a6bb7, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5597 +10, 2, 538fc8ec58a371c654ae2369acf7ff0d8df7a4e3ff178c8d6960edc4bf7d8506, 010539217d7c1ec6bf99027cba42a6ad1925a721640dd971a9c8df0c0265788f, c12696a39fef53d899c413ad6241313794b50ffe873f2efcaef9515deb9a2601, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5055 +10, 3, 538fc8ec58a371c654ae2369acf7ff0d8df7a4e3ff178c8d6960edc4bf7d8506, 01050c5d9d85cea75c933969196e0f01bbcf93cf5efd6fcb19e19b766f92b2a1, 23d37b23fbbbe110e7bf9895c93a1632a69f37b5e24b294b0a23d19632d7571d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4103 +11, 0, 9b01190712cc2b617960429079e1e99f5fcc066f89ed4d81e83c5ae60142bf11, 0109093319f061ecb6e666823ada8b4d0b2d874602d82ee8ed3e373c6cc56217, 49029a0333b014053c3f1f821e2c51274e03ad031c3f9d762136e10e3d251358, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4110 +11, 1, 9b01190712cc2b617960429079e1e99f5fcc066f89ed4d81e83c5ae60142bf11, 01045c6f75ff26b547da8b0e3e4d81f7789035b6139f2b53dfbf9261e1505e8d, 69e2db4bbbbd538f4b5c6de70837d155f398e876d95b5cd1c6da8341f24a039a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4110 +11, 2, 9b01190712cc2b617960429079e1e99f5fcc066f89ed4d81e83c5ae60142bf11, 0107083255da35ebdbeecb2038a8fea675742c0b5b016de71b573baa6308bcc2, 5dd0ea87019b456f5d23141b9e5c9422a0618ea102ec97b237efdee486b7cca5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4015 +11, 3, 9b01190712cc2b617960429079e1e99f5fcc066f89ed4d81e83c5ae60142bf11, 010735110790c4eed4fdb1d41f8bd3a28667d59d7eaeebd62aa738015fb59eed, 58fd1aa50da65a2169b6cf0742ea569dc3c7a3f1de9c43e5f73e9c47a82c0a3c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4015 +0, 0, 4d6951e72244e6d70bc3b0b6a6550ecb961c97cacd42e6aad2ff90edf065fc6b, 010643f21a4e13e83868b4ac8160fde045a61539b8d43df48f1d3c28b1b10ff3, a346ddbe9c5b2f8f432c4e734904657c1a043a45d2ebbc436409a50f37c7c990, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4024 +0, 1, 4d6951e72244e6d70bc3b0b6a6550ecb961c97cacd42e6aad2ff90edf065fc6b, 010a144bf3109ad87a0627dca4ab50ef1b7b0b6f18515eccb4dc1611dcd81c47, bdc015579ce60294299d6eac7e99a86dfada3e1b4e29e13920acd7a40e63387c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4024 +0, 2, 4d6951e72244e6d70bc3b0b6a6550ecb961c97cacd42e6aad2ff90edf065fc6b, 01071fe11e193c2fcd0947988b4d125b5397d48c5776def9fcbd56ce6f92f49f, 68ce9b1946965e6ff15e631a3792fece82cc2fcf8deaf7561b02664de2195e51, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3990 +0, 3, 4d6951e72244e6d70bc3b0b6a6550ecb961c97cacd42e6aad2ff90edf065fc6b, 010455fb5fd6d346f2c74b612072ddd502e7a17d433ba537db2da96e00cabeaa, 03cdfd62beb8c9c4fe9805a75677ec4466cf222e73923d6deccadf0174f14edd, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3990 +2, 0, d269d3c9e83f250a5d471d5d7c3f2b8c068728600216cef27524b3d1b6c5b371, 010428f05fa25f9fe8dad343ca0a2c8f64049d8a9f7e38e033130f3e0d847b87, ced7587ab3b4f773e15289b1ff5e656642cd551d431cdc9c847ff378a88cad36, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4366 +2, 1, d269d3c9e83f250a5d471d5d7c3f2b8c068728600216cef27524b3d1b6c5b371, 0107319e04b5dc666cff589612f47418fffa5537fbe829e91a1a47110b57559c, 564c0ef36f9b208c1a954328e7f9a3f54ca423bc73db67a4c0d5f3c90547cdee, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4084 +2, 2, d269d3c9e83f250a5d471d5d7c3f2b8c068728600216cef27524b3d1b6c5b371, 01085ce85cdb2f9a6316f90cbe6b7b79bd8e2cb69f27a48f9eb48a2c454082ce, 0fc40360c900954f92a6ef47ae075ab204ae06d7ac5bef846cb99daa4727065d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4084 +2, 3, d269d3c9e83f250a5d471d5d7c3f2b8c068728600216cef27524b3d1b6c5b371, 01013f78fa10e1bf931096a2a8df7960b9de2f6b85a8b7e1cca8bad5b4c13d94, 7f04fc765ff3dab3d2148bd9d445c72370ca3f335e2551a742a911d3983a6558, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4084 +3, 0, cf0f607131740aec1059970e2de315626ecbe9dcc9b8e7fd03a744189a969fbb, 01061c061e969aac1afe128561b711fbf2f3462d407a0db322a63dbcd31d47cc, f29c2b98d0c2008b9c1c17bb491eea892c3b36d6913823a562d53f6c8f8190c5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4151 +3, 1, cf0f607131740aec1059970e2de315626ecbe9dcc9b8e7fd03a744189a969fbb, 010708e6d5efa482aa0eeedbb5195fa33e0cd85966b746cbf4368486523b3d9d, ffcdbf851bc811fefb8ed19e030c45b22210df3de3bbddb0a119db24655f40b2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4025 +3, 2, cf0f607131740aec1059970e2de315626ecbe9dcc9b8e7fd03a744189a969fbb, 01040424a692fcddb6f3c89680e02136ed8354bde8e75bec98f5944eb90e0b8e, 90e40d22ec747058cadc0d3d78bf37ea55a9efc17f5a9e9fe9331633283b832e, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4003 +3, 3, cf0f607131740aec1059970e2de315626ecbe9dcc9b8e7fd03a744189a969fbb, 010716ba575d16fd951744d958c36c0c7868c413a8145d8021f02d9ac8d18ba3, e8005fe308a6ca1000a38daf4efcc6ba4115c37fbd2413a93a216e2ba67ab94c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3950 +8, 0, eb1dd1de89c1808e995cf5ec9484ce99a44b6beef8f5696451eb45d1979a1afd, 01071074f94c7b46b48a562ef152b999f9bbd66885f30c862e67a1c058e12ebe, 3c9adc3aa8162877139de8552efd270d501866db15115e472d50d257497aa690, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4485 +8, 1, eb1dd1de89c1808e995cf5ec9484ce99a44b6beef8f5696451eb45d1979a1afd, 010a409e45c1036344945f841fecc19b5baf9b52b9e38ab1a53095749554dbe7, fa885262e5ac6af13bbde9086829e06b79b139516aadec74863f226ca63b2372, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4485 +8, 2, eb1dd1de89c1808e995cf5ec9484ce99a44b6beef8f5696451eb45d1979a1afd, 0105085e801eec72f2c302fd800d6055523c14de552532999b7e7ff148042cc0, 2d807da12bdf39158396cb94db480dcf875f3bacbad8193d6f94451198129584, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4470 +8, 3, eb1dd1de89c1808e995cf5ec9484ce99a44b6beef8f5696451eb45d1979a1afd, 010a0beaef18d340a523a50ed3803cb104c2c17ff19bd130d55908f2985fde64, 6886d2f62b722505da3c028aa52fb0e5b25d080d4babb38dfdd7cc7ffe1feeac, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4037 +4, 0, b7616f473c023eecd7d77b3562cae096fe5ced3e616aea14824a9c608b108f73, 010507d263112a994edbf20868e6d37bd3fee9d39900ca36e40b28b28a1f4722, bdb772d32bbc40e208a0b64780ce4029e024aba2334d3577f1af2741dafcca04, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4296 +4, 1, b7616f473c023eecd7d77b3562cae096fe5ced3e616aea14824a9c608b108f73, 01053be2154acdcf30778947d25c378bcaf91c5db0253c2e5dd488122ffd81d3, cb35fca83b2b502a30e080815b7800889df8485656be917bdcd504ac2d236ffb, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4296 +4, 2, b7616f473c023eecd7d77b3562cae096fe5ced3e616aea14824a9c608b108f73, 010707cd33ecd3a3a06b0de75b57dfb7055a4186c420aa6019946bd8d6729114, d6f0e8c19648292a186ebafe8585f4b5e3041218f16c5c0e78a88d900b83ea62, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4296 +4, 3, b7616f473c023eecd7d77b3562cae096fe5ced3e616aea14824a9c608b108f73, 0101274ec706e0b52f3ea3e663184c20ca41e72ea769051100d24757c922bdcd, c13078cff28f022685405d7460b916b45d880484a138d3a05e8cf9d0b86340f6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4296 +7, 0, e097286c70975340ceaa8f469be4ea733b00eff7c9fc94baa92d1b6c466f3d12, 010511b7804235c69f7cf9279f433479a9793d83367390698dac95197168d502, 30ef7c37d83e5709ad6724b883129db22f9c7e19ab9ea37e2dad50e46d0f325f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4093 +7, 1, e097286c70975340ceaa8f469be4ea733b00eff7c9fc94baa92d1b6c466f3d12, 010945a965acca76c03ce5ea9e3a4c0ecb16dbbdc7bbf156dbd4ef0801f34bc4, 86dbfe6a602ee07d7e45aa4227978a0c2dfc18570df92568f07863c1e6912242, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4093 +7, 2, e097286c70975340ceaa8f469be4ea733b00eff7c9fc94baa92d1b6c466f3d12, 010525f4baea1a21cf4e44914e93553e558f24b866186ef7f7f15145cdfe3401, 376b4e8d2b7110d20c0fdab95e491529757eb706032aaa80b7adba7726215f77, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4090 +7, 3, e097286c70975340ceaa8f469be4ea733b00eff7c9fc94baa92d1b6c466f3d12, 010527dd5047bee95ae94b75c75ad22a200aab49c66bab70888c0257b7268d2b, 4e4edc07ff71d34de5d56352bcc4befd03ed5b0703d8b977ff3ecf2998722ed9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4083 +6, 0, 3ffa69241233915f4e17236bc03b30ad5f49c1b0630647d9548867b23d915ff7, 01061fc31fe4df546a8e9a11045a2f89c0ea0b9a9f7fc4737b6182bf5f32baca, 80ded011a9caa347a6603f224e6152b002fe2bfb7bf12f5b9a72b78010f9fbea, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4990 +6, 1, 3ffa69241233915f4e17236bc03b30ad5f49c1b0630647d9548867b23d915ff7, 01030bad30670ebfbdd4433acaf8e0af5a2a7cb682211761530d17ed3bcc1249, b7cd4af3a75eb78a0626c76b25b5a4b1be566bf4a9f8d351b14d5a3509e710f7, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4007 +6, 2, 3ffa69241233915f4e17236bc03b30ad5f49c1b0630647d9548867b23d915ff7, 010427015a5d4dd334101291097f7a303b05b86181f621179cab664acb041ad7, c5c1aef28e07f52345152f892f50d5f3c3533bdfc7d8525bcbed141513f3b9b8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4007 +6, 3, 3ffa69241233915f4e17236bc03b30ad5f49c1b0630647d9548867b23d915ff7, 0101419bd618e12b74dfbc0257842ddd87099cfc6b226e6c28eb411f7de83653, a39db8178bef1aff2f85089fb24cecee189c414202297d221cb0fbbf238b882c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4007 +1, 0, f55edf0abe2efa13b69b4d0b68781f79a1fcc3670c9170ff0c461127da9b4a59, 010431e438eab93c4be3c9da56724076726933bdbdcca30b59fedced0a79e998, 8881e7fe8fd1a2e1e08ce9423699bf0bb6997bea2d8763ab51e09497de74b29b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4298 +1, 1, f55edf0abe2efa13b69b4d0b68781f79a1fcc3670c9170ff0c461127da9b4a59, 0103101f4398732a849fdd717c7304d205bb8b54f0042fb21a25aa4d6ef3472b, 69b7a6a64c54f6ede93f291feeb6f481e9569ae79c426de617434417d802c0a3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4225 +1, 2, f55edf0abe2efa13b69b4d0b68781f79a1fcc3670c9170ff0c461127da9b4a59, 01080a2b54018c61e7524856e090cc98b2f7713babb7082a9c125c2f47a84104, 2c666088a7713615e08683bdbef7b17764522eb2ab699d4584858739ff8154f2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4086 +1, 3, f55edf0abe2efa13b69b4d0b68781f79a1fcc3670c9170ff0c461127da9b4a59, 01032363a6b032f025e22dd96d946c4c77ed741473d19d2f998bca13e80ab21a, afbca6ae4902c5a4d894acd25d57fb91577699132cdd4ab65633f1e07117be4c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3921 +9, 0, 778a5f9b13e6754fd05684f2eaafb093794ab3e864c4ef5c5a30c44b181bee0f, 01072cc7ed3c8d4511d3df97f1917002650b90dd23293df7b9693c4b32953eb4, 338df6d2204ba7c38dafbc5ada86d165c003162501108156ea530d7b3f4465cf, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5258 +9, 1, 778a5f9b13e6754fd05684f2eaafb093794ab3e864c4ef5c5a30c44b181bee0f, 010906bc116085964f6e05b17d9ced5d7638dd800f9b9f491bb62cd22ada4b6f, f2a21fcf4c55ec1bcae0716ea793523471424cce625cbc2b1c62194a46b9c763, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4607 +9, 2, 778a5f9b13e6754fd05684f2eaafb093794ab3e864c4ef5c5a30c44b181bee0f, 01030d703cc5730ff14717d864b1e50eccd7a7fd180cff06d233e8acb153f05c, 796a83a0142df9652b530143f33d8cdfce181d8c7513beef9b9d692a1c50d58b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4252 +9, 3, 778a5f9b13e6754fd05684f2eaafb093794ab3e864c4ef5c5a30c44b181bee0f, 01023d40919862ed174aa0903881d2de7dfc1741a86b834a0d1db36ea5ba0476, 51220ac298d1a70f3496834c6361a82e818c71560f35ded4730ab3bed3b34ac3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4252 +12, 0, ffa831d1e5798862fee1146cb9e7cbc23452fe633fb16752f99f32d7b8e8b5cb, 01091a674dff91b7c38f43c9fd03a6124bbed9544c301ca78a370fbda290870f, c5dc870536bd345374b2efecb2044ef9f1492d5a563d6aed0b2a8b33d18caa5c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5182 +12, 1, ffa831d1e5798862fee1146cb9e7cbc23452fe633fb16752f99f32d7b8e8b5cb, 01033aead48b1256cc6c2db85361c0da448fc99ae278322cd4349336c672e330, 8499f3a88422c7a4469a8c6b12ec73a569665ddf435f0d2f0fcb4b9d123626af, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5182 +12, 2, ffa831d1e5798862fee1146cb9e7cbc23452fe633fb16752f99f32d7b8e8b5cb, 01034f81a9323a3fe2a4c4586e40e9cfb3a144d7b7f15038dd40426bebf4547c, c3fbe32d39964bc177b563eac4d67f03a3e80b84aa592d1ca8a8371bb622ece6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5154 +12, 3, ffa831d1e5798862fee1146cb9e7cbc23452fe633fb16752f99f32d7b8e8b5cb, 01013f9bb45b3c7c67233d185dfadd279d75b79e0f9b40ff5751433ba0cf80f4, fdaf0e3152166a8c0c8ecea4215f15c75d5286d912aca439ec126ab589ddc007, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4634 +14, 0, fdca870684bd40b126f01c964f68b0518b5b5e971bacb418cbc256a5861fc038, 01011f30857cb23af64a5a0deec3c40ef9cf45cb7f6ed8ac3f21a1c892786b2a, f78d2b1033e1f2382a833b3a4c61d9c5d2e5bd1dfa075d2e691dbb25ad3b3c98, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4526 +14, 1, fdca870684bd40b126f01c964f68b0518b5b5e971bacb418cbc256a5861fc038, 01091907cf5e956040192b5db3a5edbc76c1bb088b9a77cc13490a1daeb6e977, a77c9d981f188ec8411a3d0a72c96922167c9fbfbb6665de16c7c57afb6e15f0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4420 +14, 2, fdca870684bd40b126f01c964f68b0518b5b5e971bacb418cbc256a5861fc038, 010a08b37ab285018c4ad1082e543164c2b2a13e20c4acce029604ca9d2ffb22, 2c1285b6b4a12ac7fd7195fae65371d7da402a7a4f8b10046ff9cdd1be1365d6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3949 +14, 3, fdca870684bd40b126f01c964f68b0518b5b5e971bacb418cbc256a5861fc038, 01013ba097ecdcafc088477791d2024f48b247bdd0d87d163803af5d22369d06, 9bad023b69cc8ceb4ddec62732c460c967e36265d7fee97bd1f7f6ebff089ee4, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3949 +15, 0, 287b689730e2bc1225674d3640924dcfb5133be02359cc8d14bffe70b1bf159c, 0108406db6989cf01571981674520b7362fe944c4e0483f1052f1b6422dbbf8e, a232474b89fc06f50bffe99663376e99ad545c211c438f84a2248a55894b3278, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4640 +15, 1, 287b689730e2bc1225674d3640924dcfb5133be02359cc8d14bffe70b1bf159c, 010536142e58d24d2a46c6c4108b1ea81eb72e381d2d0a11501747c6e59fe2e4, 5cf67c6acf27e226720170354cacad9f451617fdc9cc2c78155d6880ee42050c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4131 +15, 2, 287b689730e2bc1225674d3640924dcfb5133be02359cc8d14bffe70b1bf159c, 010822451956749b7695f5ab431a90e7b5e6f12cc4230b8980397cc119b4f3d6, 5124ba73fb9444bfa2158c2070f271aaf63d46b554f42a6ef1014925e031b514, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4005 +15, 3, 287b689730e2bc1225674d3640924dcfb5133be02359cc8d14bffe70b1bf159c, 0106086b217b66240fa9e86343d45e7c2680e1922aef83ed1064045e180d6ec1, 50e38e423d762b4fc02723fdca764b50f51b6a700993b56a1ffd7e43385477f1, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3913 +13, 0, 5f10c44e7cc47758dc8ed15da928d398723165c11ad92e8e065892b0c5b1ba16, 01061296db9d8060d3cec6aa3013d3236e9f524dd07e2a4b465c857ca1095fb5, 22ab32567bca60e8672429b7440d3a5658c1317be299212c07b28cc28dacd0a9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4173 +13, 1, 5f10c44e7cc47758dc8ed15da928d398723165c11ad92e8e065892b0c5b1ba16, 01073743ac0c235540589c2a8539d8aa1e35dc9cc7135f00bf96a3bfc93b8f4b, 9da7c23a6f60904b97b39f859daaa2cc154c7b60b3752a8fa66a0f976a579220, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4173 +13, 2, 5f10c44e7cc47758dc8ed15da928d398723165c11ad92e8e065892b0c5b1ba16, 010806ebf3a372c7dd00bfa473500ce244254c8e4b91ad48a19c44a73e8002a6, 21b56be5db094d08d47b64cd94ce4031b19b685bec283cb3ace2cd7f1c7a7272, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3974 +13, 3, 5f10c44e7cc47758dc8ed15da928d398723165c11ad92e8e065892b0c5b1ba16, 010430da00fe2903025447bb3862843d6e2f36728b9b9a7a85875473811f77a7, 11eada061151751e0b82093b39d3b35bbf395b22f847da6904f38aaa9e375f6b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3974 diff --git a/test/data/gt_production.csv b/test/data/gt_production.csv index 80569d03..5608faa4 100644 --- a/test/data/gt_production.csv +++ b/test/data/gt_production.csv @@ -1,97 +1,97 @@ pubkey, nonce, miningseed, score -039cc1f1560aa96daa994a2b296f22d7f2fc9503ce95321d0b8193079e5f93dc, bee1e55fc2c967601827dd80f09a6b0a47ceab37b920b57efbd23c8fdc49e38f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4254 -9b3d041660f08f692ae1005118f85c35bec491c33b51c94ef126663f4402fb1e, 1a314eb596f55aa79e7f61334072380042ea4e351ca59dc09f536f8224a39afa, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4787 -5ddde3533e040840b737304dd92b4b48d62ab209419a8bf4506e7b0497e9c614, 419569517e0908f9bae5b07159d322204053afd4564b4079fe160f775e214993, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4329 -66950904cb50245726a50f9039e2d384bb878ce3e39e0575f64eb61bf9963325, 158191c4be8a6382d9eff13a448550c7742568909abc76d8213c958e85bb02f5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4021 -beb0cbbaab3cc54e87e4b6ed4a5f6c4af2fa4f452a02252a8ec910c43fb71a43, 96725650ea0a2052e3175c0a1047f51fcc7031f1455fe168630f0ecbfce1a180, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4642 -c7f5024050c450389d341eabcea04b16e79b7a51f18bcb59284eaf40000cb7a6, 77b3965cec126453cfeb23b5b44f20d652058f7726f0e43e8c265eafba6e8418, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4138 -a6f49eb0c3fd08eeb56b4ab83bbfb0b415b02233101baff5eec26bb2d1455e34, ac8db67e608af98f13f70b611c71f527dbb626afe41128cc6dbb27da921a6db5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4323 -5c49beb4e29c921d8fad67d9b1e09fd693254f9275dfd77ded5a66acf721b4fd, 5e80e9f095fadb379b12130c4796f74ec967fc526b1face4af813a13b029fcd3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4596 -d8470f83e18ebd6bd221423f03b0e17a87872f6ef25ca8909f28078a6c300ff0, 7989ab5f03db1a93b51a8629ac6d569ff0299ee1744ddce813013e9a64ff6497, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4254 -4c83417da5f166accd29107292d5c0b7502811b4e0eef6d252c01474dce157bb, 59245fdd6fb52df651c4b3228596175e327aaaf4f4405cd37bf84ac1a9e51747, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4476 -aee5c8edcf3dfdab7ec84b7c580be8a64c0b902be6b365fe916733f05e238ac2, 56dddea0c9052587a47db7f5deaa16727663725edcbd0c5d78f9b1193bef12da, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4179 -f2112f60356004b9fe52840e06e9047d2020ba228b7e1987663491a7d90e3e85, 4893d1aeb7591ce464e26ed43e34501acf450a6d6ff3a4dbb85a3920d9011308, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4227 -050a8c2acdcc99c71b1f6e04f3785291f2980fcfbc0a2355a200eec5287a18d8, cd2744b32506fc199fb1aaf0b9edaa553d05f0f4c66e9f7d2e47140fbc7973e6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4112 -5bb6da0fd6e14135dcaee492185a223c5495936b29cabecf415094e1cf0b64a4, 1b169a9f717bd61508b009a9e149c3bf3bf192acbc2a11af6da31214ca0c1be2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4367 -ce103ac663da7295f0013f33b1eadb5295e32dbf8392f67b8085caca9d8291da, 44fdb9bc59d865bd9d87f049ea4a7a702d1a0fca20cddb7172bb10d7c17dcb06, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4373 -da39c1bc6effaa3edb88cc805e6e7bd7aad1726ade6b0dd38d54b52957f14125, 50ee325b50af29c82a0ac06b3ec85885f9e36e08f1342a938acfd9a002ce524e, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4538 -2f2a57841de709ea79ec678d1848a2dc13cb1d96d1d029278181e925ce6ce281, 29b9f9db6a5001480686637bb3f187868d23a8881b32482000a4e3b217074d54, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5044 -7bf6c1e4d203b633f0f5f5421d2d16e66db924cccc51092f1e89ededa54ad322, 490f7927f8f91dbeab6434e4f0ca660642d9157267613ded72a8315df821d838, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4272 -a6f0ae22305a9a0336a2c7d97e1568697cdb62e1aedfc9874a1ac1915b4e3c67, 332f47bf272e90de5befc330c0ae65b88a981c6c28819c9df8bbc7fccd7176c8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5126 -1f710bfadc49f5a1bed75fb10054979a3b4856a48e3b75184f7b056d3a23ac41, 6d5adaad8e0ea9208390e5649d2e0c6365ee84cc1611ae5f765424d7992b8322, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4227 -36f621af4c726d3e5bc3cce3d26a4418c2272024fd8177110a6a64fcc9071afd, 6fa998c1ee8efba299b8b51e865a13b939df3189d94c0b939f86c8b2d9fb7893, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4002 -8117882a9cbfce4ca0df28f72a818773ba80377417e8c73d2c0a56e8c6e4ae22, c762e691c4918cc826e72b5277fa2c11f4ff9cb2a01efefabe6ab8f6c8c5be3e, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4124 -a7a69484aeb270fda2ce28d9b6d4ef6f1e73edde9adea5404f81b7ae265a24bc, 4b18c6bf59aeaf1f96a11956eba1c2ac7bbddbed8e0f12ec5a94dcc8107a5cb6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4500 -a84e3c009575dd77299c18d600d0252f9d0480d34c5da189a7625a0274f83cc8, e0c96c11ba7a7137aa7fe213dd34dee838a598acf0d76776e5c3778307c1e631, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4334 -dc100c8d058c0cf6df88dbf4fe01f6023b173be3bedd6b86dee4b8eecc72e058, 179fdf1f611d6c27a08eb533b826d169a0bd3d33e6c329ded1eee76fcefcd2dd, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4259 -087fa2a7ada94299393d118b19c6360699845db46f559eba3ee5b8bc494f2a8f, 0f200d7799483ecf52a4af5c3a69111fd29a14bd7a03a82f9f39f1913b29e5f8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4347 -8f9069a8eb962dc47afac9ea7d4a77d7ab801c3e932183f9dcda9c8cd7fdaf9b, ab4f54740589f62c4e35c2b7a2508a3f7fb23872afd62771e57deea64bc540d9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4477 -ecbce54faf01fbf9015cf9b19be9bbf42e9530be81fe09fc92c666eace8bfc15, 86acf64f19dc27151febf20c2549d49b05f4dc87290e108e935ebd6d49950cee, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4513 -36e28fe48780115b126255d893208b16f04505687e9aafd17ba73dbe543a8319, ab908ba2a064b7381f26c7a02cfaa2d8b7689d5f24877aaad99e25e0714ca19b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4711 -ee4fca45aa951e2a2c0e535fddda71cd1c0722f1b30bf74416d9ac6df8787e52, b53606d34971e204c260e1c5c2bcb88450f429b76340d0860046bf2a67cc359f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4634 -25b3ace18ee066c5cb561d32bfc89f69f20199741519d148a1d00312191e1f0f, 06ab93f88f0fe2aaa54efe1abf60cceef2b21d15c378d66096398c8b82947c7d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4032 -a7149ab5f6c9f3ec2dc997564839a050de78dcf54e255d0019b4d81c3675bb56, accab93bf12ddf0a8b2ee43bcb94fe01d265eacd6df6aa8bcf133a5d359a79f6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4964 -d4aaeaf020007d1349590d77e2f779ad48f9a6cf720d04ca6a65fbbad81dc050, 7a40d7032c899ac35f224bd0a7be2c12244e9b2b4a32852b2ec880d526929ece, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 5357 -5fd98a26b2bcd498daef005d5035336777164fd594690d083ffc07c2750a00bf, f8c4c01377b01fb07d914d37301d2eee2349e015128adac766c676913f7c056e, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4538 -acc5d85d4bdb5d3e63879ee9975db8dd89474eaa143fd3a3b0dd9b406e6fb19a, 2097e387f6f27d6f630532f3407189adfebfae9bfe4308f0a4a718b72756cb64, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4251 -5329b924cde1d5be74ae7b98fd5dcd6c6cfc497abaabcfd13ba6ad76b3e80a73, 409da8d39041e54f20851bc0ae095cb8b80ba393af8531ea6f1977e14e900bca, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4435 -bd50121a1a9982b2e6f1becff5beced13d3aeb3b8292b6d64eaa641a85c6af36, e2c0de9cf3488fb4db674d9b6bdb7261044556a5ff21d66781c975316624ce8b, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4064 -f09fa2dd45236ebd36dfca4443787119dcefdd152723cf5f4346f16c33611ca4, ab4f459cae0455dcee65258b8c8340e3429cb43d826001a52542b0b10b2d3051, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4618 -5df6390b68e67e1cff09fea890e14b204f8b5e9ef847b14a05b7b32f83576c73, d2c779399ff772a44024e4991f32463193e7d21e958b926ead5186fad44d276e, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4658 -0c5c814529d514ed2084f87554a1278e303aa627c720c21502509e6453b46e75, 4d2317b4d7645a12954929dd48e1541ab6c333eeb590fed06bc795876d399b86, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4805 -1ec0460a6d6ecba87d61bde51c2b27c758cefecc568cd807b5d34c51d4d9f427, d0aaeea09c9dee00f7062fabcf47fd61f2c288b80295a820ec4222a18c2f67cd, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 5385 -e91795a4cc4ea7a134da695bd2ca1638e3510d2ed21a6207c34b25c920925556, 2cfdfbd4047379ca02f323115287c8132cbd1ab84ae94bb4142147ccfaabcde9, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4104 -547ec5ed8156d06f369b8642d81f542aeb06d8be0fddfb82d499722def67ab8d, 58315e20f5e05abcbb1c225435e985dc08edb01a51bb472e5a2caa1ebea54636, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4293 -d8e5cac99aa0e4d53d13a2a5efcb44533def7116cd8cab23b3e8ffe2e6e36033, db5d8b11d57d382bb4a174aeaeea1934089ca1ce87f695b02e969aa2d804323b, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4490 -11e6f5455923f425403174d26957f3c4343082676f2786a504b175a37e095257, 3d2b359ee811655af3afa43d4b6bcef022d9b086bf7b90196f8d2fd319cef2e9, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4101 -3c5904c5eef6ae6bd1c5f04b329bf8263f5de55ed27b416caec5d1c7ba8a1563, 4595621029e319dd6a712f40b4694bb41a198b97fcb5e1431e36b43d2a49f70c, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4229 -bd57f4064ecfd309f405a10536844d1ba5ee33ef367100f8a7bb8c0d30564c09, 3e3fe3eb8a8e57fd02bf0aed9362a8fa922792c23466f23910a07f5ecd6a5891, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4399 -8cd870e8a74f72c9293b449aff6f2703eee771a2e804ab3b0e85e90110a70ebe, e4043488f279cd9c732d5074e615a25c2684e84c82d86c742627ea7a5eacca33, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4427 -2aad58dee6a39af963379f65fe80379c9787fa0ef39ac6cfca83883b39d88f80, fe7024fbe4ed4989b6004ecc78ef92db203631b606d3bcc0266406bc2dc2c95d, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4211 -2944a74d098bc8cb2c49c7058fa5c1749b6b0762e7953d2909d081a621322bc9, a4251d12a25da7049ac54781cfcf729548fba470abd9aab5071854aa33651f48, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4392 -59423970ab4e01dcd75b21c9bb28039cc754cbd9504f1baf7690ad67f653d9bc, 8cb0ca3bc6616437eac5c040fcbc82de45a9374a686c68be73e1ece507fbe7a2, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4234 -4111344f7654a20f63f205251e233949ced281978307fe80f5994101ba30fff5, e0cb33733e52c5cfe2f342b834e6be21a4c8ba142184a5a712fbe01d2f321ca0, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4192 -cb2e9d7b14e92a7acd3606295181a85b3a3834812c85a58c5d8e8fb966d52e80, ffe82f2f47efff7e3c7c1f8489126be98246f789a0d5cff4e62e251d7e9637b5, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4246 -6acfa4e805379c4ad2c8fed920eb94bea5c66753f76c9e8903cb9f061b9a8568, 3291da84832c5e9fd2ec14ab4bc3852a9f501635962490595f92fccdb66104aa, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4277 -5950a4ccc1ee66af76e4ddec06caa7a0992cb2f59b7fe0468edd64f7bf7ac3e5, 8f6afeae5b25d78e7a9105fe200525e0012cd20ccf172dd0f610dbf804c1d731, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4140 -971e704c6ef92be3bedbdf77b8829804ee21011c6a2a3e79a68b1d4ad1cf826f, 19be39d709b2b1cd553f4ade9553d982a5ebc2028c7d61fa2693ce7c701ccf2b, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4122 -95ebe78a1bec9a12aa57462a7818c047b9cae0376666d0640905e81ff6e9e356, 7a418455ac94a63d6726f6fdecbf32d9426dd990ba0548e31e710703639980f2, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4418 -8782eea2fe0934ef90467ece2754218f16e02f731277ec7f244217f0ee0bb764, 7904e219e0265db109364c873d81f45e7e7522e881f9646faeba35fcc49985af, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4189 -7097a76021713afceeaa610bb79cffa218a8f6235ea1aefe078e957210170a47, 4068216c2a24e21043646f0f6b411628892c9350517451f528e632dc757ddabd, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4174 -e2c85076c6767b991f71921fa6e3bdd669a3e1e739385b4f0330250c828bf8ec, 017471a30b0129216db7a28a5da7dd2b16c4bc856c85d5135985119e4a99e6d3, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4336 -983e19c755f902f0026754de7fc89dbfba6ccc17f161a6f69c94e27ae5f3b8f4, 63301609c9ae6610af2aa1282c70531380a6d90e2b87d36ced6a84108f64755f, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4686 -66f9328e9c1a79595d52798a25d3459552afa6af9aa52ceb7ec76f38a8f3bd39, 0b71ad39ef27577b06bceab5f264201b2e8087143e0df90c69b0e206042470f1, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4307 -befe30abf1c5e24965d39b7ff3f2fc33fc4698be8a33c87aa4f8536ddb8a1996, ad87664522487ee7986f5ce7c26ff962b5feba171551b15b047d9a7053dedca5, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4235 -0363966c6d6ea832ddb1df165841a3479df9ec8d2ce0cea8a9400360f3cecc34, 391a80f81e72894605743026c249995103834322ce94fd953107a781583c2f23, 25b115eca9f2c1c0b3ada40faa46c248f2919d8e5f6e7fa2866d4d96ca1c0eaf, 4472 -2cfd43630593e07902ff52948ce387aaf0e2bb0e11952f8a0c91a33672203bcc, 157ebdf68c866bef7f93adafa1490ce1c15c3b951f23461f5327b53b3546726a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4874 -f99a06858d3d9dd741b0d1ce08556aab5c4b383b8199920d776daea35634b1b6, 494feb7026395e0ce15b603a2e424b79cea1d46da11fead0d84c29cae57299a5, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4657 -09bf69a84c357ff346577e5d17748c2b3d5b3ed84e9329759b131c2b94e7f4b5, 108075e0dfc82821119e0f323ab789ef809dc39b203db23ee3242f3a42ec0a80, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4728 -7ba937604ac1bdd1cdda1954b6b6a93eda265f468a49f4941f8de0980caed213, eaf8c0959fdd124b1756144818ba32a60f4fe12454bc342e25a74b13b745241a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4274 -060f7e1a8c8cb6f938b3116ff25884b6eb87fafe9ec957d07ca28ee58fea68ee, fdead1f49e206d1892dc77b38a8b0fc8a286d01683ee994f7fe591c6f66b2bdd, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4212 -377741f3abd82ac5b90df9edf3d59ca9ab1fea5376dadb40242c87f55a6c287a, 9669b5d4bf3ef9184daac77ab44f0e3ce4e46752e2a7fd68ebecfbcf6109fe3a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 5152 -7a4fcbb032e872b47f2a33fbb2d27fa06965165a91ce284a8d29a43bb4d118aa, edd6bd4e7bff26c7e441011422fcfe753bded55005a5221046884efb2afafaa9, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4144 -6ceb9db060f76ff7a170037cf3fb0413f3bcb5b2cbdf7f42106d30fe6516be95, 9c83bb2e063e73e7338944f8babdfd18d01ed1d13c9baa43fa7489fb52f176c2, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 3956 -43e55c8788cad0c02cdf142a7f7e4683b332ed6a8efcc831c580aab3e9a1b27e, d9990b01cbe2746aafedc17d14f5a84675938f952eaeee0b3b5764e4bf054074, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4236 -cd0e79b31ed4e699811ca0fcbaf81cb3b0e08188acfc38b6a98e3f6d077e6fe7, 3625fb00a429fd89c4ce1c4f92303cececfbccfd7bc10b1f00d98e8e8540fbd6, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4421 -61217450605f85dfd56bb5c1c046db8018bec5c533f3d5dd92732ffdf8786e60, a165d267e0fa10de03d6149b6853930ba3f9a244f3dd07a66b202f064535dc6a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4330 -c7f29ca4e434a0283049a26b8dba34072bf7a90e2116a5374171b0137789fa34, cce63e8a939e4dbbcff17212af5f390f7b5d5c7ac7c1c4bdc9db353b324b092c, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 5074 -19c0127c42618672cda53ef8998cf7a79bce5eda48aba533c4c157db73bf1430, 63f09dee35a7ad227266f20295f406311a3563765e51dcab2135f589127c5750, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4916 -93970bbe1875f9bd9766a225f95aa3d3e748ae4ff7a629e8d8067582ae6874be, 4e9fc8003d5384be5dde14ed04c9bb256dfc34882ae7c51873a74aba28e300c5, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4419 -6b17204cc22a12de5171f59978a5598908a3778e9d635e925190e312f27a5104, eb631efad97aa19034488b9b19fd26ac670fb584ed0f7eeb0bff2d4f1e29f0c4, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4692 -d0d2996accf533fac438f273ab65edc50eb5a756a727fa9c51308f8b8608583a, fd0ad8a263a4cae7cc1cba87fa8c5af6b0c74694cf6dcc9c56e4867d1467b53e, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4544 -f075dc821b7f48bb638dd898b9c1336abe8ac4b8ba5c964b5900c8b26fb89223, 88ed66223b41c277d464e33e1eca9e022df91b476a316050c9fe296c2d27a55a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4127 -c14ff23a8dd058c87e49edb5b6e0433e24238911ec1c7c179cd8338ad3f1eea5, c35e26a31a309a160cd348f3540aef3277f3ca07eb869c59634b6986f44ae8d0, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4488 -7baf3e8717e443d1f3a2459c6e4368b79a6233f75e236aa6367b5eaaa2deb8cc, 853d8bc62f96344aed6f69a683ff7bb15f82839fecadbbad8b2eeda3713b73d6, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 5043 -aaab6229bd572c77a7a89b3b9a7bd17cfd085528ac5471fc358b1a4c6d011c4a, 1276013c419de03614ac9da61dc28c732b5d706529e5492caab405db613d6340, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4216 -f73ab8675d3eef76ffd330ad2ef8c2dbdb2f5fe8e76bb7c5458639f2e2cdb7b4, 5c3dba3a97e7cf0d9b92e5cbb8228d5b6ea3f9dc54fba6c288e6181b03ba369f, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4014 -adede9708eecf496012fb63c6dba5926c896d077ff1bd2e8a911b4eced495d82, 66cbfcfbd434f0a4c066c6e42c54c8a5ff544eb81834c14781850421d3b87d28, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4549 -0c5a86ecee90b2275137aead69f1005ef9f1edc420845fa667ea020f98c8d064, b033d35f89a05b58388d4cfd3f2cb855ae4a3925353926b6f267a06d63fc9420, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4144 -f2ce52d93723e7c386d4604876a3c3ae7a06a295411ea2e3719f0d5b5e2a5e44, f3b1935402df86ee59dd2913a11fb327451542645a62bb8e5217a459e242e820, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4309 -f067e50164c5eab1194c3b77cc119365c01ec9501a7adf5becc7804c46695dff, f342f24d14e52fb7441f409386ab457e353caa06ee395beaa25e27be5e1a7c7b, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4517 -7a33c852aef3009f1a8096124c4e166115fd14c1619bf8a1275b94292787c274, 1f704e695d2b807cd9b3f877ab12fa6d5cabe77127a589fb2ae6df94470e7717, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4352 -34cd3c37f0cbda82af0b62b0b3c75887a1332c5a855218c75fe47e2d6bc6aeec, 35df8cd36c809e50e74990932a316fe5ecefcbb19fe3f4a8d027fff912202998, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4042 -dc679985b8b7d22ca9dcf52c947f826d9cf6f36582b70f7282ac1b67a0c46ae8, 80c2186977f4180ad43db66bb7d5596d7e42410dbaf7e84734b71d589f46724e, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4054 -17f3b29ae0d930361208d16169894ee4ed9b5846bd01147d0c7bcf05ae7d25e4, 5ef965812a3918a2dec24cedfb23a306a0305e40e52d9b38ff00b2a0d2ebe616, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4480 -836eb6226f84ecc4a270ea559f2b3418cdc0bb9eb66a26bcceaa7e45c802cdee, 9a7512dc2a55c148ee1900e85f9bec223bdd73ba25b650873433164d196f2349, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4849 -6e8090bf760bbd8dd5146ebe3420223b8be6b30b65e714220c27d1c2ab62423e, 29ad5c73e1f39167a370020bc2ed3af84e640bfcde167a6981f0114723fd7ac9, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4296 -9c256502b40844cd9f2b11ed9eb5472441731b66797542837dbc68ee2524cb98, 5627705fd251600ddff839d02077e646f85dbb08fc08947e9098d0e2c289022a, 96d417dd9476db171dfdb190e1e2890fcc8ac78965ad0a65d7c2d2705d49d5d5, 4454 +5c6abfcb759cfacf849b67bd300b165b96084ca0e71176cfb7c5a33dc08bbaad, 83117509e50f639121de2895d6ab1be7414f6e4eb47a584b23c868eb02efbcda, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4834 +3e09a895669d7bc0a0178ee5a222efa0aadced0323b1d9979d8c91245bd2d807, e04b504d08bc81e18ffb0c5e26053179a02462db28e087053d0f190774d9ed41, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4299 +69848177b7d38c8dc9e4d19b7105ee6a5cb7fc2336a05775183343566ba2c96f, cf663581e3966026b8eb3cbfcca5123587eb54dc4be7840409b90f6b3bbbcf90, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4111 +43b689a5bf647c1be885a1edcad5b556c82d653af74e5b550259071e41b585a2, 2dd0f2368db7d54246c1eb6b25769cbec11afaa1cfc91dc2a8c8f30aa9e8fdbd, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4408 +7cbe7074fa28b6b729ae68dca950f3771d4d366ebd7143ba8e8c2760acfe86fe, f5797c25536802d6c8090650d59575863c26c838475ecdbc91f8acc86e22113c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4079 +528018592df920788f63e08e67bb28ec93773d1abaec524cc3e272fed477b256, 475fc1adc179f7d2863f15f0bc581d9028e6bfedd98f294e5bac85c833c0f146, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4332 +8a98ab944c81eb261eb5139fc559d38194ec8cebff331bfd8f6c3ee120be9727, 892ecbb404e49bdce6a635322ca7d0079bf1a10f70c1a7ba5a1ad5750bfab59c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4191 +252d81094d6463586ea147fc81b8c52f9c2c662b7b46a9ce7c24b40713eb23f6, 65c4101fbad875daf96c2336bc1765949528d7347d6d620530177d631c87f872, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4002 +b5222770b54b868a918dff38148836037e24c57205f69c05af18dffcae566caa, f96f40960247a776db986f1188ea577cfced37a8d3cf9d829c1fb482c694ff03, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4014 +233bd25bb925a17e74b57644d01a8f02aa190d5fc458a3e9146af6a18fbfc4e5, d970c14ae7dde13424696cfe13c2537df036ea1982c1265df7e1791d834f0d9d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4053 +4abe622ff9ad1579f45bfcbe745fabf5b08feb40c957b3b3b599e3965865000e, d8ab02479da9e51d2e8e98248f8448fc933e586f1ea660e48d361aad4fc95efe, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4656 +1e2049d7f096d7139a9d0953e3de346e29d869e42088acfb0e105b1e5beec400, 50016a62dd87e15506817198a9227e1b4cb51f478c2cff5710d27424cf63d210, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 5361 +b79d9aea466940965878d446c5afc50cca28ce50fcbe69ebada6b38c5cfca999, d37aa7a0ba4fb51628de0747ab5de0a76bfe0ddbdb930e0cc440612229b5faa3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4017 +3974d191cf4fc9b4c32997ef596817950c7c9a76310ae8a49d1a3340575fcbcd, a558d8dca066bedca197c2c0d5b77e55db6531531e0668434e8a9b7883fb18ec, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4088 +97683b0f82cecfd2fe4cdfdb2e6cfd7c1955ffeefdbe6a43b1d45ae623db38b2, d67466d62add63979c892058f3107b5f950fdc750558e679727e3fbf8c74f443, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4146 +a776ed3457761d8d5c3ccbb99b9bfc4c7c1c13fcc4cc00a885a36beab298565d, 36301c6a59665c4dec2cfaa970bd9f1379236601521c0e284cb859933b5bba02, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4005 +f219619241de2bedf3cc635d3e90a0c085628d0bf90d33a17bc49e1d59932b49, 4b234c876cdadefa0ebd99f6821a042f6973ce06771a31c00ca23d4cd1ad15fc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4556 +bd094d0209ab1ea3b4d2f5ba0a17639deb046e5252fef7a7f297fe4e2a231bd8, a8915bcde2eed41171cb46b189fb2b5bb5cc55e769162a55c6489729bd6224c8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3998 +1be2fe6b05634c1f4b2724b1731192edf9da6d82201bd424781581b840a74c97, 7719b80b4343d5538ba4d343cd408aa1327a3ebe824eea3162771c6d7041b644, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3881 +5823092689e26cab35c4548e187b27968db44ee2ebc9462e7825dc629ec3cd5a, 82b10fd72b1eee4e17647cd782d9b65b5e6dc2e42816f95ace4cefb85aa330db, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4266 +1a597194d68fd4472f1e9b0e0246048091a68f276edd7b3d098ec302c5c5dbb0, 722423c42f5c37c3fd546d305f7bac2a7b3b3d502e88f94dc9d777b4b0595ef2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4196 +69f7f8a0a5785812a99987b520198033099c7711364131330f349adf84043da0, 9650ee05ea97d8dfe539e561a95d13bb0ed3010122cd83d7cb2de12e17117e78, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4051 +96d97311d817be86a1e92669190d61b5f74a6349300cf9866e8947ae2eeb8b56, f9d2d3f44d659b2fcdd1cb790d05e9e07ae8c02a639dc6fac51128d2e647adf2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4846 +0fb2d3f10a46a8589ce717c9fcf2d0964a7587dad5ed77c5e9fd77c770e6f96c, 0aaf6507ae1b6741d33dcd9df870382e909968a4913ed120ace7e8a8c5db97b0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4294967295 +ebf175e23874afbfdc78b553e2a18d6d437b1315791038a05fa27cf23eb43834, 7353fb920de39d5a7157248ccd7bed40bcb0bebdd845c178394b3990c62b16e4, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4194 +3b3811ad2d7cae19dc1ae072995144777b38834df1c7afeb42acba448e89bc94, c79c8599b14a928e590bc08267b46687b669e44f8c2f17ec5cd546757898b23f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4279 +b24eb5baaeaed3c61f518ca9977e0a3ed7f096d6ded8928c9e82796db54c6db0, 6128372fb6122ce0580c8be4ccbcdf8dedda6e715e1346bdc07ce8af2fd64004, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3984 +86af826199eff97b4096cdc0a65c39d3a7999943d5c972b986a4790de3c6834e, ef1d45ca4e7798874551587794b46c2f8a326a65ef2606a816edf640a4e64f35, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4198 +95f4c1fe80bd070241363c54a7cacb2700f676d27ef3be667ea4c4468972faec, 1d7f731c233ce24a8ef8c8554e632e559b86a36077e9ef383aa35555a99ba93a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4112 +2fa2b7502405b29c91d860624137128d014805c731c995f1e8ea0618375bef71, 487c98844fd5e508b91bb51fe106c29053b2d789ce34f83eb25b9236d044ce54, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4445 +43a22b7e74dbdcb06eaf05af45e973ecd0516618c0cdc5c5f862eba3930ec6ca, d45e68a4f7bc514ddadddd88563cb1149d1a4627176cbc889c646bd993f7d9dc, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4525 +c78d83d544cde30940a7c2e79dacd89b7be67bfc2f675246b0c900c3348be47b, 7f07b4b55f5461eaca22747939121bcdd480eaa3e52fe6eaae533e87b5bab6f0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4079 +0868b5d914ab9fd4286e8c1288d0ad20279cfecc4fb62f7a0261349593868034, 2b18adeef5443c713cdd1eb669bd522f811e148b51becee2ec7fa076121fef3d, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4068 +8c67119e8866f54e299659373bfcdb50658f132f5838f5f98f2a6cccfa694582, 6f1983a520ad271eb4b3a0feb3d78379df1aa1ca6b1d39270f0800b052220206, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4035 +567a12bb956b64450bdc1873cb916ffb2e5aa647b44a6e7c694b96ac4939f80f, 3f4be93dd2bd342345973a164c36991a2eaa123ab679e2b662cf79f3fadaa1f3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3967 +cef39307dc5f8b905ffaa40b74dd7a6ae28e9b20ad410aec447fc1e3faeb3109, 6d898904e6a64658c756f2bd12ca465fe79a8e177bcc7c08ad72acb543d455d3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3995 +b5f70c7915c32b6304048d682a5f56a27a6e6647e03ea626a9ce66a1e5b74882, a07872db7e1665817caa07bfc7c05fcf8a316ce17601aa64a280095ee4a47f3f, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3994 +05bd14c740eccef297fe0c36dbce6dfcf3a573c5c557dd1762d8a91df58db2ad, f181307bc0c696c266e89d214846dcef1f2ae46fa6ab0c755f7021bafcec2458, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4241 +3c635171bf5e2c20d0464af2752f665927946f7da667f26a6169212d0c7b9e19, 71b317f8d47857d19c26ce96f47c246f24bc359bc94aec5fae3617815eaa0627, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3924 +4cf7eced94f73f92f1051c49993c02464a46ed9b4cf992b8a1b32e82c8e7ee53, 81249a9488535d742ac2f0604a56d0e03731fdf883909f81a67140ee2543a7ae, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4830 +b21455f84dd299a4e7eecf59bc16a4dac3693bed9d7dc39205a790f387218891, 091300da9360265d7f95e331d1aa7cc5bd34d20ad29d644c7187614c1c96855e, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4048 +7a916acd1bf9c4ce52457eadb627bc8de362fd77f7ce0362e810dbed21547378, 2050a738dd2fb5a268cd781a4881dddb52066b7d75817bd06f296ef5be6fa856, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4104 +272470c8ca87d42d07870a914c9e980b32104b32de7b9a51b8c7c984f79c2c69, 56f4c134b9cb6dec98c81a8401fb1971cf78211d2ad4f79f3d7dfc8d9b0301a5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4483 +1a3f8d93141ad5e3ac1a90e9f46d1f32631bf694bed474c7962d651e1fd26881, 4add95fc66f5aa5d9938c8e32dddc7c70ad054f114507cb95e14ae42bb253d96, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3987 +76e7e980a4199b13a0417db3d722a4558c586885bce7e6679467835f6fc253ff, 5e524a4a4c0e39e44a83e2040a2ffc4b536098e1a4659cef228bf326d7130b54, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4072 +e20a1940ddaa1371af7e7ea919f8064fbe50df43830bab732931aa1900b7acbf, 82be747d83026cc2bb90f1f551a5abd16ac9d635c4723004751c26ac15549a1b, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4012 +59b8ff49ba21f829bcb682a9e51acd7c4bd43726f8e8131e7c0399dc15d1e3e0, 18a3e10ca629fa686ba5f4cbd5440dddfe82f874175f6d77b822012a1cf54b0a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4250 +87a835df7f5e7703fc019bedb0e94a7ff63a715ebdce83d5e0fb178119e4f06e, cecfc2663701707933ef70ad09a70de9c4cb49c324e5a666bf2941b481065ac0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4029 +ff23e4cea9eef8e9ccbdb7febbe8a696c77398b42ee05b3eb7fce8ffba9f22d0, 647d7cd0fc289c2f923ab330f2fd39d476c284e41bdd98e9e1d517e3014e0511, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3958 +de7688270f4f7f936bb26eea7e35340b5de7ae9ef76ec13e160864dd133d88ba, 229d383c968182630f5216ae7621c8e8d509a2facb150dc0853b14c88ead9698, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3873 +5d6908a6dab0c1a5842f8fbbef4b9056aa937c8391b95b2d821241bad4640ce3, 35c4480634c28602acffa168e4a46f402ae7e006e1a6226d4a17d0abcf377a1a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3968 +8d47429fff684d4ecfb63e5a71bce0fbc73812897d2306c267065354e14d63bc, 2e05ceae39061864f93d20a95c48a778de47ea4505976e8aa5a4d53eff95f389, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4294967295 +86c34012e9381c9983251165bf62206dfccda9d51e05349b852424b51a5f5791, 60979a1a72c4557f8238411fd06230ad600ad5bc5cd0cd528d27e04d048bb3f9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4327 +73ac3e8e5d99461848fe285df80ff1207c0c1fe9e474120e44a7da1d6cc680af, e076dc58b469918cc5edb8813eff740951bad4f52e5a532b69271e62dd4604c2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4171 +2df1baf2e452551a257fa2171e7e9e1f60c729b088dd70293c0e3ce9d1259fed, d3b9433e36f40ae4663ac9cac9e6086129b4b9a6d4c713cd431f3dd4574157aa, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4071 +950a839b99846d1a7a9d3abaa180636d785915bd85fbdad3565dab051f7dd889, dcb4544c304036f9c50a23a2a2677e29f4d401455f5ee714ce70ae2c40d964b8, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4104 +633fbdaa31e766e271591f8e6cf2a5e39087166cd48fdebdc3fd44f57c07fd04, 4f37c954c7a00bff07f402eca1fe37062f3b33342b8891c4d4564d0588239a84, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4013 +dba0248958481896f41ffe6d255636f65f311dfa518fb6ff7fb1dde7d7ba083c, fa909b9c1d0a7f4ef21142ee1b10a6fba3562dc83676c4e34862165650480ab0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4031 +51493015b513ab69f8f44def0202a6783a6a33ba19c4a43ddd393696e6c6de09, d7946e834695f2e0ef98b7a34b720f70deab58add43e678634c2e6ac6b225f6a, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3952 +7e0923ab0d299c31ffe97ce203e48e816fbce737e7603057bc14698c50e454cb, 7e79180baf44db64e4e65f512b335bc16d8e214709d913455334962812e66ea5, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4119 +836253c93cfc7660d008e3ca8f5dbb4fb970daf0c91ed4fc99468d44e481cf7e, 9e3776397e6b4d06500f5d5f985bf3d6225d884677d0b802b906b8f37a10cea0, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4128 +204a5494b217b0fa69ed9c8580c28ea64055fc8c7f22082f574dc271f8ef83d4, f24f9d48d1ef5dd8b386d10e77010b2c73555e2a8020e82ff6c2680e4637bcd2, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4134 +ddc72b919c2359cd39b7939aa65a4ecd97fd399d66f2a1b78fbcab52c4ead007, 4020b0d9a331befeaf3964ac11435baa56827bf3c82bce6de0c0e6facad7f465, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3948 +49c4411d211cf2e58ef0eee11727922ca997ab48e1fced6afc73e67901f7fe4b, b4f5fc06c493a28645f74f49ad5931c4facef97821e752e27404a0f5a515c808, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4063 +1e9416b24af8a319b2559c37d46b3e8e2dd9fcdd688d9b0c9d87f5bbaedf988c, 6671e7215aa9b2e8bdafdd6c956e6c079469537c2714782992b660cbacb12aa6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4294967295 +83e18b0f2174692da8efc778104fd8ad559a8877def7a78d19fc3e7703b6f908, 17dab68f4b8027af7edb6dbeb8ee92225398763cae2a06176034c26305b2d50c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3961 +42a88e5f44f823ce807cc95980312f9a9064a0734064712eedb21d84227f5f8d, 6161140438c6001c2997a4614b4c02e71f45721eea5a0380b9b5a0f0a9369b67, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3926 +4f54d8fdfa317ee4a0132781e6923c850774fbdeffedb5a7cc32b4b3bba723a8, cbdfe8ac2cfd9ec657fbafe379a6fd31b8e62e4b39d8b5fcd325a6c01cf2c212, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3981 +d2dfcd4e82677b03cf1ca02f6ff518cff7b991a9ad139bf10dc3d4dc0a49a720, da69a84301141ca55e791ef6596eb8379f76db494d7186f64f1cd869ee537135, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3975 +2287e5cc8f335e611a972a4b5a2f88dc0973fafde8e43b0ad63418e8f12e7f6c, d9b558052a2435163012591e988cd0838d2e3aa7244c26a1b147c8a3d2998156, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3960 +bee65d4b28d44ea33146dea8b373eec6d5e2d8827d3ea92fff17425e05feec25, 75bc42813109f42075d132dd809867203a728477c80eb20c15a2842da431beee, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3946 +a3b1f532d43d656d7bed4b9d2ff8a44190fe3642d0228bf81a1aaef6c95fd2e2, 305b210d72143091c54d0bf96584aa928c61a75bba3e94d905b860bbeb509190, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4172 +54908b3b71a0f46a23125e35c2852497e42afd57413c8098eb286046676ea102, 261465d24c4e55ca775b4aa5626124de3dd222641d5baf8d95635b1683cf3212, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3994 +f9bb99de949e8f9c70fa8ae4ea0533fe4fd02c8172c97aa47c65f0a86a1a9733, 76550a5bf77ca1bccf693d7a36688a5e753eb459618dd4f80d5389e579204baf, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4038 +347963dadbe084b61bb4001be819e02668519ccd27b15d7743685e391ec30e28, 51a478d941a5629d3459e498d8cb6b0b95ac5bf3822aee799d40755ee1033e81, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4082 +37300bb5fbf3521a7247943ff8bed828c2db93fa837001eb0625883f2b30846e, 2eef924fc10970e83f06fef44bf919a9179f4dce6332b366e64993d6e8b6dc99, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4076 +2c84e6be8d6e60989311f0de9c2176b7172dec9b41717e5d8cdbce17b359e586, 987bcae10727dd07db008123363dcf05b648e3d3be585dbb750c4b4ce7b04a17, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4022 +a044257dba17f3f818ea7867dc8d2210272fdf9a6c49d7293dd2d6c8f74c38e0, e58f5d95d7634aa49c36a3af0bb639dfff50d56cc009fc6ad6966680d8b38432, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4058 +b863c20a85a4f855d09d6459fbbd44ee2675beb82589b88d891119e6ffaf9a22, 846f5192c83d0b83b04946cd656e7d22f5fc9279e738ed67802a1526fc9ada75, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4051 +8842676e4ef79dd65c96c8838490e0f71c5e0587fd472392f1c2568aa4b16d0e, 2350214ac3af8c12542a3f59c2c9ef563dfad32dc129880cb21314cac35801f6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4002 +88ca5952066524cd9af9c4d452c0d40adedebdcd15be2cca4cfa7c25d8fed68e, 827a81e2f47c4b9d06e88a9c6c4b378505f5da182130e931976f786d9327f2d3, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4022 +e1bc3740297568ba64ce7e65c78e8e3910ec87bbcafb461257d4ba92da25e00f, 94dfb94f48cf22ba93783bd3fe767717a0fecf0be4b6f407764f24a99c906a72, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4054 +5c31de2265febcfa88b17f7cc4ac351bd0d55d1e27f8bb3114cdfb73929dd793, f779b1c36dc62d90f2d134c5f024c2fc9458dad86d2437c4b2adcae4c26ab8e9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4096 +6d36632caef4b8005dd52f0cb2478c73782a8b769861c6a481617477c845c9c0, 2d4a99d7e6cc7e8b6dbdada85c673168c9ca18933e900860da05b45d84b45f48, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4011 +9765fb5c321cbb53694351e3d6b608ff4e91bccf85867e6f36b8a9b588d99daf, c9c12209c36e8ee50c4cb6271aa34deb8b19fe5947be9775fdca32d1ed11e3f4, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3982 +70ba464f9f4ae5cdbf84645ded1860f9040eb73d8f4217bc37fd5de96efc0e1f, a6b9fcdb4d4ce11c947b23dce9a72bd57cf5e39dd929b49c4353942014ceb689, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4030 +f35ab2abc229b23a84e747b56ba655aa80ec43f8123639de199b339ce0988afd, 763f7bb911922d08c3b21d6346b02ed124319262329774a1e1e9108ed9f36e3c, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4038 +ba3929fa4120a0e76bfb93a3d345169ead7d551ffbc067217b75d0da3d3239e9, c216ed9e232d0fa509a82bba3aa1fb910c77e2829ba403e2adb4388d4d778ef9, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3945 +0c94637c6ef6411516af889a492757b2125031c975fe6ac0e311e0354cf5343d, a7bc7631babeddd33aacac22fccd6eb8f97e1b6280fd5209d07df7ced41b20fa, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4217 +b6eaae827e70dc56049a0cf6b9714eda2a254f23de838a015b7f96b1aab466d4, a2dd155fdcf3480ad4e49e782d3ac61c152191c83bfb19f7e00a9951fe6e16f6, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4554 +c091ca2227954b5334e5485a8a0a4fb3b1cc9dcd3fe3d5b5cbdf5764962766e8, 10b88367348be5e3ff5f070f0cdb035f3edd0c571a18dd45b43bb41f43554ca4, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4419 +773bc69bbc4d0aba482e0c3f9da033d2099694ab1470cf01712db4f4c9c68041, bf02268b41216a96fe90692d60adb3cc3b35516f533e991530495f50ffdf9926, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4436 +2c648cb8f34052477c5b5c668a42ce9ec1d6f93b0a13fdac21a85ad391fb6b4a, 04083fc4261e3b3b6b5df0562394724fe906704d65ac8d8fdbb2231ede1eafcb, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4073 +8039cd218bb0968fc8408bc001cbb5a6fcb5816a0bc661830fc904bde80a1ae7, c59fd8b643fdbf435dd1f114467c9029dccfac89220f66c11d0bab9ac4a14792, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 3924 +d722442174e215ccdba594ac37c83abe81e07590ccc77b6dac24836e9f4ddfe4, ddd764aa3f1e1711ca8ffb23228d29a85a1d7b9c9202b08ee182a33c9c0f5246, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4050 +17d5a1fef4b69dc6a1aa176383a2a64a71a68036c20726b031034058ad430321, 36688dd1635866dc842651320845bf1b25ba17bb57ffe403e20c1342c3c08491, 8b12add89bc264e01038b61b784e0778a03cd3025e1faeedda6598f197ceccc0, 4060 diff --git a/test/score.cpp b/test/score.cpp index 519210be..af534272 100644 --- a/test/score.cpp +++ b/test/score.cpp @@ -548,7 +548,7 @@ TEST(TestQubicScoreFunction, Bpp9000AntColonyRegression) const unsigned char* pool = pools[chain.poolIndex].data(); score_engine::ScoreBpp9000::ANN parent; - engine->deriveRootANN(chain.pubkey.m256i_u8, pool, parent); // depth 0's parent = the derived root + engine->deriveRootANN(uniqueSeeds[chain.poolIndex].m256i_u8, pool, parent); // depth 0's parent = the shared epoch root for (size_t d = 0; d < chain.nodes.size(); ++d) { const AntNode& node = chain.nodes[d]; @@ -876,42 +876,53 @@ TEST(TestQubicScoreAntColony, AnnSurvivesExpandAndCompact) EXPECT_EQ(memcmp(&restored, &original, sizeof(original)), 0) << "expand/compact is not lossless"; } -TEST(TestQubicScoreAntColony, RootAnnIsDeterministicAndPerIdentity) +// The root is shared per epoch: it is a function of the root seed (the epoch-start spectrum digest) +// alone, so the same seed must give the identical root every time, and a different seed (a different +// epoch) must give a different root. +TEST(TestQubicScoreAntColony, RootAnnIsDeterministicAndShared) { AntFixture f; ASSERT_TRUE(makeAntFixture(f)); - const m256i pkA = makePubkey(4); - const m256i pkB = makePubkey(5); + const m256i seedA = makePubkey(4); + const m256i seedB = makePubkey(5); AntEngine::ANN a1; AntEngine::ANN a2; AntEngine::ANN b1; - f.engine->deriveRootANN(pkA.m256i_u8, f.pool.data(), a1); - // Deriving another identity's root overwrites initValue.lutInit, which is the state a1 came from. - f.engine->deriveRootANN(pkB.m256i_u8, f.pool.data(), b1); - f.engine->deriveRootANN(pkA.m256i_u8, f.pool.data(), a2); + f.engine->deriveRootANN(seedA.m256i_u8, f.pool.data(), a1); + // Deriving another epoch's root overwrites initValue.lutInit, which is the state a1 came from. + f.engine->deriveRootANN(seedB.m256i_u8, f.pool.data(), b1); + f.engine->deriveRootANN(seedA.m256i_u8, f.pool.data(), a2); EXPECT_EQ(memcmp(&a1, &a2, sizeof(a1)), 0) << "root depends on engine state"; - EXPECT_NE(memcmp(&a1, &b1, sizeof(a1)), 0) << "two identities share a root"; + EXPECT_NE(memcmp(&a1, &b1, sizeof(a1)), 0) << "two different seeds share a root"; } // A child is a function of (parent, pubkey, nonce, anchor). Same inputs must give the same score AND -// the same inherited LUT; a different parent must not give the same child. +// the same inherited LUT; a different parent must not give the same child. The root is shared, so +// the second parent is a mutated child of the root rather than another identity's root. TEST(TestQubicScoreAntColony, ChildIsDeterministicAndInheritsParent) { AntFixture f; ASSERT_TRUE(makeAntFixture(f)); + const m256i seed = makePubkey(4); const m256i pk = makePubkey(6); const m256i nonce = makeAntNonce(5, 3, 41); const m256i anchor = makePubkey(12); AntEngine::ANN parentA; + f.engine->deriveRootANN(seed.m256i_u8, f.pool.data(), parentA); + + m256i improvingNonce; + ASSERT_TRUE(findImprovingNonce(*f.engine, parentA, pk, anchor, f.pool.data(), improvingNonce)) + << "no nonce improved on the root, so no distinct second parent can be built"; + f.engine->computeScoreFromParent(parentA, pk.m256i_u8, improvingNonce.m256i_u8, anchor.m256i_u8, f.pool.data()); AntEngine::ANN parentB; - f.engine->deriveRootANN(pk.m256i_u8, f.pool.data(), parentA); - f.engine->deriveRootANN(makePubkey(7).m256i_u8, f.pool.data(), parentB); + f.engine->getBestANN(parentB); + ASSERT_NE(memcmp(&parentA, &parentB, sizeof(parentA)), 0) << "the mutated child equals the root"; const unsigned int s1 = f.engine->computeScoreFromParent( parentA, pk.m256i_u8, nonce.m256i_u8, anchor.m256i_u8, f.pool.data()); @@ -968,18 +979,17 @@ TEST(TestQubicScoreAntColony, NonCanonicalNonceIsRejected) AntFixture f; ASSERT_TRUE(makeAntFixture(f)); + const m256i seed = makePubkey(4); const m256i pk = makePubkey(10); const m256i anchor = makePubkey(30); - AntEngine::ANN parentA; - AntEngine::ANN parentB; - f.engine->deriveRootANN(pk.m256i_u8, f.pool.data(), parentA); - f.engine->deriveRootANN(makePubkey(11).m256i_u8, f.pool.data(), parentB); + AntEngine::ANN parent; + f.engine->deriveRootANN(seed.m256i_u8, f.pool.data(), parent); constexpr unsigned char maxK = (unsigned char)AntCfg::numberOfMutations; const m256i good = makeAntNonce(3, 5, 62); // A rejected nonce and a timed-out walk - f.engine->computeScoreFromParent(parentA, pk.m256i_u8, good.m256i_u8, anchor.m256i_u8, f.pool.data()); + f.engine->computeScoreFromParent(parent, pk.m256i_u8, good.m256i_u8, anchor.m256i_u8, f.pool.data()); AntEngine::ANN afterA; f.engine->getBestANN(afterA); @@ -995,7 +1005,7 @@ TEST(TestQubicScoreAntColony, NonCanonicalNonceIsRejected) for (unsigned int i = 0; i < numberOfBadNonces; i++) { // The bad nonce is early rejected in computeScoreFromParent() - EXPECT_EQ(f.engine->computeScoreFromParent(parentB, pk.m256i_u8, bad[i].m256i_u8, anchor.m256i_u8, f.pool.data()), + EXPECT_EQ(f.engine->computeScoreFromParent(parent, pk.m256i_u8, bad[i].m256i_u8, anchor.m256i_u8, f.pool.data()), score_engine::INVALID_SCORE_VALUE) << "non-canonical nonce " << i << " accepted"; AntEngine::ANN now; @@ -1003,8 +1013,11 @@ TEST(TestQubicScoreAntColony, NonCanonicalNonceIsRejected) EXPECT_EQ(memcmp(&now, &afterA, sizeof(now)), 0) << "rejected nonce " << i << " still ran the walk"; } - // Now after bad nonce, we feed good nonce, we expect this is ok - f.engine->computeScoreFromParent(parentB, pk.m256i_u8, good.m256i_u8, anchor.m256i_u8, f.pool.data()); + // Now after bad nonces, an improving canonical nonce on the same parent must move bestANN again + m256i good2; + ASSERT_TRUE(findImprovingNonce(*f.engine, parent, pk, anchor, f.pool.data(), good2)) + << "no nonce improved on the parent, so the post-rejection walk check would be vacuous"; + f.engine->computeScoreFromParent(parent, pk.m256i_u8, good2.m256i_u8, anchor.m256i_u8, f.pool.data()); AntEngine::ANN afterB; f.engine->getBestANN(afterB); EXPECT_NE(memcmp(&afterB, &afterA, sizeof(afterB)), 0) << "canonical nonce was not scored";