Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0a76f8ae2 | |||
| cc57c906b4 | |||
| e80d672833 | |||
| fcdc9a58b0 | |||
| 514867c5d9 | |||
| c464e6c59d | |||
| 11ed086d1e | |||
| 5511cfae6b | |||
| 1dda8b3006 | |||
| 68c4e38411 | |||
| ed996c8e9d |
@@ -49,6 +49,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies + clang-tidy
|
||||
run: |
|
||||
|
||||
@@ -42,6 +42,7 @@ set(CORE_SOURCES
|
||||
bootstrap.cpp
|
||||
checkpoints.cpp
|
||||
crypter.cpp
|
||||
hdwallet.cpp
|
||||
crypto_ecdh.cpp
|
||||
crypto_ecdsa.cpp
|
||||
db.cpp
|
||||
@@ -306,6 +307,7 @@ if(BUILD_QT)
|
||||
qt/trianglesunits.cpp
|
||||
qt/qvaluecombobox.cpp
|
||||
qt/askpassphrasedialog.cpp
|
||||
qt/hdseeddialog.cpp
|
||||
qt/notificator.cpp
|
||||
qt/qtipcserver.cpp
|
||||
qt/rpcconsole.cpp
|
||||
|
||||
@@ -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
|
||||
@@ -36,6 +36,7 @@ namespace Checkpoints
|
||||
// 17650 to the live tip so stale-bootstrap / low-trust forks below
|
||||
// this height are rejected outright. Hash from the canonical chain.
|
||||
{ 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")},
|
||||
{ 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")},
|
||||
};
|
||||
|
||||
// Published UTXO snapshot file SHA256, keyed by snapshot height.
|
||||
@@ -47,6 +48,7 @@ namespace Checkpoints
|
||||
// here. The corresponding (height, blockhash) must already exist in
|
||||
// mapCheckpoints / mapCheckpointsTestnet.
|
||||
static std::map<int, uint256> mapSnapshotHashes = {
|
||||
{ 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")},
|
||||
};
|
||||
|
||||
static std::map<int, uint256> mapSnapshotHashesTestnet = {
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
|
||||
#define CLIENT_VERSION_MAJOR 5
|
||||
#define CLIENT_VERSION_MINOR 9
|
||||
#define CLIENT_VERSION_REVISION 13
|
||||
#define CLIENT_VERSION_REVISION 15
|
||||
#define CLIENT_VERSION_BUILD 0
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1081,6 +1081,37 @@ bool AppInit2()
|
||||
if (!LoadBlockIndex())
|
||||
return InitError(_("Error loading blkindex.dat"));
|
||||
|
||||
// triangles fix (pitfall #61): initialize pindexFinalized from the
|
||||
// hardcoded checkpoint on startup, BEFORE the daemon opens any peer
|
||||
// connections or processes any block messages.
|
||||
//
|
||||
// Without this, pindexFinalized stays NULL on a fresh restart even when
|
||||
// we have 2.2M blocks on disk, because the auto-checkpoint code in
|
||||
// ActivateBestChain() at main.cpp:2459 only sets it when
|
||||
// !IsInitialBlockDownload(). If the chain tip is more than 24h stale
|
||||
// (which happens on every restart with a synced chain), IsInitialBlockDownload()
|
||||
// returns true and pindexFinalized never gets set.
|
||||
//
|
||||
// The downstream reorg guard at main.cpp:2198 short-circuits when
|
||||
// pindexFinalized is NULL, which allowed a 3,755-block minority fork
|
||||
// to overwrite a healthy 2,206,004-block chain on 2026-06-16. Loading
|
||||
// the hardcoded checkpoint from checkpoints.cpp (block 2,205,000) on
|
||||
// startup means the reorg guard is always active whenever the
|
||||
// checkpointed block is in our local mapBlockIndex.
|
||||
{
|
||||
CBlockIndex* pCheckpoint = Checkpoints::GetLastCheckpoint(mapBlockIndex);
|
||||
if (pCheckpoint && pCheckpoint != pindexFinalized)
|
||||
{
|
||||
pindexFinalized = pCheckpoint;
|
||||
printf("STARTUP-CHECKPOINT: pindexFinalized set to block %d (%s) from hardcoded checkpoint\n",
|
||||
pindexFinalized->nHeight, pindexFinalized->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
}
|
||||
else if (!pCheckpoint)
|
||||
{
|
||||
printf("STARTUP-CHECKPOINT: WARNING — hardcoded checkpoint not in local block index, pindexFinalized remains NULL\n");
|
||||
}
|
||||
}
|
||||
|
||||
// If the block index is empty but blk0001.dat exists (bootstrap download),
|
||||
// fast-import: build the index directly from the block file without re-writing
|
||||
// data. Batches LevelDB commits every 200K blocks for speed.
|
||||
|
||||
+53
-1
@@ -4457,8 +4457,60 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
||||
{
|
||||
// Find the last block the caller has in the main chain
|
||||
pindex = locator.GetBlockIndex();
|
||||
|
||||
// triangles fix: handle broken pnext chain.
|
||||
// GetBlockIndex() returns pindexGenesisBlock when no locator
|
||||
// hash matches our main chain (peer is on a different fork or
|
||||
// a stale local state). pindexGenesisBlock->pnext is always
|
||||
// null, which would cause the for-loop below to send ZERO
|
||||
// headers, leaving the peer stuck (logged as "getheaders -1").
|
||||
//
|
||||
// Mirror the getblocks handler: if the locator matches nothing
|
||||
// on our main chain, serve our headers from genesis so the peer
|
||||
// can discover the canonical chain. Then fall back to a tip-
|
||||
// backwards walk if pnext is null for any other reason (this
|
||||
// happens when LoadBlockIndex() didn't fully heal pnext links,
|
||||
// or the chain was bootstrapped from a snapshot).
|
||||
//
|
||||
// pitfall #61 guard: if pindexFinalized is set (from the startup
|
||||
// hardcoded-checkpoint init in init.cpp), serve from there instead
|
||||
// of genesis. This prevents a fork peer from feeding us their
|
||||
// short chain back via getheaders — the peer only learns our
|
||||
// canonical chain from the finalized point forward, and their
|
||||
// conflicting fork gets rejected at the reorg check in
|
||||
// Reorganize() because the fork point is below pindexFinalized.
|
||||
if (!locator.IsNull() && pindex == pindexGenesisBlock &&
|
||||
pindexGenesisBlock && locator.GetTipHash() != pindexGenesisBlock->GetBlockHash())
|
||||
{
|
||||
if (pindexFinalized && pindexFinalized->pnext)
|
||||
{
|
||||
printf("getheaders: fork detected from peer %s, serving headers from finalized block %d (not genesis) — pitfall #61 guard\n",
|
||||
pfrom->addr.ToString().c_str(), pindexFinalized->nHeight);
|
||||
pindex = pindexFinalized;
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("WARNING: peer getheaders locator has no common blocks — serving headers from genesis (peer may be on a fork)\n");
|
||||
pindex = pindexGenesisBlock;
|
||||
}
|
||||
}
|
||||
|
||||
if (pindex)
|
||||
pindex = pindex->pnext;
|
||||
{
|
||||
if (pindex->pnext)
|
||||
{
|
||||
pindex = pindex->pnext;
|
||||
}
|
||||
else
|
||||
{
|
||||
// pnext is null — fall back to walking from pindexBest
|
||||
// backwards to find the block immediately after pindex
|
||||
CBlockIndex* pWalk = pindexBest;
|
||||
while (pWalk && pWalk->pprev != pindex)
|
||||
pWalk = pWalk->pprev;
|
||||
pindex = pWalk; // null if pindex is already the tip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vector<CBlock> vHeaders;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2026 The Triangles developers
|
||||
// Distributed under the MIT/X11 software license.
|
||||
#include "hdseeddialog.h"
|
||||
#include "walletmodel.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QFont>
|
||||
|
||||
HDSeedDialog::HDSeedDialog(QWidget *parent)
|
||||
: QDialog(parent), model(0), seedText(0), statusLabel(0)
|
||||
{
|
||||
setWindowTitle(tr("HD Seed Phrase (BIP39)"));
|
||||
resize(560, 360);
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
|
||||
QLabel *intro = new QLabel(tr(
|
||||
"A 24-word seed phrase is a complete backup of this wallet. Anyone who has it "
|
||||
"can spend your coins. Write it down on paper and keep it offline."), this);
|
||||
intro->setWordWrap(true);
|
||||
layout->addWidget(intro);
|
||||
|
||||
seedText = new QPlainTextEdit(this);
|
||||
seedText->setPlaceholderText(tr(
|
||||
"Your 24-word phrase appears here when you generate or reveal it. "
|
||||
"To restore, paste an existing 24-word phrase here and click 'Restore from Phrase'."));
|
||||
QFont mono("monospace");
|
||||
mono.setStyleHint(QFont::Monospace);
|
||||
seedText->setFont(mono);
|
||||
layout->addWidget(seedText);
|
||||
|
||||
statusLabel = new QLabel(this);
|
||||
statusLabel->setWordWrap(true);
|
||||
layout->addWidget(statusLabel);
|
||||
|
||||
QHBoxLayout *btns = new QHBoxLayout();
|
||||
QPushButton *genBtn = new QPushButton(tr("Generate New"), this);
|
||||
QPushButton *showBtn = new QPushButton(tr("Reveal for Backup"), this);
|
||||
QPushButton *restoreBtn = new QPushButton(tr("Restore from Phrase"), this);
|
||||
QPushButton *closeBtn = new QPushButton(tr("Close"), this);
|
||||
btns->addWidget(genBtn);
|
||||
btns->addWidget(showBtn);
|
||||
btns->addWidget(restoreBtn);
|
||||
btns->addStretch();
|
||||
btns->addWidget(closeBtn);
|
||||
layout->addLayout(btns);
|
||||
|
||||
connect(genBtn, SIGNAL(clicked()), this, SLOT(onGenerate()));
|
||||
connect(showBtn, SIGNAL(clicked()), this, SLOT(onShow()));
|
||||
connect(restoreBtn, SIGNAL(clicked()), this, SLOT(onRestore()));
|
||||
connect(closeBtn, SIGNAL(clicked()), this, SLOT(accept()));
|
||||
}
|
||||
|
||||
void HDSeedDialog::setModel(WalletModel *modelIn)
|
||||
{
|
||||
model = modelIn;
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void HDSeedDialog::refreshStatus()
|
||||
{
|
||||
if (!model || !statusLabel) return;
|
||||
if (model->hdEnabled())
|
||||
statusLabel->setText(tr("Status: HD seed is ACTIVE. Use 'Reveal for Backup' to view your phrase."));
|
||||
else
|
||||
statusLabel->setText(tr("Status: no HD seed yet. Use 'Generate New' to create one."));
|
||||
}
|
||||
|
||||
void HDSeedDialog::onGenerate()
|
||||
{
|
||||
if (!model) return;
|
||||
if (model->hdEnabled()) {
|
||||
QMessageBox::warning(this, tr("HD seed already set"),
|
||||
tr("This wallet already has an HD seed. Use 'Reveal for Backup' to view it."));
|
||||
return;
|
||||
}
|
||||
if (QMessageBox::question(this, tr("Generate new seed"),
|
||||
tr("Generate a new 24-word HD seed for this wallet?"),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
WalletModel::UnlockContext ctx(model->requestUnlock());
|
||||
if (!ctx.isValid()) return;
|
||||
|
||||
QString mnemonic, err;
|
||||
if (!model->hdNew(mnemonic, err)) {
|
||||
QMessageBox::critical(this, tr("Error"), err);
|
||||
return;
|
||||
}
|
||||
seedText->setPlainText(mnemonic);
|
||||
QMessageBox::information(this, tr("Write this down"),
|
||||
tr("Your new 24-word seed phrase is shown above. Write it on paper and store it safely "
|
||||
"and offline. This is the only backup of this wallet."));
|
||||
refreshStatus();
|
||||
}
|
||||
|
||||
void HDSeedDialog::onShow()
|
||||
{
|
||||
if (!model) return;
|
||||
WalletModel::UnlockContext ctx(model->requestUnlock());
|
||||
if (!ctx.isValid()) return;
|
||||
|
||||
QString mnemonic, err;
|
||||
if (!model->hdShow(mnemonic, err)) {
|
||||
QMessageBox::critical(this, tr("Error"), err);
|
||||
return;
|
||||
}
|
||||
seedText->setPlainText(mnemonic);
|
||||
}
|
||||
|
||||
void HDSeedDialog::onRestore()
|
||||
{
|
||||
if (!model) return;
|
||||
QString phrase = seedText->toPlainText().trimmed();
|
||||
if (phrase.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("No phrase"),
|
||||
tr("Paste a 24-word phrase into the box first."));
|
||||
return;
|
||||
}
|
||||
if (QMessageBox::question(this, tr("Restore from phrase"),
|
||||
tr("Restore the HD seed from the phrase in the box and rescan the chain? "
|
||||
"This replaces the current HD seed."),
|
||||
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
WalletModel::UnlockContext ctx(model->requestUnlock());
|
||||
if (!ctx.isValid()) return;
|
||||
|
||||
QString err;
|
||||
if (!model->hdRestore(phrase, err)) {
|
||||
QMessageBox::critical(this, tr("Error"), err);
|
||||
return;
|
||||
}
|
||||
QMessageBox::information(this, tr("Restored"),
|
||||
tr("HD seed restored and the chain was rescanned for your funds."));
|
||||
refreshStatus();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2026 The Triangles developers
|
||||
// Distributed under the MIT/X11 software license.
|
||||
#ifndef HDSEEDDIALOG_H
|
||||
#define HDSEEDDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class WalletModel;
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QPlainTextEdit;
|
||||
class QLabel;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
/** Generate, reveal (for backup), and restore the wallet's BIP39 HD seed phrase. */
|
||||
class HDSeedDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HDSeedDialog(QWidget *parent = 0);
|
||||
void setModel(WalletModel *model);
|
||||
|
||||
private:
|
||||
WalletModel *model;
|
||||
QPlainTextEdit *seedText;
|
||||
QLabel *statusLabel;
|
||||
void refreshStatus();
|
||||
|
||||
private slots:
|
||||
void onGenerate();
|
||||
void onShow();
|
||||
void onRestore();
|
||||
};
|
||||
|
||||
#endif // HDSEEDDIALOG_H
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "trianglesunits.h"
|
||||
#include "guiconstants.h"
|
||||
#include "askpassphrasedialog.h"
|
||||
#include "hdseeddialog.h"
|
||||
#include "notificator.h"
|
||||
#include "guiutil.h"
|
||||
#include "rpcconsole.h"
|
||||
@@ -484,6 +485,8 @@ void TrianglesGUI::createActions(bool fIsTestnet)
|
||||
backupWalletAction->setStatusTip(tr("Backup wallet to another location"));
|
||||
changePassphraseAction = new QAction(QIcon(":/menu_16/passphrase"), tr("&Change Passphrase..."), this);
|
||||
changePassphraseAction->setStatusTip(tr("Change the passphrase used for wallet encryption"));
|
||||
hdSeedAction = new QAction(QIcon(":/menu_16/passphrase"), tr("&Seed Phrase (HD Backup)..."), this);
|
||||
hdSeedAction->setStatusTip(tr("Generate, restore, or back up your 24-word HD seed phrase"));
|
||||
unlockWalletAction = new QAction(QIcon(":/menu_16/unlock"), tr("&Unlock Wallet..."), this);
|
||||
unlockWalletAction->setStatusTip(tr("Unlock wallet"));
|
||||
unlockWalletStakingAction = new QAction(QIcon(":/menu_16/unlock"), tr("&Unlock Wallet for staking..."), this);
|
||||
@@ -509,6 +512,7 @@ void TrianglesGUI::createActions(bool fIsTestnet)
|
||||
connect(encryptWalletAction, SIGNAL(triggered(bool)), this, SLOT(encryptWallet(bool)));
|
||||
connect(backupWalletAction, SIGNAL(triggered()), this, SLOT(backupWallet()));
|
||||
connect(changePassphraseAction, SIGNAL(triggered()), this, SLOT(changePassphrase()));
|
||||
connect(hdSeedAction, SIGNAL(triggered()), this, SLOT(hdSeedManager()));
|
||||
connect(unlockWalletAction, SIGNAL(triggered()), this, SLOT(unlockWallet()));
|
||||
connect(unlockWalletStakingAction, SIGNAL(triggered()), this, SLOT(unlockWalletStaking()));
|
||||
connect(lockWalletAction, SIGNAL(triggered()), this, SLOT(lockWallet()));
|
||||
@@ -538,6 +542,7 @@ void TrianglesGUI::createMenuBar()
|
||||
QMenu *settings = appMenuBar->addMenu(tr("&Settings"));
|
||||
settings->addAction(encryptWalletAction);
|
||||
settings->addAction(changePassphraseAction);
|
||||
settings->addAction(hdSeedAction);
|
||||
settings->addAction(unlockWalletAction);
|
||||
settings->addAction(lockWalletAction);
|
||||
settings->addSeparator();
|
||||
@@ -1448,6 +1453,7 @@ void TrianglesGUI::menuOperationsRequested()
|
||||
QAction* unlockWalletStaking = menu.addAction(QIcon(":/menu_16/unlock"), tr("&Unlock Wallet...").remove('&').remove("..."));
|
||||
QAction* lockWallet = menu.addAction(QIcon(":/menu_16/lock"), tr("&Lock Wallet...").remove('&').remove("..."));
|
||||
QAction* changePassword = menu.addAction(QIcon(":/menu_16/passphrase"), tr("&Change Passphrase...").remove('&').remove("..."));
|
||||
QAction* hdSeed = menu.addAction(QIcon(":/menu_16/passphrase"), tr("Seed Phrase (HD Backup)..."));
|
||||
QAction* signMessage = menu.addAction(QIcon(":/menu_16/sign"), tr("Sign &message...").remove('&').remove("..."));
|
||||
QAction* verifySignature = menu.addAction(QIcon(":/menu_16/verify"), tr("&Verify message...").remove('&').remove("..."));
|
||||
|
||||
@@ -1508,6 +1514,10 @@ void TrianglesGUI::menuOperationsRequested()
|
||||
if (walletModel->getEncryptionStatus() == WalletModel::Unlocked || walletModel->getEncryptionStatus() == WalletModel::Locked)
|
||||
changePassphrase();
|
||||
}
|
||||
else if (selected == hdSeed)
|
||||
{
|
||||
hdSeedManager();
|
||||
}
|
||||
else if (selected == signMessage)
|
||||
{
|
||||
gotoSignMessageTab();
|
||||
@@ -1614,6 +1624,15 @@ void TrianglesGUI::changePassphrase()
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
void TrianglesGUI::hdSeedManager()
|
||||
{
|
||||
if (!walletModel)
|
||||
return;
|
||||
HDSeedDialog dlg(this);
|
||||
dlg.setModel(walletModel);
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
|
||||
void TrianglesGUI::unlockWalletStaking()
|
||||
{
|
||||
|
||||
@@ -131,6 +131,7 @@ private:
|
||||
QAction *encryptWalletAction;
|
||||
QAction *backupWalletAction;
|
||||
QAction *changePassphraseAction;
|
||||
QAction *hdSeedAction;
|
||||
QAction *unlockWalletAction;
|
||||
QAction *unlockWalletStakingAction;
|
||||
QAction *lockWalletAction;
|
||||
@@ -243,6 +244,8 @@ private slots:
|
||||
void backupWallet();
|
||||
/** Change encrypted wallet passphrase */
|
||||
void changePassphrase();
|
||||
/** Open the HD seed phrase (generate/restore/backup) dialog */
|
||||
void hdSeedManager();
|
||||
/** Ask for passphrase to unlock wallet temporarily */
|
||||
void unlockWallet();
|
||||
/** Ask for passphrase to unlock wallet temporarily - FOR STAKING ONLY */
|
||||
|
||||
@@ -676,3 +676,49 @@ void WalletModel::listLockedCoins(std::vector<COutPoint>& vOutpts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// ---- HD wallet (BIP39/BIP32) ----
|
||||
bool WalletModel::hdEnabled() const
|
||||
{
|
||||
return wallet->IsHDEnabled();
|
||||
}
|
||||
|
||||
bool WalletModel::hdNew(QString &mnemonicOut, QString &errorOut)
|
||||
{
|
||||
std::string mnemonic, strError;
|
||||
if (!wallet->SetHDSeed("", "", true, mnemonic, strError)) {
|
||||
errorOut = QString::fromStdString(strError);
|
||||
return false;
|
||||
}
|
||||
wallet->TopUpKeyPool();
|
||||
mnemonicOut = QString::fromStdString(mnemonic);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WalletModel::hdRestore(const QString &mnemonic, QString &errorOut)
|
||||
{
|
||||
std::string out, strError;
|
||||
if (!wallet->SetHDSeed(mnemonic.toStdString(), "", false, out, strError)) {
|
||||
errorOut = QString::fromStdString(strError);
|
||||
return false;
|
||||
}
|
||||
wallet->TopUpKeyPool();
|
||||
{
|
||||
LOCK2(cs_main, wallet->cs_wallet);
|
||||
wallet->ScanForWalletTransactions(pindexGenesisBlock, true);
|
||||
wallet->ReacceptWalletTransactions();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WalletModel::hdShow(QString &mnemonicOut, QString &errorOut)
|
||||
{
|
||||
std::string mnemonic;
|
||||
if (!wallet->GetHDMnemonic(mnemonic)) {
|
||||
errorOut = QObject::tr("Wallet has no HD seed (use 'Generate New').");
|
||||
return false;
|
||||
}
|
||||
mnemonicOut = QString::fromStdString(mnemonic);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,12 @@ public:
|
||||
// Wallet backup
|
||||
bool backupWallet(const QString &filename);
|
||||
|
||||
// ---- HD wallet (BIP39/BIP32) ----
|
||||
bool hdEnabled() const;
|
||||
bool hdNew(QString &mnemonicOut, QString &errorOut);
|
||||
bool hdRestore(const QString &mnemonic, QString &errorOut);
|
||||
bool hdShow(QString &mnemonicOut, QString &errorOut);
|
||||
|
||||
// RAI object for unlocking wallet, returned by requestUnlock()
|
||||
class UnlockContext
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include <thread>
|
||||
#include <fstream>
|
||||
#include <cstring>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
@@ -122,11 +124,59 @@ bool CTorEmbedded::Start(int socks, int hsPort, bool enableHiddenService)
|
||||
// Prepare Tor data directory under the wallet's data dir
|
||||
torDataDir = (::GetDataDir() / "tor_data").string();
|
||||
fs::create_directories(torDataDir);
|
||||
// CRITICAL: Tor refuses to use a DataDirectory readable by other users.
|
||||
// Without 0700, tor_run_main() returns -1 and the embedded Tor never starts.
|
||||
fs::permissions(torDataDir, fs::perms::owner_all, fs::perm_options::replace);
|
||||
|
||||
// triangles fix: auto-repair `state`-as-file corruption (pitfall #19).
|
||||
// Tor's atomic state-write pattern is: write `state.tmp` → rename to `state`.
|
||||
// If the daemon is killed or the process crashes mid-write, the rename can
|
||||
// fail and `state` may be left as a regular file (or a partial file). On
|
||||
// next start, Tor sees "State file ... is not a file? Failing." and dies
|
||||
// with code -1 ("Reading config failed"). This was hit on DNS3 on
|
||||
// 2026-05-24 and on the TRI-LAPTOP GUI wallet on 2026-06-15. The user-facing
|
||||
// symptom is "Tor failed to start. Triangles requires Tor to operate." and
|
||||
// the only fix was manually renaming the corrupt file. Detect this state
|
||||
// here and auto-rename so the daemon is self-healing.
|
||||
{
|
||||
fs::path statePath = fs::path(torDataDir) / "state";
|
||||
std::error_code ec;
|
||||
if (fs::exists(statePath, ec) && !fs::is_directory(statePath, ec)) {
|
||||
// state is a file (or symlink to one) — quarantine it
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto t = std::chrono::system_clock::to_time_t(now);
|
||||
char ts[32];
|
||||
std::strftime(ts, sizeof(ts), "%Y%m%d-%H%M%S", std::gmtime(&t));
|
||||
fs::path quarantine = fs::path(torDataDir) /
|
||||
(std::string("state.corrupt-") + ts);
|
||||
try {
|
||||
fs::rename(statePath, quarantine, ec);
|
||||
if (ec) {
|
||||
// rename can fail on Windows if dest exists; remove then rename
|
||||
fs::remove(quarantine, ec);
|
||||
fs::rename(statePath, quarantine, ec);
|
||||
}
|
||||
printf("Tor state was a file (corrupt) — quarantined to %s for inspection. Tor will recreate state/ as a directory.\n",
|
||||
quarantine.filename().string().c_str());
|
||||
} catch (const std::exception& e) {
|
||||
printf("WARNING: could not quarantine corrupt Tor state file %s: %s\n",
|
||||
statePath.string().c_str(), e.what());
|
||||
// Last resort: try to remove it so Tor can proceed
|
||||
fs::remove(statePath, ec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string hsDir;
|
||||
if (hiddenServiceEnabled) {
|
||||
hsDir = (fs::path(torDataDir) / "hidden_service").string();
|
||||
fs::create_directories(hsDir);
|
||||
// CRITICAL: Tor rejects hidden service directories that are not 0700
|
||||
// ("Permissions on directory ... are too permissive") and aborts config
|
||||
// validation with code -1. This was the root cause of "Embedded Tor
|
||||
// exited with code -1" — fs::create_directories honors umask (0022 on
|
||||
// most Linux systems), leaving the dir at 0755. Force 0700 after creation.
|
||||
fs::permissions(hsDir, fs::perms::owner_all, fs::perm_options::replace);
|
||||
}
|
||||
|
||||
// Build the argv for tor_run_main
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
|
||||
+95
-2
@@ -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,66 @@ 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);
|
||||
}
|
||||
// Replace any pre-existing (random) keypool with HD-derived keys so that
|
||||
// getnewaddress immediately hands out deterministic m/44'/2222'/0'/0/i keys.
|
||||
NewKeyPool();
|
||||
mnemonicOut = m;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user