diff --git a/src/bip32.cpp b/src/bip32.cpp index 6423974..6ba946a 100644 --- a/src/bip32.cpp +++ b/src/bip32.cpp @@ -114,6 +114,43 @@ static bool HmacSha512(const unsigned char* key, int keyLen, len == 64; } +static bool PrivateKeyToCompressedPubKey(const std::vector& privateKey, + unsigned char out[33], + std::string& errorOut) +{ + if (!IsValidPrivateKey(privateKey)) + { + errorOut = "private key is invalid"; + return false; + } + + BIGNUM* bn = BN_bin2bn(&privateKey[0], 32, NULL); + BN_CTX* ctx = BN_CTX_new(); + EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_secp256k1); + EC_POINT* pub = group ? EC_POINT_new(group) : NULL; + + bool ok = false; + if (bn && ctx && group && pub && + EC_POINT_mul(group, pub, bn, NULL, NULL, ctx)) + { + size_t n = EC_POINT_point2oct(group, pub, POINT_CONVERSION_COMPRESSED, + out, 33, ctx); + ok = n == 33; + } + + BN_free(bn); + BN_CTX_free(ctx); + EC_POINT_free(pub); + EC_GROUP_free(group); + + if (!ok) + { + errorOut = "could not serialize parent public key"; + return false; + } + return true; +} + bool BIP39EntropyToMnemonic(const std::vector& entropy, std::string& mnemonicOut, std::string& errorOut) @@ -284,10 +321,10 @@ bool BIP32MasterFromSeed(const std::vector& seed, return true; } -bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, - unsigned int childIndex, - BIP32PrivateNode& childOut, - std::string& errorOut) +bool BIP32DeriveChild(const BIP32PrivateNode& parent, + unsigned int childNumber, + BIP32PrivateNode& childOut, + std::string& errorOut) { childOut.privateKey.clear(); childOut.chainCode.clear(); @@ -297,20 +334,21 @@ bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, errorOut = "parent private key or chain code is invalid"; return false; } - if (childIndex >= 0x80000000U) + unsigned char data[37]; + if (childNumber & BIP32_HARDENED) { - errorOut = "child index must be non-hardened; hardening is applied here"; - return false; + data[0] = 0x00; + memcpy(data + 1, &parent.privateKey[0], 32); } - - const unsigned int hardened = childIndex | 0x80000000U; - unsigned char data[37]; - data[0] = 0x00; - memcpy(data + 1, &parent.privateKey[0], 32); - data[33] = (unsigned char)((hardened >> 24) & 0xff); - data[34] = (unsigned char)((hardened >> 16) & 0xff); - data[35] = (unsigned char)((hardened >> 8) & 0xff); - data[36] = (unsigned char)(hardened & 0xff); + else + { + if (!PrivateKeyToCompressedPubKey(parent.privateKey, data, errorOut)) + return false; + } + data[33] = (unsigned char)((childNumber >> 24) & 0xff); + data[34] = (unsigned char)((childNumber >> 16) & 0xff); + data[35] = (unsigned char)((childNumber >> 8) & 0xff); + data[36] = (unsigned char)(childNumber & 0xff); unsigned char I[64]; if (!HmacSha512(&parent.chainCode[0], 32, data, sizeof(data), I)) @@ -363,4 +401,43 @@ bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, return true; } +bool BIP32DerivePath(const BIP32PrivateNode& root, + const std::vector& path, + BIP32PrivateNode& nodeOut, + std::string& errorOut) +{ + nodeOut.privateKey.clear(); + nodeOut.chainCode.clear(); + errorOut.clear(); + if (!IsValidPrivateKey(root.privateKey) || root.chainCode.size() != 32) + { + errorOut = "root private key or chain code is invalid"; + return false; + } + + BIP32PrivateNode cur = root; + for (size_t i = 0; i < path.size(); i++) + { + BIP32PrivateNode next; + if (!BIP32DeriveChild(cur, path[i], next, errorOut)) + return false; + cur = next; + } + nodeOut = cur; + return true; +} + +bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, + unsigned int childIndex, + BIP32PrivateNode& childOut, + std::string& errorOut) +{ + if (childIndex >= BIP32_HARDENED) + { + errorOut = "child index must be non-hardened; hardening is applied here"; + return false; + } + return BIP32DeriveChild(parent, childIndex | BIP32_HARDENED, childOut, errorOut); +} + } // namespace bitflash diff --git a/src/bip32.h b/src/bip32.h index a029444..e9862d5 100644 --- a/src/bip32.h +++ b/src/bip32.h @@ -5,7 +5,7 @@ // // This module is deliberately independent from wallet.dat and GUI code. It is // the auditable foundation: mnemonic generation/validation, seed derivation, -// and hardened private child derivation over secp256k1. +// and private child derivation over secp256k1. #ifndef BITFLASH_BIP32_H #define BITFLASH_BIP32_H @@ -16,6 +16,8 @@ namespace bitflash { +static const unsigned int BIP32_HARDENED = 0x80000000U; + struct BIP32PrivateNode { std::vector privateKey; // 32-byte scalar @@ -43,6 +45,16 @@ bool BIP32MasterFromSeed(const std::vector& seed, BIP32PrivateNode& nodeOut, std::string& errorOut); +bool BIP32DeriveChild(const BIP32PrivateNode& parent, + unsigned int childNumber, + BIP32PrivateNode& childOut, + std::string& errorOut); + +bool BIP32DerivePath(const BIP32PrivateNode& root, + const std::vector& path, + BIP32PrivateNode& nodeOut, + std::string& errorOut); + bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent, unsigned int childIndex, BIP32PrivateNode& childOut, diff --git a/src/db.cpp b/src/db.cpp index 294920b..f4cd806 100644 --- a/src/db.cpp +++ b/src/db.cpp @@ -651,6 +651,18 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) { ssValue >> nHDNext; } + else if (strType == "hdschema") + { + ssValue >> nHDKeySchema; + } + else if (strType == "hdreceivenext") + { + ssValue >> nHDReceiveNext; + } + else if (strType == "hdchangenext") + { + ssValue >> nHDChangeNext; + } else if (strType == "pool") { int64 nIndex; @@ -707,6 +719,30 @@ bool CWalletDB::LoadWallet(vector& vchDefaultKeyRet) return false; } + if (HaveHDSeed()) + { + if (nHDKeySchema == HD_SCHEMA_NONE) + { + nHDKeySchema = HD_SCHEMA_LEGACY; + printf("LoadWallet: deterministic seed has no schema metadata; " + "treating it as legacy m/index'\n"); + } + else if (nHDKeySchema != HD_SCHEMA_LEGACY) + { + printf("LoadWallet: deterministic wallet schema %d (%s) is not " + "supported by this build\n", + nHDKeySchema, HDKeySchemaName(nHDKeySchema).c_str()); + return false; + } + } + else + { + nHDKeySchema = HD_SCHEMA_NONE; + nHDNext = 0; + nHDReceiveNext = 0; + nHDChangeNext = 0; + } + // fGenerateBitcoins and nMineMode only mean anything together, and a // wallet.dat can easily hold one without the other: Bitcoin 0.1.0 already // wrote fGenerateBitcoins when mining was toggled, years before this fork diff --git a/src/db.h b/src/db.h index 01b8583..8a41828 100644 --- a/src/db.h +++ b/src/db.h @@ -392,8 +392,10 @@ class CWalletDB : public CDB // twelve words are the user's to write down, and keeping a copy of them in // the file they are meant to protect defeats the point of having them. // - // "hdnext" is the next child index to derive, so the sequence a restore - // reproduces is the same one this wallet handed out. + // "hdschema" identifies the derivation layout. Wallets created before the + // field existed are inferred as HD_SCHEMA_LEGACY when a seed is present. + // "hdnext" is that legacy layout's next child index. The receive/change + // counters are reserved for the BIP44 layout that uses separate chains. bool ReadHDMaster(vector& vchMasterRet, vector& vchChainCodeRet) { vchMasterRet.clear(); @@ -413,6 +415,21 @@ class CWalletDB : public CDB return Write(string("hdnext"), nNext); } + bool WriteHDSchema(int nSchema) + { + return Write(string("hdschema"), nSchema); + } + + bool WriteHDReceiveNext(unsigned int nNext) + { + return Write(string("hdreceivenext"), nNext); + } + + bool WriteHDChangeNext(unsigned int nNext) + { + return Write(string("hdchangenext"), nNext); + } + // Keys generated ahead of time and not yet handed out. The private key of // each is already stored under its own "key" record by AddKey; a "pool" // record only says that this one is still unspoken for. diff --git a/src/gui.cpp b/src/gui.cpp index 2335bf9..b1c122c 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1247,6 +1247,8 @@ static void DrawWalletSafetyDialog() ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "No recovery phrase. Only a file backup can rebuild this wallet."); + if (g_recoveryAudit.fHaveSeed) + ImGui::Text("Derivation schema: %s", HDKeySchemaName(g_recoveryAudit.nSchema).c_str()); ImGui::Text("Phrase-backed spendable balance: %s BTF", FmtMoney(g_recoveryAudit.nRecoverableCredit).c_str()); ImGui::Text("Wallet.dat-only spendable balance: %s BTF", diff --git a/src/main.cpp b/src/main.cpp index 75647ba..62812d4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -229,6 +229,18 @@ map > mapKeyPool; vector vchHDMaster; vector vchHDChainCode; unsigned int nHDNext = 0; +int nHDKeySchema = HD_SCHEMA_NONE; +unsigned int nHDReceiveNext = 0; +unsigned int nHDChangeNext = 0; + +string HDKeySchemaName(int nSchema) +{ + if (nSchema == HD_SCHEMA_LEGACY) + return "legacy-hd"; + if (nSchema == HD_SCHEMA_BIP44) + return "bip44"; + return "none"; +} static bool ClearKeyPoolRecords(string& strErrorRet) { @@ -307,13 +319,19 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) return false; if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || - !CWalletDB().WriteHDNext(1)) + !CWalletDB().WriteHDSchema(HD_SCHEMA_LEGACY) || + !CWalletDB().WriteHDNext(1) || + !CWalletDB().WriteHDReceiveNext(0) || + !CWalletDB().WriteHDChangeNext(0)) { - strErrorRet = "could not write the seed to wallet.dat"; + strErrorRet = "could not write the seed metadata to wallet.dat"; return false; } vchHDMaster = master.privateKey; vchHDChainCode = master.chainCode; + nHDKeySchema = HD_SCHEMA_LEGACY; + nHDReceiveNext = 0; + nHDChangeNext = 0; // Index 0 is spoken for before it is derived, so a failure below cannot // leave the pool free to hand it out as an ordinary key. The cost of @@ -3668,7 +3686,10 @@ WalletRecoveryAudit GetWalletRecoveryAudit() CRITICAL_BLOCK(cs_keyPool) { audit.fHaveSeed = HaveHDSeed(); + audit.nSchema = nHDKeySchema; audit.nDerivedKnown = nHDNext; + audit.nReceiveNext = nHDReceiveNext; + audit.nChangeNext = nHDChangeNext; if (audit.fHaveSeed) { string strError; diff --git a/src/main.h b/src/main.h index 4886f55..04742f6 100644 --- a/src/main.h +++ b/src/main.h @@ -147,10 +147,14 @@ extern map > mapKeyPool; // --- Deterministic wallet (BIP32) ----------------------------------------- // -// When a seed is present the key pool is derived from it -- m/0'/n, hardened, -// n increasing -- instead of being made of random keys. That is what lets a -// recovery phrase bring a wallet back: the same twelve words reproduce the same -// keys in the same order. +// When a seed is present today, the key pool is derived from it by the legacy +// Bitflash path -- m/index', hardened, index increasing -- instead of being +// made of random keys. That is what lets a recovery phrase bring a wallet back: +// the same twelve words reproduce the same keys in the same order. +// +// BIP44 needs a different path family and separate receive/change counters. +// The schema fields below let future wallets opt into that without making old +// m/index' coins disappear. // // A wallet without a seed keeps working exactly as before. Nothing here // migrates an existing wallet on its own: the seed is created only when the @@ -161,7 +165,14 @@ extern map > mapKeyPool; extern vector vchHDMaster; // 32-byte master private key (IL) extern vector vchHDChainCode; // 32 bytes (IR) extern unsigned int nHDNext; // next child index to derive +static const int HD_SCHEMA_NONE = 0; +static const int HD_SCHEMA_LEGACY = 1; // m/index' +static const int HD_SCHEMA_BIP44 = 2; // m/44'/coin_type'/account'/change/index +extern int nHDKeySchema; +extern unsigned int nHDReceiveNext; // future BIP44 external chain +extern unsigned int nHDChangeNext; // future BIP44 internal chain inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.size() == 32; } +string HDKeySchemaName(int nSchema); // Install a seed derived from a mnemonic, replacing any existing one. The // default receiving key is derived immediately, so the next visible address is @@ -221,7 +232,10 @@ struct WalletRecoveryAudit { bool fHaveSeed; bool fDeriveComplete; + int nSchema; unsigned int nDerivedKnown; + unsigned int nReceiveNext; + unsigned int nChangeNext; int nRecoverableTx; int nLegacyTx; int nRecoverableImmatureTx; @@ -236,7 +250,10 @@ struct WalletRecoveryAudit { fHaveSeed = false; fDeriveComplete = true; + nSchema = HD_SCHEMA_NONE; nDerivedKnown = 0; + nReceiveNext = 0; + nChangeNext = 0; nRecoverableTx = 0; nLegacyTx = 0; nRecoverableImmatureTx = 0; diff --git a/src/selftest.cpp b/src/selftest.cpp index df55bcd..973a920 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -342,6 +342,10 @@ static int RunWalletHDSelfTest() nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), "a valid phrase installs a seed") ? 0 : 1; nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; + nFail += Check(nHDKeySchema == HD_SCHEMA_LEGACY, + "a new recovery phrase records the legacy HD schema") ? 0 : 1; + nFail += Check(nHDReceiveNext == 0 && nHDChangeNext == 0, + "unused BIP44 receive/change counters start at zero") ? 0 : 1; int nPoolAfterSeed = 0; CRITICAL_BLOCK(cs_keyPool) @@ -370,10 +374,13 @@ static int RunWalletHDSelfTest() "the pre-seed address is kept, named apart from the new one") ? 0 : 1; std::vector vchBefore = vchHDMaster; + int nSchemaBefore = nHDKeySchema; nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), "an invalid phrase is refused") ? 0 : 1; nFail += Check(vchHDMaster == vchBefore, "a refused phrase leaves the existing seed alone") ? 0 : 1; + nFail += Check(nHDKeySchema == nSchemaBefore, + "a refused phrase leaves the derivation schema alone") ? 0 : 1; // The property the whole feature exists for: the same words give back // the same keys, in the same order. @@ -475,6 +482,10 @@ static int RunWalletHDSelfTest() WalletRecoveryAudit audit = GetWalletRecoveryAudit(); nFail += Check(audit.fHaveSeed, "the recovery audit reports the phrase") ? 0 : 1; + nFail += Check(audit.nSchema == HD_SCHEMA_LEGACY, + "the recovery audit reports the derivation schema") ? 0 : 1; + nFail += Check(audit.nReceiveNext == 0 && audit.nChangeNext == 0, + "the recovery audit reports receive/change counters") ? 0 : 1; nFail += Check(audit.nLegacyCredit == 5 * COIN, "the recovery audit finds wallet.dat-only balance") ? 0 : 1; nFail += Check(audit.nRecoverableCredit == 7 * COIN, diff --git a/src/test_bip32.cpp b/src/test_bip32.cpp index 610a651..6c11510 100644 --- a/src/test_bip32.cpp +++ b/src/test_bip32.cpp @@ -5,6 +5,8 @@ #include "bip32.h" +#include + #include #include #include @@ -57,6 +59,76 @@ static std::string ToHex(const std::vector& v) return out; } +static bool DecodeBase58Check(const std::string& str, std::vector& out) +{ + static const char* pszBase58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + out.clear(); + + std::vector b256(str.size() * 733 / 1000 + 1); + for (char c : str) + { + const char* p = strchr(pszBase58, c); + if (!p) + return false; + int carry = (int)(p - pszBase58); + for (std::vector::reverse_iterator it = b256.rbegin(); + it != b256.rend(); ++it) + { + carry += 58 * (*it); + *it = (unsigned char)(carry & 0xff); + carry >>= 8; + } + if (carry != 0) + return false; + } + + size_t zeros = 0; + while (zeros < str.size() && str[zeros] == '1') + zeros++; + std::vector::iterator it = b256.begin(); + while (it != b256.end() && *it == 0) + ++it; + + std::vector full; + full.assign(zeros, 0); + while (it != b256.end()) + full.push_back(*it++); + if (full.size() < 4) + return false; + + std::vector payload(full.begin(), full.end() - 4); + unsigned char h1[SHA256_DIGEST_LENGTH]; + unsigned char h2[SHA256_DIGEST_LENGTH]; + SHA256(&payload[0], payload.size(), h1); + SHA256(h1, sizeof(h1), h2); + if (memcmp(h2, &full[full.size() - 4], 4) != 0) + return false; + + out = payload; + return true; +} + +static bool DecodeXPrvNode(const std::string& xprv, BIP32PrivateNode& nodeOut) +{ + nodeOut.privateKey.clear(); + nodeOut.chainCode.clear(); + + std::vector payload; + if (!DecodeBase58Check(xprv, payload)) + return false; + if (payload.size() != 78) + return false; + if (payload[0] != 0x04 || payload[1] != 0x88 || + payload[2] != 0xad || payload[3] != 0xe4) + return false; + if (payload[45] != 0x00) + return false; + + nodeOut.chainCode.assign(payload.begin() + 13, payload.begin() + 45); + nodeOut.privateKey.assign(payload.begin() + 46, payload.end()); + return true; +} + int main() { std::string err; @@ -124,6 +196,53 @@ int main() "pre-hardened index rejected"); } + printf("bip32_paths\n"); + { + std::vector seed = FromHex("000102030405060708090a0b0c0d0e0f"); + BIP32PrivateNode master; + CHECK(BIP32MasterFromSeed(seed, master, err), "path test master derives"); + + struct PathVector + { + const char* name; + std::vector path; + const char* xprv; + }; + + std::vector vectors; + vectors.push_back({"m", + {}, + "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"}); + vectors.push_back({"m/0'", + {0 | BIP32_HARDENED}, + "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7"}); + vectors.push_back({"m/0'/1", + {0 | BIP32_HARDENED, 1}, + "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"}); + vectors.push_back({"m/0'/1/2'", + {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED}, + "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM"}); + vectors.push_back({"m/0'/1/2'/2", + {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED, 2}, + "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"}); + vectors.push_back({"m/0'/1/2'/2/1000000000", + {0 | BIP32_HARDENED, 1, 2 | BIP32_HARDENED, 2, 1000000000}, + "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76"}); + + for (const PathVector& v : vectors) + { + BIP32PrivateNode actual; + BIP32PrivateNode expected; + std::string label = std::string("derives ") + v.name; + CHECK(BIP32DerivePath(master, v.path, actual, err), label.c_str()); + label = std::string("decodes vector ") + v.name; + CHECK(DecodeXPrvNode(v.xprv, expected), label.c_str()); + label = std::string("matches vector ") + v.name; + CHECK(actual.privateKey == expected.privateKey && + actual.chainCode == expected.chainCode, label.c_str()); + } + } + printf("\n%s (%d failures)\n", g_fail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", g_fail); return g_fail == 0 ? 0 : 1; diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp index 41b4ed2..1fad3da 100644 --- a/src/walletcmd.cpp +++ b/src/walletcmd.cpp @@ -283,7 +283,12 @@ int CmdRecoveryAudit() printf("Wallet recovery audit\n"); printf(" recovery phrase: %s\n", audit.fHaveSeed ? "present" : "not installed"); if (audit.fHaveSeed) + { + printf(" derivation schema: %s\n", HDKeySchemaName(audit.nSchema).c_str()); printf(" derived keys known to this wallet: %u\n", audit.nDerivedKnown); + printf(" receive/change counters: %u/%u\n", + audit.nReceiveNext, audit.nChangeNext); + } if (!audit.fDeriveComplete) printf(" derivation warning: %s\n", audit.strDeriveError.c_str()); printf(" total spendable balance: %s BTF\n", FormatMoney(nTotal).c_str());