Skip to content

Commit 35f2b2a

Browse files
authored
chore: add more checks for block (#115) (#126)
* Zcash: add timestamps verification * Remove irrelevant comment * Fix error message * Fix typo * Add timestamp & version verification for bitcoin * litecoin: add timestamp verification * Align error message
1 parent a32fecd commit 35f2b2a

File tree

6 files changed

+152
-46
lines changed

6 files changed

+152
-46
lines changed

btc-types/src/network.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,19 @@ use near_sdk::near;
22

33
use crate::u256::U256;
44

5-
pub const ZCASH_MEDIAN_TIME_SPAN: usize = 11;
5+
pub const MEDIAN_TIME_SPAN: usize = 11;
6+
7+
/**
8+
* Maximum amount of time that a block timestamp is allowed to be ahead of the
9+
* median-time-past of the previous block.
10+
*/
11+
pub const MAX_FUTURE_BLOCK_TIME_MTP: u32 = 90 * 60;
12+
13+
/**
14+
* Maximum amount of time that a block timestamp is allowed to be ahead of the
15+
* current local time.
16+
*/
17+
pub const MAX_FUTURE_BLOCK_TIME_LOCAL: u32 = 2 * 60 * 60;
618

719
#[near(serializers = [borsh, json])]
820
#[derive(Clone, Copy, Debug)]

contract/src/bitcoin.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use crate::utils::BlocksGetter;
1+
use crate::utils::{get_median_time_past, BlocksGetter};
22
use crate::{BtcLightClient, BtcLightClientExt, Header, U256};
33
use btc_types::header::ExtendedHeader;
4-
use btc_types::network::{Network, NetworkConfig};
4+
use btc_types::network::{Network, NetworkConfig, MAX_FUTURE_BLOCK_TIME_LOCAL};
55
use btc_types::utils::target_from_bits;
6-
use near_sdk::{near, require};
6+
use near_sdk::{env, near, require};
77

88
#[near]
99
impl BtcLightClient {
@@ -15,16 +15,33 @@ impl BtcLightClient {
1515
("Bitcoin".to_owned(), self.network)
1616
}
1717

18+
// reference implementation: https://github.com/bitcoin/bitcoin/blob/ae024137bda9fe189f4e7ccf26dbaffd44cbbeb6/src/validation.cpp#L4200
1819
pub(crate) fn check_pow(&self, block_header: &Header, prev_block_header: &ExtendedHeader) {
1920
let config = self.get_config();
2021
let expected_bits = get_next_work_required(&config, block_header, prev_block_header, self);
2122

2223
require!(
2324
expected_bits == block_header.bits,
24-
format!(
25-
"Error: Incorrect target. Expected bits: {:?}, Actual bits: {:?}",
26-
expected_bits, block_header.bits
27-
)
25+
"bad-diffbits: incorrect proof of work"
26+
);
27+
28+
// Check timestamp against prev
29+
require!(
30+
block_header.time > get_median_time_past(prev_block_header.clone(), self),
31+
"time-too-old: block's timestamp is too early"
32+
);
33+
34+
// Check timestamp
35+
let current_timestamp = u32::try_from(env::block_timestamp_ms() / 1000).unwrap(); // Convert to seconds
36+
require!(
37+
block_header.time <= current_timestamp + MAX_FUTURE_BLOCK_TIME_LOCAL,
38+
"time-too-new: block timestamp too far in the future"
39+
);
40+
41+
// Reject blocks with outdated version
42+
require!(
43+
block_header.version >= 4,
44+
"bad-version: block version must be at least 4"
2845
);
2946
}
3047
}

contract/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ impl BtcLightClient {
381381
#[cfg(feature = "zcash")]
382382
{
383383
require!(
384-
btc_types::network::ZCASH_MEDIAN_TIME_SPAN
384+
btc_types::network::MEDIAN_TIME_SPAN
385385
+ usize::try_from(config.pow_averaging_window).unwrap()
386386
== submit_blocks.len() - 1,
387387
"ERR_NOT_ENOUGH_BLOCKS_FOR_ZCASH"

contract/src/litecoin.rs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use crate::utils::BlocksGetter;
1+
use crate::utils::{get_median_time_past, BlocksGetter};
22
use crate::{BtcLightClient, BtcLightClientExt, Header, U256};
33
use btc_types::header::ExtendedHeader;
4-
use btc_types::network::{Network, NetworkConfig};
4+
use btc_types::network::{Network, NetworkConfig, MAX_FUTURE_BLOCK_TIME_LOCAL};
55
use btc_types::utils::target_from_bits;
6-
use near_sdk::{near, require};
6+
use near_sdk::{env, near, require};
77

88
#[near]
99
impl BtcLightClient {
@@ -15,16 +15,34 @@ impl BtcLightClient {
1515
("Litecoin".to_owned(), self.network)
1616
}
1717

18+
// Reference implementation: https://github.com/litecoin-project/litecoin/blob/09a67c25495e2398437d6a388ee96fb6a266460e/src/validation.cpp#L3630
1819
pub(crate) fn check_pow(&self, block_header: &Header, prev_block_header: &ExtendedHeader) {
1920
let config = self.get_config();
2021
let expected_bits = get_next_work_required(&config, block_header, prev_block_header, self);
2122

23+
// Check proof of work
2224
require!(
2325
expected_bits == block_header.bits,
24-
format!(
25-
"Error: Incorrect target. Expected bits: {:?}, Actual bits: {:?}",
26-
expected_bits, block_header.bits
27-
)
26+
"bad-diffbits: incorrect proof of work"
27+
);
28+
29+
// Check timestamp against prev
30+
require!(
31+
block_header.time > get_median_time_past(prev_block_header.clone(), self),
32+
"time-too-old: block's timestamp is too early"
33+
);
34+
35+
// Check timestamp
36+
let current_timestamp = u32::try_from(env::block_timestamp_ms() / 1000).unwrap(); // Convert to seconds
37+
require!(
38+
block_header.time <= current_timestamp + MAX_FUTURE_BLOCK_TIME_LOCAL,
39+
"time-too-new: block timestamp too far in the future"
40+
);
41+
42+
// Reject blocks with outdated version
43+
require!(
44+
block_header.version >= 4,
45+
"bad-version: block version must be at least 4"
2846
);
2947
}
3048
}

contract/src/utils.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,22 @@ pub trait BlocksGetter {
55
#[allow(unused)]
66
fn get_header_by_height(&self, height: u64) -> ExtendedHeader;
77
}
8+
9+
#[allow(unused)]
10+
pub fn get_median_time_past(
11+
block_header: ExtendedHeader,
12+
prev_block_getter: &impl BlocksGetter,
13+
) -> u32 {
14+
use btc_types::network::MEDIAN_TIME_SPAN;
15+
16+
let mut median_time = [0u32; MEDIAN_TIME_SPAN];
17+
let mut current_header = block_header;
18+
19+
for i in 0..MEDIAN_TIME_SPAN {
20+
median_time[i] = current_header.block_header.time;
21+
current_header = prev_block_getter.get_prev_header(&current_header.block_header);
22+
}
23+
24+
median_time.sort_unstable();
25+
median_time[median_time.len() / 2]
26+
}

contract/src/zcash.rs

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{utils::BlocksGetter, BtcLightClient, BtcLightClientExt};
22
use btc_types::{
33
header::{ExtendedHeader, Header},
4-
network::{Network, ZcashConfig},
4+
network::{Network, ZcashConfig, MAX_FUTURE_BLOCK_TIME_LOCAL, MAX_FUTURE_BLOCK_TIME_MTP},
55
u256::U256,
66
utils::target_from_bits,
77
};
@@ -17,16 +17,43 @@ impl BtcLightClient {
1717
("Zcash".to_owned(), self.network)
1818
}
1919

20+
// Reference implementation: https://github.com/zcash/zcash/blob/v6.2.0/src/main.cpp#L5019
2021
pub(crate) fn check_pow(&self, block_header: &Header, prev_block_header: &ExtendedHeader) {
21-
let expected_bits =
22+
let next_work_result =
2223
zcash_get_next_work_required(&self.get_config(), block_header, prev_block_header, self);
2324

2425
require!(
25-
expected_bits == block_header.bits,
26-
format!(
27-
"Error: Incorrect target. Expected bits: {:?}, Actual bits: {:?}",
28-
expected_bits, block_header.bits
29-
)
26+
next_work_result.expected_bits == block_header.bits,
27+
"bad-diffbits: incorrect proof of work"
28+
);
29+
30+
// Check timestamp against prev
31+
require!(
32+
block_header.time > next_work_result.prev_block_median_time_past,
33+
"time-too-old: block time is before the median time of the previous block"
34+
);
35+
36+
// Check future timestamp soft fork rule introduced in v2.1.1-1.
37+
// This retrospectively activates at block height 2 for mainnet and regtest,
38+
// and 6 blocks after Blossom activation for testnet.
39+
//
40+
// MAX_FUTURE_BLOCK_TIME_MTP is typically 129600 seconds (36 hours) in Zcash
41+
require!(
42+
block_header.time
43+
<= next_work_result.prev_block_median_time_past + MAX_FUTURE_BLOCK_TIME_MTP,
44+
"time-too-far-ahead-of-mtp: block timestamp is too far ahead of median-time-past"
45+
);
46+
47+
// Check timestamp
48+
let current_timestamp = u32::try_from(env::block_timestamp_ms() / 1000).unwrap(); // Convert to seconds
49+
require!(
50+
block_header.time <= current_timestamp + MAX_FUTURE_BLOCK_TIME_LOCAL,
51+
"time-too-new: block timestamp is too far ahead of local time"
52+
);
53+
54+
require!(
55+
block_header.version >= 4,
56+
"bad-version: block version must be at least 4"
3057
);
3158

3259
// Check Equihash solution
@@ -41,40 +68,29 @@ impl BtcLightClient {
4168
}
4269
}
4370

71+
struct NextWorkResult {
72+
expected_bits: u32,
73+
prev_block_median_time_past: u32,
74+
}
75+
4476
// Reference implementation: https://github.com/zcash/zcash/blob/v6.2.0/src/pow.cpp#L20
4577
fn zcash_get_next_work_required(
4678
config: &ZcashConfig,
4779
block_header: &Header,
4880
prev_block_header: &ExtendedHeader,
4981
prev_block_getter: &impl BlocksGetter,
50-
) -> u32 {
51-
use btc_types::network::ZCASH_MEDIAN_TIME_SPAN;
52-
53-
if let Some(pow_allow_min_difficulty_blocks_after_height) =
54-
config.pow_allow_min_difficulty_blocks_after_height
55-
{
56-
// Comparing with >= because this function returns the work required for the block after prev_block_header
57-
if prev_block_header.block_height >= pow_allow_min_difficulty_blocks_after_height {
58-
// Special difficulty rule for testnet:
59-
// If the new block's timestamp is more than 6 * block interval minutes
60-
// then allow mining of a min-difficulty block.
61-
if i64::from(block_header.time)
62-
> i64::from(prev_block_header.block_header.time) + config.pow_target_spacing() * 6
63-
{
64-
return config.proof_of_work_limit_bits;
65-
}
66-
}
67-
}
82+
) -> NextWorkResult {
83+
use btc_types::network::MEDIAN_TIME_SPAN;
6884

6985
// Find the first block in the averaging interval
7086
// and the median time past for the first and last blocks in the interval
7187
let mut current_header = prev_block_header.clone();
7288
let mut total_target = U256::ZERO;
73-
let mut median_time = [0u32; ZCASH_MEDIAN_TIME_SPAN];
89+
let mut median_time = [0u32; MEDIAN_TIME_SPAN];
7490

7591
let prev_block_median_time_past = {
7692
for i in 0..usize::try_from(config.pow_averaging_window).unwrap() {
77-
if i < ZCASH_MEDIAN_TIME_SPAN {
93+
if i < MEDIAN_TIME_SPAN {
7894
median_time[i] = current_header.block_header.time;
7995
}
8096

@@ -91,14 +107,33 @@ fn zcash_get_next_work_required(
91107
};
92108

93109
let first_block_in_interval_median_time_past = {
94-
for i in 0..ZCASH_MEDIAN_TIME_SPAN {
110+
for i in 0..MEDIAN_TIME_SPAN {
95111
median_time[i] = current_header.block_header.time;
96112
current_header = prev_block_getter.get_prev_header(&current_header.block_header);
97113
}
98114
median_time.sort_unstable();
99115
median_time[median_time.len() / 2]
100116
};
101117

118+
if let Some(pow_allow_min_difficulty_blocks_after_height) =
119+
config.pow_allow_min_difficulty_blocks_after_height
120+
{
121+
// Comparing with >= because this function returns the work required for the block after prev_block_header
122+
if prev_block_header.block_height >= pow_allow_min_difficulty_blocks_after_height {
123+
// Special difficulty rule for testnet:
124+
// If the new block's timestamp is more than 6 * block interval minutes
125+
// then allow mining of a min-difficulty block.
126+
if i64::from(block_header.time)
127+
> i64::from(prev_block_header.block_header.time) + config.pow_target_spacing() * 6
128+
{
129+
return NextWorkResult {
130+
expected_bits: config.proof_of_work_limit_bits,
131+
prev_block_median_time_past,
132+
};
133+
}
134+
}
135+
}
136+
102137
// The protocol specification leaves MeanTarget(height) as a rational, and takes the floor
103138
// only after dividing by AveragingWindowTimespan in the computation of Threshold(height):
104139
// <https://zips.z.cash/protocol/protocol.pdf#diffadjustment>
@@ -108,12 +143,17 @@ fn zcash_get_next_work_required(
108143
let average_target = total_target
109144
/ U256::from(<i64 as TryInto<u64>>::try_into(config.pow_averaging_window).unwrap());
110145

111-
zcash_calculate_next_work_required(
146+
let expected_bits = zcash_calculate_next_work_required(
112147
config,
113148
average_target,
114149
prev_block_median_time_past,
115150
first_block_in_interval_median_time_past,
116-
)
151+
);
152+
153+
NextWorkResult {
154+
expected_bits,
155+
prev_block_median_time_past,
156+
}
117157
}
118158

119159
fn zcash_calculate_next_work_required(

0 commit comments

Comments
 (0)