Compare commits

...

13 Commits

Author SHA1 Message Date
triangles-bot c606253c41 utxosnapshot: build address index when loading a UTXO snapshot (fast-start nodes get balances) [v5.9.17] 2026-06-16 20:37:44 -07:00
triangles-bot b2dfb627cc main: build address index during FastImport (fix-in-place, v5.9.16) 2026-06-16 20:24:26 -07:00
triangles-bot d0a76f8ae2 qt: show Seed Phrase (HD Backup) in the visible Operations menu (v5.9.15)
The HD seed action was only added to the standard Qt menu bar, which the
skinned GUI hides. Add it to menuOperationsRequested() so users can actually
reach Generate / Reveal-for-backup / Restore from the Operations menu.
2026-06-16 16:24:12 -07:00
SamiAhmed7777 cc57c906b4 Merge PR #6: HD seed-phrase wallet + fast-sync checkpoint/snapshot (v5.9.14)
HD wallet (BIP39/BIP32 seed phrases) - daemon + Qt
2026-06-15 20:44:52 -07:00
Sami e80d672833 checkpoints: add 2206004 checkpoint + UTXO snapshot hash (fast new-node sync) 2026-06-15 20:31:02 -07:00
Sami fcdc9a58b0 ci(lint): checkout secp256k1 submodule for clang-tidy (fixes configure) 2026-06-15 19:26:43 -07:00
Sami 514867c5d9 wallet(HD): flush keypool on seed set so getnewaddress yields HD keys immediately 2026-06-15 19:16:03 -07:00
Sami c464e6c59d wallet(HD): Qt UI - Seed Phrase dialog (generate/restore/backup)
Adds HDSeedDialog (Settings > Seed Phrase) with Generate New / Reveal for Backup / Restore from Phrase, driven by new WalletModel HD methods. Restore rescans the chain. Requires wallet unlock via the standard UnlockContext.
2026-06-15 19:16:03 -07:00
Sami 11ed086d1e 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.
2026-06-15 19:15:43 -07:00
Hermes 5511cfae6b v5.9.14 + pitfall #61 guard: initialize pindexFinalized on startup
ROOT CAUSE of the 2026-06-16 minority-fork reorg:

The v5.9.14 getheaders handler serves headers from pindexGenesisBlock
when a fork peer sends a locator that doesn't match our main chain. The
intent was to help fork peers learn the canonical chain. The bug: this
allows the fork peer to feed us THEIR short chain back via getheaders,
and we accept it because:

1. pindexFinalized is NULL on a fresh restart (the auto-checkpoint code
   in ActivateBestChain() at main.cpp:2459 only sets it when
   !IsInitialBlockDownload(), but a synced daemon restarting with a
   chain tip > 24h stale is considered IBD by the time check at
   main.cpp:1331).

2. With pindexFinalized = NULL, the reorg guard at main.cpp:2198
   short-circuits: 'if (pindexFinalized && pfork->nHeight < ...)'.

3. The fork peer's 3,755-block chain gets accepted, overwriting our
   healthy 2,206,004-block chain.

THE FIX (two parts):

A. init.cpp:1083-1113 — after LoadBlockIndex(), call
   Checkpoints::GetLastCheckpoint(mapBlockIndex) to initialize
   pindexFinalized from the hardcoded checkpoint (block 2,205,000).
   This is a no-op on the first ~10 seconds of the daemon's life
   (during the initial IBD walk), but as soon as we sync past block
   2,205,000, the checkpoint is in mapBlockIndex and pindexFinalized
   is set for the daemon's entire lifetime.

B. main.cpp:4473-4496 — in the getheaders fork-detection branch,
   serve from pindexFinalized->pnext (the block after our last
   finalized checkpoint) instead of pindexGenesisBlock. This is the
   safe equivalent of the v5.9.14 'serve from genesis' logic — the
   peer learns our canonical chain from the most recently finalized
   point forward, and their short fork gets rejected at the reorg
   guard in Reorganize() because the fork point is below
   pindexFinalized.

Combined: a fork peer can no longer drag us below block 2,205,000
because (a) pindexFinalized is always set on a synced restart, and
(b) the reorg guard now sees a non-NULL pindexFinalized and rejects
any fork below it.

Tested manually by simulating a restart with the v5.9.14 binary on
a chain that had been reorged to a minority fork; the new build
refuses the reorg and prints 'STARTUP-CHECKPOINT: pindexFinalized set
to block 2205000' on startup, then 'getheaders: fork detected from
peer ... serving headers from finalized block 2205000' on the first
fork peer's getheaders request.
2026-06-15 18:37:53 -07:00
Sami Ahmed 1dda8b3006 Auto-repair corrupt Tor state file on daemon startup (pitfall #19)
Tor's atomic state-write is: write state.tmp, then rename to state.
If the daemon is killed mid-write (pkill -9, OOM, power loss, disk-full),
the rename can fail and 'state' is left as a regular file instead of a
directory. On next start, Tor's config validator refuses to use it:

  [warn] State file '...' is not a file? Failing.
  [err] set_options: Bug: Acting on config options left us in a broken state. Dying.
  [err] Reading config failed--see warnings above.

The daemon then reports 'Tor failed to start. Triangles requires Tor to
operate.' (the error from the 2026-06-15 TRI-LAPTOP GUI wallet
screenshot) and refuses to come up at all.

Hit before on DNS3 (2026-05-24, fixed by manual 'mv state state.file.bak;
mkdir state' on the operator) and on the laptop today. The fix was always
the same one-liner; this patch makes the daemon do it itself.

Behavior:
- Detect: if tor_data/state exists and is NOT a directory, it's corrupt
- Quarantine: rename to tor_data/state.corrupt-YYYYMMDD-HHMMSS for
  inspection (the user might want to recover the file's contents)
- If rename fails (Windows anti-virus holds the file, etc.), retry with
  remove-then-rename, and as a last resort just remove() so Tor can
  proceed
- Print a clear 'Tor state was a file (corrupt) — quarantined to ...'
  log line so the operator can see it happened
- Tor then recreates state/ as a fresh directory and bootstraps normally

This is a pure-additive change (no existing behavior modified). Builds
clean with the existing C++20 + libtor.a toolchain. No version bump
needed - will roll into the next CI cycle.
2026-06-15 16:29:05 -07:00
Sami Ahmed 68c4e38411 Fix getheaders pnext null bug: serve main chain from genesis, tip-backwards fallback
Two related bugs in the getheaders handler at main.cpp:4441:

1. Locator-mismatch returns 0 headers:
   GetBlockIndex() falls through to pindexGenesisBlock when no locator
   hash matches the main chain. pindexGenesisBlock->pnext is null, so
   the for-loop exits immediately and the peer gets an empty headers
   response. This was the DNS3 headers-first sync stall (pitfall #36):
   'getheaders -1 to 0000...' logged repeatedly.

   Mirror the getblocks handler (line 4387): detect the mismatch, log
   a warning, and serve headers from genesis so the peer can discover
   the canonical chain.

2. Broken pnext chain at any height:
   Even when GetBlockIndex() returns a valid (non-genesis) block, its
   pnext can be null — this happens when LoadBlockIndex() didn't fully
   heal pnext links (e.g., the node was bootstrapped from a snapshot,
   or the chain was interrupted by a crash). On tridock every pnext
   link was null despite 2.2M blocks (the June 11 'Heal pnext links'
   commit addressed a similar symptom for GetKernelStakeModifier() but
   doesn't reach into the getheaders handler).

   Fall back to a tip-backwards walk from pindexBest when pnext is
   null: O(N) but correct, and only triggers on the broken path.

   (References the design in references/getheaders-pnext-fix.md from
   the v5.9.10 deploy notes — that fix was never actually committed,
   only prototyped and dropped during the June 4 git cleanup.)

Bump to v5.9.14 (also embeds the embedded-Tor 0700 fix from ed996c8).

Refs: SKILL.md pitfall #36
2026-06-15 15:43:03 -07:00
Sami Ahmed ed996c8e9d Fix embedded Tor bootstrap (code -1): force 0700 on hidden service dir
Tor's config validator refuses to start a hidden service on any directory
whose permissions are not 0700. The daemon's fs::create_directories() honors
the process umask (0022 on Linux), leaving hidden_service/ at 0755. tor_run_main()
returned -1 with:

  [warn] Permissions on directory .../hidden_service are too permissive.
  [warn] Failed to parse/validate config: Failed to configure rendezvous options.

This was misdiagnosed as a libtor.a build problem (the June 4 rebuild was a
red herring). The real fix is two fs::permissions() calls after create_directories().

Reproduced with a standalone harness linking libtor.a, fixed, SOCKS port 19099
came up in 1 second and Tor began bootstrapping normally.

Also force 0700 on the DataDirectory itself - same validator, same rule.

Refs: SKILL.md pitfall #59
2026-06-15 15:32:28 -07:00
24 changed files with 1221 additions and 6 deletions
+1
View File
@@ -49,6 +49,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Install dependencies + clang-tidy
run: |
+2
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
@@ -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
+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
+2
View File
@@ -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
View File
@@ -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 17
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
+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
+31
View File
@@ -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.
+83 -2
View File
@@ -3771,8 +3771,9 @@ bool FastImportBlockFile()
int64_t nFees = 0;
unsigned int nTxPos = nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION)
- (2 * GetSizeOfCompactSize(0)) + GetSizeOfCompactSize(block.vtx.size());
for (const CTransaction& tx : block.vtx)
for (size_t nTxIdx = 0; nTxIdx < block.vtx.size(); nTxIdx++)
{
const CTransaction& tx = block.vtx[nTxIdx];
uint256 hashTx = tx.GetHash();
CDiskTxPos posThisTx(1, nBlockPos, nTxPos);
txdb.UpdateTxIndex(hashTx, CTxIndex(posThisTx, tx.vout.size()));
@@ -3790,6 +3791,18 @@ bool FastImportBlockFile()
CUtxoEntry utxo;
if (txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, utxo))
nTxValueIn += utxo.nValue;
if (fAddressIndex && !utxo.scriptPubKey.empty() && utxo.nValue != 0)
{
int nAType; uint160 aHash;
if (GetAddressFromScript(utxo.scriptPubKey, nAType, aHash))
{
txdb.EraseAddressUtxo(nAType, aHash, txin.prevout.hash, txin.prevout.n);
int64_t nABal = 0;
txdb.ReadAddressBalance(nAType, aHash, nABal);
nABal -= utxo.nValue;
txdb.WriteAddressBalance(nAType, aHash, nABal);
}
}
txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n);
}
nBlockValueIn += nTxValueIn;
@@ -3808,6 +3821,20 @@ bool FastImportBlockFile()
utxo.fCoinStake = tx.IsCoinStake();
utxo.nTxTime = tx.nTime;
txdb.WriteUtxo(hashTx, k, utxo);
if (fAddressIndex && !tx.vout[k].scriptPubKey.empty() && tx.vout[k].nValue != 0)
{
int nAType; uint160 aHash;
if (GetAddressFromScript(tx.vout[k].scriptPubKey, nAType, aHash))
{
txdb.WriteAddressUtxo(nAType, aHash, hashTx, k,
tx.vout[k].nValue, pindexNew->nHeight, tx.vout[k].scriptPubKey);
int64_t nABal = 0;
txdb.ReadAddressBalance(nAType, aHash, nABal);
nABal += tx.vout[k].nValue;
txdb.WriteAddressBalance(nAType, aHash, nABal);
txdb.WriteAddressTxId(nAType, aHash, pindexNew->nHeight, (int)nTxIdx, hashTx);
}
}
}
}
}
@@ -3859,6 +3886,8 @@ bool FastImportBlockFile()
// Final commit
if (pindexBest)
{
if (fAddressIndex)
UpdateAddressIndexSyncState(txdb, pindexBest);
txdb.WriteHashBestChain(hashBestChain);
// Write sync checkpoint
@@ -4457,8 +4486,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;
+142
View File
@@ -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();
}
+34
View File
@@ -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
+19
View File
@@ -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()
{
+3
View File
@@ -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 */
+46
View File
@@ -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;
}
+6
View File
@@ -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
{
+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;
}
+50
View File
@@ -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
+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);
+35
View File
@@ -8,6 +8,12 @@
#include "checkpoints.h"
#include "util.h"
#include "ui_interface.h"
#include "addressindex.h"
#include <variant>
// defined in main.cpp
extern bool fAddressIndex;
#include <filesystem>
@@ -190,6 +196,22 @@ bool DumpSnapshot(const fs::path& destPath,
return true;
}
// Extract (type, hash160) from a scriptPubKey for the address index.
// Mirrors GetAddressFromScript() in main.cpp (which is file-static there).
static bool SnapAddressFromScript(const CScript& script, int& nType, uint160& hashBytes)
{
CTxDestination dest;
if (!ExtractDestination(script, dest))
return false;
if (const CKeyID* keyId = std::get_if<CKeyID>(&dest)) {
nType = ADDR_TYPE_P2PKH; hashBytes = *keyId; return true;
}
if (const CScriptID* scriptId = std::get_if<CScriptID>(&dest)) {
nType = ADDR_TYPE_P2SH; hashBytes = *scriptId; return true;
}
return false;
}
// ---------------------------------------------------------------------------
// LoadSnapshot - load a UTXO snapshot into a fresh LevelDB
// ---------------------------------------------------------------------------
@@ -383,6 +405,19 @@ bool LoadSnapshot(const fs::path& snapshotPath,
strError = "WriteUtxo failed at index " + std::to_string(i);
break;
}
// Address index: snapshot UTXOs are all unspent -> credit balance + record UTXO.
if (::fAddressIndex && !entry.scriptPubKey.empty() && entry.nValue != 0) {
int nAType; uint160 aHash;
if (SnapAddressFromScript(entry.scriptPubKey, nAType, aHash)) {
txdb.WriteAddressUtxo(nAType, aHash, txhash, nIndex,
entry.nValue, entry.nHeight, entry.scriptPubKey);
int64_t nABal = 0;
txdb.ReadAddressBalance(nAType, aHash, nABal);
nABal += entry.nValue;
txdb.WriteAddressBalance(nAType, aHash, nABal);
}
}
nBatchSize++;
if (nBatchSize >= 50000) {
+95 -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,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;
}
+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);