upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-08-01 12:56:16 -0300
committerGitHub <noreply@github.com>2026-08-01 12:56:16 -0300
commit7131c30c33c980f0ba1d37dfc6a3fbe8c319813e (patch)
tree668de28f9aec4f8f350146f1d824b487752cf747 /src
parentb426948b966405b65f3ea6c1994164b462980694 (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.cpp8
-rw-r--r--src/db.h13
-rw-r--r--src/main.cpp75
-rw-r--r--src/main.h26
-rw-r--r--src/rpc.cpp9
5 files changed, 118 insertions, 13 deletions
diff --git a/src/db.cpp b/src/db.cpp
index a503cae..3af87fb 100644
--- a/src/db.cpp
+++ b/src/db.cpp
@@ -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;
diff --git a/src/db.h b/src/db.h
index 22efc77..f51047e 100644
--- a/src/db.h
+++ b/src/db.h
@@ -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
178CCriticalSection cs_keyPool;
179map<int64, vector<unsigned char> > mapKeyPool;
180
181void 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
208vector<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()))
diff --git a/src/main.h b/src/main.h
index a50485f..1d9efac 100644
--- a/src/main.h
+++ b/src/main.h
@@ -124,6 +124,32 @@ FILE* OpenBlockFile(unsigned int nFile, unsigned int nBlockPos, const char* pszM
124FILE* AppendBlockFile(unsigned int& nFileRet); 124FILE* AppendBlockFile(unsigned int& nFileRet);
125bool AddKey(const CKey& key); 125bool AddKey(const CKey& key);
126vector<unsigned char> GenerateNewKey(); 126vector<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.
140static const int KEYPOOL_SIZE = 100;
141extern CCriticalSection cs_keyPool;
142extern 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.
146void 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.
152vector<unsigned char> GetKeyFromPool();
127bool AddToWallet(const CWalletTx& wtxIn); 153bool AddToWallet(const CWalletTx& wtxIn);
128void ReacceptWalletTransactions(); 154void 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);