@spirobel/monero-wallet-api 0.3.18 → 0.4.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/dist/api.d.ts +5 -2
- package/dist/api.js +5 -1
- package/dist/io/atomicWrite.js +3 -1
- package/dist/io/indexedDB.js +7 -2
- package/dist/io/logging.d.ts +40 -0
- package/dist/io/logging.js +172 -0
- package/dist/keypairs-seeds/keypairs.d.ts +21 -0
- package/dist/keypairs-seeds/keypairs.js +21 -0
- package/dist/node-interaction/binaryEndpoints.d.ts +31 -2
- package/dist/node-interaction/binaryEndpoints.js +61 -43
- package/dist/node-interaction/jsonEndpoints.d.ts +257 -130
- package/dist/node-interaction/jsonEndpoints.js +159 -119
- package/dist/node-interaction/nodeUrl.d.ts +21 -6
- package/dist/node-interaction/nodeUrl.js +50 -8
- package/dist/scanning-syncing/backgroundWorker.d.ts +10 -6
- package/dist/scanning-syncing/backgroundWorker.js +88 -39
- package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.d.ts +44 -0
- package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.js +355 -0
- package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.d.ts +15 -0
- package/dist/scanning-syncing/blocksbuffer/blocksbufferCoordination.js +28 -0
- package/dist/scanning-syncing/{scanresult → blocksbuffer}/getBlocksbinBuffer.d.ts +5 -5
- package/dist/scanning-syncing/{scanresult → blocksbuffer}/getBlocksbinBuffer.js +7 -5
- package/dist/scanning-syncing/connectionStatus.d.ts +33 -9
- package/dist/scanning-syncing/connectionStatus.js +89 -19
- package/dist/scanning-syncing/scanSettings.d.ts +21 -0
- package/dist/scanning-syncing/scanSettings.js +29 -0
- package/dist/scanning-syncing/scanresult/reorg.d.ts +0 -11
- package/dist/scanning-syncing/scanresult/reorg.js +39 -16
- package/dist/scanning-syncing/scanresult/scanCache.d.ts +16 -2
- package/dist/scanning-syncing/scanresult/scanCache.js +65 -5
- package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +18 -0
- package/dist/scanning-syncing/scanresult/scanCacheOpened.js +56 -11
- package/dist/scanning-syncing/scanresult/scanCoordination.d.ts +127 -0
- package/dist/scanning-syncing/scanresult/scanCoordination.js +602 -0
- package/dist/scanning-syncing/scanresult/scanLoop.d.ts +20 -0
- package/dist/scanning-syncing/scanresult/scanLoop.js +116 -0
- package/dist/scanning-syncing/scanresult/scanResult.d.ts +24 -3
- package/dist/scanning-syncing/scanresult/scanResult.js +159 -97
- package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
- package/dist/scanning-syncing/worker-mains/coordinator-main.d.ts +1 -0
- package/dist/scanning-syncing/worker-mains/coordinator-main.js +24 -0
- package/dist/scanning-syncing/worker-mains/cpubound-main.d.ts +5 -0
- package/dist/scanning-syncing/worker-mains/cpubound-main.js +122 -0
- package/dist/scanning-syncing/worker-mains/worker.d.ts +1 -1
- package/dist/scanning-syncing/worker-mains/worker.js +62 -6
- package/dist/viewpair/ViewPair.d.ts +30 -7
- package/dist/viewpair/ViewPair.js +56 -118
- package/dist/wasm-processing/wasmFile.js +1 -1
- package/package.json +22 -24
- package/dist/io/extension.d.ts +0 -18
- package/dist/io/extension.js +0 -11
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NodeUrl, type BlockInfo, type CacheRange, type GetBlocksResultMeta } from "../../api";
|
|
2
|
+
import { type ConnectionSatusLastPacket, type ConnectionStatus, type ConnectionStatusSync } from "../connectionStatus";
|
|
3
|
+
export type GetBlocksBinBufferItem = {
|
|
4
|
+
local_uuid: string;
|
|
5
|
+
get_blocks_result_meta: GetBlocksResultMeta;
|
|
6
|
+
data: Uint8Array;
|
|
7
|
+
};
|
|
8
|
+
export declare const MAX_BLOCKS_BUFFER_SIZE = 10;
|
|
9
|
+
export type BlocksBufferIteratorResult = IteratorYieldResult<BlocksBufferLoopResult>;
|
|
10
|
+
export type BlocksBufferLoopResult = ConnectionSatusLastPacket | ConnectionStatusSync | GetBlocksBinBufferItem;
|
|
11
|
+
export declare function blocksBufferFetchLoop(node_url: string, start_height: number, blocks_buffer: GetBlocksBinBufferItem[], // pass by reference
|
|
12
|
+
connection_status: ConnectionStatus, // we make a local copy of this and pass last_packet and sync updates seperately
|
|
13
|
+
max_blocks_buffer_size?: number, anchor_range?: CacheRange, stopSync?: AbortSignal): AsyncGenerator<BlocksBufferLoopResult>;
|
|
14
|
+
export declare function makeBlocksBufferItem(result_meta: GetBlocksResultMeta, get_blocks_bin: Uint8Array): GetBlocksBinBufferItem;
|
|
15
|
+
export declare function popBlocksBufferItemFromIndex(blocks_buffer: GetBlocksBinBufferItem[], index: number): GetBlocksBinBufferItem | undefined;
|
|
16
|
+
export declare function findBufferitemBySplitHeight(blocks_buffer: GetBlocksBinBufferItem[], split_height: BlockInfo): GetBlocksBinBufferItem | undefined;
|
|
17
|
+
export declare function findBufferItemIndexByLocalId(blocks_buffer: GetBlocksBinBufferItem[], local_uuid: string): number;
|
|
18
|
+
export declare function popBlocksBufferItemsFromSplitHeight(blocks_buffer: GetBlocksBinBufferItem[], split_height: BlockInfo): GetBlocksBinBufferItem[];
|
|
19
|
+
export declare function doRPCrequest(nodeUrl: NodeUrl, current_range: CacheRange, stopSync?: AbortSignal): Promise<Uint8Array<ArrayBufferLike>>;
|
|
20
|
+
export type BlocksBufferScanStatus = {
|
|
21
|
+
current_range: CacheRange;
|
|
22
|
+
scanned_ranges: CacheRange[];
|
|
23
|
+
};
|
|
24
|
+
export type BlocksBufferReorgResult = BlocksBufferScanStatus & {
|
|
25
|
+
split_height?: BlockInfo;
|
|
26
|
+
};
|
|
27
|
+
export declare function initScannedRanges(node_url: string, start_height: number, scanned_ranges?: CacheRange[]): Promise<BlocksBufferScanStatus>;
|
|
28
|
+
export declare function updateBlocksBufferScanHeight(current_range: CacheRange, result_meta: GetBlocksResultMeta, scanned_ranges: CacheRange[]): Promise<BlocksBufferReorgResult>;
|
|
29
|
+
export declare function makeNewBlocksBufferScanRange(newRange: CacheRange, scanned_ranges: CacheRange[]): CacheRange;
|
|
30
|
+
export declare function handleBlocksBufferReorg(current_range: CacheRange, result_meta: GetBlocksResultMeta, scanned_ranges: CacheRange[], oldRange: CacheRange): Promise<BlocksBufferReorgResult>;
|
|
31
|
+
/**
|
|
32
|
+
* getBlocks.bin monero RPC call will block clients as peers,
|
|
33
|
+
* after 3 attempts of fetching a height higher than tip,
|
|
34
|
+
* this is ugly but it is what it is, so we need to do a get_info
|
|
35
|
+
* rpc call to get the tip height and reduce the start_height to the tip height,
|
|
36
|
+
* if it is larger than the tip height
|
|
37
|
+
* @param start_height
|
|
38
|
+
* @param node_url
|
|
39
|
+
* @returns Promise<number> a promise with the new potentially reduced start_height
|
|
40
|
+
*/
|
|
41
|
+
export declare function reduceStartHeightToTip(start_height: number, node_url: string): Promise<number>;
|
|
42
|
+
export declare class CatastrophicReorgError extends Error {
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { findRange, get_block_headers_range, get_info, mergeRanges, NodeUrl, sleep, } from "../../api";
|
|
2
|
+
import {} from "../connectionStatus";
|
|
3
|
+
import { log } from "../../io/logging";
|
|
4
|
+
export const MAX_BLOCKS_BUFFER_SIZE = 10;
|
|
5
|
+
// runs forever, fetching blocks from the node.
|
|
6
|
+
// handles scan level reorg detection and catastrophic reorg
|
|
7
|
+
export async function* blocksBufferFetchLoop(node_url, start_height, blocks_buffer, // pass by reference
|
|
8
|
+
connection_status, // we make a local copy of this and pass last_packet and sync updates seperately
|
|
9
|
+
max_blocks_buffer_size = MAX_BLOCKS_BUFFER_SIZE, anchor_range, stopSync) {
|
|
10
|
+
connection_status = structuredClone(connection_status);
|
|
11
|
+
const nodeUrl = await NodeUrl.create(node_url);
|
|
12
|
+
log("blocksBufferFetchLoop", ["NodeUrl:", nodeUrl.node_url, "start:", start_height]);
|
|
13
|
+
start_height = await reduceStartHeightToTip(start_height, nodeUrl.node_url);
|
|
14
|
+
// initialise ranges on first call
|
|
15
|
+
let { current_range, scanned_ranges } = await initScannedRanges(nodeUrl.node_url, start_height, anchor_range ? [anchor_range] : []);
|
|
16
|
+
connection_status.sync.scanned_ranges = scanned_ranges;
|
|
17
|
+
connection_status.last_packet = {
|
|
18
|
+
status: "no_connection_yet",
|
|
19
|
+
bytes_read: 0,
|
|
20
|
+
node_url: nodeUrl.node_url,
|
|
21
|
+
timestamp: new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
yield connection_status.last_packet;
|
|
24
|
+
while (true) {
|
|
25
|
+
// avoid overwriting last_packet in case of catastrophic reorg
|
|
26
|
+
// this generator will block on this. coordinator should rethrow the CatastrophicReorgError
|
|
27
|
+
if (connection_status.last_packet.status === "catastrophic_reorg") {
|
|
28
|
+
yield connection_status.last_packet;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (blocks_buffer.length >= max_blocks_buffer_size) {
|
|
32
|
+
connection_status.last_packet = {
|
|
33
|
+
status: "blocks_buffer_full",
|
|
34
|
+
bytes_read: 0,
|
|
35
|
+
node_url: node_url,
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
};
|
|
38
|
+
yield connection_status.last_packet;
|
|
39
|
+
await sleep(5000);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
log("blocksBufferFetchLoop", ["fetching", nodeUrl.node_url, current_range.start, "->", current_range.end]);
|
|
43
|
+
const get_blocks_bin = await doRPCrequest(nodeUrl, current_range, stopSync);
|
|
44
|
+
const result_meta = await nodeUrl.loadGetBlocksBinResponse(get_blocks_bin);
|
|
45
|
+
log("blocksBufferFetchLoop", "response: " + result_meta.block_infos.length + " blocks");
|
|
46
|
+
connection_status.last_packet = {
|
|
47
|
+
status: "OK",
|
|
48
|
+
bytes_read: get_blocks_bin.length,
|
|
49
|
+
node_url: node_url,
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
};
|
|
52
|
+
yield connection_status.last_packet;
|
|
53
|
+
// no new blocks: at tip, sleep and retry
|
|
54
|
+
if (!result_meta.block_infos.length) {
|
|
55
|
+
await sleep(1000);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const bufferItem = makeBlocksBufferItem(result_meta, get_blocks_bin);
|
|
59
|
+
try {
|
|
60
|
+
const parse_result = await updateBlocksBufferScanHeight(current_range, result_meta, connection_status.sync.scanned_ranges);
|
|
61
|
+
connection_status.sync.scanned_ranges = parse_result.scanned_ranges;
|
|
62
|
+
current_range = parse_result.current_range;
|
|
63
|
+
if (parse_result.split_height) {
|
|
64
|
+
// push a new ReorgInfo entry for this reorg
|
|
65
|
+
if (!connection_status.sync.reorg_info) {
|
|
66
|
+
connection_status.sync.reorg_info = {
|
|
67
|
+
split_heights: [parse_result.split_height],
|
|
68
|
+
removed_outputs: [],
|
|
69
|
+
reverted_spends: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
connection_status.sync.reorg_info.split_heights.push(parse_result.split_height);
|
|
74
|
+
}
|
|
75
|
+
//WE DONT DO THIS. its an elegant little thing that causes trouble
|
|
76
|
+
// as a result we would have to track for each wallet what workitems
|
|
77
|
+
// have been processed already
|
|
78
|
+
// the logic for what has been done becomes much more complex
|
|
79
|
+
// when we dont know what exactly was added
|
|
80
|
+
// NVM got it to work with workToBeDoneForBatch to work without passing explicit blocks
|
|
81
|
+
// pop blocks at/above the split height if there are any
|
|
82
|
+
// (buffer may be empty on a fresh generator start)
|
|
83
|
+
// if (blocks_buffer.length > 0) {
|
|
84
|
+
// popBlocksBufferItemsFromSplitHeight(
|
|
85
|
+
// blocks_buffer,
|
|
86
|
+
// parse_result.split_height,
|
|
87
|
+
// );
|
|
88
|
+
// }
|
|
89
|
+
blocks_buffer.push(bufferItem);
|
|
90
|
+
yield bufferItem;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
blocks_buffer.push(bufferItem);
|
|
94
|
+
yield bufferItem;
|
|
95
|
+
}
|
|
96
|
+
//TODO: only write this to disk after blocks_buffer_changed has been handled by wallets.
|
|
97
|
+
//look for same comment in blocksBufferCoordination.ts
|
|
98
|
+
yield connection_status.sync;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
//cat reorg
|
|
102
|
+
if (error instanceof CatastrophicReorgError) {
|
|
103
|
+
connection_status.last_packet = {
|
|
104
|
+
status: "catastrophic_reorg",
|
|
105
|
+
bytes_read: 0,
|
|
106
|
+
node_url,
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
};
|
|
109
|
+
yield connection_status.last_packet;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function makeBlocksBufferItem(result_meta, get_blocks_bin) {
|
|
118
|
+
const bufferItem = {
|
|
119
|
+
local_uuid: crypto.randomUUID(),
|
|
120
|
+
data: get_blocks_bin,
|
|
121
|
+
get_blocks_result_meta: result_meta,
|
|
122
|
+
};
|
|
123
|
+
return bufferItem;
|
|
124
|
+
}
|
|
125
|
+
export function popBlocksBufferItemFromIndex(blocks_buffer, index) {
|
|
126
|
+
if (index < 0 || index >= blocks_buffer.length)
|
|
127
|
+
return;
|
|
128
|
+
return blocks_buffer.splice(index, 1)[0];
|
|
129
|
+
}
|
|
130
|
+
export function findBufferitemBySplitHeight(blocks_buffer, split_height) {
|
|
131
|
+
for (const item of blocks_buffer) {
|
|
132
|
+
const infos = item.get_blocks_result_meta.block_infos;
|
|
133
|
+
if (!infos?.length)
|
|
134
|
+
continue;
|
|
135
|
+
const start = infos[0].block_height;
|
|
136
|
+
const end = infos.at(-1).block_height;
|
|
137
|
+
if (start <= split_height.block_height &&
|
|
138
|
+
split_height.block_height <= end) {
|
|
139
|
+
return item;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function findBufferItemIndexByLocalId(blocks_buffer, local_uuid) {
|
|
144
|
+
return blocks_buffer.findIndex((item) => item.local_uuid === local_uuid);
|
|
145
|
+
}
|
|
146
|
+
export function popBlocksBufferItemsFromSplitHeight(blocks_buffer, split_height) {
|
|
147
|
+
const found = findBufferitemBySplitHeight(blocks_buffer, split_height);
|
|
148
|
+
if (!found) {
|
|
149
|
+
// buffer is empty or doesn't contain the split height.
|
|
150
|
+
// this can happen on a fresh generator start, nothing to pop.
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
const foundIndex = findBufferItemIndexByLocalId(blocks_buffer, found.local_uuid);
|
|
154
|
+
if (foundIndex < 0) {
|
|
155
|
+
// item was removed between finding it and getting its index
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
// pop from this index onward, everything at or above the split
|
|
159
|
+
const removed = [];
|
|
160
|
+
for (let i = blocks_buffer.length - 1; i >= foundIndex; i--) {
|
|
161
|
+
removed.unshift(popBlocksBufferItemFromIndex(blocks_buffer, i));
|
|
162
|
+
}
|
|
163
|
+
return removed;
|
|
164
|
+
}
|
|
165
|
+
export async function doRPCrequest(nodeUrl, current_range, stopSync) {
|
|
166
|
+
const get_blocks_bin = await nodeUrl.getBlocksBinExecuteRequest({
|
|
167
|
+
block_ids: current_range.block_hashes.map((b) => b.block_hash),
|
|
168
|
+
}, stopSync);
|
|
169
|
+
return get_blocks_bin;
|
|
170
|
+
}
|
|
171
|
+
export async function initScannedRanges(node_url, start_height, scanned_ranges = []) {
|
|
172
|
+
{
|
|
173
|
+
let current_height = start_height;
|
|
174
|
+
// merge existing ranges & find end of current range
|
|
175
|
+
scanned_ranges = mergeRanges(scanned_ranges);
|
|
176
|
+
let current_range = findRange(scanned_ranges, current_height);
|
|
177
|
+
let start_block_hash = current_range?.block_hashes[0];
|
|
178
|
+
if (!start_block_hash) {
|
|
179
|
+
const blockHeaderResponse = (await get_block_headers_range(node_url, {
|
|
180
|
+
start_height,
|
|
181
|
+
end_height: start_height,
|
|
182
|
+
})).headers[0];
|
|
183
|
+
start_block_hash = {
|
|
184
|
+
block_hash: blockHeaderResponse.hash,
|
|
185
|
+
block_height: blockHeaderResponse.height,
|
|
186
|
+
block_timestamp: blockHeaderResponse.timestamp,
|
|
187
|
+
};
|
|
188
|
+
const newRange = {
|
|
189
|
+
start: start_block_hash.block_height,
|
|
190
|
+
end: start_block_hash.block_height,
|
|
191
|
+
block_hashes: [start_block_hash, start_block_hash, start_block_hash],
|
|
192
|
+
};
|
|
193
|
+
current_range = newRange;
|
|
194
|
+
scanned_ranges.push(newRange);
|
|
195
|
+
}
|
|
196
|
+
if (!start_block_hash)
|
|
197
|
+
throw new Error("could not find start block hash");
|
|
198
|
+
if (current_range == null || !current_range?.block_hashes.length)
|
|
199
|
+
throw new Error("current_range was malformed. block_hashes is empty");
|
|
200
|
+
return { current_range, scanned_ranges };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export async function updateBlocksBufferScanHeight(current_range, result_meta, scanned_ranges) {
|
|
204
|
+
let last_block_hash_of_result = result_meta.block_infos.at(-1);
|
|
205
|
+
let current_blockhash = current_range?.block_hashes.at(0);
|
|
206
|
+
if (!current_blockhash)
|
|
207
|
+
throw new Error("current_range passed to updateScanHeight was malformed. block_hashes is empty");
|
|
208
|
+
if (!last_block_hash_of_result)
|
|
209
|
+
return { current_range, scanned_ranges }; // block_infos empty, no change (we are at tip and there was no new block)
|
|
210
|
+
// if last blockhash is undefined it means there was not reorg, we are at tip, block_infos is empty ( no new blocks )
|
|
211
|
+
const oldRange = findRange(scanned_ranges, current_blockhash.block_height);
|
|
212
|
+
if (!oldRange)
|
|
213
|
+
throw new Error(`could not find scan range for height ${current_blockhash.block_height},\n that means the blocks in the response from getBlocks.bin do not overlap\n with the scanned ranges in the cache. This should not happen, as even if \n we are starting from a new start_height that has been supplied to scanWithCache,\n it has been found as an existing range in the cache, or it has been\n added as a new range before we started scannning.`);
|
|
214
|
+
// now we need to find the block_infos of old range in the new geblocksbin response result block_infos
|
|
215
|
+
// if we cant find the new range, there was a reorg and we need to clean all outputs after that and log what happened
|
|
216
|
+
let first_block_hash = result_meta.block_infos.at(0);
|
|
217
|
+
if (!first_block_hash)
|
|
218
|
+
return { current_range, scanned_ranges }; // block_infos empty, no change (we are at tip and there was no new block) current_range; // should never happen, if there is last_block_hash there should be first_block_hash
|
|
219
|
+
// if the first block hash in the response is not the same as the last block hash in the old range, there was a reorg
|
|
220
|
+
if (!(first_block_hash.block_hash === current_blockhash.block_hash)) {
|
|
221
|
+
return await handleBlocksBufferReorg(current_range, result_meta, scanned_ranges, oldRange);
|
|
222
|
+
}
|
|
223
|
+
// scan only happens in one direction,
|
|
224
|
+
// to scan earlier ranges: abort and recall with smaller start_height
|
|
225
|
+
// getblocksbin will return up to 1000 blocks at once
|
|
226
|
+
// so this should never happen, except if we just popped a block (but that case is handled above in the reorg case)
|
|
227
|
+
if (current_blockhash.block_height > last_block_hash_of_result.block_height)
|
|
228
|
+
throw new Error(`current scan height was larger than block height of last block from latest scan result. \n Most likely connected to faulty node / catastrophic reorg.\n current height: ${current_blockhash.block_height}, new height: ${last_block_hash_of_result.block_height}`);
|
|
229
|
+
// guard against degenerate single-block ranges
|
|
230
|
+
if (current_blockhash.block_height === last_block_hash_of_result.block_height) {
|
|
231
|
+
oldRange.end = last_block_hash_of_result.block_height;
|
|
232
|
+
oldRange.block_hashes[0] = last_block_hash_of_result;
|
|
233
|
+
return { current_range: oldRange, scanned_ranges };
|
|
234
|
+
}
|
|
235
|
+
// 1. add new scanned range
|
|
236
|
+
let anchor = undefined;
|
|
237
|
+
let anchor_candidate = undefined;
|
|
238
|
+
if (oldRange.block_hashes.length >= 3) {
|
|
239
|
+
const old_anchor = oldRange.block_hashes.at(-1);
|
|
240
|
+
const old_anchor_candidate = oldRange.block_hashes.at(-2);
|
|
241
|
+
anchor = old_anchor;
|
|
242
|
+
anchor_candidate = old_anchor_candidate;
|
|
243
|
+
if (
|
|
244
|
+
// if the old range has an anchor, and the anchor is more than 200 blocks old
|
|
245
|
+
typeof old_anchor?.block_height === "number" &&
|
|
246
|
+
current_blockhash.block_height - old_anchor.block_height > 200) {
|
|
247
|
+
anchor = old_anchor_candidate; // use the anchor_candidate as anchor
|
|
248
|
+
// new anchor_candidate: is the one 100 blocks in, or the old scan tip
|
|
249
|
+
anchor_candidate =
|
|
250
|
+
result_meta.block_infos.slice(-100)[0] || oldRange?.block_hashes.at(0); // use use the old scan tip as anchor candidate
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// if there is no old anchor, use the one 100 blocks in, or the last block hash
|
|
254
|
+
anchor =
|
|
255
|
+
anchor ||
|
|
256
|
+
result_meta.block_infos.slice(-100)[0] ||
|
|
257
|
+
last_block_hash_of_result;
|
|
258
|
+
// carry over the old anchor candidate or use the last block
|
|
259
|
+
anchor_candidate = anchor_candidate || last_block_hash_of_result;
|
|
260
|
+
const newRange = {
|
|
261
|
+
start: current_blockhash.block_height,
|
|
262
|
+
end: last_block_hash_of_result.block_height,
|
|
263
|
+
block_hashes: [last_block_hash_of_result, anchor_candidate, anchor],
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
current_range: makeNewBlocksBufferScanRange(newRange, scanned_ranges),
|
|
267
|
+
scanned_ranges,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
export function makeNewBlocksBufferScanRange(newRange, scanned_ranges) {
|
|
271
|
+
scanned_ranges.push(newRange);
|
|
272
|
+
// merge existing ranges & find end of current range
|
|
273
|
+
const merged = mergeRanges(scanned_ranges);
|
|
274
|
+
scanned_ranges.length = 0;
|
|
275
|
+
scanned_ranges.push(...merged);
|
|
276
|
+
// if we hit the end of a range we already scanned, move scan tip to the end
|
|
277
|
+
const fastForward = findRange(scanned_ranges, newRange.end);
|
|
278
|
+
if (fastForward)
|
|
279
|
+
return fastForward;
|
|
280
|
+
return newRange;
|
|
281
|
+
}
|
|
282
|
+
export async function handleBlocksBufferReorg(current_range, result_meta, scanned_ranges, oldRange) {
|
|
283
|
+
// we need to check where anchor candidate is and if not found, try the same for anchor
|
|
284
|
+
// if else throw on catastrophic reorg
|
|
285
|
+
for (const block_hash of oldRange.block_hashes) {
|
|
286
|
+
const split_height_index = result_meta.block_infos.findIndex((b) => b.block_hash === block_hash.block_hash);
|
|
287
|
+
const split_height = result_meta.block_infos[split_height_index];
|
|
288
|
+
// still a chance to find the split height, (could be candidate_anchor or anchor)
|
|
289
|
+
if (!split_height)
|
|
290
|
+
continue;
|
|
291
|
+
// we found the split height
|
|
292
|
+
// find current range in scanned ranges and change its end value + latest_block_hash
|
|
293
|
+
oldRange.end = split_height.block_height;
|
|
294
|
+
oldRange.block_hashes[0] = split_height;
|
|
295
|
+
// fix current_range
|
|
296
|
+
let anchor = result_meta.block_infos.slice(-100)[0]; // if we got lots of new blocks
|
|
297
|
+
const old_anchor = oldRange.block_hashes.at(-1);
|
|
298
|
+
if (!anchor &&
|
|
299
|
+
old_anchor &&
|
|
300
|
+
split_height.block_height > old_anchor.block_height //if we did not get many new blocks + split height was candidate anchor
|
|
301
|
+
) {
|
|
302
|
+
anchor = old_anchor; // we keep the anchor the same
|
|
303
|
+
}
|
|
304
|
+
if (!anchor)
|
|
305
|
+
anchor = split_height;
|
|
306
|
+
let last_block_hash_of_result = result_meta.block_infos.at(-1);
|
|
307
|
+
const end = last_block_hash_of_result.block_height;
|
|
308
|
+
const start = current_range.start > end ? end : current_range.start;
|
|
309
|
+
// preserve the old deep anchor (3rd element) so subsequent reorgs
|
|
310
|
+
// have a fallback beyond the split height. without this, both anchors
|
|
311
|
+
// point to the same height and the next pop past it is catastrophic.
|
|
312
|
+
const oldDeepAnchor = oldRange.block_hashes.at(-1);
|
|
313
|
+
const newAnchor = oldDeepAnchor && oldDeepAnchor.block_height < anchor.block_height
|
|
314
|
+
? oldDeepAnchor
|
|
315
|
+
: anchor;
|
|
316
|
+
const newRange = {
|
|
317
|
+
start,
|
|
318
|
+
end,
|
|
319
|
+
block_hashes: [last_block_hash_of_result, anchor, newAnchor],
|
|
320
|
+
};
|
|
321
|
+
return {
|
|
322
|
+
current_range: makeNewBlocksBufferScanRange(newRange, scanned_ranges),
|
|
323
|
+
scanned_ranges,
|
|
324
|
+
split_height,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
// we tried all the block hashes and could not find the split height
|
|
328
|
+
throw new CatastrophicReorgError("Could not find reorg split height. Most likely connected to faulty node / catastrophic reorg.");
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* getBlocks.bin monero RPC call will block clients as peers,
|
|
332
|
+
* after 3 attempts of fetching a height higher than tip,
|
|
333
|
+
* this is ugly but it is what it is, so we need to do a get_info
|
|
334
|
+
* rpc call to get the tip height and reduce the start_height to the tip height,
|
|
335
|
+
* if it is larger than the tip height
|
|
336
|
+
* @param start_height
|
|
337
|
+
* @param node_url
|
|
338
|
+
* @returns Promise<number> a promise with the new potentially reduced start_height
|
|
339
|
+
*/
|
|
340
|
+
export async function reduceStartHeightToTip(start_height, node_url) {
|
|
341
|
+
const getInfo = await get_info(node_url);
|
|
342
|
+
if (start_height > getInfo.height - 1) {
|
|
343
|
+
const oldStartHeight = start_height;
|
|
344
|
+
start_height = getInfo.height - 1;
|
|
345
|
+
log("reduceStartHeightToTip", [
|
|
346
|
+
"start height was larger than daemon height, setting start_height=" +
|
|
347
|
+
start_height,
|
|
348
|
+
" oldStartHeight=" + oldStartHeight,
|
|
349
|
+
]);
|
|
350
|
+
}
|
|
351
|
+
return start_height;
|
|
352
|
+
}
|
|
353
|
+
export class CatastrophicReorgError extends Error {
|
|
354
|
+
name = "CatastrophicReorgError";
|
|
355
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type GetBlocksBinBufferItem, type ConnectionStatus, type BlocksBufferLoopResult, type CacheRange } from "../../api";
|
|
2
|
+
export type SetupBlocksBufferGeneratorParams = {
|
|
3
|
+
nodeUrl: string;
|
|
4
|
+
startHeight: number;
|
|
5
|
+
anchor_range?: CacheRange;
|
|
6
|
+
stopSync?: AbortSignal;
|
|
7
|
+
maxBufferItems?: number;
|
|
8
|
+
scanSettingsPath?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function setupBlocksBufferGenerator(params: SetupBlocksBufferGeneratorParams): Promise<{
|
|
11
|
+
generator: AsyncGenerator<BlocksBufferLoopResult, any, any>;
|
|
12
|
+
blocksBuffer: GetBlocksBinBufferItem[];
|
|
13
|
+
connection_status: ConnectionStatus;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function handleConnectionStatusChanges(event: BlocksBufferLoopResult, scanSettingsPath?: string): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { blocksBufferFetchLoop, readWriteConnectionStatusFile, CatastrophicReorgError, readOrInitConnectionStatus, } from "../../api";
|
|
2
|
+
import { log } from "../../io/logging";
|
|
3
|
+
export async function setupBlocksBufferGenerator(params) {
|
|
4
|
+
const blocksBuffer = [];
|
|
5
|
+
const connection_status = await readOrInitConnectionStatus(params.scanSettingsPath);
|
|
6
|
+
const generator = blocksBufferFetchLoop(params.nodeUrl, params.startHeight, blocksBuffer, connection_status, params.maxBufferItems, params.anchor_range, params.stopSync);
|
|
7
|
+
return { generator, blocksBuffer, connection_status };
|
|
8
|
+
}
|
|
9
|
+
export async function handleConnectionStatusChanges(event, scanSettingsPath) {
|
|
10
|
+
if ("local_uuid" in event && typeof event.local_uuid === "string")
|
|
11
|
+
return;
|
|
12
|
+
if ("status" in event) {
|
|
13
|
+
await readWriteConnectionStatusFile((cs2) => {
|
|
14
|
+
cs2.last_packet = event;
|
|
15
|
+
}, scanSettingsPath);
|
|
16
|
+
if (event.status === "catastrophic_reorg") {
|
|
17
|
+
log("handleConnectionStatusChanges", "catastrophic reorg, stopping");
|
|
18
|
+
throw new CatastrophicReorgError("[blocksbufferCoordinator] catastrophic reorg, stopping");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if ("scanned_ranges" in event) {
|
|
22
|
+
//TODO: only write this to disk after blocks_buffer_changed has been handled by wallets.
|
|
23
|
+
//look for same comment in blocksBufferFetchLoop.ts
|
|
24
|
+
await readWriteConnectionStatusFile((cs2) => {
|
|
25
|
+
cs2.sync = event;
|
|
26
|
+
}, scanSettingsPath);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { BlockInfo, GetBlocksBinRequest, ViewPair } from "../../api";
|
|
2
2
|
import type { ScanSetting } from "../scanSettings";
|
|
3
|
-
import { type CacheRange } from "
|
|
4
|
-
export declare function writeGetblocksBinBuffer(getBlocksBinResponseContent: Uint8Array, block_infos: BlockInfo[], pathPrefix?: string): Promise<
|
|
5
|
-
export type
|
|
3
|
+
import { type CacheRange } from "../scanresult/scanCache";
|
|
4
|
+
export declare function writeGetblocksBinBuffer(getBlocksBinResponseContent: Uint8Array, block_infos: BlockInfo[], pathPrefix?: string): Promise<GetBlocksBinBufferItemFilename | undefined>;
|
|
5
|
+
export type GetBlocksBinBufferItemFilename = {
|
|
6
6
|
start: number;
|
|
7
7
|
end: number;
|
|
8
8
|
filename: string;
|
|
9
9
|
date: string;
|
|
10
10
|
last_block_hash: string;
|
|
11
11
|
};
|
|
12
|
-
export declare function readGetblocksBinBuffer(current_height: number, pathPrefix?: string): Promise<
|
|
12
|
+
export declare function readGetblocksBinBuffer(current_height: number, pathPrefix?: string): Promise<GetBlocksBinBufferItemFilename[]>;
|
|
13
13
|
export type BlocksGenerator = AsyncGenerator<Uint8Array>;
|
|
14
14
|
export type SlaveViewPair = {
|
|
15
15
|
viewpair: ViewPair;
|
|
@@ -22,7 +22,7 @@ declare module "bun" {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
export declare function trimGetBlocksBinBuffer(nonHaltedWallets: ScanSetting[], pathPrefix?: string): Promise<void>;
|
|
25
|
-
export declare function readGetblocksBinBufferItems(pathPrefix?: string): Promise<
|
|
25
|
+
export declare function readGetblocksBinBufferItems(pathPrefix?: string): Promise<GetBlocksBinBufferItemFilename[]>;
|
|
26
26
|
export interface HasGetBlocksBinExecuteRequestMethod {
|
|
27
27
|
getBlocksBinExecuteRequest: (params: GetBlocksBinRequest, stopSync?: AbortSignal) => Promise<Uint8Array>;
|
|
28
28
|
}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { atomicWrite } from "../../io/atomicWrite";
|
|
2
2
|
import { readDir } from "../../io/readDir";
|
|
3
|
-
import { readCacheFileDefaultLocation } from "
|
|
3
|
+
import { readCacheFileDefaultLocation, } from "../scanresult/scanCache";
|
|
4
4
|
export async function writeGetblocksBinBuffer(getBlocksBinResponseContent, block_infos, pathPrefix) {
|
|
5
5
|
if (!block_infos.length)
|
|
6
6
|
return;
|
|
7
7
|
const start = block_infos[0].block_height;
|
|
8
|
-
const end = block_infos.at(-1)
|
|
9
|
-
const block_hash = block_infos.at(-1)
|
|
8
|
+
const end = block_infos.at(-1).block_height;
|
|
9
|
+
const block_hash = block_infos.at(-1).block_hash;
|
|
10
10
|
const bufferItems = await readGetblocksBinBufferItems(pathPrefix);
|
|
11
11
|
if (bufferItems.find((bi) => bi.last_block_hash === block_hash))
|
|
12
12
|
return;
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const date = Date.now().toString();
|
|
14
|
+
const filename = `${date}-${start}-${end}-${block_hash}-getblocks.bin`;
|
|
15
|
+
await atomicWrite(`${pathPrefix ?? ""}getblocksbinbuffer/${filename}`, getBlocksBinResponseContent);
|
|
16
|
+
return { start, end, filename, date, last_block_hash: block_hash };
|
|
15
17
|
}
|
|
16
18
|
export async function readGetblocksBinBuffer(current_height, pathPrefix) {
|
|
17
19
|
const bufferItems = await readGetblocksBinBufferItems(pathPrefix);
|
|
@@ -1,15 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
import { type CacheRange, type ReorgInfo } from "../api";
|
|
2
|
+
export type ConnectionStatusOptions = "OK" | "partial_read" | "connection_failed" | "blocks_buffer_full" | "no_connection_yet" | "catastrophic_reorg";
|
|
3
|
+
export type ConnectionSatusLastPacket = {
|
|
4
|
+
status: ConnectionStatusOptions;
|
|
5
|
+
bytes_read: number;
|
|
6
|
+
node_url: string;
|
|
7
|
+
timestamp: string;
|
|
8
|
+
};
|
|
9
|
+
export type ConnectionStatusSync = {
|
|
10
|
+
reorg_info?: ReorgInfo;
|
|
11
|
+
scanned_ranges: CacheRange[];
|
|
12
|
+
daemon_height: number;
|
|
13
|
+
current_scan_height: number;
|
|
14
|
+
eta: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
};
|
|
2
17
|
export type ConnectionStatus = {
|
|
3
|
-
last_packet:
|
|
4
|
-
|
|
5
|
-
bytes_read: number;
|
|
6
|
-
node_url: string;
|
|
7
|
-
timestamp: string;
|
|
8
|
-
};
|
|
18
|
+
last_packet: ConnectionSatusLastPacket;
|
|
19
|
+
sync: ConnectionStatusSync;
|
|
9
20
|
};
|
|
10
21
|
export declare const DEFAULT_CONNECTION_STATUS_PREFIX = "ConnectionStatus-";
|
|
22
|
+
export declare function msToHHMM(ms: number): string;
|
|
23
|
+
export declare function emptyConnectionStatus(overrides?: Partial<ConnectionStatus>): ConnectionStatus;
|
|
24
|
+
export declare function updateSyncETA(daemon_height: number, current_scan_height: number, last_height: number | null, last_timestamp: number | null, scan_settings_path?: string): Promise<{
|
|
25
|
+
last_height: number;
|
|
26
|
+
last_timestamp: number;
|
|
27
|
+
}>;
|
|
11
28
|
export declare function connectionStatusFilePath(scan_settings_path?: string): string;
|
|
12
29
|
export declare function readConnectionStatusDefaultLocation(scan_settings_path?: string): Promise<ConnectionStatus | undefined>;
|
|
13
30
|
export declare function readConnectionStatusFile(connectionStatusFilePath: string): Promise<ConnectionStatus | undefined>;
|
|
14
|
-
export declare function writeConnectionStatusFile(
|
|
15
|
-
export declare function readWriteConnectionStatusFile(writeCB: (cs: ConnectionStatus
|
|
31
|
+
export declare function writeConnectionStatusFile(connectionStatus: ConnectionStatus, scan_settings_path?: string): Promise<number>;
|
|
32
|
+
export declare function readWriteConnectionStatusFile(writeCB: (cs: ConnectionStatus) => void, scan_settings_path?: string): Promise<ConnectionStatus>;
|
|
33
|
+
/**
|
|
34
|
+
* read the connection status from disk (typical /path/to/ConnectionStatus-ScanSettings.json)
|
|
35
|
+
* and persist empty inital status if not found
|
|
36
|
+
* @param scan_settings_path path to the scan settings file /path/to/ScanSettings.json
|
|
37
|
+
* @returns connection status or empty initalized connection status
|
|
38
|
+
*/
|
|
39
|
+
export declare function readOrInitConnectionStatus(scan_settings_path?: string): Promise<ConnectionStatus>;
|