Skip to content
Closed
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
109 changes: 93 additions & 16 deletions src/bip32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,43 @@ static bool HmacSha512(const unsigned char* key, int keyLen,
len == 64;
}

static bool PrivateKeyToCompressedPubKey(const std::vector<unsigned char>& 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<unsigned char>& entropy,
std::string& mnemonicOut,
std::string& errorOut)
Expand Down Expand Up @@ -284,10 +321,10 @@ bool BIP32MasterFromSeed(const std::vector<unsigned char>& 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();
Expand All @@ -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))
Expand Down Expand Up @@ -363,4 +401,43 @@ bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent,
return true;
}

bool BIP32DerivePath(const BIP32PrivateNode& root,
const std::vector<unsigned int>& 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
14 changes: 13 additions & 1 deletion src/bip32.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,6 +16,8 @@
namespace bitflash
{

static const unsigned int BIP32_HARDENED = 0x80000000U;

struct BIP32PrivateNode
{
std::vector<unsigned char> privateKey; // 32-byte scalar
Expand Down Expand Up @@ -43,6 +45,16 @@ bool BIP32MasterFromSeed(const std::vector<unsigned char>& 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<unsigned int>& path,
BIP32PrivateNode& nodeOut,
std::string& errorOut);

bool BIP32DeriveHardenedChild(const BIP32PrivateNode& parent,
unsigned int childIndex,
BIP32PrivateNode& childOut,
Expand Down
36 changes: 36 additions & 0 deletions src/db.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,18 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& 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;
Expand Down Expand Up @@ -707,6 +719,30 @@ bool CWalletDB::LoadWallet(vector<unsigned char>& 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
Expand Down
21 changes: 19 additions & 2 deletions src/db.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<unsigned char>& vchMasterRet, vector<unsigned char>& vchChainCodeRet)
{
vchMasterRet.clear();
Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 23 additions & 2 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,18 @@ map<int64, vector<unsigned char> > mapKeyPool;
vector<unsigned char> vchHDMaster;
vector<unsigned char> 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)
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
25 changes: 21 additions & 4 deletions src/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,14 @@ extern map<int64, vector<unsigned char> > 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
Expand All @@ -161,7 +165,14 @@ extern map<int64, vector<unsigned char> > mapKeyPool;
extern vector<unsigned char> vchHDMaster; // 32-byte master private key (IL)
extern vector<unsigned char> 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
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Loading