diff options
| author | Bitflash <btf01@proton.me> | 2026-08-01 12:56:16 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-01 12:56:16 -0300 |
| commit | 7131c30c33c980f0ba1d37dfc6a3fbe8c319813e (patch) | |
| tree | 668de28f9aec4f8f350146f1d824b487752cf747 /src | |
| parent | b426948b966405b65f3ea6c1994164b462980694 (diff) | |
wallet: generate keys ahead of use so a backup outlives the next block (#85)
The wallet made a key per address and per block mined, so a backup only ever
contained the keys that existed the moment it was taken -- mine one block and
the coin lands on a key the backup has never heard of. The README warned about
it, which is an admission rather than a fix.
Keys are now generated a hundred at a time and handed out one at a time, each
written to wallet.dat before it can be pointed at. A backup covers the next
hundred addresses. Deterministic derivation (#47) stays open; this buys back the
distance between a backup and the next mistake.
Diffstat (limited to 'src')
| -rw-r--r-- | src/db.cpp | 8 | ||||
| -rw-r--r-- | src/db.h | 13 | ||||
| -rw-r--r-- | src/main.cpp | 75 | ||||
| -rw-r--r-- | src/main.h | 26 | ||||
| -rw-r--r-- | src/rpc.cpp | 9 |
5 files changed, 118 insertions, 13 deletions
| @@ -636,6 +636,14 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& vchDefaultKeyRet) | |||
| 636 | { | 636 | { |
| 637 | ssValue >> vchDefaultKeyRet; | 637 | ssValue >> vchDefaultKeyRet; |
| 638 | } | 638 | } |
| 639 | else if (strType == "pool") | ||
| 640 | { | ||
| 641 | int64 nIndex; | ||
| 642 | ssKey >> nIndex; | ||
| 643 | vector<unsigned char> vchPubKey; | ||
| 644 | ssValue >> vchPubKey; | ||
| 645 | mapKeyPool[nIndex] = vchPubKey; | ||
| 646 | } | ||
| 639 | else if (strType == "setting") /// or settings or option or options or config? | 647 | else if (strType == "setting") /// or settings or option or options or config? |
| 640 | { | 648 | { |
| 641 | string strKey; | 649 | string strKey; |
| @@ -386,6 +386,19 @@ public: | |||
| 386 | return Write(string("defaultkey"), vchPubKey); | 386 | return Write(string("defaultkey"), vchPubKey); |
| 387 | } | 387 | } |
| 388 | 388 | ||
| 389 | // Keys generated ahead of time and not yet handed out. The private key of | ||
| 390 | // each is already stored under its own "key" record by AddKey; a "pool" | ||
| 391 | // record only says that this one is still unspoken for. | ||
| 392 | bool WritePool(int64 nIndex, const vector<unsigned char>& vchPubKey) | ||
| 393 | { | ||
| 394 | return Write(make_pair(string("pool"), nIndex), vchPubKey); | ||
| 395 | } | ||
| 396 | |||
| 397 | bool ErasePool(int64 nIndex) | ||
| 398 | { | ||
| 399 | return Erase(make_pair(string("pool"), nIndex)); | ||
| 400 | } | ||
| 401 | |||
| 389 | template<typename T> | 402 | template<typename T> |
| 390 | bool ReadSetting(const string& strKey, T& value) | 403 | bool ReadSetting(const string& strKey, T& value) |
| 391 | { | 404 | { |
diff --git a/src/main.cpp b/src/main.cpp index f63f575..b027cef 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -175,6 +175,58 @@ vector<unsigned char> GenerateNewKey() | |||
| 175 | return key.GetPubKey(); | 175 | return key.GetPubKey(); |
| 176 | } | 176 | } |
| 177 | 177 | ||
| 178 | CCriticalSection cs_keyPool; | ||
| 179 | map<int64, vector<unsigned char> > mapKeyPool; | ||
| 180 | |||
| 181 | void TopUpKeyPool() | ||
| 182 | { | ||
| 183 | CRITICAL_BLOCK(cs_keyPool) | ||
| 184 | { | ||
| 185 | while ((int)mapKeyPool.size() < KEYPOOL_SIZE) | ||
| 186 | { | ||
| 187 | // Indices only ever go up. Reusing one would mean two pool records | ||
| 188 | // racing for the same slot after an erase. | ||
| 189 | int64 nIndex = 1; | ||
| 190 | if (!mapKeyPool.empty()) | ||
| 191 | nIndex = (--mapKeyPool.end())->first + 1; | ||
| 192 | |||
| 193 | CKey key; | ||
| 194 | key.MakeNewKey(); | ||
| 195 | // AddKey writes the private key. Order matters: the key is in | ||
| 196 | // wallet.dat before anything can point at it, so a crash between | ||
| 197 | // the two writes costs an unused key, never a usable one. | ||
| 198 | if (!AddKey(key)) | ||
| 199 | return; | ||
| 200 | vector<unsigned char> vchPubKey = key.GetPubKey(); | ||
| 201 | if (!CWalletDB().WritePool(nIndex, vchPubKey)) | ||
| 202 | return; | ||
| 203 | mapKeyPool[nIndex] = vchPubKey; | ||
| 204 | } | ||
| 205 | } | ||
| 206 | } | ||
| 207 | |||
| 208 | vector<unsigned char> GetKeyFromPool() | ||
| 209 | { | ||
| 210 | CRITICAL_BLOCK(cs_keyPool) | ||
| 211 | { | ||
| 212 | TopUpKeyPool(); | ||
| 213 | if (!mapKeyPool.empty()) | ||
| 214 | { | ||
| 215 | map<int64, vector<unsigned char> >::iterator mi = mapKeyPool.begin(); | ||
| 216 | int64 nIndex = mi->first; | ||
| 217 | vector<unsigned char> vchPubKey = mi->second; | ||
| 218 | mapKeyPool.erase(mi); | ||
| 219 | CWalletDB().ErasePool(nIndex); | ||
| 220 | return vchPubKey; | ||
| 221 | } | ||
| 222 | } | ||
| 223 | // Nothing in the pool and nothing could be added -- disk full, wallet | ||
| 224 | // locked open elsewhere. Generating on demand is what this code did for | ||
| 225 | // its whole life, so fall back to it rather than refuse to mine. | ||
| 226 | printf("GetKeyFromPool() : pool empty, generating a key on demand\n"); | ||
| 227 | return GenerateNewKey(); | ||
| 228 | } | ||
| 229 | |||
| 178 | 230 | ||
| 179 | 231 | ||
| 180 | 232 | ||
| @@ -3061,8 +3113,11 @@ bool BitcoinMiner(int nThreadId) | |||
| 3061 | RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1); | 3113 | RandomXInitDataset(nThreads > 0 ? (int)nThreads : 1); |
| 3062 | } | 3114 | } |
| 3063 | 3115 | ||
| 3064 | CKey key; | 3116 | // Drawn from the pool, so it is already in wallet.dat before a single hash |
| 3065 | key.MakeNewKey(); | 3117 | // is computed. The old code generated a key here and only saved it if this |
| 3118 | // thread happened to win a block, which meant a backup taken while mining | ||
| 3119 | // could not spend what the very next block paid. | ||
| 3120 | vector<unsigned char> vchPubKey = GetKeyFromPool(); | ||
| 3066 | CBigNum bnExtraNonce = 0; | 3121 | CBigNum bnExtraNonce = 0; |
| 3067 | while (fGenerateBitcoins) | 3122 | while (fGenerateBitcoins) |
| 3068 | { | 3123 | { |
| @@ -3113,7 +3168,7 @@ bool BitcoinMiner(int nThreadId) | |||
| 3113 | txNew.vout.resize(1); | 3168 | txNew.vout.resize(1); |
| 3114 | // Always pay coinbase to own wallet key. In operator mode, the pool | 3169 | // Always pay coinbase to own wallet key. In operator mode, the pool |
| 3115 | // server distributes shares to miners via SendMoney() after each block. | 3170 | // server distributes shares to miners via SendMoney() after each block. |
| 3116 | txNew.vout[0].scriptPubKey << key.GetPubKey() << OP_CHECKSIG; | 3171 | txNew.vout[0].scriptPubKey << vchPubKey << OP_CHECKSIG; |
| 3117 | 3172 | ||
| 3118 | 3173 | ||
| 3119 | // | 3174 | // |
| @@ -3234,13 +3289,13 @@ bool BitcoinMiner(int nThreadId) | |||
| 3234 | } | 3289 | } |
| 3235 | else | 3290 | else |
| 3236 | { | 3291 | { |
| 3237 | // Save key | 3292 | // The key that just got paid was written to wallet.dat |
| 3238 | if (!AddKey(key)) | 3293 | // when it left the pool, so there is nothing to save |
| 3239 | { | 3294 | // here -- only a fresh one to take for the next block. |
| 3240 | RandomXDestroyMinerVM(rxvm); | 3295 | // Two threads never draw the same key: the pool hands |
| 3241 | return false; | 3296 | // them out one at a time under cs_keyPool, which is |
| 3242 | } | 3297 | // what keeps concurrent coinbases distinct (#58). |
| 3243 | key.MakeNewKey(); | 3298 | vchPubKey = GetKeyFromPool(); |
| 3244 | 3299 | ||
| 3245 | // Process this block the same as if we had received it from another node | 3300 | // Process this block the same as if we had received it from another node |
| 3246 | if (!ProcessBlock(NULL, pblock.release())) | 3301 | if (!ProcessBlock(NULL, pblock.release())) |
| @@ -124,6 +124,32 @@ FILE* OpenBlockFile(unsigned int nFile, unsigned int nBlockPos, const char* pszM | |||
| 124 | FILE* AppendBlockFile(unsigned int& nFileRet); | 124 | FILE* AppendBlockFile(unsigned int& nFileRet); |
| 125 | bool AddKey(const CKey& key); | 125 | bool AddKey(const CKey& key); |
| 126 | vector<unsigned char> GenerateNewKey(); | 126 | vector<unsigned char> GenerateNewKey(); |
| 127 | |||
| 128 | // --- Key pool ------------------------------------------------------------- | ||
| 129 | // | ||
| 130 | // A backup of this wallet only ever contained the keys that existed the moment | ||
| 131 | // it was taken. Mine a block or take a new address afterwards and the coin | ||
| 132 | // lands on a key the backup has never heard of -- it is on the chain, and the | ||
| 133 | // file you carefully put away cannot spend it. The LEIA-ME shipped with a | ||
| 134 | // warning about this, which is an admission, not a fix. | ||
| 135 | // | ||
| 136 | // Keys are now generated a hundred at a time and handed out one by one, so a | ||
| 137 | // backup covers the next hundred addresses this node will use. It is the same | ||
| 138 | // answer Bitcoin arrived at, and it is not deterministic derivation (#47) -- | ||
| 139 | // it just buys back the distance between a backup and the next mistake. | ||
| 140 | static const int KEYPOOL_SIZE = 100; | ||
| 141 | extern CCriticalSection cs_keyPool; | ||
| 142 | extern map<int64, vector<unsigned char> > mapKeyPool; | ||
| 143 | |||
| 144 | // Fill the pool back up to KEYPOOL_SIZE. Every key it creates is written to | ||
| 145 | // wallet.dat before it is offered to anybody. | ||
| 146 | void TopUpKeyPool(); | ||
| 147 | |||
| 148 | // Hand out the oldest unused key. Falls back to generating one on the spot if | ||
| 149 | // the pool cannot be filled, which is exactly what this code did before the | ||
| 150 | // pool existed -- a wallet that cannot pre-generate should still be able to | ||
| 151 | // mine. | ||
| 152 | vector<unsigned char> GetKeyFromPool(); | ||
| 127 | bool AddToWallet(const CWalletTx& wtxIn); | 153 | bool AddToWallet(const CWalletTx& wtxIn); |
| 128 | void ReacceptWalletTransactions(); | 154 | void ReacceptWalletTransactions(); |
| 129 | // Returns how many wallet transactions the chain corrected. | 155 | // Returns how many wallet transactions the chain corrected. |
diff --git a/src/rpc.cpp b/src/rpc.cpp index 3db8b94..97d8429 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp | |||
| @@ -146,7 +146,9 @@ static bool RebuildJob() | |||
| 146 | CBlockIndex* pindexPrev = pindexBest; | 146 | CBlockIndex* pindexPrev = pindexBest; |
| 147 | unsigned int nBits = GetNextWorkRequired(pindexPrev); | 147 | unsigned int nBits = GetNextWorkRequired(pindexPrev); |
| 148 | 148 | ||
| 149 | CKey key; key.MakeNewKey(); | 149 | // From the pool, so the template's payout key is in wallet.dat before |
| 150 | // any miner is handed work against it. | ||
| 151 | vector<unsigned char> vchPubKey = GetKeyFromPool(); | ||
| 150 | static std::atomic<uint32_t> sExtra{0}; | 152 | static std::atomic<uint32_t> sExtra{0}; |
| 151 | uint32_t extraNonce = ++sExtra; | 153 | uint32_t extraNonce = ++sExtra; |
| 152 | 154 | ||
| @@ -160,7 +162,7 @@ static bool RebuildJob() | |||
| 160 | // is mined and submitted like any other. #58. | 162 | // is mined and submitted like any other. #58. |
| 161 | txNew.vin[0].scriptSig << (pindexPrev ? pindexPrev->nHeight + 1 : 0) << nBits << (CBigNum)extraNonce; | 163 | txNew.vin[0].scriptSig << (pindexPrev ? pindexPrev->nHeight + 1 : 0) << nBits << (CBigNum)extraNonce; |
| 162 | txNew.vout.resize(1); | 164 | txNew.vout.resize(1); |
| 163 | txNew.vout[0].scriptPubKey << key.GetPubKey() << OP_CHECKSIG; | 165 | txNew.vout[0].scriptPubKey << vchPubKey << OP_CHECKSIG; |
| 164 | 166 | ||
| 165 | CBlock block; | 167 | CBlock block; |
| 166 | block.vtx.push_back(txNew); | 168 | block.vtx.push_back(txNew); |
| @@ -194,7 +196,8 @@ static bool RebuildJob() | |||
| 194 | (unsigned int)GetAdjustedTime()); | 196 | (unsigned int)GetAdjustedTime()); |
| 195 | block.nBits = nBits; | 197 | block.nBits = nBits; |
| 196 | block.nNonce = 1; | 198 | block.nNonce = 1; |
| 197 | AddKey(key); | 199 | // No AddKey here: the pool wrote this key to wallet.dat before it was |
| 200 | // handed out. | ||
| 198 | 201 | ||
| 199 | StratumJob job; | 202 | StratumJob job; |
| 200 | job.jobId = ToHex(&extraNonce, 4); | 203 | job.jobId = ToHex(&extraNonce, 4); |