Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified data/bpp9000.task
Binary file not shown.
55 changes: 30 additions & 25 deletions doc/ant_colony_mining.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
|
+----+----+
| |
Expand All @@ -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.
Expand All @@ -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.

Expand All @@ -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
Expand All @@ -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).
Expand Down Expand Up @@ -153,17 +154,20 @@ 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[3..31] || anchorTickDigest)` (`nonce[0..2]` zeroed) -
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
Expand Down Expand Up @@ -201,14 +205,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
Expand Down Expand Up @@ -254,7 +259,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
Expand Down Expand Up @@ -301,7 +306,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
Expand Down Expand Up @@ -364,7 +369,7 @@ 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];
```

Expand Down
2 changes: 1 addition & 1 deletion src/mining/ant_colony/ant_colony.h
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,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;

Expand Down
38 changes: 21 additions & 17 deletions src/mining/score_bpp9000.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename Params>
struct ScoreBpp9000
{
Expand All @@ -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)
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down
54 changes: 43 additions & 11 deletions src/mining/score_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,16 @@ struct ScoreEngine
ScoreBpp9000<Bpp9000ParamsT> _bpp9000Score;
unsigned char lastNonceByte0;

// The inheritable per-neuron LUT the ant colony branches on.
using AntAnn = typename ScoreBpp9000<Bpp9000ParamsT>::ANN;

void initMemory()
{
setMem(&_bpp9000Score, sizeof(ScoreBpp9000<Bpp9000ParamsT>), 0);

_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)
{
Expand All @@ -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<Bpp9000ParamsT>::INFINITE_ERROR)
? (unsigned int)ScoreBpp9000<Bpp9000ParamsT>::numberOfWindows
Expand All @@ -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<Bpp9000ParamsT>::isCanonicalStandaloneNonce(nonce);
default:
return false;
}
}

// Each engine owns its canonical ant-nonce rule
static bool isCanonicalAntNonce(const unsigned char* nonce)
{
switch (getAlgoType(nonce))
Expand All @@ -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()
{
Expand Down
2 changes: 1 addition & 1 deletion src/network_messages/ant_colony_message.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/public_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ static constexpr unsigned int NEURAXON_SOLUTION_THRESHOLD_DEFAULT = 1;
// and hash-verified at node init
static unsigned short 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 };
Expand Down
2 changes: 1 addition & 1 deletion src/qubic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3438,7 +3438,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)
Expand Down
Loading
Loading