@@ -14,6 +14,19 @@ const clap = @import("clap");
14
14
const rpc = @import ("rpc/rpc.zig" );
15
15
const db = @import ("db/db.zig" );
16
16
const sqlite = @import ("sqlite" );
17
+ const Thread = std .Thread ;
18
+ const WaitGroup = Thread .WaitGroup ;
19
+
20
+ const KEYS_GAP = 100 ;
21
+
22
+ fn generateKeys (pubkeys : * std .AutoHashMap ([20 ]u8 , keypath.KeyPath (5 )), kp : keypath .KeyPath (4 ), extended_pubkey : bip32.ExtendedPublicKey , start : usize , end : usize ) ! void {
23
+ for (start .. end ) | i | {
24
+ const index = @as (u32 , @intCast (i ));
25
+ const pubkey = try bip32 .deriveChildFromExtendedPublicKey (extended_pubkey , index );
26
+ const pubkey_hash = try pubkey .key .toHash ();
27
+ try pubkeys .put (pubkey_hash , kp .extendPath (index , false ));
28
+ }
29
+ }
17
30
18
31
pub fn main () ! void {
19
32
std .debug .print ("WALL-E. Bitcoin Wallet written in Zig.\n Indexer...\n " , .{});
@@ -87,7 +100,7 @@ pub fn main() !void {
87
100
}
88
101
}
89
102
90
- const block_height = try rpc .getBlockCount (allocator , & client , rpc_location , auth );
103
+ var block_height = try rpc .getBlockCount (allocator , & client , rpc_location , auth );
91
104
std .debug .print ("Total blocks {d}\n " , .{block_height });
92
105
std .debug .print ("Current blocks {?d}\n " , .{current_block_height });
93
106
if (current_block_height != null and block_height == current_block_height .? ) {
@@ -109,8 +122,17 @@ pub fn main() !void {
109
122
// use hashmap to store public key hash for fast check
110
123
var pubkeys = std .AutoHashMap ([20 ]u8 , keypath .KeyPath (5 )).init (allocator );
111
124
defer pubkeys .deinit ();
125
+
126
+ // remove, useless
112
127
var keypaths = std .AutoHashMap (keypath .KeyPath (5 ), keypath .Descriptor ).init (allocator );
113
128
defer keypaths .deinit ();
129
+
130
+ // this is used to store the last used index for every keypath (both internal and external) for every descriptor.
131
+ // it is necessary to keep track of the gap between the last generated key and the last used index
132
+ // if gap < KEYS_GAP, generate missing keys
133
+ var keypath_last_used_index = std .AutoHashMap (keypath .KeyPath (4 ), u32 ).init (allocator );
134
+ defer keypath_last_used_index .deinit ();
135
+
114
136
var descriptors_map = std .AutoHashMap (keypath .KeyPath (3 ), [111 ]u8 ).init (allocator );
115
137
defer descriptors_map .deinit ();
116
138
for (descriptors ) | descriptor | {
@@ -123,47 +145,30 @@ pub fn main() !void {
123
145
// For every descriptor we get the latest used index (if one) and generate all the keys to this latest index + 1
124
146
// Otherwise we only derive the first keypath (.../0)
125
147
for (descriptors ) | descriptor | {
126
- const keypath_cap = descriptor .keypath .getStrCap (null ) + 2 ; // + 2 for /0 or /1 (internal / external)
127
- const keypath_internal_str = try allocator .alloc (u8 , keypath_cap );
128
- defer allocator .free (keypath_internal_str );
129
- const keypath_external_str = try allocator .alloc (u8 , keypath_cap );
130
- defer allocator .free (keypath_external_str );
131
- const partial_keypath_str = try descriptor .keypath .toStr (allocator , null );
132
- defer allocator .free (partial_keypath_str );
133
- _ = try std .fmt .bufPrint (keypath_internal_str , "{s}/{d}" , .{ partial_keypath_str , keypath .change_internal_chain });
134
- _ = try std .fmt .bufPrint (keypath_external_str , "{s}/{d}" , .{ partial_keypath_str , keypath .change_external_chain });
135
-
136
- const last_internal_index = try db .getLastUsedIndexFromOutputs (& database , keypath_internal_str );
137
- const last_external_index = try db .getLastUsedIndexFromOutputs (& database , keypath_internal_str );
138
- var last_indexes : [2 ]i64 = [2 ]i64 { -1 , -1 };
139
- if (last_internal_index != null ) {
140
- last_indexes [0 ] = last_internal_index .? ;
141
- }
142
- if (last_external_index != null ) {
143
- last_indexes [1 ] = last_external_index .? ;
144
- }
145
-
146
- for (last_indexes , 0.. ) | last_index , t | {
147
- // This depends on the order specified above. Improve this behaviour pls.
148
- const change_type : u32 = if (t == 0 ) keypath .change_internal_chain else keypath .change_external_chain ;
149
- for (0.. @as (usize , @intCast (last_index + 2 ))) | i | {
150
- const pubkey_addr = descriptors_map .get (descriptor .keypath );
151
- if (pubkey_addr == null ) {
152
- std .debug .print ("descriptor not found for path {d}'/{d}'/{d}'\n " , .{ descriptor .keypath .path [0 ].value , descriptor .keypath .path [1 ].value , descriptor .keypath .path [2 ].value });
153
- continue ;
154
- }
155
-
156
- const kp = keypath .KeyPath (5 ){ .path = [5 ]keypath.KeyPathElement { descriptor .keypath .path [0 ], descriptor .keypath .path [1 ], descriptor .keypath .path [2 ], keypath.KeyPathElement { .value = change_type , .is_hardened = false }, keypath.KeyPathElement { .value = @as (u32 , @intCast (i )), .is_hardened = false } } };
157
-
158
- const extended_pubkey = try ExtendedPublicKey .fromAddress (pubkey_addr .? );
159
- try keypaths .put (kp , keypath.Descriptor { .extended_key = pubkey_addr .? , .keypath = descriptor .keypath , .private = false });
160
-
161
- const account_kp = keypath .KeyPath (2 ){ .path = [2 ]keypath.KeyPathElement { keypath.KeyPathElement { .value = kp .path [3 ].value , .is_hardened = false }, keypath.KeyPathElement { .value = kp .path [4 ].value , .is_hardened = false } } };
162
- const pubkey = try bip32 .deriveChildFromKeyPath (bip32 .ExtendedPublicKey , extended_pubkey , 2 , account_kp );
163
- const pubkey_hash = try pubkey .key .toHash ();
164
- try pubkeys .put (pubkey_hash , kp );
165
- }
166
- }
148
+ const internal_keypath = descriptor .keypath .extendPath (keypath .internal_chain , false );
149
+ const external_keypath = descriptor .keypath .extendPath (keypath .external_chain , false );
150
+ const internal_keypath_str = try internal_keypath .toStr (allocator , null );
151
+ defer allocator .free (internal_keypath_str );
152
+ const external_keypath_str = try external_keypath .toStr (allocator , null );
153
+ defer allocator .free (external_keypath_str );
154
+
155
+ const last_internal_index = try db .getLastUsedIndexFromOutputs (& database , internal_keypath_str );
156
+ const last_external_index = try db .getLastUsedIndexFromOutputs (& database , external_keypath_str );
157
+
158
+ const descriptor_pubkey_addr = descriptors_map .get (descriptor .keypath ).? ;
159
+ const descriptor_pubkey = try ExtendedPublicKey .fromAddress (descriptor_pubkey_addr );
160
+ const internal_pubkey = try bip32 .deriveChildFromExtendedPublicKey (descriptor_pubkey , keypath .internal_chain );
161
+ const external_pubkey = try bip32 .deriveChildFromExtendedPublicKey (descriptor_pubkey , keypath .external_chain );
162
+
163
+ const new_internal_last_index = if (last_internal_index == null ) KEYS_GAP else last_internal_index .? + KEYS_GAP ;
164
+ const new_external_last_index = if (last_external_index == null ) KEYS_GAP else last_external_index .? + KEYS_GAP ;
165
+
166
+ // 100 gap
167
+ try generateKeys (& pubkeys , internal_keypath , internal_pubkey , 0 , new_internal_last_index );
168
+ try generateKeys (& pubkeys , external_keypath , external_pubkey , 0 , new_external_last_index );
169
+
170
+ try keypath_last_used_index .put (internal_keypath , new_internal_last_index );
171
+ try keypath_last_used_index .put (external_keypath , new_external_last_index );
167
172
}
168
173
169
174
std .debug .print ("keys generated\n " , .{});
@@ -173,94 +178,60 @@ pub fn main() !void {
173
178
defer progressbar .end ();
174
179
175
180
const start : usize = if (current_block_height == null ) 0 else current_block_height .? + 1 ;
181
+ // align to the current block
182
+ var i : usize = start ;
183
+ while (true ) {
184
+ if (i >= block_height ) {
185
+ block_height = try rpc .getBlockCount (allocator , & client , res .args .location .? , auth );
186
+ }
176
187
177
- for (start .. block_height + 1 ) | i | {
178
- // [72]u8 is for txid + vout in hex format
179
- var outputs = std .ArrayList (Output ).init (aa );
180
- // [64]u8 is for txid, bool is for isCoinbase
181
- var relevant_transactions = std .AutoHashMap ([32 ]u8 , bool ).init (aa );
182
188
const blockhash = try rpc .getBlockHash (allocator , & client , res .args .location .? , auth , i );
183
-
184
189
const raw_transactions = try rpc .getBlockRawTx (aa , & client , res .args .location .? , auth , blockhash );
185
190
var raw_transactions_map = std .AutoHashMap ([32 ]u8 , []u8 ).init (aa );
186
191
187
- const block_transactions = try aa . alloc ( tx .Transaction , raw_transactions . len );
188
- for (raw_transactions , 0 .. ) | tx_raw , j | {
192
+ var block_transactions = std . AutoHashMap ([ 32 ] u8 , tx .Transaction ). init ( aa );
193
+ for (raw_transactions ) | tx_raw | {
189
194
const transaction = try tx .decodeRawTx (aa , tx_raw );
190
- block_transactions [j ] = transaction ;
195
+ try raw_transactions_map .put (try transaction .txid (), tx_raw );
196
+ try block_transactions .put (try transaction .txid (), transaction );
191
197
}
192
198
193
- // Following BIP44 a wallet must not allow the derivation of a new address if the previous one is not used
194
- // So we start by creating 1 new key (both internal and external)
195
- // Everytime we found a new output we need to generate the next key and re-index the same block to be sure all outputs are included
196
- while (true ) blk : {
197
- for (block_transactions , 0.. ) | transaction , k | {
198
- const raw = raw_transactions [k ];
199
- const txid = try transaction .txid ();
200
- try raw_transactions_map .put (txid , raw );
201
-
202
- const txoutputs = try getOutputsFor (aa , transaction , pubkeys );
203
- if (txoutputs .items .len == 0 ) {
204
- continue ;
205
- }
206
-
207
- _ = try relevant_transactions .getOrPutValue (txid , transaction .isCoinbase ());
208
-
209
- for (0.. txoutputs .items .len ) | j | {
210
- const txoutput = txoutputs .items [j ];
211
- // We need to generate the key with idx + n if it doesnt already exists
212
- const kp = txoutput .keypath .? ;
213
- const next = kp .getNext (1 );
214
- const existing = keypaths .get (next );
215
-
216
- // If we are generating a new key and the output is new we start re-indexing the block
217
- // This ensure the fact that we collect all the outputs since the new key could have been used in previous tx in the same block
218
- //var vout_hex: [8]u8 = undefined;
219
- //try utils.intToHexStr(u32, txoutput.outpoint.vout, &vout_hex);
220
- //var key: [72]u8 = undefined;
221
- //_ = try std.fmt.bufPrint(&key, "{s}{s}", .{ try utils.bytesToHex(64, &txoutput.outpoint.txid), vout_hex });
222
- if (existing == null ) {
223
- const descriptor = keypaths .get (kp );
224
- // Generate the next key
225
- const account_pubkey = try ExtendedPublicKey .fromAddress (descriptor .? .extended_key );
226
- const account_kp = keypath .KeyPath (2 ){ .path = [2 ]keypath.KeyPathElement { keypath.KeyPathElement { .value = next .path [3 ].value , .is_hardened = false }, keypath.KeyPathElement { .value = next .path [4 ].value , .is_hardened = false } } };
227
- const next_pubkey = try bip32 .deriveChildFromKeyPath (bip32 .ExtendedPublicKey , account_pubkey , 2 , account_kp );
228
- const next_pubkey_hash = try next_pubkey .key .toHash ();
229
- try pubkeys .put (next_pubkey_hash , next );
230
- try keypaths .put (next , descriptor .? );
231
- break :blk ;
232
- }
233
- }
234
-
235
- for (0.. txoutputs .items .len ) | j | {
236
- try outputs .append (txoutputs .items [j ]);
237
- }
199
+ const tx_outputs = try getOutputsFor (aa , block_transactions , pubkeys );
200
+ const tx_inputs = try getInputsFor (aa , block_transactions , pubkeys );
201
+
202
+ for (tx_outputs .items ) | tx_output | {
203
+ const kp = tx_output .keypath .? .truncPath (4 );
204
+ const current_last_used = keypath_last_used_index .get (kp ).? ;
205
+ if (tx_output .keypath .? .path [4 ].value > current_last_used ) {
206
+ try keypath_last_used_index .put (kp , tx_output .keypath .? .path [4 ].value );
238
207
}
239
- break ;
240
208
}
241
209
242
- const tx_inputs = try getInputsFor (aa , block_transactions , pubkeys );
243
- defer tx_inputs .deinit ();
244
-
245
- if (outputs .items .len > 0 ) {
246
- try db .saveOutputs (aa , & database , outputs .items );
210
+ if (tx_outputs .items .len > 0 ) {
211
+ try db .saveOutputs (aa , & database , tx_outputs .items );
247
212
}
248
213
if (tx_inputs .items .len > 0 ) {
249
214
try db .saveInputsAndMarkOutputs (& database , tx_inputs .items );
250
215
}
251
- if (relevant_transactions .count () > 0 ) {
252
- var it = relevant_transactions .keyIterator ();
253
- while (it .next ()) | txid | {
254
- const raw = raw_transactions_map .get (txid .* ).? ;
255
- const is_coinbase = relevant_transactions .get (txid .* ).? ;
256
- try db .saveTransaction (& database , txid .* , raw , is_coinbase , i );
257
- }
216
+ for (tx_outputs .items ) | tx_output | {
217
+ const transaction = block_transactions .get (tx_output .txid ).? ;
218
+ const raw_tx = raw_transactions_map .get (tx_output .txid ).? ;
219
+ try db .saveTransaction (& database , tx_output .txid , raw_tx , transaction .isCoinbase (), i );
258
220
}
221
+
259
222
// Since db writes are not in a single transaction we commit block as latest so that if we restart we dont't risk loosing information, once block is persisted we are sure outputs, inputs and relevant transactions in that block are persisted too. We can recover from partial commit simply reindexing the block.
260
223
try db .saveBlock (& database , try utils .hexToBytes (32 , & blockhash ), i );
261
224
262
225
progressbar .completeOne ();
263
226
_ = arena .reset (.free_all );
227
+
228
+ if (block_height == i ) {
229
+ break ;
230
+ }
231
+
232
+ // Generate new keys if needed
233
+
234
+ i += 1 ;
264
235
}
265
236
266
237
std .debug .print ("indexing completed\n " , .{});
@@ -276,29 +247,33 @@ fn scriptPubkeyToPubkeyHash(allocator: std.mem.Allocator, output: tx.TxOutput) !
276
247
return null ;
277
248
}
278
249
279
- fn getOutputsFor (allocator : std.mem.Allocator , transaction : tx.Transaction , pubkeys : std .AutoHashMap ([20 ]u8 , keypath.KeyPath (5 ))) ! std. ArrayList (Output ) {
250
+ fn getOutputsFor (allocator : std.mem.Allocator , transactions : std . AutoHashMap ([ 32 ] u8 , tx.Transaction ) , pubkeys : std .AutoHashMap ([20 ]u8 , keypath .KeyPath (5 ))) ! std .ArrayList (Output ) {
280
251
var outputs = std .ArrayList (Output ).init (allocator );
281
- for (0.. transaction .outputs .items .len ) | i | {
282
- const tx_output = transaction .outputs .items [i ];
283
- const output_pubkey_hash = try scriptPubkeyToPubkeyHash (allocator , tx_output );
284
- if (output_pubkey_hash == null ) {
285
- break ;
286
- }
252
+ var it = transactions .valueIterator ();
253
+ while (it .next ()) | transaction | {
254
+ for (0.. transaction .outputs .items .len ) | i | {
255
+ const tx_output = transaction .outputs .items [i ];
256
+ const output_pubkey_hash = try scriptPubkeyToPubkeyHash (allocator , tx_output );
257
+ if (output_pubkey_hash == null ) {
258
+ continue ;
259
+ }
287
260
288
- const pubkey = pubkeys .get (output_pubkey_hash .? );
289
- if (pubkey != null ) {
290
- const txid = try transaction .txid ();
291
- const vout = @as (u32 , @intCast (i ));
292
- const output = Output { .outpoint = Outpoint { .txid = txid , .vout = vout }, .keypath = pubkey .? , .amount = tx_output .amount , .unspent = true };
293
- try outputs .append (output );
261
+ const pubkey = pubkeys .get (output_pubkey_hash .? );
262
+ if (pubkey != null ) {
263
+ const txid = try transaction .txid ();
264
+ const vout = @as (u32 , @intCast (i ));
265
+ const output = Output { .outpoint = Outpoint { .txid = txid , .vout = vout }, .txid = txid , .keypath = pubkey .? , .amount = tx_output .amount , .unspent = true };
266
+ try outputs .append (output );
267
+ }
294
268
}
295
269
}
296
270
return outputs ;
297
271
}
298
272
299
- fn getInputsFor (allocator : std.mem.Allocator , transactions : [] tx.Transaction , pubkeys : std .AutoHashMap ([20 ]u8 , keypath.KeyPath (5 ))) ! std. ArrayList (Input ) {
273
+ fn getInputsFor (allocator : std.mem.Allocator , transactions : std . AutoHashMap ([ 32 ] u8 , tx.Transaction ) , pubkeys : std .AutoHashMap ([20 ]u8 , keypath .KeyPath (5 ))) ! std .ArrayList (Input ) {
300
274
var inputs = std .ArrayList (Input ).init (allocator );
301
- for (transactions ) | transaction | {
275
+ var it = transactions .valueIterator ();
276
+ while (it .next ()) | transaction | {
302
277
if (transaction .witness .items .len == 0 ) {
303
278
// No segwit, skip
304
279
return inputs ;
0 commit comments