diff --git a/packages/consensus/src/codec.cairo b/packages/consensus/src/codec.cairo index bb96cda8..e1f2d90e 100644 --- a/packages/consensus/src/codec.cairo +++ b/packages/consensus/src/codec.cairo @@ -210,7 +210,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }; let bytes = outpoint.encode(); @@ -232,7 +232,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }; let bytes = outpoint.encode(); @@ -257,7 +257,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span() @@ -287,7 +287,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span() @@ -320,7 +320,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span() @@ -338,7 +338,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span() @@ -356,7 +356,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span() @@ -418,7 +418,7 @@ mod tests { "000000000000000931112ca80c5badf6047373b1bb53587fc23344871734bbff" ), block_height: 149994_u32, - block_time: 1319114701_u32, + median_time_past: 1319114701_u32, is_coinbase: true, }, }, @@ -444,7 +444,7 @@ mod tests { "0000000000000779e6cfb6bdd06458b8e8adffea65c719edb3450d8b05f9cc57" ), block_height: 150006_u32, - block_time: 1319124571_u32, + median_time_past: 1319124571_u32, is_coinbase: true, }, }, @@ -470,7 +470,7 @@ mod tests { "0000000000000a43df068d144a5854b92d5a866d1c25f324f80077b86acb74e1" ), block_height: 150005_u32, - block_time: 1319122014_u32, + median_time_past: 1319122014_u32, is_coinbase: true, }, }, @@ -496,7 +496,7 @@ mod tests { "000000000000010d9f3d69f259027feaa1fe8637a01300db0536b33fc552351d" ), block_height: 149935_u32, - block_time: 1319066844_u32, + median_time_past: 1319066844_u32, is_coinbase: true, }, }, @@ -522,7 +522,7 @@ mod tests { "00000000000001283f1ade495834dab6b796ba8d94f8db0e5625b2eaf3bd1490" ), block_height: 149940_u32, - block_time: 1319069371_u32, + median_time_past: 1319069371_u32, is_coinbase: true, }, }, @@ -548,7 +548,7 @@ mod tests { "00000000000009b7da59908141bc7e4716497200cb2d7bdaa5c93d0c9c642eb1" ), block_height: 149895_u32, - block_time: 1319035283_u32, + median_time_past: 1319035283_u32, is_coinbase: true, }, }, @@ -574,7 +574,7 @@ mod tests { "00000000000006c23e77cedfd97ea2d6434371236e0a373d22e77e2a4a8a52b5" ), block_height: 149814_u32, - block_time: 1318990721_u32, + median_time_past: 1318990721_u32, is_coinbase: true, }, }, @@ -600,7 +600,7 @@ mod tests { "0000000000000b4ece814065c9a591382cc90447efdd302ec5d618ffaa04e023" ), block_height: 149984_u32, - block_time: 1319104909_u32, + median_time_past: 1319104909_u32, is_coinbase: true, }, }, @@ -662,7 +662,7 @@ mod tests { "00000000000002db188274c80ae6a97f67a7d5f355815cca6d30a48e0bb01153" ), block_height: 206120_u32, - block_time: 1351856022_u32, + median_time_past: 1351856022_u32, is_coinbase: true, }, }, @@ -761,7 +761,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![ @@ -827,7 +827,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![ @@ -851,7 +851,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![ @@ -915,7 +915,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![ diff --git a/packages/consensus/src/types/chain_state.cairo b/packages/consensus/src/types/chain_state.cairo index 75b421e1..73cf20ff 100644 --- a/packages/consensus/src/types/chain_state.cairo +++ b/packages/consensus/src/types/chain_state.cairo @@ -7,7 +7,7 @@ use core::fmt::{Display, Formatter, Error}; use crate::validation::{ difficulty::{validate_bits, adjust_difficulty}, coinbase::validate_coinbase, - timestamp::{validate_timestamp, next_prev_timestamps}, + timestamp::{validate_timestamp, next_prev_timestamps, compute_median_time_past}, work::{validate_proof_of_work, compute_total_work}, block::{compute_and_validate_tx_data, validate_bip30_block_hash}, }; @@ -34,6 +34,7 @@ pub struct ChainState { /// it's possible that one block could have an earlier timestamp /// than a block that came before it in the chain. pub prev_timestamps: Span, + /// Median Time Past (MTP) of the current block } /// Represents the initial state after genesis block. @@ -62,9 +63,11 @@ pub impl BlockValidatorImpl of BlockValidator { ) -> Result { let block_height = self.block_height + 1; - validate_timestamp(self.prev_timestamps, block.header.time)?; let prev_block_time = *self.prev_timestamps[self.prev_timestamps.len() - 1]; let prev_timestamps = next_prev_timestamps(self.prev_timestamps, block.header.time); + let median_time_past = compute_median_time_past(prev_timestamps); + + validate_timestamp(median_time_past, block.header.time)?; let txid_root = match block.data { TransactionData::MerkleRoot(root) => root, diff --git a/packages/consensus/src/types/transaction.cairo b/packages/consensus/src/types/transaction.cairo index c0735766..bf1f3cb6 100644 --- a/packages/consensus/src/types/transaction.cairo +++ b/packages/consensus/src/types/transaction.cairo @@ -96,10 +96,12 @@ pub struct OutPoint { /// Used to validate coinbase tx spending (not sooner than 100 blocks) and relative timelocks /// (it has been more than X block since the transaction containing this output was mined). pub block_height: u32, - /// The time of the block that contains this output (meta field). - /// Used to validate relative timelocks (it has been more than X seconds since the transaction - /// containing this output was mined). - pub block_time: u32, + /// The median time past of the block that contains this output (meta field). + /// This is the median timestamp of the previous 11 blocks. + /// Used to validate relative timelocks based on time (BIP 68 and BIP 112). + /// It ensures that the transaction containing this output has been mined for more than X + /// seconds. + pub median_time_past: u32, // Determine if the outpoint is a coinbase transaction // Has 100 or more block confirmation, // is added when block are queried @@ -180,7 +182,7 @@ impl OutPointDisplay of Display { data: {}, block_hash: {}, block_height: {}, - block_time: {}, + median_time_past: {}, is_coinbase: {}, }}", *self.txid, @@ -188,7 +190,7 @@ impl OutPointDisplay of Display { *self.data, *self.block_hash, *self.block_height, - *self.block_time, + *self.median_time_past, *self.is_coinbase ); f.buffer.append(@str); @@ -228,7 +230,7 @@ mod tests { block_hash: 0x00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee_u256 .into(), block_height: 9, - block_time: 1650000000, + median_time_past: 1650000000, is_coinbase: false, }; assert_eq!( diff --git a/packages/consensus/src/types/utreexo.cairo b/packages/consensus/src/types/utreexo.cairo index 99eba2f5..9dad6880 100644 --- a/packages/consensus/src/types/utreexo.cairo +++ b/packages/consensus/src/types/utreexo.cairo @@ -340,7 +340,7 @@ mod tests { cached: false }, block_height: 9, - block_time: 1231473279, + median_time_past: 1231473279, block_hash: hex_to_hash_rev( "000000008d9dc510f23c2657fc4f67bea30078cc05a90eb89e84cc475c080805" ), diff --git a/packages/consensus/src/types/utxo_set.cairo b/packages/consensus/src/types/utxo_set.cairo index e005e5ec..5c11d5b3 100644 --- a/packages/consensus/src/types/utxo_set.cairo +++ b/packages/consensus/src/types/utxo_set.cairo @@ -152,7 +152,7 @@ mod tests { }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, } } diff --git a/packages/consensus/src/validation/block.cairo b/packages/consensus/src/validation/block.cairo index 4f8cd263..11b7ac3c 100644 --- a/packages/consensus/src/validation/block.cairo +++ b/packages/consensus/src/validation/block.cairo @@ -34,7 +34,7 @@ pub fn compute_and_validate_tx_data( txs: Span, block_hash: Digest, block_height: u32, - block_time: u32, + median_time_past: u32, ref utxo_set: UtxoSet ) -> Result<(u64, Digest, Digest), ByteArray> { let mut txids: Array = array![]; @@ -82,7 +82,7 @@ pub fn compute_and_validate_tx_data( data: *output, block_hash, block_height, - block_time, + median_time_past, is_coinbase: true, }; inner_result = utxo_set.add(outpoint); @@ -95,7 +95,7 @@ pub fn compute_and_validate_tx_data( } else { let fee = match validate_transaction( - tx, block_hash, block_height, block_time, txid, ref utxo_set + tx, block_hash, block_height, median_time_past, txid, ref utxo_set ) { Result::Ok(fee) => fee, Result::Err(err) => { diff --git a/packages/consensus/src/validation/coinbase.cairo b/packages/consensus/src/validation/coinbase.cairo index 29685315..50a4ab87 100644 --- a/packages/consensus/src/validation/coinbase.cairo +++ b/packages/consensus/src/validation/coinbase.cairo @@ -252,7 +252,7 @@ mod tests { data: TxOut { value: 0_64, ..Default::default(), }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -266,7 +266,7 @@ mod tests { data: TxOut { value: 0_64, ..Default::default(), }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -296,7 +296,7 @@ mod tests { data: TxOut { value: 0_64, ..Default::default(), }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -315,7 +315,7 @@ mod tests { data: TxOut { value: 0_64, ..Default::default(), }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -338,7 +338,7 @@ mod tests { data: TxOut { value: 0_64, ..Default::default(), }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -375,7 +375,7 @@ mod tests { data: TxOut { value: 0_64, ..Default::default(), }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![ @@ -413,7 +413,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -619,7 +619,7 @@ mod tests { data: Default::default(), block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![ @@ -691,4 +691,3 @@ mod tests { assert_eq!(result, false); } } - diff --git a/packages/consensus/src/validation/locktime.cairo b/packages/consensus/src/validation/locktime.cairo index 99c4844b..ac981161 100644 --- a/packages/consensus/src/validation/locktime.cairo +++ b/packages/consensus/src/validation/locktime.cairo @@ -61,7 +61,7 @@ pub fn validate_absolute_locktime( /// If relative locktime is enabled, ensure the input's locktime is respected. /// https://learnmeabitcoin.com/technical/transaction/input/sequence/ pub fn validate_relative_locktime( - input: @TxIn, block_height: u32, block_time: u32 + input: @TxIn, block_height: u32, median_time_past: u32 ) -> Result<(), ByteArray> { let sequence = *input.sequence; if sequence & SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 { @@ -85,13 +85,13 @@ pub fn validate_relative_locktime( // // https://github.com/bitcoin/bitcoin/blob/712a2b5453cdf2568fece94b969d6e0923b6ba16/src/consensus/tx_verify.cpp#L74 let lock_time = value * 512; - let absolute_lock_time = *input.previous_output.block_time + lock_time; - if absolute_lock_time >= block_time { + let absolute_lock_time = *input.previous_output.median_time_past + lock_time; + if absolute_lock_time >= median_time_past { return Result::Err( format!( - "Relative time-based lock time is not respected: current time {}, outpoint time {}, lock time {} seconds", - block_time, - *input.previous_output.block_time, + "Relative time-based lock time is not respected: current MTP {}, outpoint MTP {}, lock time {} seconds", + median_time_past, + *input.previous_output.median_time_past, lock_time ) ); @@ -135,7 +135,7 @@ mod tests { block_hash: 0x000000007bc154e0fa7ea32218a72fe2c1bb9f86cf8c9ebf9a715ed27fdb229a_u256 .into(), block_height: 100, - block_time: 1600000000, + median_time_past: 1600000000, is_coinbase: false, }, witness: array![].span(), @@ -160,7 +160,7 @@ mod tests { block_hash: 0x00000000000000000006440de711734db5ed23115a2689539f99376c0385f8a6_u256 .into(), block_height: 603018, - block_time: 1573324462, + median_time_past: 1573324462, is_coinbase: false, }, witness: array![].span(), @@ -185,7 +185,7 @@ mod tests { block_hash: 0x0000000000000000000e0c3650a889c4831a957f2fefc3d5f74f4faba7db7565_u256 .into(), block_height: 603434, - block_time: 1573549241, + median_time_past: 1573549241, is_coinbase: false, }, witness: array![].span(), @@ -207,7 +207,7 @@ mod tests { block_hash: 0x00000000000000000006440de711734db5ed23115a2689539f99376c0385f8a6_u256 .into(), block_height: 603018, // Initial block height - block_time: 1573324462, + median_time_past: 1573324462, is_coinbase: false, }, witness: array![].span(), @@ -237,7 +237,7 @@ mod tests { block_hash: 0x0000000000000000000e0c3650a889c4831a957f2fefc3d5f74f4faba7db7565_u256 .into(), block_height: 603434, - block_time: 1573549241, // Initial block time + median_time_past: 1573549241, // Initial block time is_coinbase: false, }, witness: array![].span(), @@ -266,7 +266,7 @@ mod tests { block_hash: 0x000000007bc154e0fa7ea32218a72fe2c1bb9f86cf8c9ebf9a715ed27fdb229a_u256 .into(), block_height: 100, // Previous block height - block_time: 1600000000, // Previous block time + median_time_past: 1600000000, // Previous block time is_coinbase: false, }, witness: array![].span(), diff --git a/packages/consensus/src/validation/timestamp.cairo b/packages/consensus/src/validation/timestamp.cairo index bfd1fcfb..20817dc0 100644 --- a/packages/consensus/src/validation/timestamp.cairo +++ b/packages/consensus/src/validation/timestamp.cairo @@ -2,8 +2,8 @@ //! //! Read more: https://learnmeabitcoin.com/technical/block/time/ -/// Check that the block time is greater than the median of the 11 most recent timestamps. -pub fn validate_timestamp(prev_timestamps: Span, block_time: u32) -> Result<(), ByteArray> { +/// Compute the Median Time Past (MTP) from the previous timestamps. +pub fn compute_median_time_past(prev_timestamps: Span) -> u32 { // Sort the last 11 timestamps // adapted from : // https://github.com/keep-starknet-strange/alexandria/blob/main/packages/sorting/src/bubble_sort.cairo @@ -37,10 +37,15 @@ pub fn validate_timestamp(prev_timestamps: Span, block_time: u32) -> Result }; }; - if block_time > *sorted_prev_timestamps.at(sorted_prev_timestamps.len() - 6) { + *sorted_prev_timestamps.at(sorted_prev_timestamps.len() - 6) +} + +/// Check that the block time is greater than the Median Time Past (MTP). +pub fn validate_timestamp(median_time_past: u32, block_time: u32) -> Result<(), ByteArray> { + if block_time > median_time_past { Result::Ok(()) } else { - Result::Err("Median time is greater than or equal to block's timestamp") + Result::Err("Median time past is greater than or equal to block's timestamp") } } @@ -54,26 +59,37 @@ pub fn next_prev_timestamps(prev_timestamps: Span, block_time: u32) -> Span #[cfg(test)] mod tests { - use super::{validate_timestamp, next_prev_timestamps}; + use super::{compute_median_time_past, validate_timestamp, next_prev_timestamps}; #[test] - fn test_validate_timestamp() { + fn test_compute_median_time_past() { let prev_timestamps = array![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].span(); - let mut block_time = 12_u32; + let mtp = compute_median_time_past(prev_timestamps); + assert_eq!(mtp, 6, "Expected MTP to be 6"); + + let unsorted_timestamps = array![1, 3, 2, 5, 4, 6, 8, 7, 9, 10, 11].span(); + let mtp = compute_median_time_past(unsorted_timestamps); + assert_eq!(mtp, 6, "Expected MTP to be 6 for unsorted"); + } - // new timestamp is greater than the last timestamp - let result = validate_timestamp(prev_timestamps, block_time); - assert(result.is_ok(), 'Expected target to be valid'); + #[test] + fn test_validate_timestamp() { + let mtp = 6_u32; + let mut block_time = 7_u32; - // new timestamp is strictly greater than the median of the last 11 timestamps - block_time = 7; - let result = validate_timestamp(prev_timestamps, block_time); - assert(result.is_ok(), 'Expected target to be valid'); + // new timestamp is greater than MTP + let result = validate_timestamp(mtp, block_time); + assert(result.is_ok(), 'Expected timestamp to be valid'); - // new timestamp is equal to the median of the last 11 timestamps + // new timestamp is equal to MTP block_time = 6; - let result = validate_timestamp(prev_timestamps, block_time); - assert!(result.is_err(), "Median time is greater than block's timestamp"); + let result = validate_timestamp(mtp, block_time); + assert!(result.is_err(), "MTP is greater than or equal to block's timestamp"); + + // new timestamp is less than MTP + block_time = 5; + let result = validate_timestamp(mtp, block_time); + assert!(result.is_err(), "MTP is greater than block's timestamp"); } #[test] @@ -87,42 +103,23 @@ mod tests { } #[test] - fn test_validate_timestamp_single_unchronological() { - let prev_timestamps = array![1, 2, 3, 4, 5, 7, 6, 8, 9, 10, 11].span(); - let mut block_time = 12_u32; - - // new timestamp is greater than the last timestamp - let result = validate_timestamp(prev_timestamps, block_time); - assert(result.is_ok(), 'Expected target to be valid'); - - // new timestamp is strictly greater than the median of the last 11 timestamps(sorted) - block_time = 7; - let result = validate_timestamp(prev_timestamps, block_time); - assert(result.is_ok(), 'Expected target to be valid'); - - // new timestamp is equal to the median of the last 11 timestamps(sorted) - block_time = 6; - let result = validate_timestamp(prev_timestamps, block_time); - assert!(result.is_err(), "Median time is greater than block's timestamp"); - } - - #[test] - fn test_validate_timestamp_unsorted() { + fn test_validate_timestamp_with_unsorted_input() { let prev_timestamps = array![1, 3, 2, 5, 4, 6, 8, 7, 9, 10, 11].span(); + let mtp = compute_median_time_past(prev_timestamps); let mut block_time = 12_u32; - // new timestamp is greater than the last timestamp - let result = validate_timestamp(prev_timestamps, block_time); - assert(result.is_ok(), 'Expected target to be valid'); + // new timestamp is greater than MTP + let result = validate_timestamp(mtp, block_time); + assert(result.is_ok(), 'Expected timestamp to be valid'); - // new timestamp is strictly greater than the median of the last 11 timestamps(sorted) - block_time = 7; - let result = validate_timestamp(prev_timestamps, block_time); - assert(result.is_ok(), 'Expected target to be valid'); - - // new timestamp is equal to the median of the last 11 timestamps(sorted) + // new timestamp is equal to MTP block_time = 6; - let result = validate_timestamp(prev_timestamps, block_time); - assert!(result.is_err(), "Median time is greater than block's timestamp"); + let result = validate_timestamp(mtp, block_time); + assert!(result.is_err(), "MTP is greater than or equal to block's timestamp"); + + // new timestamp is less than MTP + block_time = 5; + let result = validate_timestamp(mtp, block_time); + assert!(result.is_err(), "MTP is greater than block's timestamp"); } } diff --git a/packages/consensus/src/validation/transaction.cairo b/packages/consensus/src/validation/transaction.cairo index be13699f..075e91e4 100644 --- a/packages/consensus/src/validation/transaction.cairo +++ b/packages/consensus/src/validation/transaction.cairo @@ -17,7 +17,7 @@ pub fn validate_transaction( tx: @Transaction, block_hash: Digest, block_height: u32, - block_time: u32, + median_time_past: u32, txid: Digest, ref utxo_set: UtxoSet ) -> Result { @@ -54,7 +54,7 @@ pub fn validate_transaction( } if !is_input_final(*input.sequence) { - inner_result = validate_relative_locktime(input, block_height, block_time); + inner_result = validate_relative_locktime(input, block_height, median_time_past); if inner_result.is_err() { break; } @@ -70,7 +70,7 @@ pub fn validate_transaction( if !is_tx_final { // If at least one input is not final - validate_absolute_locktime(*tx.lock_time, block_height, block_time)?; + validate_absolute_locktime(*tx.lock_time, block_height, median_time_past)?; } // Validate and process transaction outputs @@ -82,7 +82,13 @@ pub fn validate_transaction( // Adds outpoint to the cache if the corresponding transaction output will be used // as a transaction input in the same block(s), or adds it to the utreexo otherwise. let outpoint = OutPoint { - txid, vout, data: *output, block_hash, block_height, block_time, is_coinbase: false, + txid, + vout, + data: *output, + block_hash, + block_height, + median_time_past, + is_coinbase: false, }; inner_result = utxo_set.add(outpoint); @@ -164,7 +170,7 @@ mod tests { data: TxOut { value: 100, ..Default::default() }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: true, }, witness: array![].span(), @@ -240,7 +246,7 @@ mod tests { data: TxOut { value: 100, ..Default::default() }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -277,7 +283,7 @@ mod tests { data: TxOut { value: 100, ..Default::default() }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -332,7 +338,7 @@ mod tests { data: TxOut { value: 100, ..Default::default() }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -390,7 +396,7 @@ mod tests { data: TxOut { value: 100, ..Default::default() }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -444,7 +450,7 @@ mod tests { data: TxOut { value: 100, ..Default::default() }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -496,7 +502,7 @@ mod tests { data: TxOut { value: 100, ..Default::default() }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: true, }, witness: array![].span(), @@ -541,7 +547,7 @@ mod tests { data: TxOut { value: 100, ..Default::default() }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: true, }, witness: array![].span(), @@ -586,7 +592,7 @@ mod tests { data: TxOut { value: 100, pk_script: @from_hex(""), cached: true }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -631,7 +637,7 @@ mod tests { data: TxOut { value: 100, pk_script: @from_hex(""), cached: false }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -679,7 +685,7 @@ mod tests { data: TxOut { value: 100, pk_script: @from_hex(""), cached: true }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -730,7 +736,7 @@ mod tests { data: TxOut { value: 100, pk_script: @from_hex(""), cached: false }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -758,7 +764,7 @@ mod tests { data: *tx.outputs[0], block_hash, block_height, - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }; let outpoint_hash = outpoint.hash(); @@ -789,7 +795,7 @@ mod tests { data: TxOut { value: 100, pk_script: @from_hex(""), cached: true }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -805,7 +811,7 @@ mod tests { data: TxOut { value: 100, pk_script: @from_hex(""), cached: true }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -857,7 +863,7 @@ mod tests { data: TxOut { value: 100, pk_script: @from_hex(""), cached: false }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), @@ -873,7 +879,7 @@ mod tests { data: TxOut { value: 100, pk_script: @from_hex(""), cached: false }, block_hash: Default::default(), block_height: Default::default(), - block_time: Default::default(), + median_time_past: Default::default(), is_coinbase: false, }, witness: array![].span(), diff --git a/scripts/data/generate_data.py b/scripts/data/generate_data.py index 1dcd405d..f6aa37e0 100755 --- a/scripts/data/generate_data.py +++ b/scripts/data/generate_data.py @@ -54,23 +54,24 @@ def request_rpc(method: str, params: list): ) +def compute_median_time_past(prev_timestamps): + """Compute the Median Time Past (MTP) from the previous timestamps.""" + sorted_timestamps = sorted(prev_timestamps) + return sorted_timestamps[len(sorted_timestamps) // 2] + + def fetch_chain_state_fast(block_height: int): - """Fetches chain state at the end of a specific block with given height. - Chain state is a just a block header extended with extra fields: - - prev_timestamps - - epoch_start_time - """ - # Chain state at height H is the state after applying block H + """Fetches chain state at the end of a specific block with given height.""" block_hash = request_rpc("getblockhash", [block_height]) head = request_rpc("getblockheader", [block_hash]) - # If block is downloaded take it locally data = get_timestamp_data(block_height)[str(block_height)] head["prev_timestamps"] = [int(t) for t in data["previous_timestamps"]] if block_height < 2016: head["epoch_start_time"] = 1231006505 else: head["epoch_start_time"] = int(data["epoch_start_time"]) + head["median_time_past"] = int(data["median_time"]) return head @@ -96,6 +97,7 @@ def fetch_chain_state(block_height: int): ) prev_timestamps.insert(0, int(prev_header["time"])) head["prev_timestamps"] = prev_timestamps + head["median_time_past"] = compute_median_time_past(prev_timestamps) # In order to init epoch start we need to query block header at epoch start if block_height < 2016: @@ -117,6 +119,9 @@ def next_chain_state(head: dict, blocks: list): # and all the blocks we applied to it prev_timestamps = head["prev_timestamps"] + list(map(lambda x: x["time"], blocks)) next_head["prev_timestamps"] = prev_timestamps[-11:] + next_head["median_time_past"] = compute_median_time_past( + next_head["prev_timestamps"] + ) # Update epoch start time if necessary if head["height"] // 2016 != block_height // 2016: @@ -143,6 +148,7 @@ def format_chain_state(head: dict): "current_target": str(bits_to_target(head["bits"])), "epoch_start_time": head["epoch_start_time"], "prev_timestamps": head["prev_timestamps"], + "median_time_past": head["median_time_past"], } @@ -162,7 +168,7 @@ def bits_to_target(bits: str) -> int: return mantissa << (8 * (exponent - 3)) -def fetch_block(block_hash: str, fast: bool): +def fetch_block(block_hash: str, fast: bool, current_chain_state: dict): """Downloads block with transactions (and referred UTXOs) from RPC given the block hash.""" block = request_rpc("getblock", [block_hash, 2]) @@ -173,37 +179,41 @@ def fetch_block(block_hash: str, fast: bool): ) block["data"] = { - tx["txid"]: resolve_transaction(tx, previous_outputs) + tx["txid"]: resolve_transaction( + tx, previous_outputs, current_chain_state["median_time_past"] + ) for tx in tqdm(block["tx"], "Resolving transactions") } return block -def resolve_transaction(transaction: dict, previous_outputs: dict): +def resolve_transaction( + transaction: dict, previous_outputs: dict, median_time_past: int +): """Resolves transaction inputs and formats the content according to the Cairo type.""" return { "version": transaction["version"], - # Skip the first 4 bytes (version) and take the next 4 bytes (marker + flag) "is_segwit": transaction["hex"][8:12] == "0001", "inputs": [ - resolve_input(input, previous_outputs) for input in transaction["vin"] + resolve_input(input, previous_outputs, median_time_past) + for input in transaction["vin"] ], "outputs": [format_output(output) for output in transaction["vout"]], "lock_time": transaction["locktime"], } -def resolve_input(input: dict, previous_outputs: dict): +def resolve_input(input: dict, previous_outputs: dict, median_time_past: int): """Resolves referenced UTXO and formats the transaction inputs according to the Cairo type.""" if input.get("coinbase"): return format_coinbase_input(input) else: if previous_outputs: previous_output = format_outpoint( - previous_outputs[(input["txid"], input["vout"])] + previous_outputs[(input["txid"], input["vout"])], median_time_past ) else: - previous_output = resolve_outpoint(input) + previous_output = resolve_outpoint(input, median_time_past) return { "script": f'0x{input["scriptSig"]["hex"]}', "sequence": input["sequence"], @@ -212,9 +222,8 @@ def resolve_input(input: dict, previous_outputs: dict): } -def format_outpoint(previous_output): +def format_outpoint(previous_output, median_time_past): """Formats output according to the Cairo type.""" - return { "txid": previous_output["txid"], "vout": int(previous_output["vout"]), @@ -225,12 +234,12 @@ def format_outpoint(previous_output): }, "block_hash": previous_output["block_hash"], "block_height": int(previous_output["block_height"]), - "block_time": int(previous_output["block_time"]), + "median_time_past": median_time_past, "is_coinbase": previous_output["is_coinbase"], } -def resolve_outpoint(input: dict): +def resolve_outpoint(input: dict, median_time_past: int): """Fetches transaction and block header for the referenced output, formats resulting outpoint according to the Cairo type. """ @@ -242,7 +251,7 @@ def resolve_outpoint(input: dict): "data": format_output(tx["vout"][input["vout"]]), "block_hash": tx["blockhash"], "block_height": block["height"], - "block_time": block["time"], + "median_time_past": median_time_past, "is_coinbase": tx["vin"][0].get("coinbase") is not None, } @@ -258,7 +267,7 @@ def format_coinbase_input(input: dict): "data": {"value": 0, "pk_script": "0x", "cached": False}, "block_hash": "0" * 64, "block_height": 0, - "block_time": 0, + "median_time_past": 0, "is_coinbase": False, }, "witness": [ @@ -317,6 +326,29 @@ def format_header(header: dict): } +def apply_chain_state(current_state: dict, new_block: dict) -> dict: + """Computes the next chain state given the current state and a new block.""" + next_state = new_block.copy() + + # Update prev_timestamps + next_state["prev_timestamps"] = current_state["prev_timestamps"][1:] + [ + new_block["time"] + ] + + # Compute new median time past + next_state["median_time_past"] = compute_median_time_past( + next_state["prev_timestamps"] + ) + + # Update epoch start time + if current_state["height"] // 2016 != next_state["height"] // 2016: + next_state["epoch_start_time"] = get_epoch_start_time(next_state["height"]) + else: + next_state["epoch_start_time"] = current_state["epoch_start_time"] + + return next_state + + def generate_data( mode: str, initial_height: int, @@ -335,9 +367,9 @@ def generate_data( """ if fast: - print("Fetching chain state (fast)...") + print("Fetching initial chain state (fast)...") else: - print("Fetching chain state...") + print("Fetching initial chain state...") print(f"blocks: {initial_height} - {initial_height + num_blocks - 1}") @@ -352,45 +384,22 @@ def generate_data( utreexo_data = {} for i in range(num_blocks): - # Interblock cache - tmp_utxo_set = {} + print( + f"\rFetching block {initial_height + i + 1}/{initial_height + num_blocks}", + end="", + flush=True, + ) + if mode == "light": block = fetch_block_header(next_block_hash) elif mode in ["full", "utreexo"]: - print( - f"\rFetching block {initial_height + i}/{initial_height + num_blocks}", - end="", - flush=True, - ) - block = fetch_block(next_block_hash, fast) - - # Build UTXO set and mark outputs spent within the same block (span). - # Also set "cached" flag for the inputs that spend those UTXOs. - for txid, tx in block["data"].items(): - for tx_input in tx["inputs"]: - outpoint = ( - tx_input["previous_output"]["txid"], - tx_input["previous_output"]["vout"], - ) - if outpoint in tmp_utxo_set: - tx_input["previous_output"]["data"]["cached"] = True - tmp_utxo_set[outpoint]["cached"] = True - - for idx, output in enumerate(tx["outputs"]): - outpoint = (txid, idx) - tmp_utxo_set[outpoint] = output - - # Do another pass to mark UTXOs spent within the same block (span) with "cached" flag. - for txid, tx in block["data"].items(): - for idx, output in enumerate(tx["outputs"]): - outpoint = (txid, idx) - if outpoint in tmp_utxo_set and tmp_utxo_set[outpoint]["cached"]: - tx["outputs"][idx]["cached"] = True + block = fetch_block(next_block_hash, fast, chain_state) else: raise NotImplementedError(mode) - next_block_hash = block["nextblockhash"] blocks.append(block) + chain_state = apply_chain_state(chain_state, block) + next_block_hash = block["nextblockhash"] block_formatter = ( format_block if mode == "light" else format_block_with_transactions diff --git a/scripts/data/generate_utreexo_data.py b/scripts/data/generate_utreexo_data.py index 5b97d6fb..97360dea 100644 --- a/scripts/data/generate_utreexo_data.py +++ b/scripts/data/generate_utreexo_data.py @@ -114,7 +114,7 @@ def __repr__(self): vout={self.vout}\n\ tx_out={self.data}\n\ block_height={self.block_height}\n\ - block_time={self.block_time}\n\ + median_time_past={self.median_time_past}\n\ block_hash={self.block_hash}\n\ is_coinbase={self.is_coinbase})" @@ -175,7 +175,7 @@ def handle_txin(self, inputs: list) -> list: cached=outpoint["data"]["cached"], ), block_height=outpoint["block_height"], - block_time=outpoint["block_time"], + median_time_past=outpoint["median_time_past"], block_hash=outpoint["block_hash"], is_coinbase=outpoint["is_coinbase"], ) @@ -193,7 +193,7 @@ def handle_txout( outputs: list, block_hash: str, block_height: int, - block_time: int, + median_time_past: int, txid: str, is_coinbase: bool, ): @@ -211,7 +211,7 @@ def handle_txout( cached=output["cached"], ), block_height=block_height, - block_time=block_time, + median_time_past=median_time_past, block_hash=block_hash, is_coinbase=is_coinbase, )