wallet(HD): native BIP39/BIP32 HD wallet - daemon side

Adds deterministic HD key derivation (path m/44'/2222'/0'/0/i, matching the TRIdock web wallet) wired into CWallet: HD seed stored in wallet.dat (encrypted with the wallet master key when the wallet is encrypted), keypool derived from the seed, and new RPC commands hdnew/hdrestore/hdshow/hdinfo. Crypto core verified standalone against the official BIP39 vector and triWallet.js addresses.
This commit is contained in:
Sami
2026-06-15 14:24:24 -07:00
committed by Sami Ahmed
parent 5511cfae6b
commit 11ed086d1e
11 changed files with 764 additions and 3 deletions
+1
View File
@@ -42,6 +42,7 @@ set(CORE_SOURCES
bootstrap.cpp
checkpoints.cpp
crypter.cpp
hdwallet.cpp
crypto_ecdh.cpp
crypto_ecdsa.cpp
db.cpp
+263
View File
@@ -0,0 +1,263 @@
// BIP39 English wordlist (2048 words, canonical). Auto-generated; do not edit.
#ifndef TRIANGLES_BIP39_ENGLISH_H
#define TRIANGLES_BIP39_ENGLISH_H
static const char* const BIP39_WORDLIST_EN[2048] = {
"abandon","ability","able","about","above","absent","absorb","abstract",
"absurd","abuse","access","accident","account","accuse","achieve","acid",
"acoustic","acquire","across","act","action","actor","actress","actual",
"adapt","add","addict","address","adjust","admit","adult","advance",
"advice","aerobic","affair","afford","afraid","again","age","agent",
"agree","ahead","aim","air","airport","aisle","alarm","album",
"alcohol","alert","alien","all","alley","allow","almost","alone",
"alpha","already","also","alter","always","amateur","amazing","among",
"amount","amused","analyst","anchor","ancient","anger","angle","angry",
"animal","ankle","announce","annual","another","answer","antenna","antique",
"anxiety","any","apart","apology","appear","apple","approve","april",
"arch","arctic","area","arena","argue","arm","armed","armor",
"army","around","arrange","arrest","arrive","arrow","art","artefact",
"artist","artwork","ask","aspect","assault","asset","assist","assume",
"asthma","athlete","atom","attack","attend","attitude","attract","auction",
"audit","august","aunt","author","auto","autumn","average","avocado",
"avoid","awake","aware","away","awesome","awful","awkward","axis",
"baby","bachelor","bacon","badge","bag","balance","balcony","ball",
"bamboo","banana","banner","bar","barely","bargain","barrel","base",
"basic","basket","battle","beach","bean","beauty","because","become",
"beef","before","begin","behave","behind","believe","below","belt",
"bench","benefit","best","betray","better","between","beyond","bicycle",
"bid","bike","bind","biology","bird","birth","bitter","black",
"blade","blame","blanket","blast","bleak","bless","blind","blood",
"blossom","blouse","blue","blur","blush","board","boat","body",
"boil","bomb","bone","bonus","book","boost","border","boring",
"borrow","boss","bottom","bounce","box","boy","bracket","brain",
"brand","brass","brave","bread","breeze","brick","bridge","brief",
"bright","bring","brisk","broccoli","broken","bronze","broom","brother",
"brown","brush","bubble","buddy","budget","buffalo","build","bulb",
"bulk","bullet","bundle","bunker","burden","burger","burst","bus",
"business","busy","butter","buyer","buzz","cabbage","cabin","cable",
"cactus","cage","cake","call","calm","camera","camp","can",
"canal","cancel","candy","cannon","canoe","canvas","canyon","capable",
"capital","captain","car","carbon","card","cargo","carpet","carry",
"cart","case","cash","casino","castle","casual","cat","catalog",
"catch","category","cattle","caught","cause","caution","cave","ceiling",
"celery","cement","census","century","cereal","certain","chair","chalk",
"champion","change","chaos","chapter","charge","chase","chat","cheap",
"check","cheese","chef","cherry","chest","chicken","chief","child",
"chimney","choice","choose","chronic","chuckle","chunk","churn","cigar",
"cinnamon","circle","citizen","city","civil","claim","clap","clarify",
"claw","clay","clean","clerk","clever","click","client","cliff",
"climb","clinic","clip","clock","clog","close","cloth","cloud",
"clown","club","clump","cluster","clutch","coach","coast","coconut",
"code","coffee","coil","coin","collect","color","column","combine",
"come","comfort","comic","common","company","concert","conduct","confirm",
"congress","connect","consider","control","convince","cook","cool","copper",
"copy","coral","core","corn","correct","cost","cotton","couch",
"country","couple","course","cousin","cover","coyote","crack","cradle",
"craft","cram","crane","crash","crater","crawl","crazy","cream",
"credit","creek","crew","cricket","crime","crisp","critic","crop",
"cross","crouch","crowd","crucial","cruel","cruise","crumble","crunch",
"crush","cry","crystal","cube","culture","cup","cupboard","curious",
"current","curtain","curve","cushion","custom","cute","cycle","dad",
"damage","damp","dance","danger","daring","dash","daughter","dawn",
"day","deal","debate","debris","decade","december","decide","decline",
"decorate","decrease","deer","defense","define","defy","degree","delay",
"deliver","demand","demise","denial","dentist","deny","depart","depend",
"deposit","depth","deputy","derive","describe","desert","design","desk",
"despair","destroy","detail","detect","develop","device","devote","diagram",
"dial","diamond","diary","dice","diesel","diet","differ","digital",
"dignity","dilemma","dinner","dinosaur","direct","dirt","disagree","discover",
"disease","dish","dismiss","disorder","display","distance","divert","divide",
"divorce","dizzy","doctor","document","dog","doll","dolphin","domain",
"donate","donkey","donor","door","dose","double","dove","draft",
"dragon","drama","drastic","draw","dream","dress","drift","drill",
"drink","drip","drive","drop","drum","dry","duck","dumb",
"dune","during","dust","dutch","duty","dwarf","dynamic","eager",
"eagle","early","earn","earth","easily","east","easy","echo",
"ecology","economy","edge","edit","educate","effort","egg","eight",
"either","elbow","elder","electric","elegant","element","elephant","elevator",
"elite","else","embark","embody","embrace","emerge","emotion","employ",
"empower","empty","enable","enact","end","endless","endorse","enemy",
"energy","enforce","engage","engine","enhance","enjoy","enlist","enough",
"enrich","enroll","ensure","enter","entire","entry","envelope","episode",
"equal","equip","era","erase","erode","erosion","error","erupt",
"escape","essay","essence","estate","eternal","ethics","evidence","evil",
"evoke","evolve","exact","example","excess","exchange","excite","exclude",
"excuse","execute","exercise","exhaust","exhibit","exile","exist","exit",
"exotic","expand","expect","expire","explain","expose","express","extend",
"extra","eye","eyebrow","fabric","face","faculty","fade","faint",
"faith","fall","false","fame","family","famous","fan","fancy",
"fantasy","farm","fashion","fat","fatal","father","fatigue","fault",
"favorite","feature","february","federal","fee","feed","feel","female",
"fence","festival","fetch","fever","few","fiber","fiction","field",
"figure","file","film","filter","final","find","fine","finger",
"finish","fire","firm","first","fiscal","fish","fit","fitness",
"fix","flag","flame","flash","flat","flavor","flee","flight",
"flip","float","flock","floor","flower","fluid","flush","fly",
"foam","focus","fog","foil","fold","follow","food","foot",
"force","forest","forget","fork","fortune","forum","forward","fossil",
"foster","found","fox","fragile","frame","frequent","fresh","friend",
"fringe","frog","front","frost","frown","frozen","fruit","fuel",
"fun","funny","furnace","fury","future","gadget","gain","galaxy",
"gallery","game","gap","garage","garbage","garden","garlic","garment",
"gas","gasp","gate","gather","gauge","gaze","general","genius",
"genre","gentle","genuine","gesture","ghost","giant","gift","giggle",
"ginger","giraffe","girl","give","glad","glance","glare","glass",
"glide","glimpse","globe","gloom","glory","glove","glow","glue",
"goat","goddess","gold","good","goose","gorilla","gospel","gossip",
"govern","gown","grab","grace","grain","grant","grape","grass",
"gravity","great","green","grid","grief","grit","grocery","group",
"grow","grunt","guard","guess","guide","guilt","guitar","gun",
"gym","habit","hair","half","hammer","hamster","hand","happy",
"harbor","hard","harsh","harvest","hat","have","hawk","hazard",
"head","health","heart","heavy","hedgehog","height","hello","helmet",
"help","hen","hero","hidden","high","hill","hint","hip",
"hire","history","hobby","hockey","hold","hole","holiday","hollow",
"home","honey","hood","hope","horn","horror","horse","hospital",
"host","hotel","hour","hover","hub","huge","human","humble",
"humor","hundred","hungry","hunt","hurdle","hurry","hurt","husband",
"hybrid","ice","icon","idea","identify","idle","ignore","ill",
"illegal","illness","image","imitate","immense","immune","impact","impose",
"improve","impulse","inch","include","income","increase","index","indicate",
"indoor","industry","infant","inflict","inform","inhale","inherit","initial",
"inject","injury","inmate","inner","innocent","input","inquiry","insane",
"insect","inside","inspire","install","intact","interest","into","invest",
"invite","involve","iron","island","isolate","issue","item","ivory",
"jacket","jaguar","jar","jazz","jealous","jeans","jelly","jewel",
"job","join","joke","journey","joy","judge","juice","jump",
"jungle","junior","junk","just","kangaroo","keen","keep","ketchup",
"key","kick","kid","kidney","kind","kingdom","kiss","kit",
"kitchen","kite","kitten","kiwi","knee","knife","knock","know",
"lab","label","labor","ladder","lady","lake","lamp","language",
"laptop","large","later","latin","laugh","laundry","lava","law",
"lawn","lawsuit","layer","lazy","leader","leaf","learn","leave",
"lecture","left","leg","legal","legend","leisure","lemon","lend",
"length","lens","leopard","lesson","letter","level","liar","liberty",
"library","license","life","lift","light","like","limb","limit",
"link","lion","liquid","list","little","live","lizard","load",
"loan","lobster","local","lock","logic","lonely","long","loop",
"lottery","loud","lounge","love","loyal","lucky","luggage","lumber",
"lunar","lunch","luxury","lyrics","machine","mad","magic","magnet",
"maid","mail","main","major","make","mammal","man","manage",
"mandate","mango","mansion","manual","maple","marble","march","margin",
"marine","market","marriage","mask","mass","master","match","material",
"math","matrix","matter","maximum","maze","meadow","mean","measure",
"meat","mechanic","medal","media","melody","melt","member","memory",
"mention","menu","mercy","merge","merit","merry","mesh","message",
"metal","method","middle","midnight","milk","million","mimic","mind",
"minimum","minor","minute","miracle","mirror","misery","miss","mistake",
"mix","mixed","mixture","mobile","model","modify","mom","moment",
"monitor","monkey","monster","month","moon","moral","more","morning",
"mosquito","mother","motion","motor","mountain","mouse","move","movie",
"much","muffin","mule","multiply","muscle","museum","mushroom","music",
"must","mutual","myself","mystery","myth","naive","name","napkin",
"narrow","nasty","nation","nature","near","neck","need","negative",
"neglect","neither","nephew","nerve","nest","net","network","neutral",
"never","news","next","nice","night","noble","noise","nominee",
"noodle","normal","north","nose","notable","note","nothing","notice",
"novel","now","nuclear","number","nurse","nut","oak","obey",
"object","oblige","obscure","observe","obtain","obvious","occur","ocean",
"october","odor","off","offer","office","often","oil","okay",
"old","olive","olympic","omit","once","one","onion","online",
"only","open","opera","opinion","oppose","option","orange","orbit",
"orchard","order","ordinary","organ","orient","original","orphan","ostrich",
"other","outdoor","outer","output","outside","oval","oven","over",
"own","owner","oxygen","oyster","ozone","pact","paddle","page",
"pair","palace","palm","panda","panel","panic","panther","paper",
"parade","parent","park","parrot","party","pass","patch","path",
"patient","patrol","pattern","pause","pave","payment","peace","peanut",
"pear","peasant","pelican","pen","penalty","pencil","people","pepper",
"perfect","permit","person","pet","phone","photo","phrase","physical",
"piano","picnic","picture","piece","pig","pigeon","pill","pilot",
"pink","pioneer","pipe","pistol","pitch","pizza","place","planet",
"plastic","plate","play","please","pledge","pluck","plug","plunge",
"poem","poet","point","polar","pole","police","pond","pony",
"pool","popular","portion","position","possible","post","potato","pottery",
"poverty","powder","power","practice","praise","predict","prefer","prepare",
"present","pretty","prevent","price","pride","primary","print","priority",
"prison","private","prize","problem","process","produce","profit","program",
"project","promote","proof","property","prosper","protect","proud","provide",
"public","pudding","pull","pulp","pulse","pumpkin","punch","pupil",
"puppy","purchase","purity","purpose","purse","push","put","puzzle",
"pyramid","quality","quantum","quarter","question","quick","quit","quiz",
"quote","rabbit","raccoon","race","rack","radar","radio","rail",
"rain","raise","rally","ramp","ranch","random","range","rapid",
"rare","rate","rather","raven","raw","razor","ready","real",
"reason","rebel","rebuild","recall","receive","recipe","record","recycle",
"reduce","reflect","reform","refuse","region","regret","regular","reject",
"relax","release","relief","rely","remain","remember","remind","remove",
"render","renew","rent","reopen","repair","repeat","replace","report",
"require","rescue","resemble","resist","resource","response","result","retire",
"retreat","return","reunion","reveal","review","reward","rhythm","rib",
"ribbon","rice","rich","ride","ridge","rifle","right","rigid",
"ring","riot","ripple","risk","ritual","rival","river","road",
"roast","robot","robust","rocket","romance","roof","rookie","room",
"rose","rotate","rough","round","route","royal","rubber","rude",
"rug","rule","run","runway","rural","sad","saddle","sadness",
"safe","sail","salad","salmon","salon","salt","salute","same",
"sample","sand","satisfy","satoshi","sauce","sausage","save","say",
"scale","scan","scare","scatter","scene","scheme","school","science",
"scissors","scorpion","scout","scrap","screen","script","scrub","sea",
"search","season","seat","second","secret","section","security","seed",
"seek","segment","select","sell","seminar","senior","sense","sentence",
"series","service","session","settle","setup","seven","shadow","shaft",
"shallow","share","shed","shell","sheriff","shield","shift","shine",
"ship","shiver","shock","shoe","shoot","shop","short","shoulder",
"shove","shrimp","shrug","shuffle","shy","sibling","sick","side",
"siege","sight","sign","silent","silk","silly","silver","similar",
"simple","since","sing","siren","sister","situate","six","size",
"skate","sketch","ski","skill","skin","skirt","skull","slab",
"slam","sleep","slender","slice","slide","slight","slim","slogan",
"slot","slow","slush","small","smart","smile","smoke","smooth",
"snack","snake","snap","sniff","snow","soap","soccer","social",
"sock","soda","soft","solar","soldier","solid","solution","solve",
"someone","song","soon","sorry","sort","soul","sound","soup",
"source","south","space","spare","spatial","spawn","speak","special",
"speed","spell","spend","sphere","spice","spider","spike","spin",
"spirit","split","spoil","sponsor","spoon","sport","spot","spray",
"spread","spring","spy","square","squeeze","squirrel","stable","stadium",
"staff","stage","stairs","stamp","stand","start","state","stay",
"steak","steel","stem","step","stereo","stick","still","sting",
"stock","stomach","stone","stool","story","stove","strategy","street",
"strike","strong","struggle","student","stuff","stumble","style","subject",
"submit","subway","success","such","sudden","suffer","sugar","suggest",
"suit","summer","sun","sunny","sunset","super","supply","supreme",
"sure","surface","surge","surprise","surround","survey","suspect","sustain",
"swallow","swamp","swap","swarm","swear","sweet","swift","swim",
"swing","switch","sword","symbol","symptom","syrup","system","table",
"tackle","tag","tail","talent","talk","tank","tape","target",
"task","taste","tattoo","taxi","teach","team","tell","ten",
"tenant","tennis","tent","term","test","text","thank","that",
"theme","then","theory","there","they","thing","this","thought",
"three","thrive","throw","thumb","thunder","ticket","tide","tiger",
"tilt","timber","time","tiny","tip","tired","tissue","title",
"toast","tobacco","today","toddler","toe","together","toilet","token",
"tomato","tomorrow","tone","tongue","tonight","tool","tooth","top",
"topic","topple","torch","tornado","tortoise","toss","total","tourist",
"toward","tower","town","toy","track","trade","traffic","tragic",
"train","transfer","trap","trash","travel","tray","treat","tree",
"trend","trial","tribe","trick","trigger","trim","trip","trophy",
"trouble","truck","true","truly","trumpet","trust","truth","try",
"tube","tuition","tumble","tuna","tunnel","turkey","turn","turtle",
"twelve","twenty","twice","twin","twist","two","type","typical",
"ugly","umbrella","unable","unaware","uncle","uncover","under","undo",
"unfair","unfold","unhappy","uniform","unique","unit","universe","unknown",
"unlock","until","unusual","unveil","update","upgrade","uphold","upon",
"upper","upset","urban","urge","usage","use","used","useful",
"useless","usual","utility","vacant","vacuum","vague","valid","valley",
"valve","van","vanish","vapor","various","vast","vault","vehicle",
"velvet","vendor","venture","venue","verb","verify","version","very",
"vessel","veteran","viable","vibrant","vicious","victory","video","view",
"village","vintage","violin","virtual","virus","visa","visit","visual",
"vital","vivid","vocal","voice","void","volcano","volume","vote",
"voyage","wage","wagon","wait","walk","wall","walnut","want",
"warfare","warm","warrior","wash","wasp","waste","water","wave",
"way","wealth","weapon","wear","weasel","weather","web","wedding",
"weekend","weird","welcome","west","wet","whale","what","wheat",
"wheel","when","where","whip","whisper","wide","width","wife",
"wild","will","win","window","wine","wing","wink","winner",
"winter","wire","wisdom","wise","wish","witness","wolf","woman",
"wonder","wood","wool","word","work","world","worry","worth",
"wrap","wreck","wrestle","wrist","write","wrong","yard","year",
"yellow","you","young","youth","zebra","zero","zone","zoo",
};
#endif
+223
View File
@@ -0,0 +1,223 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license.
#include "hdwallet.h"
#include "bip39_english.h"
#include <cstring>
#include <algorithm>
#include <openssl/sha.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <secp256k1.h>
namespace hd {
// ---- secp256k1 context (self-contained; independent of crypto_ecdsa) ------
static secp256k1_context* HDContext()
{
static secp256k1_context* ctx = NULL;
if (!ctx)
ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
return ctx;
}
static void HmacSha512(const unsigned char* key, size_t keylen,
const unsigned char* data, size_t datalen,
unsigned char out[64])
{
unsigned int len = 64;
HMAC(EVP_sha512(), key, (int)keylen, data, datalen, out, &len);
}
// Binary search the (lexicographically sorted) BIP39 English wordlist.
static int WordIndex(const std::string& w)
{
int lo = 0, hi = 2047;
while (lo <= hi) {
int mid = (lo + hi) / 2;
int c = w.compare(BIP39_WORDLIST_EN[mid]);
if (c == 0) return mid;
if (c < 0) hi = mid - 1; else lo = mid + 1;
}
return -1;
}
static std::vector<std::string> SplitWords(const std::string& s)
{
std::vector<std::string> out;
size_t i = 0, n = s.size();
while (i < n) {
while (i < n && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) i++;
size_t j = i;
while (j < n && !(s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r')) j++;
if (j > i) out.push_back(s.substr(i, j - i));
i = j;
}
return out;
}
// ---- BIP39 ----------------------------------------------------------------
std::string GenerateMnemonic(int strengthBits)
{
if (strengthBits != 128 && strengthBits != 256) strengthBits = 256;
int entBytes = strengthBits / 8;
std::vector<unsigned char> ent(entBytes);
if (RAND_bytes(&ent[0], entBytes) != 1) return std::string();
// checksum = first (ENT/32) bits of SHA256(entropy)
unsigned char hash[32];
SHA256(&ent[0], entBytes, hash);
int csBits = strengthBits / 32;
// bit buffer = entropy || checksum bits
std::vector<unsigned char> bits = ent;
bits.push_back(hash[0]); // up to 8 checksum bits live in hash[0]
int totalBits = strengthBits + csBits;
int words = totalBits / 11;
std::string out;
for (int i = 0; i < words; i++) {
int idx = 0;
for (int b = 0; b < 11; b++) {
int bitpos = i * 11 + b;
int byte = bitpos / 8, off = 7 - (bitpos % 8);
int bit = (bits[byte] >> off) & 1;
idx = (idx << 1) | bit;
}
if (i) out += ' ';
out += BIP39_WORDLIST_EN[idx];
}
return out;
}
bool CheckMnemonic(const std::string& mnemonic)
{
std::vector<std::string> w = SplitWords(mnemonic);
size_t nw = w.size();
if (nw != 12 && nw != 15 && nw != 18 && nw != 21 && nw != 24) return false;
int totalBits = (int)nw * 11;
int csBits = totalBits / 33;
int entBits = totalBits - csBits;
if (entBits % 8 != 0) return false;
int entBytes = entBits / 8;
// unpack 11-bit indices into a bit buffer
std::vector<unsigned char> buf((totalBits + 7) / 8, 0);
for (size_t i = 0; i < nw; i++) {
int idx = WordIndex(w[i]);
if (idx < 0) return false;
for (int b = 0; b < 11; b++) {
int bit = (idx >> (10 - b)) & 1;
int bitpos = (int)i * 11 + b;
int byte = bitpos / 8, off = 7 - (bitpos % 8);
if (bit) buf[byte] |= (1 << off);
}
}
std::vector<unsigned char> ent(buf.begin(), buf.begin() + entBytes);
unsigned char hash[32];
SHA256(&ent[0], entBytes, hash);
// compare csBits checksum bits
for (int b = 0; b < csBits; b++) {
int bitpos = entBits + b;
int byte = bitpos / 8, off = 7 - (bitpos % 8);
int got = (buf[byte] >> off) & 1;
int want = (hash[b / 8] >> (7 - (b % 8))) & 1;
if (got != want) return false;
}
return true;
}
bool MnemonicToSeed(const std::string& mnemonic, const std::string& passphrase,
unsigned char seed64[64])
{
std::string salt = "mnemonic" + passphrase;
int rc = PKCS5_PBKDF2_HMAC(mnemonic.c_str(), (int)mnemonic.size(),
(const unsigned char*)salt.c_str(), (int)salt.size(),
2048, EVP_sha512(), 64, seed64);
return rc == 1;
}
// ---- BIP32 ----------------------------------------------------------------
bool MasterFromSeed(const unsigned char* seed, size_t seedlen, ExtKey& out)
{
unsigned char I[64];
HmacSha512((const unsigned char*)"Bitcoin seed", 12, seed, seedlen, I);
memcpy(out.key, I, 32);
memcpy(out.chaincode, I + 32, 32);
if (!secp256k1_ec_seckey_verify(HDContext(), out.key)) return false;
out.valid = true;
return true;
}
bool CKDpriv(const ExtKey& parent, uint32_t index, ExtKey& child)
{
if (!parent.valid) return false;
secp256k1_context* ctx = HDContext();
unsigned char data[37];
size_t dlen = 0;
if (index & HARDENED) {
data[0] = 0x00;
memcpy(data + 1, parent.key, 32);
dlen = 33;
} else {
// serP(point(parent.key)) = 33-byte compressed pubkey
secp256k1_pubkey pk;
if (!secp256k1_ec_pubkey_create(ctx, &pk, parent.key)) return false;
size_t plen = 33;
secp256k1_ec_pubkey_serialize(ctx, data, &plen, &pk, SECP256K1_EC_COMPRESSED);
dlen = 33;
}
data[dlen + 0] = (index >> 24) & 0xff;
data[dlen + 1] = (index >> 16) & 0xff;
data[dlen + 2] = (index >> 8) & 0xff;
data[dlen + 3] = index & 0xff;
dlen += 4;
unsigned char I[64];
HmacSha512(parent.chaincode, 32, data, dlen, I);
memcpy(child.key, parent.key, 32);
// child = (IL + parent) mod n ; rejects invalid (IL>=n or result 0)
if (!secp256k1_ec_seckey_tweak_add(ctx, child.key, I)) return false;
memcpy(child.chaincode, I + 32, 32);
child.valid = true;
return true;
}
bool DerivePath(const ExtKey& master, const std::vector<uint32_t>& path, ExtKey& out)
{
ExtKey cur = master;
for (size_t i = 0; i < path.size(); i++) {
ExtKey nxt;
if (!CKDpriv(cur, path[i], nxt)) return false;
cur = nxt;
}
out = cur;
return true;
}
bool DeriveTriangles(const std::string& mnemonic, const std::string& passphrase,
uint32_t account, uint32_t change, uint32_t index,
unsigned char privOut[32])
{
unsigned char seed[64];
if (!MnemonicToSeed(mnemonic, passphrase, seed)) return false;
ExtKey master;
if (!MasterFromSeed(seed, 64, master)) return false;
std::vector<uint32_t> path;
path.push_back(44u | HARDENED);
path.push_back(TRI_COIN_TYPE | HARDENED);
path.push_back(account | HARDENED);
path.push_back(change);
path.push_back(index);
ExtKey leaf;
if (!DerivePath(master, path, leaf)) return false;
memcpy(privOut, leaf.key, 32);
return true;
}
} // namespace hd
+50
View File
@@ -0,0 +1,50 @@
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license.
//
// Native BIP39 (mnemonic) + BIP32 (HD) key derivation for Triangles.
// Produces keys identical to the TRIdock web wallet (derivation path
// m/44'/2222'/0'/0/i, coin type 2222), so a 24-word phrase round-trips
// between the Qt/daemon wallet and the web wallet.
#ifndef TRIANGLES_HDWALLET_H
#define TRIANGLES_HDWALLET_H
#include <string>
#include <vector>
#include <cstdint>
#include <cstddef>
namespace hd {
static const uint32_t HARDENED = 0x80000000u;
static const uint32_t TRI_COIN_TYPE = 2222u; // matches triWallet.js
// A BIP32 extended private key (private scalar + chain code).
struct ExtKey {
unsigned char key[32];
unsigned char chaincode[32];
bool valid;
ExtKey() : valid(false) { }
};
// ---- BIP39 ----------------------------------------------------------------
// Generate a new mnemonic. strengthBits must be 128 (12 words) or 256 (24).
std::string GenerateMnemonic(int strengthBits = 256);
// Validate word membership + checksum.
bool CheckMnemonic(const std::string& mnemonic);
// PBKDF2-HMAC-SHA512(mnemonic, "mnemonic"+passphrase, 2048) -> 64-byte seed.
bool MnemonicToSeed(const std::string& mnemonic, const std::string& passphrase,
unsigned char seed64[64]);
// ---- BIP32 ----------------------------------------------------------------
bool MasterFromSeed(const unsigned char* seed, size_t seedlen, ExtKey& out);
bool CKDpriv(const ExtKey& parent, uint32_t index, ExtKey& child);
bool DerivePath(const ExtKey& master, const std::vector<uint32_t>& path, ExtKey& out);
// ---- High level -----------------------------------------------------------
// Derive the 32-byte private scalar for m/44'/coinType'/account'/change/index.
bool DeriveTriangles(const std::string& mnemonic, const std::string& passphrase,
uint32_t account, uint32_t change, uint32_t index,
unsigned char privOut[32]);
} // namespace hd
#endif // TRIANGLES_HDWALLET_H
+75
View File
@@ -1858,3 +1858,78 @@ Value makekeypair(const Array& params, bool fHelp)
result.push_back(Pair("PublicKey", HexStr(key.GetPubKey().Raw())));
return result;
}
Value hdinfo(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 0)
throw runtime_error("hdinfo\nReturns HD (BIP39/BIP32) wallet status.");
Object obj;
obj.push_back(Pair("hdenabled", pwalletMain->IsHDEnabled()));
obj.push_back(Pair("coin_type", 2222));
obj.push_back(Pair("derivation_path", "m/44'/2222'/0'/0/i"));
obj.push_back(Pair("nextindex", (int64_t)pwalletMain->nHDChainIndex));
return obj;
}
Value hdnew(const Array& params, bool fHelp)
{
if (fHelp || params.size() > 1)
throw runtime_error(
"hdnew [passphrase]\n"
"Generate a NEW 24-word HD seed phrase, activate it as this wallet's\n"
"deterministic seed, and return the phrase. WRITE IT DOWN: it is the\n"
"only backup of every address this wallet derives.");
EnsureWalletIsUnlocked();
if (pwalletMain->IsHDEnabled())
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet already has an HD seed; use 'hdshow' to back it up.");
string passphrase = params.size() > 0 ? params[0].get_str() : "";
string mnemonic, strError;
if (!pwalletMain->SetHDSeed("", passphrase, true, mnemonic, strError))
throw JSONRPCError(RPC_WALLET_ERROR, strError);
pwalletMain->TopUpKeyPool();
Object obj;
obj.push_back(Pair("mnemonic", mnemonic));
obj.push_back(Pair("words", 24));
obj.push_back(Pair("warning", "Write these 24 words down and keep them secret and offline. Anyone with them can spend your coins."));
return obj;
}
Value hdrestore(const Array& params, bool fHelp)
{
if (fHelp || params.size() < 1 || params.size() > 2)
throw runtime_error(
"hdrestore \"mnemonic\" [passphrase]\n"
"Activate an HD seed from an existing 24-word phrase, derive the keypool\n"
"and rescan the chain for funds on the derived addresses.");
EnsureWalletIsUnlocked();
string mnemonic = params[0].get_str();
string passphrase = params.size() > 1 ? params[1].get_str() : "";
string out, strError;
if (!pwalletMain->SetHDSeed(mnemonic, passphrase, false, out, strError))
throw JSONRPCError(RPC_WALLET_ERROR, strError);
pwalletMain->TopUpKeyPool();
{
LOCK2(cs_main, pwalletMain->cs_wallet);
pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
pwalletMain->ReacceptWalletTransactions();
}
Object obj;
obj.push_back(Pair("restored", true));
return obj;
}
Value hdshow(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 0)
throw runtime_error(
"hdshow\nReveal the wallet's HD mnemonic for backup. The wallet must be unlocked.");
EnsureWalletIsUnlocked();
string mnemonic;
if (!pwalletMain->GetHDMnemonic(mnemonic))
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet has no HD seed (use 'hdnew' to create one).");
Object obj;
obj.push_back(Pair("mnemonic", mnemonic));
obj.push_back(Pair("warning", "Keep these words secret and offline."));
return obj;
}
+4
View File
@@ -305,6 +305,10 @@ static const CRPCCommand vRPCCommands[] =
{ "settxfee", &settxfee, false, false },
{ "listsinceblock", &listsinceblock, false, false },
{ "dumpprivkey", &dumpprivkey, false, false },
{ "hdnew", &hdnew, false, false },
{ "hdrestore", &hdrestore, false, false },
{ "hdshow", &hdshow, false, false },
{ "hdinfo", &hdinfo, true, false },
{ "dumpwallet", &dumpwallet, true, false },
{ "importwallet", &importwallet, false, false },
{ "importprivkey", &importprivkey, false, false },
+4
View File
@@ -155,6 +155,10 @@ extern json_spirit::Value getwalletinfo(const json_spirit::Array& params, bool f
extern json_spirit::Value dumpwallet(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value importwallet(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value dumpprivkey(const json_spirit::Array& params, bool fHelp); // in rpcdump.cpp
extern json_spirit::Value hdnew(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value hdrestore(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value hdshow(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value hdinfo(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value importprivkey(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getsubsidy(const json_spirit::Array& params, bool fHelp);
+92 -2
View File
@@ -7,12 +7,14 @@
#include "wallet.h"
#include "walletdb.h"
#include "crypter.h"
#include "hdwallet.h"
#include "ui_interface.h"
#include "base58.h"
#include "kernel.h"
#include "coincontrol.h"
#include "addressindex.h"
#include "util.h"
#include <cstring>
#include <memory>
#include <algorithm>
#include <random>
@@ -129,7 +131,12 @@ CPubKey CWallet::GenerateNewKey()
RandAddSeedPerfmon();
CKey key;
key.MakeNewKey(fCompressed);
bool fUsedHD = false;
if (fHDEnabled && !hdMnemonic.empty()) {
if (DeriveHDKey(nHDChainIndex, key)) { fUsedHD = true; fCompressed = true; }
}
if (!fUsedHD)
key.MakeNewKey(fCompressed);
// Compressed public keys were introduced in version 0.6.0
if (fCompressed)
@@ -145,6 +152,11 @@ CPubKey CWallet::GenerateNewKey()
if (!AddKey(key))
throw std::runtime_error("CWallet::GenerateNewKey() : AddKey failed");
if (fUsedHD) {
nHDChainIndex++;
if (fFileBacked)
CWalletDB(strWalletFile).WriteHDChain(nHDChainIndex);
}
return key.GetPubKey();
}
@@ -209,6 +221,9 @@ bool CWallet::Lock()
if (fDebug)
printf("Locking wallet.\n");
if (IsCrypted())
hdMnemonic.clear(); // keep only the encrypted copy while locked
{
LOCK(cs_wallet);
CWalletDB wdb(strWalletFile);
@@ -233,8 +248,14 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase)
return false;
if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey))
return false;
if (CCryptoKeyStore::Unlock(vMasterKey))
if (CCryptoKeyStore::Unlock(vMasterKey)) {
if (fHDEnabled && hdMnemonic.empty() && !vchCryptedHDMnemonic.empty()) {
CSecret sec;
if (DecryptSecret(vMasterKey, vchCryptedHDMnemonic, hdMnemonicIV, sec))
hdMnemonic.assign(sec.begin(), sec.end());
}
return true;
}
}
SecureMsgWalletUnlocked();
@@ -407,6 +428,15 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
return false;
}
if (fHDEnabled && !hdMnemonic.empty()) {
CSecret sec(hdMnemonic.begin(), hdMnemonic.end());
uint256 iv = GetRandHash();
std::vector<unsigned char> cipher;
if (!EncryptSecret(vMasterKey, sec, iv, cipher)) { dbEnc->TxnAbort(); return false; }
hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher;
dbEnc->WriteHDCryptedMnemonic(iv, cipher);
}
SetMinVersion(WalletFeature::WalletCrypt, dbEnc.get(), true);
if (!dbEnc->TxnCommit())
@@ -2848,3 +2878,63 @@ void CWallet::GetKeyBirthTimes(std::map<CKeyID, int64_t> &mapKeyBirth) const {
}
// ---- HD wallet (BIP39/BIP32) implementation ----
bool CWallet::DeriveHDKey(int64_t index, CKey& keyOut) const
{
if (hdMnemonic.empty())
return false;
unsigned char priv[32];
if (!hd::DeriveTriangles(hdMnemonic, "", 0, 0, (uint32_t)index, priv))
return false;
CSecret secret(priv, priv + 32);
memset(priv, 0, sizeof(priv));
keyOut.SetSecret(secret, true); // HD keys are compressed
return true;
}
bool CWallet::GetHDMnemonic(std::string& mnemonicOut) const
{
if (!fHDEnabled || hdMnemonic.empty())
return false;
mnemonicOut = hdMnemonic;
return true;
}
bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passphrase, bool fGenerate, std::string& mnemonicOut, std::string& strError)
{
LOCK(cs_wallet);
if (IsLocked()) { strError = "Wallet is locked; unlock it before setting an HD seed."; return false; }
std::string m = mnemonicIn;
if (m.empty()) {
if (!fGenerate) { strError = "No mnemonic supplied."; return false; }
m = hd::GenerateMnemonic(256);
if (m.empty()) { strError = "Failed to generate mnemonic."; return false; }
}
if (!hd::CheckMnemonic(m)) { strError = "Invalid mnemonic (unknown word or bad checksum)."; return false; }
unsigned char priv[32];
if (!hd::DeriveTriangles(m, passphrase, 0, 0, 0, priv)) { strError = "Key derivation failed."; return false; }
memset(priv, 0, sizeof(priv));
hdMnemonic = m;
fHDEnabled = true;
nHDChainIndex = 0;
if (fFileBacked) {
CWalletDB wdb(strWalletFile);
if (IsCrypted()) {
CSecret sec(m.begin(), m.end());
uint256 iv = GetRandHash();
std::vector<unsigned char> cipher;
if (!EncryptSecret(vMasterKey, sec, iv, cipher)) { strError = "Failed to encrypt seed."; return false; }
hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher;
wdb.WriteHDCryptedMnemonic(iv, cipher);
} else {
wdb.WriteHDMnemonic(m);
}
wdb.WriteHDChain(nHDChainIndex);
}
mnemonicOut = m;
return true;
}
+17
View File
@@ -105,6 +105,8 @@ public:
fFileBacked = false;
nMasterKeyMaxID = 0;
pwalletdbEncryption = nullptr;
fHDEnabled = false;
nHDChainIndex = 0;
nOrderPosNext = 0;
nCachedStakeWeight = 0;
nCachedStakeWeightTime = 0;
@@ -124,6 +126,13 @@ public:
CPubKey vchDefaultKey;
int64_t nTimeFirstKey;
// ---- HD (BIP39/BIP32) wallet state ----
bool fHDEnabled; // an HD seed has been set
int64_t nHDChainIndex; // next external index (m/44'/2222'/0'/0/n)
std::string hdMnemonic; // in-memory phrase (present when unlocked/unencrypted)
std::vector<unsigned char> vchCryptedHDMnemonic; // encrypted phrase (loaded, decrypted on unlock)
uint256 hdMnemonicIV; // IV for the encrypted phrase
// check whether we are allowed to upgrade (or already support) to the named feature
bool CanSupportFeature(WalletFeature wf) { return nWalletMaxVersion >= static_cast<int>(wf); }
@@ -133,6 +142,14 @@ public:
// keystore implementation
// Generate a new key
CPubKey GenerateNewKey();
// ---- HD wallet (BIP39/BIP32) ----
bool IsHDEnabled() const { return fHDEnabled; }
bool SetHDSeed(const std::string& mnemonicIn, const std::string& passphrase, bool fGenerate, std::string& mnemonicOut, std::string& strError);
bool GetHDMnemonic(std::string& mnemonicOut) const;
bool DeriveHDKey(int64_t index, CKey& keyOut) const;
bool LoadHDMnemonic(const std::string& m) { hdMnemonic = m; fHDEnabled = true; return true; }
bool LoadCryptedHDMnemonic(const uint256& iv, const std::vector<unsigned char>& cipher) { hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher; fHDEnabled = true; return true; }
// Adds a key to the store, and saves it to disk.
bool AddKey(const CKey& key);
// Adds a key to the store, without saving it to disk (used by LoadWallet)
+20 -1
View File
@@ -429,6 +429,24 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
pwallet->mapKeyMetadata[keyid] = CKeyMetadata(keypool.nTime);
}
else if (strType == "hdmnemonic")
{
std::string m;
ssValue >> m;
pwallet->LoadHDMnemonic(m);
}
else if (strType == "hdcmnemonic")
{
std::pair<uint256, std::vector<unsigned char> > cm;
ssValue >> cm;
pwallet->LoadCryptedHDMnemonic(cm.first, cm.second);
}
else if (strType == "hdchain")
{
int64_t n;
ssValue >> n;
pwallet->nHDChainIndex = n;
}
else if (strType == "version")
{
ssValue >> wss.nFileVersion;
@@ -461,7 +479,8 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
static bool IsKeyType(string strType)
{
return (strType== "key" || strType == "wkey" ||
strType == "mkey" || strType == "ckey");
strType == "mkey" || strType == "ckey" ||
strType == "hdmnemonic" || strType == "hdcmnemonic");
}
DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
+15
View File
@@ -169,6 +169,21 @@ public:
return Write(std::string("defaultkey"), vchPubKey.Raw());
}
bool WriteHDMnemonic(const std::string& mnemonic) {
nWalletDBUpdated++;
Erase(std::string("hdcmnemonic"));
return Write(std::string("hdmnemonic"), mnemonic);
}
bool WriteHDCryptedMnemonic(const uint256& iv, const std::vector<unsigned char>& cipher) {
nWalletDBUpdated++;
Erase(std::string("hdmnemonic"));
return Write(std::string("hdcmnemonic"), std::make_pair(iv, cipher));
}
bool WriteHDChain(int64_t nIndex) {
nWalletDBUpdated++;
return Write(std::string("hdchain"), nIndex);
}
bool ReadPool(int64_t nPool, CKeyPool& keypool)
{
return Read(std::make_pair(std::string("pool"), nPool), keypool);