chia-tools 0.40.0

Utility functions and types used by the Chia blockchain full node
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
use clap::Parser;

use chia_consensus::consensus_constants::ConsensusConstants;
use chia_consensus::consensus_constants::TEST_CONSTANTS;
use chia_consensus::flags::ConsensusFlags;
use chia_consensus::run_block_generator::{run_block_generator, run_block_generator2};
use chia_protocol::{Bytes32, Coin};
use chia_tools::iterate_blocks;
use rusqlite::Connection;
use std::collections::HashMap;
use std::collections::HashSet;
use std::io::Write;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::thread::available_parallelism;
use std::time::{Duration, Instant};

use hex_literal::hex;

/// Validates a blockchain database (must use v2 schema)
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[allow(clippy::struct_excessive_bools)]
struct Args {
    /// Path to blockchain database file to validate
    file: String,

    /// The number of paralell thread to run block generators in
    #[arg(short = 'j', long)]
    num_jobs: Option<usize>,

    /// Start at this block height. Assume all blocks up to this height are
    /// valid. This is meant for resuming validation or re-producing a failure
    #[arg(short, long, default_value_t = 0)]
    start: u32,

    /// Validate blockchain against a height-to-hash file
    #[arg(long)]
    height_to_hash: Option<String>,

    /// Don't validate block signatures (saves time)
    #[arg(long, default_value_t = false)]
    skip_signature_validation: bool,

    /// use testnet 11 constants instead of mainnet. This is required when
    /// validating testnet blockchain database.s
    #[arg(long, default_value_t = false)]
    testnet: bool,
}

const MAINNET_CONSTANTS: ConsensusConstants = TEST_CONSTANTS;
const TESTNET11_CONSTANTS: ConsensusConstants = ConsensusConstants {
    agg_sig_me_additional_data: Bytes32::new(hex!(
        "37a90eb5185a9c4439a91ddc98bbadce7b4feba060d50116a067de66bf236615"
    )),
    agg_sig_parent_additional_data: Bytes32::new(hex!(
        "c0754ae8602c47489b5394af8972c58238c4389d715f0585ca512d9428395e62"
    )),
    agg_sig_puzzle_additional_data: Bytes32::new(hex!(
        "2e63e4ca0796d9ef8e8a748d740f4b8632c4d994ad6cce51bd61a6612d602697"
    )),
    agg_sig_amount_additional_data: Bytes32::new(hex!(
        "cf15f86103bee6260b0e020a1ba02bcf61230fe209592543399dcf9267f8dfcc"
    )),
    agg_sig_puzzle_amount_additional_data: Bytes32::new(hex!(
        "02c0ecb453e75bd77823dd0affd3f224d968012a8c6c6c423801cc30dd5eb347"
    )),
    agg_sig_parent_amount_additional_data: Bytes32::new(hex!(
        "fc5eaa82087943fbee8683d42ae7a2a7aac0d4eecd4c98d71c228b9c62bf9497"
    )),
    agg_sig_parent_puzzle_additional_data: Bytes32::new(hex!(
        "54c3ed8017f77354acca4000b40424396a369740e5a504467784f392b961ab37"
    )),
    difficulty_constant_factor: 10_052_721_566_054,
    difficulty_starting: 30,
    epoch_blocks: 768,
    genesis_challenge: Bytes32::new(hex!(
        "37a90eb5185a9c4439a91ddc98bbadce7b4feba060d50116a067de66bf236615"
    )),
    genesis_pre_farm_farmer_puzzle_hash: Bytes32::new(hex!(
        "08296fc227decd043aee855741444538e4cc9a31772c4d1a9e6242d1e777e42a"
    )),
    genesis_pre_farm_pool_puzzle_hash: Bytes32::new(hex!(
        "3ef7c233fc0785f3c0cae5992c1d35e7c955ca37a423571c1607ba392a9d12f7"
    )),
    mempool_block_buffer: 10,
    min_plot_size_v1: 18,
    sub_slot_iters_starting: 67_108_864,
    // forks activated from the beginning on testnet11
    hard_fork_height: 0,
    plot_filter_128_height: 6_029_568,
    plot_filter_64_height: 11_075_328,
    plot_filter_32_height: 16_121_088,
    ..MAINNET_CONSTANTS
};

fn main() {
    let args = Args::parse();

    let constants = if args.testnet {
        &TESTNET11_CONSTANTS
    } else {
        &MAINNET_CONSTANTS
    };

    let num_cores = args
        .num_jobs
        .unwrap_or_else(|| available_parallelism().unwrap().into());

    let pool = blocking_threadpool::Builder::new()
        .num_threads(num_cores)
        .queue_len(num_cores + 5)
        .build();

    let error_count = Arc::new(AtomicUsize::new(0));
    let mut last_height = args.start;
    let mut last_time = Instant::now();
    println!(
        r"THIS TOOL DOES NOT VALIDATE ALL ASPECTS OF A BLOCKCHAIN DATABASE
features that are validated:
  * block hashes and heights
  * some conditions
  * block signatures (unless disabled by command line option)
  * the coin_record table
"
    );
    println!("opening blockchain database file: {}", args.file);

    let connection = Connection::open(&args.file).expect("failed to open database file");
    let mut select_spends = connection
        .prepare("SELECT coin_name FROM coin_record WHERE spent_index == ?;")
        .expect("failed to prepare SQL statement finding spent coins");
    let mut select_created = connection
        .prepare(
            "SELECT coin_name, coinbase, puzzle_hash, coin_parent, amount FROM coin_record WHERE confirmed_index == ?;",
        )
        .expect("failed to prepare SQL statement finding created coins");
    let mut select_peak = connection
        .prepare("SELECT hash FROM current_peak WHERE key == 0;")
        .expect("failed to prepare SQL statement finding peak");

    let mut peak_row = select_peak.query([]).expect("failed to query current peak");
    let peak_hash = peak_row
        .next()
        .expect("missing peak")
        .expect("missing peak")
        .get::<_, [u8; 32]>(0)
        .expect("missing peak");

    let mut prev_hash = constants.genesis_challenge;
    let mut prev_height: i64 = args.start as i64 - 1;

    let height_to_hash: Option<Vec<Bytes32>> = args.height_to_hash.map(|hth| {
        std::fs::read(hth)
            .expect("failed to read height-to-hash")
            .chunks(32)
            .map(|v| -> Bytes32 { v.try_into().unwrap() })
            .collect()
    });

    println!("iterating over blocks starting at height {}", args.start);
    iterate_blocks(&args.file, args.start, None, |height, block, block_refs| {
        // If we don't start validation from height 0, we need to initialize the
        // expected prev-hash based on the first block we pull from the DB
        if args.start != 0 && prev_hash == constants.genesis_challenge {
            prev_hash = block.prev_header_hash();
        }

        if block.prev_header_hash() != prev_hash {
            println!(
                "at height {height} the previous header hash mismatches. {} expected {} from height {}",
                block.prev_header_hash(),
                prev_hash,
                prev_height,
            );
            error_count.fetch_add(1, Ordering::Relaxed);
        }
        if block.height() != height {
            println!(
                "at height {height} the height recorded in the block mismatches, {}",
                block.height(),
            );
            error_count.fetch_add(1, Ordering::Relaxed);
        }
        if height != (prev_height + 1) as u32 {
            println!(
                "at height {height} the the block height did not increment by 1, from previous block (at height {prev_height})"
            );
            error_count.fetch_add(1, Ordering::Relaxed);
        }
        prev_hash = block.header_hash();
        prev_height = height as i64;
        if let Some(hth) = &height_to_hash {
            if hth.len() > height as usize && hth[height as usize] != prev_hash {
                println!(
                    "at height {height} the block hash ({prev_hash}) does not match the height-to-hash file ({})",
                    hth[height as usize]
                );
                error_count.fetch_add(1, Ordering::Relaxed);
            }
        }
        let mut removals = HashSet::<[u8; 32]>::new();
        // height 0 is not a transaction block so unspent coins have a
        // spent_index of 0 to indicate that they have not been spent.
        if height != 0 {
            let mut removals_rows = select_spends
                .query([height])
                .expect("failed to query spent coins");
            while let Ok(Some(row)) = removals_rows.next() {
                removals.insert(row.get::<_, [u8; 32]>(0).expect("missing coin_name"));
            }
        }
        let mut additions_rows = select_created
            .query([height])
            .expect("failed to query created coins");
        // coin-id -> (puzzle-hash, parent-coin, amount, reward)
        let mut additions = HashMap::<[u8; 32], ([u8; 32], [u8; 32], u64, bool)>::new();
        while let Ok(Some(row)) = additions_rows.next() {
            let coin_name = row.get::<_, [u8; 32]>(0).expect("missing coin_name");
            let reward = row.get::<_, bool>(1).expect("missing coinbase");
            let ph = row.get::<_, [u8; 32]>(2).expect("missing puzzle_hash");
            let parent = row.get::<_, [u8; 32]>(3).expect("missing parent");
            let amount = u64::from_be_bytes(row.get::<_, [u8; 8]>(4).expect("missing amount"));
            additions.insert(coin_name, (ph, parent, amount, reward));
        }

        // first ensure that the reward coins for this block are all included in
        // the coin record table.
        let rewards = block.get_included_reward_coins();
        for add in &rewards {
            let new_coin_id = add.coin_id();
            let Some((ph, _parent, amount, coin_base)) = additions.get(new_coin_id.as_slice())
            else {
                println!(
                    "at height {height} the block created a reward coin {new_coin_id} that's not in the coin_record table"
                );
                error_count.fetch_add(1, Ordering::Relaxed);
                continue;
            };
            // TODO: ensure the parent coin ID is set correctly
            if ph != add.puzzle_hash.as_slice() {
                println!(
                    "at height {height} the reward coin {new_coin_id} has an incorrect puzzle hash in the coin_record table {} expected {}",
                    hex::encode(ph),
                    add.puzzle_hash
                );
                error_count.fetch_add(1, Ordering::Relaxed);
            }
            // ensure the parent hash has the expected look
            if *amount != add.amount {
                println!(
                    "at height {height} reward coin {new_coin_id} has amount {} in coin_record table, but the block has amount {}",
                    amount, add.amount
                );
                error_count.fetch_add(1, Ordering::Relaxed);
            }
            // this is a reward coin
            if !coin_base {
                println!(
                    "at height {height} the reward coin {new_coin_id} is not marked as coin-base in the database"
                );
                error_count.fetch_add(1, Ordering::Relaxed);
            }
            additions.remove(new_coin_id.as_slice());
        }
        if block.transactions_generator.is_none() {
            // this is not a transaction block
            // there should be no coins in the coin table spent at this height.
            if !removals.is_empty() {
                println!(
                    "block at height {height} is not a transaction block, but the coin_record table has coins spent at this block height"
                );
                for coin_id in removals {
                    println!("  id: {}", hex::encode(coin_id));
                }
                error_count.fetch_add(1, Ordering::Relaxed);
            }
            // there should not be any non-reward coins created in this block
            if !additions.is_empty() {
                println!(
                    "block at height {height} is not a transaction block, but the coin_record table has coins created at this block height"
                );
                for (coin_id, (ph, parent, amount, reward)) in additions {
                    println!(
                        "  id: {} - {} {} {amount} {}",
                        hex::encode(coin_id),
                        hex::encode(ph),
                        hex::encode(parent),
                        if reward { "(coinbase)" } else { "" }
                    );
                }
                error_count.fetch_add(1, Ordering::Relaxed);
            }
            return;
        }
        let cnt = error_count.clone();
        pool.execute(move || {
                let ti = block.transactions_info.as_ref().expect("transactions_info");
                let generator = block
                    .transactions_generator
                    .as_ref()
                    .expect("transactions_generator");

                // after the hard fork, we run blocks without paying for the CLVM generator ROM
                let block_runner = if height >= constants.hard_fork_height {
                    run_block_generator2
                } else {
                    run_block_generator
                };
                let flags = (if args.skip_signature_validation {
                        ConsensusFlags::DONT_VALIDATE_SIGNATURE
                    } else {
                        ConsensusFlags::empty()
                    }) | ConsensusFlags::LIMIT_HEAP;
                let (_a, conditions) = block_runner(
                    generator,
                    &block_refs,
                    ti.cost,
                    flags,
                    &ti.aggregated_signature,
                    None,
                    constants,
                )
                .expect("failed to run block generator");

                if conditions.cost != ti.cost {
                    println!("at height {height} block header has cost of {}, expected {}", ti.cost, conditions.cost);
                    cnt.fetch_add(1, Ordering::Relaxed);
                }

                for spend in &conditions.spends {
                    let coin_name = *spend.coin_id;
                    if !removals.remove(coin_name.as_slice()) {
                        println!("at height {height} could not find coin {coin_name} in coin_record table, which is being spent at height {height}");
                        cnt.fetch_add(1, Ordering::Relaxed);
                    }
                    for add in &spend.create_coin {
                        let new_coin_id = Coin::new(coin_name, add.puzzle_hash, add.amount).coin_id();
                        let Some((ph, parent, amount, coin_base)) = additions.get(new_coin_id.as_slice()) else {
                            println!("at height {height} the block created a coin {new_coin_id} that's not in the coin_record table");
                            cnt.fetch_add(1, Ordering::Relaxed);
                            continue;
                        };
                        if ph != add.puzzle_hash.as_slice() {
                            println!("at height {height} the spent coin with id {new_coin_id} has a mismatching puzzle hash {} expected {}", add.puzzle_hash, Bytes32::from(ph));
                            cnt.fetch_add(1, Ordering::Relaxed);
                        }
                        if parent != coin_name.as_slice() {
                            println!("at height {height} the spent coin with id {new_coin_id} has a mismatching parent {} expected {}", coin_name, Bytes32::from(parent));
                            cnt.fetch_add(1, Ordering::Relaxed);
                        }
                        if *amount != add.amount {
                            println!("at height {height} the spent coin with id {new_coin_id} has a mismatching amount {} expected {}", *amount, add.amount);
                            cnt.fetch_add(1, Ordering::Relaxed);
                        }
                        // this is not a reward coin
                        if *coin_base {
                            println!("at height {height}, the created coin {new_coin_id} is incorrectly marked as coin-base in the database");
                            cnt.fetch_add(1, Ordering::Relaxed);
                        }
                        additions.remove(new_coin_id.as_slice());
                    }
                }
                if !removals.is_empty() {
                    println!("at height {height} the coin_table has {} extra spends", removals.len());
                    for coin_id in removals {
                        println!("  id: {}", hex::encode(coin_id));
                    }
                    cnt.fetch_add(1, Ordering::Relaxed);
                }

                if !additions.is_empty() {
                    println!("at height {height} the coin_table has {} extra coin additions", additions.len());
                    for (coin_id, (ph, parent, amount, reward)) in additions {
                        println!("  id: {} - {} {} {amount} {}",
                            hex::encode(coin_id),
                            hex::encode(ph),
                            hex::encode(parent),
                            if reward { "(coinbase)" } else {""}
                        );
                    }
                    cnt.fetch_add(1, Ordering::Relaxed);
                }
            });

        assert_eq!(pool.panic_count(), 0);
        if last_time.elapsed() > Duration::new(2, 0) {
            let rate = f64::from(height - last_height) / last_time.elapsed().as_secs_f64();
            print!("\rheight: {height} ({rate:0.1} blocks/s)   ");
            let _ = std::io::stdout().flush();
            last_height = height;
            last_time = Instant::now();
        }
    });

    pool.join();
    assert_eq!(pool.panic_count(), 0);

    if peak_hash != prev_hash.as_slice() {
        println!(
            "peak hash (in database) does not match the chain {}, expected {}",
            Bytes32::from(peak_hash),
            prev_hash
        );
        error_count.fetch_add(1, Ordering::Relaxed);
    }

    assert_eq!(
        error_count.load(Ordering::Relaxed),
        0,
        "exiting with failures"
    );

    println!("\nALL DONE, success!");
}