Skip to content

Commit

Permalink
feature(data_structures): switch block time when v2.0 activates
Browse files Browse the repository at this point in the history
  • Loading branch information
drcpu-github committed Jul 15, 2024
1 parent 0a8869e commit d99bd83
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 59 deletions.
8 changes: 4 additions & 4 deletions config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,13 +432,13 @@ pub struct Tapi {
/// Configuration related to protocol versions.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct Protocol {
pub v1_7: Option<Epoch>,
pub v1_8: Option<Epoch>,
pub v2_0: Option<Epoch>,
pub v1_7: Option<(Epoch, u16)>,
pub v1_8: Option<(Epoch, u16)>,
pub v2_0: Option<(Epoch, u16)>,
}

impl Protocol {
pub fn iter(&self) -> IntoIter<(ProtocolVersion, Option<Epoch>), 3> {
pub fn iter(&self) -> IntoIter<(ProtocolVersion, Option<(Epoch, u16)>), 3> {
[
(ProtocolVersion::V1_7, self.v1_7),
(ProtocolVersion::V1_8, self.v1_8),
Expand Down
4 changes: 2 additions & 2 deletions config/src/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,8 @@ pub trait Defaults {
100
}

fn protocol_versions(&self) -> HashMap<ProtocolVersion, Epoch> {
[(ProtocolVersion::V1_7, 0)].into_iter().collect()
fn protocol_versions(&self) -> HashMap<ProtocolVersion, (Epoch, u16)> {
[(ProtocolVersion::V1_7, (0, 45))].into_iter().collect()
}
}

Expand Down
101 changes: 83 additions & 18 deletions data_structures/src/chain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4478,6 +4478,12 @@ pub struct EpochConstants {

/// Period between checkpoints, in seconds
pub checkpoints_period: u16,

/// Timestamp of checkpoint (in seconds) when v2 started)
pub checkpoint_zero_timestamp_v2: i64,

/// Period between checkpoints, in seconds, starting at v2
pub checkpoints_period_v2: u16,
}

// This default is only used for tests
Expand All @@ -4487,48 +4493,107 @@ impl Default for EpochConstants {
checkpoint_zero_timestamp: 0,
// This cannot be 0 because we would divide by zero
checkpoints_period: 1,
// Variables for v2
checkpoint_zero_timestamp_v2: i64::MAX,
// This cannot be 0 because we would divide by zero
checkpoints_period_v2: 1,
}
}
}

impl EpochConstants {
/// Calculate the last checkpoint (current epoch) at the supplied timestamp
pub fn epoch_at(&self, timestamp: i64) -> Result<Epoch, EpochCalculationError> {
let zero = self.checkpoint_zero_timestamp;
let period = self.checkpoints_period;
let elapsed = timestamp - zero;
if timestamp >= self.checkpoint_zero_timestamp_v2 {
let epochs_pre_v2 = match Epoch::try_from(
self.checkpoint_zero_timestamp_v2 - self.checkpoint_zero_timestamp,
) {
Ok(epoch) => epoch / Epoch::from(self.checkpoints_period),
Err(_) => {
return Err(EpochCalculationError::CheckpointZeroInTheFuture(
self.checkpoint_zero_timestamp,
));
}
};
let epochs_post_v2 =
match Epoch::try_from(timestamp - self.checkpoint_zero_timestamp_v2) {
Ok(epoch) => epoch / Epoch::from(self.checkpoints_period_v2),
Err(_) => {
return Err(EpochCalculationError::CheckpointZeroInTheFuture(
self.checkpoint_zero_timestamp,
));
}
};

Epoch::try_from(elapsed)
.map(|epoch| epoch / Epoch::from(period))
.map_err(|_| EpochCalculationError::CheckpointZeroInTheFuture(zero))
Ok(epochs_pre_v2 + epochs_post_v2)
} else {
Epoch::try_from(timestamp - self.checkpoint_zero_timestamp)
.map(|epoch| epoch / Epoch::from(self.checkpoints_period))
.map_err(|_| {
EpochCalculationError::CheckpointZeroInTheFuture(self.checkpoint_zero_timestamp)
})
}
}

/// Calculate the timestamp for a checkpoint (the start of an epoch)
pub fn epoch_timestamp(&self, epoch: Epoch) -> Result<i64, EpochCalculationError> {
let zero = self.checkpoint_zero_timestamp;
let period = self.checkpoints_period;

Epoch::from(period)
pub fn epoch_timestamp(&self, epoch: Epoch) -> Result<(i64, bool), EpochCalculationError> {
let epoch_timestamp = Epoch::from(self.checkpoints_period)
.checked_mul(epoch)
.filter(|&x| x <= Epoch::MAX as Epoch)
.map(i64::from)
.and_then(|x| x.checked_add(zero))
.ok_or(EpochCalculationError::Overflow)
.and_then(|x| x.checked_add(self.checkpoint_zero_timestamp))
.ok_or(EpochCalculationError::Overflow);

let epoch_timestamp = match epoch_timestamp {
Ok(timestamp) => timestamp,
Err(error) => {
return Err(error);
}
};

let mut in_v2 = false;
let timestamp = if epoch_timestamp >= self.checkpoint_zero_timestamp_v2 {
in_v2 = true;

let epochs_pre_v2 = ((self.checkpoint_zero_timestamp_v2
- self.checkpoint_zero_timestamp)
/ self.checkpoints_period as i64) as u32;

Check failure on line 4560 in data_structures/src/chain/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

casting `u16` to `i64` may become silently lossy if you later change the type

error: casting `u16` to `i64` may become silently lossy if you later change the type --> data_structures/src/chain/mod.rs:4560:19 | 4560 | / self.checkpoints_period as i64) as u32; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `i64::from(self.checkpoints_period)` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#cast_lossless = note: requested on the command line with `-D clippy::cast-lossless`

Check failure on line 4560 in data_structures/src/chain/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

casting `i64` to `u32` may lose the sign of the value

error: casting `i64` to `u32` may lose the sign of the value --> data_structures/src/chain/mod.rs:4558:33 | 4558 | let epochs_pre_v2 = ((self.checkpoint_zero_timestamp_v2 | _________________________________^ 4559 | | - self.checkpoint_zero_timestamp) 4560 | | / self.checkpoints_period as i64) as u32; | |________________________________________________________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#cast_sign_loss = note: requested on the command line with `-D clippy::cast-sign-loss`

Check failure on line 4560 in data_structures/src/chain/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

casting `i64` to `u32` may truncate the value

error: casting `i64` to `u32` may truncate the value --> data_structures/src/chain/mod.rs:4558:33 | 4558 | let epochs_pre_v2 = ((self.checkpoint_zero_timestamp_v2 | _________________________________^ 4559 | | - self.checkpoint_zero_timestamp) 4560 | | / self.checkpoints_period as i64) as u32; | |________________________________________________________^ | = help: if this is intentional allow the lint with `#[allow(clippy::cast_possible_truncation)]` ... = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#cast_possible_truncation = note: requested on the command line with `-D clippy::cast-possible-truncation` help: ... or use `try_from` and handle the error accordingly | 4558 ~ let epochs_pre_v2 = u32::try_from((self.checkpoint_zero_timestamp_v2 4559 ~ - self.checkpoint_zero_timestamp) / self.checkpoints_period as i64); |

Check failure on line 4560 in data_structures/src/chain/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

casting `u16` to `i64` may become silently lossy if you later change the type

error: casting `u16` to `i64` may become silently lossy if you later change the type --> data_structures/src/chain/mod.rs:4560:19 | 4560 | / self.checkpoints_period as i64) as u32; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `i64::from(self.checkpoints_period)` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#cast_lossless = note: requested on the command line with `-D clippy::cast-lossless`

Check failure on line 4560 in data_structures/src/chain/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

casting `i64` to `u32` may lose the sign of the value

error: casting `i64` to `u32` may lose the sign of the value --> data_structures/src/chain/mod.rs:4558:33 | 4558 | let epochs_pre_v2 = ((self.checkpoint_zero_timestamp_v2 | _________________________________^ 4559 | | - self.checkpoint_zero_timestamp) 4560 | | / self.checkpoints_period as i64) as u32; | |________________________________________________________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#cast_sign_loss = note: requested on the command line with `-D clippy::cast-sign-loss`

Check failure on line 4560 in data_structures/src/chain/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

casting `i64` to `u32` may truncate the value

error: casting `i64` to `u32` may truncate the value --> data_structures/src/chain/mod.rs:4558:33 | 4558 | let epochs_pre_v2 = ((self.checkpoint_zero_timestamp_v2 | _________________________________^ 4559 | | - self.checkpoint_zero_timestamp) 4560 | | / self.checkpoints_period as i64) as u32; | |________________________________________________________^ | = help: if this is intentional allow the lint with `#[allow(clippy::cast_possible_truncation)]` ... = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#cast_possible_truncation = note: requested on the command line with `-D clippy::cast-possible-truncation` help: ... or use `try_from` and handle the error accordingly | 4558 ~ let epochs_pre_v2 = u32::try_from((self.checkpoint_zero_timestamp_v2 4559 ~ - self.checkpoint_zero_timestamp) / self.checkpoints_period as i64); |

self.checkpoint_zero_timestamp_v2
+ i64::from((epoch - epochs_pre_v2) * Epoch::from(self.checkpoints_period_v2))
} else {
epoch_timestamp
};

Ok((timestamp, in_v2))
}

/// Calculate the timestamp for when block mining should happen.
pub fn block_mining_timestamp(&self, epoch: Epoch) -> Result<i64, EpochCalculationError> {
let start = self.epoch_timestamp(epoch)?;
let (start, in_v2) = self.epoch_timestamp(epoch)?;
// TODO: analyze when should nodes start mining a block
// Start mining at the midpoint of the epoch
let seconds_before_next_epoch = self.checkpoints_period / 2;
let checkpoints_period = if in_v2 {
self.checkpoints_period_v2
} else {
self.checkpoints_period
};

let seconds_before_next_epoch = checkpoints_period / 2;

start
.checked_add(i64::from(
self.checkpoints_period - seconds_before_next_epoch,
))
.checked_add(i64::from(checkpoints_period - seconds_before_next_epoch))
.ok_or(EpochCalculationError::Overflow)
}

pub fn get_epoch_period(&self, epoch: Epoch) -> Result<u16, EpochCalculationError> {
let (_, in_v2) = self.epoch_timestamp(epoch)?;
if in_v2 {
Ok(self.checkpoints_period_v2)
} else {
Ok(self.checkpoints_period)
}
}
}

#[derive(Debug, PartialEq, Eq)]
Expand Down
31 changes: 26 additions & 5 deletions data_structures/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,18 @@ pub fn get_protocol_version(epoch: Option<Epoch>) -> ProtocolVersion {
}

/// Let the protocol versions controller know about the a protocol version, and its activation epoch.
pub fn register_protocol_version(protocol_version: ProtocolVersion, epoch: Epoch) {
pub fn register_protocol_version(
protocol_version: ProtocolVersion,
epoch: Epoch,
checkpoint_period: u16,
) {
log::debug!(
"Registering protocol version {protocol_version}, which enters into force at epoch {epoch}"
);
// This unwrap is safe as long as the lock is not poisoned.
// The lock can only become poisoned when a writer panics.
let mut protocol_info = PROTOCOL.write().unwrap();
protocol_info.register(epoch, protocol_version);
protocol_info.register(epoch, protocol_version, checkpoint_period);
}

/// Set the protocol version that we are running.
Expand All @@ -163,6 +167,23 @@ pub fn refresh_protocol_version(current_epoch: Epoch) {
set_protocol_version(current_version)
}

pub fn get_protocol_version_activation_epoch(protocol_version: ProtocolVersion) -> Epoch {
// This unwrap is safe as long as the lock is not poisoned.
// The lock can only become poisoned when a writer panics.
let protocol = PROTOCOL.write().unwrap();
protocol.all_versions.get_activation_epoch(protocol_version)
}

pub fn get_protocol_version_period(protocol_version: ProtocolVersion) -> u16 {
// This unwrap is safe as long as the lock is not poisoned.
// The lock can only become poisoned when a writer panics.
let protocol = PROTOCOL.write().unwrap();
match protocol.all_checkpoints_periods.get(&protocol_version) {
Some(period) => *period,
None => u16::MAX,
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -183,9 +204,9 @@ mod tests {
assert_eq!(version, ProtocolVersion::V1_7);

// Register the different protocol versions
register_protocol_version(ProtocolVersion::V1_7, 100);
register_protocol_version(ProtocolVersion::V1_8, 200);
register_protocol_version(ProtocolVersion::V2_0, 300);
register_protocol_version(ProtocolVersion::V1_7, 100, 45);
register_protocol_version(ProtocolVersion::V1_8, 200, 45);
register_protocol_version(ProtocolVersion::V2_0, 300, 20);

// The initial protocol version should be the default one
let version = get_protocol_version(Some(0));
Expand Down
14 changes: 12 additions & 2 deletions data_structures/src/proto/versioning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ use crate::{
pub struct ProtocolInfo {
pub current_version: ProtocolVersion,
pub all_versions: VersionsMap,
pub all_checkpoints_periods: HashMap<ProtocolVersion, u16>,
}

impl ProtocolInfo {
pub fn register(&mut self, epoch: Epoch, version: ProtocolVersion) {
self.all_versions.register(epoch, version)
pub fn register(&mut self, epoch: Epoch, version: ProtocolVersion, checkpoint_period: u16) {
self.all_versions.register(epoch, version);
self.all_checkpoints_periods
.insert(version, checkpoint_period);
}
}

Expand All @@ -60,6 +63,13 @@ impl VersionsMap {
.copied()
.unwrap_or_default()
}

pub fn get_activation_epoch(&self, version: ProtocolVersion) -> Epoch {
match self.efv.get(&version) {
Some(epoch) => *epoch,
None => Epoch::MAX,
}
}
}

/// An enumeration of different protocol versions.
Expand Down
2 changes: 1 addition & 1 deletion data_structures/src/transaction_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ pub fn transaction_inputs_sum(
})?;

// Verify that commits are only accepted after the time lock expired
let epoch_timestamp = epoch_constants.epoch_timestamp(epoch)?;
let (epoch_timestamp, _) = epoch_constants.epoch_timestamp(epoch)?;
let vt_time_lock = i64::try_from(vt_output.time_lock)?;
if vt_time_lock > epoch_timestamp {
return Err(TransactionError::TimeLock {
Expand Down
6 changes: 5 additions & 1 deletion node/src/actors/chain_manager/mining.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,11 @@ impl ChainManager {
let (collateral_age, checkpoint_period) = match &self.chain_state.chain_info {
Some(x) => (
x.consensus_constants.collateral_age,
x.consensus_constants.checkpoints_period,
// Unwraps should be safe if we have a chain_info object
self.epoch_constants
.unwrap()
.get_epoch_period(current_epoch)
.unwrap(),
),
None => {
log::error!("ChainInfo is None");
Expand Down
15 changes: 13 additions & 2 deletions node/src/actors/chain_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1277,13 +1277,18 @@ impl ChainManager {
})
.into_actor(act)
})
.map_ok(|res, act, ctx| {
.map_ok(move |res, act, ctx| {
// Broadcast vote between one and ("superblock_period" - 5) epoch checkpoints later.
// This is used to prevent the race condition described in issue #1573
// It is also used to spread the CPU load by checking superblock votes along
// the superblock period with a safe margin
let mut rng = rand::thread_rng();
let checkpoints_period = act.consensus_constants().checkpoints_period;
// Should be safe here to just call unwraps
let checkpoints_period = act
.epoch_constants
.unwrap()
.get_epoch_period(current_epoch)
.unwrap();
let superblock_period = act.consensus_constants().superblock_period;
let end_range = if superblock_period > 5 {
(superblock_period - 5) * checkpoints_period
Expand Down Expand Up @@ -4072,6 +4077,8 @@ mod tests {
chain_manager.epoch_constants = Some(EpochConstants {
checkpoint_zero_timestamp: 0,
checkpoints_period: 1_000,
checkpoint_zero_timestamp_v2: i64::MAX,
checkpoints_period_v2: 1,
});
chain_manager.chain_state.chain_info = Some(ChainInfo {
environment: Environment::default(),
Expand Down Expand Up @@ -4113,10 +4120,12 @@ mod tests {
Reputation(0),
vrf_hash_1,
false,
Power::from(0 as u64),
block_2.hash(),
Reputation(0),
vrf_hash_2,
false,
Power::from(0 as u64),
&VrfSlots::new(vec![Hash::default()]),
ProtocolVersion::V1_7,
),
Expand Down Expand Up @@ -4199,6 +4208,8 @@ mod tests {
chain_manager.epoch_constants = Some(EpochConstants {
checkpoint_zero_timestamp: 0,
checkpoints_period: 1_000,
checkpoint_zero_timestamp_v2: i64::MAX,
checkpoints_period_v2: 1,
});
chain_manager.chain_state.chain_info = Some(ChainInfo {
environment: Environment::default(),
Expand Down
23 changes: 17 additions & 6 deletions node/src/actors/epoch_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use rand::Rng;
use witnet_data_structures::{
chain::{Epoch, EpochConstants},
error::EpochCalculationError,
get_protocol_version_activation_epoch, get_protocol_version_period,
proto::versioning::ProtocolVersion,
};
use witnet_util::timestamp::{
duration_between_timestamps, get_timestamp, get_timestamp_nanos, update_global_timestamp,
Expand Down Expand Up @@ -85,15 +87,15 @@ impl EpochManager {
pub fn set_checkpoint_zero_and_period(
&mut self,
checkpoint_zero_timestamp: i64,
mut checkpoints_period: u16,
checkpoints_period: u16,
checkpoint_zero_timestamp_v2: i64,
checkpoints_period_v2: u16,
) {
if checkpoints_period == 0 {
log::warn!("Setting the checkpoint period to the minimum value of 1 second");
checkpoints_period = 1;
}
self.constants = Some(EpochConstants {
checkpoint_zero_timestamp,
checkpoints_period,
checkpoint_zero_timestamp_v2,
checkpoints_period_v2,
});
}
/// Calculate the last checkpoint (current epoch) at the supplied timestamp
Expand All @@ -113,7 +115,10 @@ impl EpochManager {
pub fn epoch_timestamp(&self, epoch: Epoch) -> EpochResult<i64> {
match &self.constants {
// Calculate (period * epoch + zero) with overflow checks
Some(x) => Ok(x.epoch_timestamp(epoch)?),
Some(x) => {
let (timestamp, _) = x.epoch_timestamp(epoch)?;
Ok(timestamp)
}
None => Err(EpochManagerError::UnknownEpochConstants),
}
}
Expand All @@ -122,9 +127,15 @@ impl EpochManager {
config_mngr::get()
.into_actor(self)
.and_then(|config, act, ctx| {
let checkpoint_zero_timestamp_v2 =
config.consensus_constants.checkpoint_zero_timestamp
+ get_protocol_version_activation_epoch(ProtocolVersion::V2_0) as i64
* config.consensus_constants.checkpoints_period as i64;
act.set_checkpoint_zero_and_period(
config.consensus_constants.checkpoint_zero_timestamp,
config.consensus_constants.checkpoints_period,
checkpoint_zero_timestamp_v2,
get_protocol_version_period(ProtocolVersion::V2_0),
);
log::info!(
"Checkpoint zero timestamp: {}, checkpoints period: {}",
Expand Down
Loading

0 comments on commit d99bd83

Please sign in to comment.