@tradelayerprotocol/tradelayer 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +13 -0
- package/.claude/skills/tl-algo/SKILL.md +255 -0
- package/.gitattributes +2 -0
- package/.github/workflows/publish.yaml +26 -0
- package/4mm.js +163 -0
- package/LICENSE +21 -0
- package/NPMSwapRefactor.zip +0 -0
- package/README.md +217 -0
- package/address.sh +26 -0
- package/algoAPI.js +581 -0
- package/analyzepsbt.js +92 -0
- package/apiEx.js +99 -0
- package/bb_hyperscalper.js +290 -0
- package/bbo_demo.js +111 -0
- package/buyer.js +622 -0
- package/client.js +50 -0
- package/createTxTest.js +26 -0
- package/createWallet.js +75 -0
- package/daytrader.js +531 -0
- package/decodeTest.js +69 -0
- package/fundingManager.js +144 -0
- package/index.js +4 -0
- package/listener.js +27 -0
- package/litecoreTxBuilder.js +1128 -0
- package/mmEx.js +356 -0
- package/networks.js +51 -0
- package/orderbook.js +200 -0
- package/package.json +34 -0
- package/perTradeQueue.js +36 -0
- package/projectsTLNPMTLNPM/package-lock.json +162 -0
- package/projectsTLNPMTLNPM/package.json +5 -0
- package/quick.js +32 -0
- package/quickFut.js +37 -0
- package/quickSell.js +37 -0
- package/relayerClient.js +117 -0
- package/run4mm.js +80 -0
- package/run_bbo_tracker.js +241 -0
- package/seller.js +443 -0
- package/session.js +45 -0
- package/setup-lin-ltc.sh +139 -0
- package/setup-lin.sh +203 -0
- package/setup-win-ltc.bat +108 -0
- package/setup-win.bat +167 -0
- package/spam_screamer_futures.js +222 -0
- package/tradelayer.js/.gitattributes +2 -0
- package/tradelayer.js/README.md +2 -0
- package/tradelayer.js/oldTests/activationTest.js +6 -0
- package/tradelayer.js/oldTests/base58.test.js +23 -0
- package/tradelayer.js/oldTests/base64Decode.test.js +16 -0
- package/tradelayer.js/oldTests/blocksRefactor.js +140 -0
- package/tradelayer.js/oldTests/checkVestBalance.js +25 -0
- package/tradelayer.js/oldTests/consensusHashProto.js +151 -0
- package/tradelayer.js/oldTests/contractOrderbook.js +243 -0
- package/tradelayer.js/oldTests/createPayload.js +0 -0
- package/tradelayer.js/oldTests/createTestnetAddr.js +43 -0
- package/tradelayer.js/oldTests/decode.js +205 -0
- package/tradelayer.js/oldTests/decodeTest.js +50 -0
- package/tradelayer.js/oldTests/displayTallyMap.js +19 -0
- package/tradelayer.js/oldTests/encodeDecode.js +340 -0
- package/tradelayer.js/oldTests/expressTest.js +29 -0
- package/tradelayer.js/oldTests/extractBlocksVanilla.js +214 -0
- package/tradelayer.js/oldTests/extractBlocksVanillaa.js +179 -0
- package/tradelayer.js/oldTests/extractPubkeyTest.js +60 -0
- package/tradelayer.js/oldTests/fillInputCacheProto.js +111 -0
- package/tradelayer.js/oldTests/getRawTxTest.js +22 -0
- package/tradelayer.js/oldTests/indexTest.js +26 -0
- package/tradelayer.js/oldTests/initTokensTest.js +32 -0
- package/tradelayer.js/oldTests/interfaceChild.js +129 -0
- package/tradelayer.js/oldTests/listenerChild.js +112 -0
- package/tradelayer.js/oldTests/opdecode.js +26 -0
- package/tradelayer.js/oldTests/options.js +79 -0
- package/tradelayer.js/oldTests/optxtest.js +116 -0
- package/tradelayer.js/oldTests/optxtest1.js +64 -0
- package/tradelayer.js/oldTests/oracle.test.js +32 -0
- package/tradelayer.js/oldTests/orderbook.test.js +36 -0
- package/tradelayer.js/oldTests/parsing.js +93 -0
- package/tradelayer.js/oldTests/payload.js +13 -0
- package/tradelayer.js/oldTests/persistenceUnitTest.js +23 -0
- package/tradelayer.js/oldTests/property.test.js +53 -0
- package/tradelayer.js/oldTests/propertyLevel.js +75 -0
- package/tradelayer.js/oldTests/propertyTest.js +32 -0
- package/tradelayer.js/oldTests/queryAddressTest.js +17 -0
- package/tradelayer.js/oldTests/salter.js +14 -0
- package/tradelayer.js/oldTests/tally.js +81 -0
- package/tradelayer.js/oldTests/tally.test.js +48 -0
- package/tradelayer.js/oldTests/tally2.js +124 -0
- package/tradelayer.js/oldTests/tally3.js +142 -0
- package/tradelayer.js/oldTests/tallyDiag.js +38 -0
- package/tradelayer.js/oldTests/testGetRaw.js +40 -0
- package/tradelayer.js/oldTests/testHexConvert.js +47 -0
- package/tradelayer.js/oldTests/testNewEncoding.js +96 -0
- package/tradelayer.js/oldTests/testNewEncoding2.js +113 -0
- package/tradelayer.js/oldTests/testNewEncoding3 +112 -0
- package/tradelayer.js/oldTests/testNewEncoding3.js +168 -0
- package/tradelayer.js/oldTests/testOPReturn.js +102 -0
- package/tradelayer.js/oldTests/testPayload.js +23 -0
- package/tradelayer.js/oldTests/testRaw.js +50 -0
- package/tradelayer.js/oldTests/testSendTooMuch.js +20 -0
- package/tradelayer.js/oldTests/testTxBuild +28 -0
- package/tradelayer.js/oldTests/testTxBuild.js +42 -0
- package/tradelayer.js/oldTests/tokenOrderbook.js +243 -0
- package/tradelayer.js/oldTests/txUtilsA.js +515 -0
- package/tradelayer.js/oldTests/validityUnitTest.js +53 -0
- package/tradelayer.js/oldTests/vaults.js +72 -0
- package/tradelayer.js/oldTests/volumeIndex.js +117 -0
- package/tradelayer.js/oldTests/volumeIndex2.js +88 -0
- package/tradelayer.js/output_base64.txt +1 -0
- package/tradelayer.js/package-lock.json +9967 -0
- package/tradelayer.js/package.json +61 -0
- package/tradelayer.js/server/index.js +88 -0
- package/tradelayer.js/server/litecoind.exe +0 -0
- package/tradelayer.js/src/activation.js +303 -0
- package/tradelayer.js/src/adjuster.js +77 -0
- package/tradelayer.js/src/amm.js +400 -0
- package/tradelayer.js/src/base256.js +55 -0
- package/tradelayer.js/src/base94.js +79 -0
- package/tradelayer.js/src/channels.js +1163 -0
- package/tradelayer.js/src/clearing.js +3109 -0
- package/tradelayer.js/src/clearlist.js +364 -0
- package/tradelayer.js/src/client.js +295 -0
- package/tradelayer.js/src/consensus.js +613 -0
- package/tradelayer.js/src/contractRegistry.js +964 -0
- package/tradelayer.js/src/db.js +89 -0
- package/tradelayer.js/src/init.js +24 -0
- package/tradelayer.js/src/insurance.js +347 -0
- package/tradelayer.js/src/interface.js +218 -0
- package/tradelayer.js/src/interfaceExpress.js +178 -0
- package/tradelayer.js/src/iou.js +509 -0
- package/tradelayer.js/src/listener.js +226 -0
- package/tradelayer.js/src/logic.js +1702 -0
- package/tradelayer.js/src/main.js +927 -0
- package/tradelayer.js/src/marginMap.js +2165 -0
- package/tradelayer.js/src/options.js +126 -0
- package/tradelayer.js/src/oracle.js +394 -0
- package/tradelayer.js/src/orderbook.js +4123 -0
- package/tradelayer.js/src/persistence.js +554 -0
- package/tradelayer.js/src/property.js +411 -0
- package/tradelayer.js/src/reOrg.js +41 -0
- package/tradelayer.js/src/scaling.js +145 -0
- package/tradelayer.js/src/tally.js +1275 -0
- package/tradelayer.js/src/tradeHistoryManager.js +552 -0
- package/tradelayer.js/src/txDecoder.js +584 -0
- package/tradelayer.js/src/txEncoder.js +610 -0
- package/tradelayer.js/src/txIndex.js +502 -0
- package/tradelayer.js/src/txUtils.js +1392 -0
- package/tradelayer.js/src/types.js +429 -0
- package/tradelayer.js/src/validity.js +3077 -0
- package/tradelayer.js/src/vaults.js +430 -0
- package/tradelayer.js/src/vesting.js +491 -0
- package/tradelayer.js/src/volumeIndex.js +618 -0
- package/tradelayer.js/src/walletInterface.js +220 -0
- package/tradelayer.js/src/walletListener.js +665 -0
- package/tradelayer.js/tests/256decode.js +82 -0
- package/tradelayer.js/tests/UTXOracle.js +205 -0
- package/tradelayer.js/tests/base94test.js +23 -0
- package/tradelayer.js/tests/cancelTxTest.js +62 -0
- package/tradelayer.js/tests/contractInterfaceTest.js +48 -0
- package/tradelayer.js/tests/decimalTest.js +65 -0
- package/tradelayer.js/tests/decoderTest.js +100 -0
- package/tradelayer.js/tests/deltaCount.js +47 -0
- package/tradelayer.js/tests/deltaCount2.js +60 -0
- package/tradelayer.js/tests/interfaceTest.js +37 -0
- package/tradelayer.js/tests/mainTest.js +53 -0
- package/tradelayer.js/tests/makeActivationTest.js +24 -0
- package/tradelayer.js/tests/maxHeightTest.js +49 -0
- package/tradelayer.js/tests/reverseHash.js +72 -0
- package/tradelayer.js/tests/sensitiveConsoleOutput.txt +267 -0
- package/tradelayer.js/tests/tallyTest.js +40 -0
- package/tradelayer.js/tests/testBuybacks.js +46 -0
- package/tradelayer.js/tests/testCodeHash.js +49 -0
- package/tradelayer.js/tests/testConsensusHash.js +91 -0
- package/tradelayer.js/tests/testDecode.js +30 -0
- package/tradelayer.js/tests/testEncodingLengths.js +129 -0
- package/tradelayer.js/tests/testGetTx +32 -0
- package/tradelayer.js/tests/testGetTx.js +32 -0
- package/tradelayer.js/tests/testHexHash.js +32 -0
- package/tradelayer.js/tests/testIndexHash.js +35 -0
- package/tradelayer.js/tests/testInitContracts.js +38 -0
- package/tradelayer.js/tests/testMaxConsensus.js +12 -0
- package/tradelayer.js/tests/testMaxSynth.js +44 -0
- package/tradelayer.js/tests/testMint.js +21 -0
- package/tradelayer.js/tests/testNetwork.js +33 -0
- package/tradelayer.js/tests/testOrderbookLoad.js +62 -0
- package/tradelayer.js/tests/testRebates.js +32 -0
- package/tradelayer.js/tests/testRedeem.js +22 -0
- package/tradelayer.js/tests/testTokenTrade.js +39 -0
- package/tradelayer.js/tests/testTxBuild.js +42 -0
- package/tradelayer.js/tests/testUTXOTrade.js +27 -0
- package/tradelayer.js/tests/tokenTradeHistory.js +27 -0
- package/tradelayer.js/tests/tradeFutures.js +40 -0
- package/tradelayer.js/tests/tradeHistoryExample.js +35 -0
- package/tradelayer.js/tests/tradeHistoryLoad.js +15 -0
- package/tradelayer.js/tests/txScanTest.js +134 -0
- package/tradelayer.js/tests/validateTest.js +136 -0
- package/tradelayer.js/tests/vestingTest.js +37 -0
- package/tradelayer.js/utils/activateMainnet.js +59 -0
- package/tradelayer.js/utils/activateMainnetDoge.js +63 -0
- package/tradelayer.js/utils/autocompactdb.js +23 -0
- package/tradelayer.js/utils/base64toHex.js +32 -0
- package/tradelayer.js/utils/broadcastDoge.js +38 -0
- package/tradelayer.js/utils/calcRedeem.js +19 -0
- package/tradelayer.js/utils/checkNetwork.js +27 -0
- package/tradelayer.js/utils/createAddress.js +48 -0
- package/tradelayer.js/utils/createAttestation.js +133 -0
- package/tradelayer.js/utils/createContract.js +118 -0
- package/tradelayer.js/utils/createOracle.js +94 -0
- package/tradelayer.js/utils/createwallet.js +20 -0
- package/tradelayer.js/utils/crossFuturesTrades.js +57 -0
- package/tradelayer.js/utils/crossTokenTrades.js +62 -0
- package/tradelayer.js/utils/dumpPriv.js +29 -0
- package/tradelayer.js/utils/generateChannel.js +34 -0
- package/tradelayer.js/utils/getInfo.js +21 -0
- package/tradelayer.js/utils/hardWipe.js +20 -0
- package/tradelayer.js/utils/hexTo64.js +16 -0
- package/tradelayer.js/utils/importAddress.js +28 -0
- package/tradelayer.js/utils/importpriv.js +20 -0
- package/tradelayer.js/utils/issueOracleContract.js +67 -0
- package/tradelayer.js/utils/issueTokens.js +41 -0
- package/tradelayer.js/utils/listunspent.js +66 -0
- package/tradelayer.js/utils/litecoinClient.js +30 -0
- package/tradelayer.js/utils/loadwallet.js +20 -0
- package/tradelayer.js/utils/publishOracle.js +113 -0
- package/tradelayer.js/utils/sendActivation.js +21 -0
- package/tradelayer.js/utils/sendChannelContractTrade.js +34 -0
- package/tradelayer.js/utils/sendChannelTokenTrade.js +34 -0
- package/tradelayer.js/utils/sendCommit.js +24 -0
- package/tradelayer.js/utils/sendDoge.js +62 -0
- package/tradelayer.js/utils/sendDogeMain.js +67 -0
- package/tradelayer.js/utils/sendDogeTx.js +46 -0
- package/tradelayer.js/utils/sendLTC.js +63 -0
- package/tradelayer.js/utils/sendMainnet.js +62 -0
- package/tradelayer.js/utils/sendTransfer.js +19 -0
- package/tradelayer.js/utils/sendVestTest.js +88 -0
- package/tradelayer.js/utils/sendWithdrawal.js +26 -0
- package/tradelayer.js/utils/simpleStart.js +8 -0
- package/tradelayer.js/utils/startStop.js +27 -0
- package/tradelayer.js/utils/structuredTrades.js +136 -0
- package/tradelayer.js/utils/verifySignature.js +90 -0
- package/tradelayer.js/utils/verifyWitnessAndScriptPubkey.js +41 -0
- package/tradelayer.js/utils/walletCache.js +172 -0
- package/tradelayer.js/utils/walletContractInterface.js +48 -0
- package/tradelayer.js/utils/walletFetchTxs.js +66 -0
- package/tradelayer.js/utils/walletUtils.js +97 -0
- package/tradelayer.js/utils/wipeDB.js +55 -0
- package/tradelayer.js/utils/wipeDBNotTx.js +50 -0
- package/txEncoder.js +529 -0
- package/utility.js +28 -0
- package/verifymessage.js +38 -0
- package/ws-transport.js +311 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
// persistence.js — Final Production-Ready Version
|
|
2
|
+
// ---------------------------------------------------------------
|
|
3
|
+
// • API identical to your previous version (getInstance + auto-init)
|
|
4
|
+
// • Option 1 network handling ("ltc" + test→"ltc-test")
|
|
5
|
+
// • Safe snapshots, safe restores, Windows-compatible
|
|
6
|
+
// • No nested backup folders inside dbDir
|
|
7
|
+
//---------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const db = require("./db.js");
|
|
12
|
+
const ClientWrapper = require("./client.js");
|
|
13
|
+
const ConsensusDatabase = require("./consensus.js");
|
|
14
|
+
|
|
15
|
+
// small async sleep helper
|
|
16
|
+
const delay = ms => new Promise(res => setTimeout(res, ms));
|
|
17
|
+
|
|
18
|
+
class Persistence {
|
|
19
|
+
static instance = null;
|
|
20
|
+
|
|
21
|
+
//-------------------------------------------------------
|
|
22
|
+
// STATIC GET INSTANCE (Main calls this!)
|
|
23
|
+
//-------------------------------------------------------
|
|
24
|
+
static async getInstance(options = {}) {
|
|
25
|
+
if (!Persistence.instance) {
|
|
26
|
+
Persistence.instance = new Persistence(options);
|
|
27
|
+
await Persistence.instance.init(options);
|
|
28
|
+
}
|
|
29
|
+
return Persistence.instance;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//-------------------------------------------------------
|
|
33
|
+
// CONSTRUCTOR (does NOT auto-init)
|
|
34
|
+
//-------------------------------------------------------
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
if (Persistence.instance) return Persistence.instance;
|
|
37
|
+
|
|
38
|
+
// store raw fields (network/test may not be valid yet)
|
|
39
|
+
this.rawNetwork = options.network || null;
|
|
40
|
+
this.rawTestFlag = typeof options.test === "boolean" ? options.test : null;
|
|
41
|
+
this.snapshotInterval =
|
|
42
|
+
typeof options.snapshotInterval === "number"
|
|
43
|
+
? options.snapshotInterval
|
|
44
|
+
: 1000;
|
|
45
|
+
|
|
46
|
+
// directories will be set after network detection
|
|
47
|
+
this.networkFull = null;
|
|
48
|
+
this.dbDir = null;
|
|
49
|
+
this.snapshotsDir = null;
|
|
50
|
+
this.backupsDir = null;
|
|
51
|
+
|
|
52
|
+
this.client = null;
|
|
53
|
+
|
|
54
|
+
Persistence.instance = this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//-------------------------------------------------------
|
|
58
|
+
// INIT — auto-called by getInstance
|
|
59
|
+
//-------------------------------------------------------
|
|
60
|
+
async init(options = {}) {
|
|
61
|
+
// we need RPC client before determining network if missing
|
|
62
|
+
if (!this.client) {
|
|
63
|
+
this.client = await ClientWrapper.getInstance(true);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// compute network name
|
|
67
|
+
let chain = this.rawNetwork;
|
|
68
|
+
let isTest = this.rawTestFlag;
|
|
69
|
+
|
|
70
|
+
if (!chain) {
|
|
71
|
+
chain = await this.client.getChain(); // "ltc" or "btc"
|
|
72
|
+
}
|
|
73
|
+
if (isTest === null) {
|
|
74
|
+
isTest = await this.client.getTest(); // boolean
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
chain = chain.toLowerCase().trim();
|
|
78
|
+
this.networkFull = `${chain}-${isTest ? "test" : "main"}`;
|
|
79
|
+
|
|
80
|
+
const baseDir = path.join(__dirname, "..", "nedb-data");
|
|
81
|
+
|
|
82
|
+
this.dbDir = path.join(baseDir, this.networkFull);
|
|
83
|
+
this.snapshotsDir = path.join(baseDir, `${this.networkFull}-snapshots`);
|
|
84
|
+
this.backupsDir = path.join(baseDir, `${this.networkFull}-backups`);
|
|
85
|
+
|
|
86
|
+
await fs.promises.mkdir(this.dbDir, { recursive: true });
|
|
87
|
+
await fs.promises.mkdir(this.snapshotsDir, { recursive: true });
|
|
88
|
+
await fs.promises.mkdir(this.backupsDir, { recursive: true });
|
|
89
|
+
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//-------------------------------------------------------
|
|
94
|
+
// RECORD & FETCH BLOCK HEADERS
|
|
95
|
+
//-------------------------------------------------------
|
|
96
|
+
async recordBlockHeader(height, hash, prevHash) {
|
|
97
|
+
const base = await db.getDatabase("persistence");
|
|
98
|
+
await base.updateAsync(
|
|
99
|
+
{ _id: `block-${height}` },
|
|
100
|
+
{
|
|
101
|
+
$set: {
|
|
102
|
+
height,
|
|
103
|
+
hash,
|
|
104
|
+
prevHash,
|
|
105
|
+
createdAt: Date.now(),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{ upsert: true }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getBlockHeader(height) {
|
|
113
|
+
const base = await db.getDatabase("persistence");
|
|
114
|
+
return base.findOneAsync({ _id: `block-${height}` });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async getLastKnownBlock() {
|
|
118
|
+
const base = await db.getDatabase("persistence");
|
|
119
|
+
const docs = await base.findAsync({ height: { $exists: true } });
|
|
120
|
+
if (!docs || docs.length === 0) return null;
|
|
121
|
+
docs.sort((a, b) => a.height - b.height);
|
|
122
|
+
return docs[docs.length - 1];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Hybrid reorg detection:
|
|
127
|
+
* 1. Fast path: compare incoming prevhash to our stored blockhash for (h-1)
|
|
128
|
+
* 2. If mismatch → confirm via RPC (canonical)
|
|
129
|
+
* 3. Only if both disagree → actual reorg
|
|
130
|
+
*
|
|
131
|
+
* Returns:
|
|
132
|
+
* false → no reorg
|
|
133
|
+
* true → confirmed reorg
|
|
134
|
+
*/
|
|
135
|
+
async checkForReorgForNewBlock(blockHeight, incomingPrevHash) {
|
|
136
|
+
try {
|
|
137
|
+
const prevHeight = blockHeight - 1;
|
|
138
|
+
if (prevHeight <= 0) return false; // genesis cannot reorg
|
|
139
|
+
|
|
140
|
+
const persistenceDB = await db.getDatabase("persistence");
|
|
141
|
+
|
|
142
|
+
// -----------------------------------------
|
|
143
|
+
// FAST PATH: LOCAL HASH CHAIN CHECK
|
|
144
|
+
// -----------------------------------------
|
|
145
|
+
const prevDoc = await persistenceDB.findOneAsync({ height: prevHeight });
|
|
146
|
+
let localPrevHash = null;
|
|
147
|
+
|
|
148
|
+
if (prevDoc && prevDoc.hash) {
|
|
149
|
+
localPrevHash = prevDoc.hash;
|
|
150
|
+
|
|
151
|
+
// If hashes match → definitely no reorg
|
|
152
|
+
if (localPrevHash === incomingPrevHash) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// If mismatch → MAYBE reorg, confirm via RPC
|
|
157
|
+
console.log(
|
|
158
|
+
`[reorg?] Local mismatch at height=${blockHeight}. ` +
|
|
159
|
+
`localPrev=${localPrevHash}, incomingPrev=${incomingPrevHash}. Confirming via RPC...`
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
// No local previous hash — could be startup or partial DB
|
|
163
|
+
// Must confirm with RPC
|
|
164
|
+
console.log(
|
|
165
|
+
`[reorg?] No local metadata at height=${prevHeight}. Checking RPC for canonical chain...`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let nodePrevHash;
|
|
170
|
+
try {
|
|
171
|
+
const rpcPrevHash = await this.client.getBlockHash(prevHeight);
|
|
172
|
+
const rpcPrevBlock = await this.client.getBlock(rpcPrevHash);
|
|
173
|
+
nodePrevHash = rpcPrevBlock.hash;
|
|
174
|
+
} catch (e) {
|
|
175
|
+
logger.warn("[reorg] RPC failure during reorg check", e);
|
|
176
|
+
return true //{ reorg: true, depth: 1, uncertain: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (nodePrevHash !== localPrevHash) {
|
|
180
|
+
return {
|
|
181
|
+
reorg: true,
|
|
182
|
+
depth: 1,
|
|
183
|
+
expected: nodePrevHash,
|
|
184
|
+
received: localPrevHash
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Now compare canonical vs incoming
|
|
189
|
+
if (nodePrevHash !== incomingPrevHash) {
|
|
190
|
+
console.log(
|
|
191
|
+
`[reorg CONFIRMED] height=${blockHeight}. ` +
|
|
192
|
+
`nodePrev=${nodePrevHash}, incomingPrev=${incomingPrevHash}`
|
|
193
|
+
);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// RPC confirms no reorg → local mismatch was stale or snapshot-induced
|
|
198
|
+
console.log(
|
|
199
|
+
`[reorg false-positive resolved] Local mismatch at height=${blockHeight} ` +
|
|
200
|
+
`but RPC chain matches incoming block. No reorg.`
|
|
201
|
+
);
|
|
202
|
+
return false;
|
|
203
|
+
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error('[persistence] Error in hybrid reorg detection:', err);
|
|
206
|
+
// safer to assume no reorg in case of RPC failure — do not reorg-loop
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
//-------------------------------------------------------
|
|
213
|
+
// REORG CHECK FOR NEW BLOCK (top-of-chain equality removed)
|
|
214
|
+
//-------------------------------------------------------
|
|
215
|
+
async detectAndHandleReorg() {
|
|
216
|
+
const last = await this.getLastKnownBlock();
|
|
217
|
+
if (!last) return null;
|
|
218
|
+
|
|
219
|
+
const nodeHash = await this.client.getBlockHash(last.height);
|
|
220
|
+
|
|
221
|
+
if (nodeHash === last.hash) return null;
|
|
222
|
+
|
|
223
|
+
console.warn(
|
|
224
|
+
`Reorg suspected: local(${last.height})=${last.hash}, node=${nodeHash}`
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
//---------------------------------------------------
|
|
228
|
+
// Find common ancestor (still useful for debugging)
|
|
229
|
+
//---------------------------------------------------
|
|
230
|
+
let h = last.height;
|
|
231
|
+
let ancestor = 0;
|
|
232
|
+
|
|
233
|
+
while (h > 0) {
|
|
234
|
+
const local = await this.getBlockHeader(h);
|
|
235
|
+
const node = await this.client.getBlockHash(h);
|
|
236
|
+
if (local && local.hash === node) {
|
|
237
|
+
ancestor = h;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
h--;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
//---------------------------------------------------
|
|
244
|
+
// Choose a snapshot — prefer the one closest BELOW last.height
|
|
245
|
+
//---------------------------------------------------
|
|
246
|
+
const snaps = await this.listSnapshots();
|
|
247
|
+
if (!snaps.length) {
|
|
248
|
+
console.warn("No snapshots available — full replay needed");
|
|
249
|
+
return {
|
|
250
|
+
restoredFrom: 0,
|
|
251
|
+
commonAncestor: ancestor,
|
|
252
|
+
lastLocal: last.height,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// pick the highest snapshot <= last.height
|
|
257
|
+
let best = null;
|
|
258
|
+
for (const s of snaps) {
|
|
259
|
+
if (s.blockHeight <= last.height) {
|
|
260
|
+
if (!best || s.blockHeight > best.blockHeight) best = s;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!best) {
|
|
264
|
+
best = snaps[0];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await this.restoreSnapshot(best);
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
restoredFrom: best.blockHeight,
|
|
271
|
+
commonAncestor: ancestor,
|
|
272
|
+
lastLocal: last.height,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
//-------------------------------------------------------
|
|
277
|
+
// CHECKPOINT (called by Main)
|
|
278
|
+
//-------------------------------------------------------
|
|
279
|
+
async maybeCheckpoint(blockHeight) {
|
|
280
|
+
if (this.snapshotInterval <= 0) return;
|
|
281
|
+
if (blockHeight % this.snapshotInterval !== 0) return;
|
|
282
|
+
|
|
283
|
+
// Compute consensus state hash
|
|
284
|
+
const consensusHash = await ConsensusDatabase.stateConsensusHash();
|
|
285
|
+
|
|
286
|
+
// Store hash keyed by height
|
|
287
|
+
await ConsensusDatabase.storeConsensusHash(blockHeight, consensusHash);
|
|
288
|
+
|
|
289
|
+
// Create snapshot (EBUSY-safe)
|
|
290
|
+
await this.createSnapshot(blockHeight, consensusHash);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
//-------------------------------------------------------
|
|
295
|
+
// SNAPSHOT CREATION (EBUSY-safe)
|
|
296
|
+
//-------------------------------------------------------
|
|
297
|
+
async createSnapshot(height, consensusHash) {
|
|
298
|
+
if (this.snapshotInterval <= 0) return;
|
|
299
|
+
|
|
300
|
+
const short = (consensusHash || "nohash").slice(0, 12);
|
|
301
|
+
const dirName = `${height}-${short}`;
|
|
302
|
+
|
|
303
|
+
const snapPath = path.join(this.snapshotsDir, dirName);
|
|
304
|
+
await fs.promises.mkdir(snapPath, { recursive: true });
|
|
305
|
+
|
|
306
|
+
const files = await fs.promises.readdir(this.dbDir);
|
|
307
|
+
const copied = [];
|
|
308
|
+
|
|
309
|
+
for (const file of files) {
|
|
310
|
+
if (!file.endsWith(".db")) continue;
|
|
311
|
+
const src = path.join(this.dbDir, file);
|
|
312
|
+
const dst = path.join(snapPath, file);
|
|
313
|
+
await this.safeCopy(src, dst);
|
|
314
|
+
copied.push(file);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
await fs.promises.writeFile(
|
|
318
|
+
path.join(snapPath, "meta.json"),
|
|
319
|
+
JSON.stringify(
|
|
320
|
+
{
|
|
321
|
+
blockHeight: height,
|
|
322
|
+
consensusHash,
|
|
323
|
+
createdAt: new Date().toISOString(),
|
|
324
|
+
files: copied,
|
|
325
|
+
},
|
|
326
|
+
null,
|
|
327
|
+
2
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
await this.pruneOldSnapshots(2);
|
|
332
|
+
await this.updateMaxProcessed(height)
|
|
333
|
+
await this.cleanupPersistence(height)
|
|
334
|
+
console.log(`Snapshot created at ${height} (${dirName})`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Delete all persistence entries older than the checkpointHeight.
|
|
339
|
+
*/
|
|
340
|
+
async cleanupPersistence(checkpointHeight) {
|
|
341
|
+
const persistenceDB = await db.getDatabase("persistence");
|
|
342
|
+
let height = checkpointHeight-1000
|
|
343
|
+
// delete everything older than the new checkpoint
|
|
344
|
+
await persistenceDB.removeAsync(
|
|
345
|
+
{ height: { $lt: height } },
|
|
346
|
+
{ multi: true }
|
|
347
|
+
);
|
|
348
|
+
console.log(`[persistence] Cleaned entries < ${checkpointHeight}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async updateMaxProcessed(height) {
|
|
352
|
+
const consensusDB = await db.getDatabase('consensus'); // Access the consensus sub-database
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
// Upsert the document with _id = 'MaxProcessedHeight'
|
|
356
|
+
await consensusDB.updateAsync(
|
|
357
|
+
{ _id: 'MaxProcessedHeight' },
|
|
358
|
+
{ $set: { value: height } },
|
|
359
|
+
{ upsert: true }
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
//console.log('MaxProcessedHeight updated:', height);
|
|
363
|
+
return height;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error('Error updating MaxProcessedHeight:', error);
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
//-------------------------------------------------------
|
|
372
|
+
// RESTORE SNAPSHOT (EBUSY-SAFE)
|
|
373
|
+
//-------------------------------------------------------
|
|
374
|
+
async restoreSnapshot(meta) {
|
|
375
|
+
const dir = path.join(this.snapshotsDir, meta.dir || `${meta.blockHeight}`);
|
|
376
|
+
|
|
377
|
+
// -----------------------------------------------------------------
|
|
378
|
+
// PRUNE OLD BACKUPS: keep only the most recent backup-* directory
|
|
379
|
+
// -----------------------------------------------------------------
|
|
380
|
+
try {
|
|
381
|
+
const backupEntries = await fs.promises.readdir(this.backupsDir);
|
|
382
|
+
const backups = [];
|
|
383
|
+
|
|
384
|
+
for (const name of backupEntries) {
|
|
385
|
+
const fullPath = path.join(this.backupsDir, name);
|
|
386
|
+
const stat = await fs.promises.stat(fullPath);
|
|
387
|
+
|
|
388
|
+
// only consider directories that look like "backup-<something>"
|
|
389
|
+
if (!stat.isDirectory()) continue;
|
|
390
|
+
if (!name.startsWith('backup-')) continue;
|
|
391
|
+
|
|
392
|
+
backups.push({
|
|
393
|
+
name,
|
|
394
|
+
fullPath,
|
|
395
|
+
mtime: stat.mtimeMs,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (backups.length > 1) {
|
|
400
|
+
// sort by newest first
|
|
401
|
+
backups.sort((a, b) => b.mtime - a.mtime);
|
|
402
|
+
|
|
403
|
+
// keep the newest one, delete all others
|
|
404
|
+
for (let i = 1; i < backups.length; i++) {
|
|
405
|
+
try {
|
|
406
|
+
await fs.promises.rm(backups[i].fullPath, {
|
|
407
|
+
recursive: true,
|
|
408
|
+
force: true,
|
|
409
|
+
});
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.error(
|
|
412
|
+
'[persistence] Error removing old backup dir:',
|
|
413
|
+
backups[i].fullPath,
|
|
414
|
+
e
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} catch (e) {
|
|
420
|
+
console.error('[persistence] Error pruning backup dirs:', e);
|
|
421
|
+
// non-fatal: continue restoring snapshot
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// -------------------------------------------------
|
|
425
|
+
// CREATE NEW BACKUP OF CURRENT DB STATE
|
|
426
|
+
// -------------------------------------------------
|
|
427
|
+
const backupDir = path.join(
|
|
428
|
+
this.backupsDir,
|
|
429
|
+
`backup-${Date.now()}`
|
|
430
|
+
);
|
|
431
|
+
await fs.promises.mkdir(backupDir, { recursive: true });
|
|
432
|
+
|
|
433
|
+
const curr = await fs.promises.readdir(this.dbDir);
|
|
434
|
+
for (const f of curr) {
|
|
435
|
+
const src = path.join(this.dbDir, f);
|
|
436
|
+
const dst = path.join(backupDir, f);
|
|
437
|
+
await this.safeCopy(src, dst);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// clear current dbDir of ONLY .db files
|
|
441
|
+
for (const f of curr) {
|
|
442
|
+
if (!f.endsWith(".db")) continue;
|
|
443
|
+
const p = path.join(this.dbDir, f);
|
|
444
|
+
await this.safeDelete(p);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// restore
|
|
448
|
+
for (const f of meta.files) {
|
|
449
|
+
const src = path.join(dir, f);
|
|
450
|
+
const dst = path.join(this.dbDir, f);
|
|
451
|
+
await this.safeCopy(src, dst);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
this.saveTrackHeight(meta.blockHeight)
|
|
455
|
+
|
|
456
|
+
console.log(
|
|
457
|
+
`Restored DB from snapshot ${meta.dir} (block=${meta.blockHeight})`
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async saveTrackHeight(saveHeight){
|
|
463
|
+
const base = await db.getDatabase('consensus')
|
|
464
|
+
await base.updateAsync(
|
|
465
|
+
{ _id: 'TrackHeight' },
|
|
466
|
+
{ $set: { value: saveHeight } },
|
|
467
|
+
{ upsert: true }
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
//-------------------------------------------------------
|
|
474
|
+
// LIST + PRUNE SNAPSHOTS
|
|
475
|
+
//-------------------------------------------------------
|
|
476
|
+
async listSnapshots() {
|
|
477
|
+
try {
|
|
478
|
+
const entries = await fs.promises.readdir(this.snapshotsDir, {
|
|
479
|
+
withFileTypes: true,
|
|
480
|
+
});
|
|
481
|
+
const dirs = entries.filter(e => e.isDirectory()).map(d => d.name);
|
|
482
|
+
|
|
483
|
+
const snaps = [];
|
|
484
|
+
for (const dir of dirs) {
|
|
485
|
+
try {
|
|
486
|
+
const meta = JSON.parse(
|
|
487
|
+
await fs.promises.readFile(
|
|
488
|
+
path.join(this.snapshotsDir, dir, "meta.json"),
|
|
489
|
+
"utf8"
|
|
490
|
+
)
|
|
491
|
+
);
|
|
492
|
+
meta.dir = dir;
|
|
493
|
+
snaps.push(meta);
|
|
494
|
+
} catch (e) {
|
|
495
|
+
console.warn(`Invalid snapshot meta in ${dir}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
snaps.sort((a, b) => a.blockHeight - b.blockHeight);
|
|
500
|
+
return snaps;
|
|
501
|
+
} catch (e) {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async pruneOldSnapshots(keep = 2) {
|
|
507
|
+
const snaps = await this.listSnapshots();
|
|
508
|
+
if (snaps.length <= keep) return;
|
|
509
|
+
|
|
510
|
+
const toDelete = snaps.slice(0, snaps.length - keep);
|
|
511
|
+
for (const s of toDelete) {
|
|
512
|
+
const p = path.join(this.snapshotsDir, s.dir);
|
|
513
|
+
await fs.promises.rm(p, { recursive: true, force: true });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
//-------------------------------------------------------
|
|
518
|
+
// EBUSY-SAFE COPY + DELETE
|
|
519
|
+
//-------------------------------------------------------
|
|
520
|
+
async safeCopy(src, dst) {
|
|
521
|
+
for (let i = 0; i < 5; i++) {
|
|
522
|
+
try {
|
|
523
|
+
await fs.promises.copyFile(src, dst);
|
|
524
|
+
return;
|
|
525
|
+
} catch (e) {
|
|
526
|
+
if (e.code === "EBUSY" || e.code === "EPERM") {
|
|
527
|
+
await delay(50);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
throw e;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
throw new Error(`safeCopy failed: ${src} → ${dst}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async safeDelete(p) {
|
|
537
|
+
for (let i = 0; i < 5; i++) {
|
|
538
|
+
try {
|
|
539
|
+
await fs.promises.unlink(p);
|
|
540
|
+
return;
|
|
541
|
+
} catch (e) {
|
|
542
|
+
if (e.code === "EBUSY" || e.code === "EPERM") {
|
|
543
|
+
await delay(50);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (e.code === "ENOENT") return;
|
|
547
|
+
throw e;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
throw new Error(`safeDelete failed: ${p}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
module.exports = Persistence;
|