diff options
| author | Bitflash <btf01@proton.me> | 2026-08-01 17:53:17 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-01 17:53:17 -0300 |
| commit | ce59d8c72c667ce638bd12f37db26d8b3408a5d2 (patch) | |
| tree | 95c62a0d0cc0a21bc3349264ce0a3c949deb32c3 /src/main.cpp | |
| parent | 593cf346dbce0a8ce46a13a73a4f78ddd044ab52 (diff) | |
wallet: derive the key pool from a seed, so a phrase could bring it back (#93)
Adds BIP32 derivation behind the wallet key pool, with no user-facing way to create a seed yet. wallet.dat gains hdmaster/hdchaincode/hdnext; the mnemonic is never stored. TopUpKeyPool derives m/n' when a seed exists and falls back to a random key, with a log line, if derivation fails. New -selftest=wallet-hd, wired into make tests on both platforms.
Diffstat (limited to 'src/main.cpp')
| -rw-r--r-- | src/main.cpp | 112 |
1 files changed, 111 insertions, 1 deletions
diff --git a/src/main.cpp b/src/main.cpp index 82a98d2..e5754e6 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -14,6 +14,7 @@ | |||
| 14 | #include <atomic> | 14 | #include <atomic> |
| 15 | #include <mutex> | 15 | #include <mutex> |
| 16 | #include <thread> | 16 | #include <thread> |
| 17 | #include "bip32.h" | ||
| 17 | #include "btfaddr.h" | 18 | #include "btfaddr.h" |
| 18 | #include "btftunnel.h" | 19 | #include "btftunnel.h" |
| 19 | #ifndef _WIN32 | 20 | #ifndef _WIN32 |
| @@ -178,6 +179,66 @@ vector<unsigned char> GenerateNewKey() | |||
| 178 | CCriticalSection cs_keyPool; | 179 | CCriticalSection cs_keyPool; |
| 179 | map<int64, vector<unsigned char> > mapKeyPool; | 180 | map<int64, vector<unsigned char> > mapKeyPool; |
| 180 | 181 | ||
| 182 | vector<unsigned char> vchHDMaster; | ||
| 183 | vector<unsigned char> vchHDChainCode; | ||
| 184 | unsigned int nHDNext = 0; | ||
| 185 | |||
| 186 | bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet) | ||
| 187 | { | ||
| 188 | if (!HaveHDSeed()) | ||
| 189 | { | ||
| 190 | strErrorRet = "no seed"; | ||
| 191 | return false; | ||
| 192 | } | ||
| 193 | |||
| 194 | bitflash::BIP32PrivateNode parent; | ||
| 195 | parent.privateKey = vchHDMaster; | ||
| 196 | parent.chainCode = vchHDChainCode; | ||
| 197 | |||
| 198 | bitflash::BIP32PrivateNode child; | ||
| 199 | if (!bitflash::BIP32DeriveHardenedChild(parent, nIndex, child, strErrorRet)) | ||
| 200 | return false; | ||
| 201 | |||
| 202 | if (!keyRet.SetSecret(child.privateKey)) | ||
| 203 | { | ||
| 204 | strErrorRet = "derived scalar is not a usable key"; | ||
| 205 | return false; | ||
| 206 | } | ||
| 207 | return true; | ||
| 208 | } | ||
| 209 | |||
| 210 | bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) | ||
| 211 | { | ||
| 212 | string strNormalized; | ||
| 213 | if (!bitflash::BIP39ValidateMnemonic(strMnemonic, strNormalized, strErrorRet)) | ||
| 214 | return false; | ||
| 215 | |||
| 216 | vector<unsigned char> vchSeed; | ||
| 217 | if (!bitflash::BIP39MnemonicToSeed(strNormalized, "", vchSeed, strErrorRet)) | ||
| 218 | return false; | ||
| 219 | |||
| 220 | bitflash::BIP32PrivateNode master; | ||
| 221 | if (!bitflash::BIP32MasterFromSeed(vchSeed, master, strErrorRet)) | ||
| 222 | return false; | ||
| 223 | |||
| 224 | CRITICAL_BLOCK(cs_keyPool) | ||
| 225 | { | ||
| 226 | // Written before the globals change, so a failed write leaves the | ||
| 227 | // wallet on the seed it already had rather than on one that exists | ||
| 228 | // only in memory. | ||
| 229 | if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || | ||
| 230 | !CWalletDB().WriteHDNext(0)) | ||
| 231 | { | ||
| 232 | strErrorRet = "could not write the seed to wallet.dat"; | ||
| 233 | return false; | ||
| 234 | } | ||
| 235 | vchHDMaster = master.privateKey; | ||
| 236 | vchHDChainCode = master.chainCode; | ||
| 237 | nHDNext = 0; | ||
| 238 | } | ||
| 239 | return true; | ||
| 240 | } | ||
| 241 | |||
| 181 | void TopUpKeyPool() | 242 | void TopUpKeyPool() |
| 182 | { | 243 | { |
| 183 | CRITICAL_BLOCK(cs_keyPool) | 244 | CRITICAL_BLOCK(cs_keyPool) |
| @@ -191,7 +252,30 @@ void TopUpKeyPool() | |||
| 191 | nIndex = (--mapKeyPool.end())->first + 1; | 252 | nIndex = (--mapKeyPool.end())->first + 1; |
| 192 | 253 | ||
| 193 | CKey key; | 254 | CKey key; |
| 194 | key.MakeNewKey(); | 255 | if (HaveHDSeed()) |
| 256 | { | ||
| 257 | // Derived, so a recovery phrase can reproduce this exact key. | ||
| 258 | // A derivation that fails is not a reason to stop filling the | ||
| 259 | // pool -- fall back to a random key, which is what a wallet | ||
| 260 | // without a seed uses anyway. It will not come back from the | ||
| 261 | // phrase, and that is better than a node that cannot mine. | ||
| 262 | string strError; | ||
| 263 | if (DeriveHDKey(nHDNext, key, strError)) | ||
| 264 | { | ||
| 265 | nHDNext++; | ||
| 266 | CWalletDB().WriteHDNext(nHDNext); | ||
| 267 | } | ||
| 268 | else | ||
| 269 | { | ||
| 270 | printf("TopUpKeyPool() : derivation at index %u failed (%s), " | ||
| 271 | "falling back to a random key\n", nHDNext, strError.c_str()); | ||
| 272 | key.MakeNewKey(); | ||
| 273 | } | ||
| 274 | } | ||
| 275 | else | ||
| 276 | { | ||
| 277 | key.MakeNewKey(); | ||
| 278 | } | ||
| 195 | // AddKey writes the private key. Order matters: the key is in | 279 | // AddKey writes the private key. Order matters: the key is in |
| 196 | // wallet.dat before anything can point at it, so a crash between | 280 | // wallet.dat before anything can point at it, so a crash between |
| 197 | // the two writes costs an unused key, never a usable one. | 281 | // the two writes costs an unused key, never a usable one. |
| @@ -310,6 +394,32 @@ bool AddToWalletIfMine(const CTransaction& tx, const CBlock* pblock) | |||
| 310 | return true; | 394 | return true; |
| 311 | } | 395 | } |
| 312 | 396 | ||
| 397 | bool CanScanWalletTransactions(string& strErrorRet) | ||
| 398 | { | ||
| 399 | strErrorRet.clear(); | ||
| 400 | |||
| 401 | if (pindexGenesisBlock == NULL || pindexBest == NULL || hashBestChain == 0 || nBestHeight < 0) | ||
| 402 | { | ||
| 403 | strErrorRet = "The block index is not loaded, so the wallet scan cannot " | ||
| 404 | "prove whether imported or restored keys own coins."; | ||
| 405 | return false; | ||
| 406 | } | ||
| 407 | |||
| 408 | if (nBestHeight == 0) | ||
| 409 | { | ||
| 410 | strErrorRet = "The local chain is only at height 0. A wallet scan would " | ||
| 411 | "inspect only the genesis block and could incorrectly " | ||
| 412 | "report zero transactions. Start the node and let it sync " | ||
| 413 | "before scanning. If this happened after copying a data " | ||
| 414 | "directory, keep wallet.dat safe and rebuild or re-download " | ||
| 415 | "the block data instead of trusting the copied database/ " | ||
| 416 | "environment."; | ||
| 417 | return false; | ||
| 418 | } | ||
| 419 | |||
| 420 | return true; | ||
| 421 | } | ||
| 422 | |||
| 313 | int ScanForWalletTransactions(CBlockIndex* pindexStart) | 423 | int ScanForWalletTransactions(CBlockIndex* pindexStart) |
| 314 | { | 424 | { |
| 315 | // Walk the chain looking for transactions that belong to keys this wallet | 425 | // Walk the chain looking for transactions that belong to keys this wallet |