Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 161 additions & 29 deletions bridges/centralized-ethereum/src/actors/wit_poller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use actix::prelude::*;
use serde_json::json;

use witnet_data_structures::{
chain::{Block, ConsensusConstants, DataRequestInfo, Hash, Hashable},
proto::versioning::ProtocolInfo,
chain::{Block, ConsensusConstants, DataRequestInfo, Epoch, Hash, Hashable},
proto::versioning::{ProtocolInfo, ProtocolVersion},
};
use witnet_net::client::tcp::{JsonRpcClient, jsonrpc};
use witnet_node::utils::stop_system_if_panicking;
Expand Down Expand Up @@ -239,8 +239,13 @@ async fn get_consensus_constants(
}
};
let consensus_constants = match result {
Ok(value) => serde_json::from_value::<ConsensusConstants>(value)
.expect("failed to deserialize consensus constants"),
Ok(value) => match serde_json::from_value::<ConsensusConstants>(value) {
Ok(consensus_constants) => consensus_constants,
Err(error) => {
log::error!("failed to deserialize consensus constants: {error}");
return Err(());
}
},
Err(e) => {
log::error!("error in getConsensusConstants call: {e:?}");
return Err(());
Expand Down Expand Up @@ -273,40 +278,94 @@ async fn get_dr_timestamp(
}
};
let block = match report {
Ok(value) => serde_json::from_value::<Block>(value).expect("failed to deserialize block"),
Ok(value) => match serde_json::from_value::<Block>(value) {
Ok(block) => block,
Err(error) => {
log::error!("failed to deserialize block {drt_block_hash}: {error}");
return Err(());
}
},
Err(e) => {
log::error!("error in getBlock call ({drt_block_hash}): {e:?}");
return Err(());
}
};
let dr_last_commit_epoch =
block.block_header.beacon.checkpoint + u32::from(dr_commits_round + 1);
let protocol_version = if protocol_info
derive_dr_last_commit_timestamp(
block.block_header.beacon.checkpoint,
dr_commits_round,
consensus_constants,
protocol_info,
)
.map_err(|error| {
log::error!("cannot derive data-request timestamp for block {drt_block_hash}: {error}");
})
}

/// Derive the timestamp of the epoch after the final commit round from the complete protocol
/// schedule returned by the node.
///
/// A resolved request can predate more than one activation, so selecting only the current or
/// immediately previous protocol is insufficient once Wit/2.1 is registered. The implicit V1.7
/// epoch-zero fallback deliberately has no period entry and must use the configured consensus base
/// clock rather than a hard-coded mainnet value.
fn derive_dr_last_commit_timestamp(
request_epoch: Epoch,
commit_round: u16,
consensus_constants: &ConsensusConstants,
protocol_info: &ProtocolInfo,
) -> Result<i64, String> {
protocol_info
.validate_timing_topology(consensus_constants)
.map_err(|error| format!("invalid protocol timing topology: {error}"))?;

let commit_epoch_delta = u32::from(commit_round)
.checked_add(1)
.ok_or_else(|| "commit-round epoch delta overflows u32".to_string())?;
let last_commit_epoch = request_epoch
.checked_add(commit_epoch_delta)
.ok_or_else(|| "last commit epoch overflows u32".to_string())?;
let protocol_version = protocol_info
.all_versions
.get_activation_epoch(protocol_info.current_version)
<= dr_last_commit_epoch
.version_for_epoch(last_commit_epoch);
let activation_epoch = match protocol_info
.all_versions
.try_get_activation_epoch(protocol_version)
{
protocol_info.current_version
} else {
protocol_info.current_version.prev()
Some(epoch) => epoch,
None if protocol_version == ProtocolVersion::MIN => 0,
None => {
return Err(format!(
"protocol {protocol_version} has no registered activation epoch"
));
}
};
let protocol_activation_timestamp = protocol_info
let activation_timestamp = protocol_info
.derive_activation_timestamp(protocol_version, consensus_constants)
.unwrap_or(consensus_constants.checkpoint_zero_timestamp);
let protocol_activation_epoch = protocol_info
.all_versions
.try_get_activation_epoch(protocol_version)
.unwrap_or_default();
let protocol_checkpoint_period = protocol_info
.ok_or_else(|| format!("cannot derive activation timestamp for {protocol_version}"))?;
let checkpoint_period = match protocol_info
.all_checkpoints_periods
.get(&protocol_version)
.unwrap_or(&45u16);

Ok(protocol_activation_timestamp
+ i64::from(
(dr_last_commit_epoch - protocol_activation_epoch)
* u32::from(*protocol_checkpoint_period),
))
.copied()
{
Some(period) => period,
None if protocol_version == ProtocolVersion::MIN => consensus_constants.checkpoints_period,
None => {
return Err(format!(
"protocol {protocol_version} has no registered checkpoint period"
));
}
};
let elapsed_epochs = last_commit_epoch.checked_sub(activation_epoch).ok_or_else(|| {
format!(
"last commit epoch {last_commit_epoch} precedes {protocol_version} activation {activation_epoch}"
)
})?;
let elapsed_seconds = i64::from(elapsed_epochs)
.checked_mul(i64::from(checkpoint_period))
.ok_or_else(|| "data-request epoch span overflows i64 seconds".to_string())?;
activation_timestamp
.checked_add(elapsed_seconds)
.ok_or_else(|| "data-request timestamp overflows i64".to_string())
}

/// Get current protocol info from the Witnet node
Expand All @@ -326,11 +385,84 @@ async fn get_protocol_info(witnet_client: Addr<JsonRpcClient>) -> Result<Protoco
}
};
match report {
Ok(value) => Ok(serde_json::from_value::<ProtocolInfo>(value)
.expect("failed to deserialize protocol info")),
Ok(value) => serde_json::from_value::<ProtocolInfo>(value).map_err(|error| {
log::error!("failed to deserialize protocol info: {error}");
}),
Err(e) => {
log::error!("Error when getting protocol info: {e:?}");
Err(())
}
}
}

#[cfg(test)]
mod tests {
use super::*;

fn multi_version_protocol_info() -> ProtocolInfo {
let mut info = ProtocolInfo::default();
info.try_register(10, ProtocolVersion::V1_8, 30).unwrap();
info.try_register(20, ProtocolVersion::V2_0, 15).unwrap();
info.try_register(30, ProtocolVersion::V2_1, 15).unwrap();
info.current_version = ProtocolVersion::V2_1;
info
}

#[test]
fn historical_request_timestamp_uses_its_exact_protocol_regime() {
let consensus_constants = ConsensusConstants {
checkpoint_zero_timestamp: 1_000,
checkpoints_period: 30,
..Default::default()
};
let info = multi_version_protocol_info();

// The last commit is epoch 13 under V1.8, two versions behind current V2.1.
assert_eq!(
derive_dr_last_commit_timestamp(12, 0, &consensus_constants, &info),
Ok(1_390)
);
}

#[test]
fn sparse_valid_schedule_uses_the_last_registered_timing_regime() {
let consensus_constants = ConsensusConstants {
checkpoint_zero_timestamp: 1_000,
checkpoints_period: 30,
..Default::default()
};
let mut info = ProtocolInfo::default();
info.try_register(20, ProtocolVersion::V2_0, 15).unwrap();
info.try_register(30, ProtocolVersion::V2_1, 15).unwrap();
info.current_version = ProtocolVersion::V2_1;

// V1.8 is intentionally absent. Epoch 20 starts at 1,600 on the base clock, and the final
// commit epoch 21 is one 15-second V2.0 checkpoint later.
assert_eq!(
derive_dr_last_commit_timestamp(20, 0, &consensus_constants, &info),
Ok(1_615)
);
}

#[test]
fn implicit_v1_7_uses_the_configured_base_period_and_overflow_fails_closed() {
let consensus_constants = ConsensusConstants {
checkpoint_zero_timestamp: 1_000,
checkpoints_period: 30,
..Default::default()
};
let info = multi_version_protocol_info();

assert_eq!(
derive_dr_last_commit_timestamp(4, 0, &consensus_constants, &info),
Ok(1_150)
);
assert!(derive_dr_last_commit_timestamp(
Epoch::MAX,
u16::MAX,
&consensus_constants,
&info,
)
.is_err());
}
}
43 changes: 40 additions & 3 deletions config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ pub struct Storage {
pub rad_hashes_index: bool,
/// RocksDB option max_open_files. -1 means unlimited.
pub max_open_files: i32,
/// Hex-encoded compressed secp256k1 public keys trusted to sign delegated-staking snapshot
/// checkpoint anchors. An empty list deliberately keeps non-genesis delegated snapshots
/// unavailable; genesis-only and pre-delegated compatibility imports do not need an anchor.
pub snapshot_trust_public_keys: Vec<String>,
}

/// JsonRPC API configuration
Expand Down Expand Up @@ -378,6 +382,12 @@ pub struct Mining {
/// Minimum value transfer transaction fee that allows being included into a block
#[partial_struct(serde(default, rename = "minimum_vtt_fee_nanowits"))]
pub minimum_vtt_fee: u64,
/// Minimum stake transaction fee that allows being retained in the local transaction pool
#[partial_struct(serde(default, rename = "minimum_st_fee_nanowits"))]
pub minimum_st_fee: u64,
/// Minimum unstake transaction fee that allows being retained in the local transaction pool
#[partial_struct(serde(default, rename = "minimum_ut_fee_nanowits"))]
pub minimum_ut_fee: u64,
}

/// NTP-related configuration
Expand All @@ -404,7 +414,10 @@ pub struct Ntp {
#[derive(PartialStruct, Debug, Clone, PartialEq, Eq)]
#[partial_struct(derive(Deserialize, Serialize, Default, Debug, Clone, PartialEq, Eq))]
pub struct Mempool {
/// Timeout to use again an UTXO spent by a pending transaction
/// Timeout to use again an UTXO spent by a pending transaction.
///
/// Zero deliberately makes the output immediately selectable again. Asynchronous Stake
/// rollback ownership is tracked by an opaque process-local generation, never by this value.
pub tx_pending_timeout: u64,
/// Maximum number of recovered transactions to include by epoch
pub max_reinserted_transactions: u32,
Expand All @@ -427,14 +440,17 @@ pub struct Protocol {
pub v1_7: Option<(Epoch, u16)>,
pub v1_8: Option<(Epoch, u16)>,
pub v2_0: Option<(Epoch, u16)>,
/// Optional Wit/2.1 activation. No network default is registered yet.
pub v2_1: Option<(Epoch, u16)>,
}

impl Protocol {
pub fn iter(&self) -> IntoIter<(ProtocolVersion, Option<(Epoch, u16)>), 3> {
pub fn iter(&self) -> IntoIter<(ProtocolVersion, Option<(Epoch, u16)>), 4> {
[
(ProtocolVersion::V1_7, self.v1_7),
(ProtocolVersion::V1_8, self.v1_8),
(ProtocolVersion::V2_0, self.v2_0),
(ProtocolVersion::V2_1, self.v2_1),
]
.into_iter()
}
Expand Down Expand Up @@ -788,6 +804,10 @@ impl Storage {
max_open_files: config
.max_open_files
.unwrap_or_else(|| defaults.storage_max_open_files()),
snapshot_trust_public_keys: config
.snapshot_trust_public_keys
.clone()
.unwrap_or_default(),
}
}

Expand All @@ -799,6 +819,7 @@ impl Storage {
utxos_in_memory: Some(self.utxos_in_memory),
rad_hashes_index: Some(self.rad_hashes_index),
max_open_files: Some(self.max_open_files),
snapshot_trust_public_keys: Some(self.snapshot_trust_public_keys.clone()),
}
}
}
Expand Down Expand Up @@ -872,6 +893,14 @@ impl Mining {
.minimum_vtt_fee
.to_owned()
.unwrap_or_else(|| defaults.mining_minimum_vtt_fee()),
minimum_st_fee: config
.minimum_st_fee
.to_owned()
.unwrap_or_else(|| defaults.mining_minimum_st_fee()),
minimum_ut_fee: config
.minimum_ut_fee
.to_owned()
.unwrap_or_else(|| defaults.mining_minimum_ut_fee()),
}
}

Expand All @@ -885,6 +914,8 @@ impl Mining {
mint_external_address: self.mint_external_address.clone(),
transactions_pool_total_weight_limit: Some(self.transactions_pool_total_weight_limit),
minimum_vtt_fee: Some(self.minimum_vtt_fee),
minimum_st_fee: Some(self.minimum_st_fee),
minimum_ut_fee: Some(self.minimum_ut_fee),
}
}
}
Expand Down Expand Up @@ -988,7 +1019,9 @@ pub struct Wallet {
/// Allow to use outputs that have not been confirmed by a superblock in new transactions
pub use_unconfirmed_utxos: bool,
/// If a transaction has not been included in a block for this amount of seconds, mark the UTXOs
/// as available again.
/// as available again. Zero deliberately makes them immediately selectable. The wallet writes
/// this availability timestamp synchronously while holding its state lock and has no deferred
/// timeout-based rollback callback, so this value is not used as reservation ownership.
pub pending_transactions_timeout_seconds: u64,
}

Expand Down Expand Up @@ -1223,6 +1256,9 @@ impl Partializable for Protocol {
v2_0: config
.v2_0
.or(defaults.get(&ProtocolVersion::V2_0).copied()),
v2_1: config
.v2_1
.or(defaults.get(&ProtocolVersion::V2_1).copied()),
}
}

Expand Down Expand Up @@ -1369,6 +1405,7 @@ mod tests {
utxos_in_memory: None,
rad_hashes_index: None,
max_open_files: None,
snapshot_trust_public_keys: None,
};
let config = Storage::from_partial(&partial_config, &Testnet);

Expand Down
12 changes: 12 additions & 0 deletions config/src/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,18 @@ pub trait Defaults {
0
}

/// Allow setting a minimum stake transaction fee to be retained in the local pool.
/// Setting it to zero accepts every consensus-valid fee.
fn mining_minimum_st_fee(&self) -> u64 {
0
}

/// Allow setting a minimum unstake transaction fee to be retained in the local pool.
/// Setting it to zero accepts every consensus-valid fee.
fn mining_minimum_ut_fee(&self) -> u64 {
0
}

/// A network-wide reward to collateral ratio for data requests to be included in a block
/// The absolute minimum accepted by the network should be defined as a consensus constant,
/// but to avoid affecting the magic number, it's a "pseudoconsensus constant" for now.
Expand Down
Loading