Compare commits
2 Commits
drazen/lp-reg
...
base
| Author | SHA1 | Date | |
|---|---|---|---|
| f25f76c1df | |||
| 29f95febe9 |
+13
-10
@@ -116,18 +116,21 @@ async-trait = "0.1.64"
|
||||
anyhow = "1.0.71"
|
||||
bip39 = { version = "2.0.0", features = ["zeroize"] }
|
||||
cfg-if = "1.0.0"
|
||||
cosmwasm-derive = "=1.0.0"
|
||||
cosmwasm-schema = "=1.0.0"
|
||||
cosmwasm-std = "=1.0.0"
|
||||
cosmwasm-storage = "=1.0.0"
|
||||
cw-utils = "=0.13.4"
|
||||
cw-storage-plus = "=0.13.4"
|
||||
cw2 = { version = "=0.13.4" }
|
||||
cw3 = { version = "=0.13.4" }
|
||||
cw3-fixed-multisig = { version = "=0.13.4" }
|
||||
cw4 = { version = "=0.13.4" }
|
||||
cosmwasm-derive = "=1.2.5"
|
||||
cosmwasm-schema = "=1.2.5"
|
||||
cosmwasm-std = "=1.2.5"
|
||||
cosmwasm-storage = "=1.2.5"
|
||||
cosmrs = "=0.8.0"
|
||||
cw-utils = "=1.0.1"
|
||||
cw-storage-plus = "=1.0.1"
|
||||
cw2 = { version = "=1.0.1" }
|
||||
cw3 = { version = "=1.0.1" }
|
||||
cw3-fixed-multisig = { version = "=1.0.1" }
|
||||
cw4 = { version = "=1.0.1" }
|
||||
cw-controllers = { version = "=1.0.1" }
|
||||
dotenvy = "0.15.6"
|
||||
generic-array = "0.14.7"
|
||||
k256 = "0.11"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4"
|
||||
once_cell = "1.7.2"
|
||||
|
||||
@@ -40,7 +40,7 @@ nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
|
||||
async-trait = { workspace = true, optional = true }
|
||||
bip39 = { workspace = true, features = ["rand"], optional = true }
|
||||
nym-config = { path = "../../config", optional = true }
|
||||
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support", features = ["rpc", "bip32", "cosmwasm"], optional = true }
|
||||
cosmrs = { workspace = true, features = ["rpc", "bip32", "cosmwasm"], optional = true }
|
||||
# note that this has the same version as used by cosmrs
|
||||
eyre = { version = "0.6", optional = true }
|
||||
cw3 = { workspace = true, optional = true }
|
||||
@@ -54,7 +54,7 @@ cosmwasm-std = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
bip39 = { workspace = true }
|
||||
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support", features = ["rpc", "bip32"] }
|
||||
cosmrs = { workspace = true, features = ["rpc", "bip32"] }
|
||||
tokio = { version = "1.24.1", features = ["rt-multi-thread", "macros"] }
|
||||
ts-rs = "6.1.2"
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ impl GasAdjustable for Gas {
|
||||
mod sealed {
|
||||
use cosmrs::tx::{self, Gas};
|
||||
use cosmrs::Coin as CosmosCoin;
|
||||
use cosmrs::{AccountId, Decimal as CosmosDecimal, Denom as CosmosDenom};
|
||||
use cosmrs::{AccountId, Denom as CosmosDenom};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
fn cosmos_denom_inner_getter(val: &CosmosDenom) -> String {
|
||||
@@ -144,29 +144,11 @@ mod sealed {
|
||||
}
|
||||
}
|
||||
|
||||
fn cosmos_decimal_inner_getter(val: &CosmosDecimal) -> u64 {
|
||||
// haha, this code is so disgusting. I'll make a PR on cosmrs to slightly alleviate those issues...
|
||||
// note: unwrap here is fine as the to_string is just returning a stringified u64 which, well, is a valid u64
|
||||
val.to_string().parse().unwrap()
|
||||
}
|
||||
|
||||
// at the time of writing it the current cosmrs' Decimal is extremely limited...
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(remote = "CosmosDecimal")]
|
||||
struct Decimal(#[serde(getter = "cosmos_decimal_inner_getter")] u64);
|
||||
|
||||
impl From<Decimal> for CosmosDecimal {
|
||||
fn from(val: Decimal) -> Self {
|
||||
val.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct Coin {
|
||||
#[serde(with = "Denom")]
|
||||
denom: CosmosDenom,
|
||||
#[serde(with = "Decimal")]
|
||||
amount: CosmosDecimal,
|
||||
amount: u128,
|
||||
}
|
||||
|
||||
impl From<Coin> for CosmosCoin {
|
||||
|
||||
@@ -39,7 +39,7 @@ pub use cosmrs::tendermint::validator::Info as TendermintValidatorInfo;
|
||||
pub use cosmrs::tendermint::Time as TendermintTime;
|
||||
pub use cosmrs::tx::{self, Gas};
|
||||
pub use cosmrs::Coin as CosmosCoin;
|
||||
pub use cosmrs::{bip32, AccountId, Decimal, Denom};
|
||||
pub use cosmrs::{bip32, AccountId, Denom};
|
||||
use cosmwasm_std::Addr;
|
||||
pub use cosmwasm_std::Coin as CosmWasmCoin;
|
||||
pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
|
||||
|
||||
@@ -14,7 +14,7 @@ clap = { version = "4.0", features = ["derive"] }
|
||||
cw-utils = { workspace = true }
|
||||
handlebars = "3.0.1"
|
||||
humantime-serde = "1.0"
|
||||
k256 = { version = "0.10", features = ["ecdsa", "sha256"] }
|
||||
k256 = { workspace = true, features = ["ecdsa", "sha256"] }
|
||||
log = { workspace = true }
|
||||
rand = {version = "0.6", features = ["std"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -25,7 +25,7 @@ toml = "0.5.6"
|
||||
url = "2.2"
|
||||
tap = "1"
|
||||
|
||||
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" }
|
||||
cosmrs = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
|
||||
nym-validator-client = { path = "../client-libs/validator-client", features = ["nyxd-client"] }
|
||||
|
||||
@@ -56,6 +56,8 @@ pub async fn generate(args: Args) {
|
||||
.expect("threshold can't be converted to Decimal"),
|
||||
},
|
||||
max_voting_period: Duration::Time(args.max_voting_period),
|
||||
executor: None,
|
||||
proposal_deposit: None,
|
||||
coconut_bandwidth_contract_address: coconut_bandwidth_contract_address.to_string(),
|
||||
coconut_dkg_contract_address: coconut_dkg_contract_address.to_string(),
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct InstantiateMsg {
|
||||
pub mix_denom: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecuteMsg {
|
||||
DepositFunds { data: DepositData },
|
||||
|
||||
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::msg::ExecuteMsg;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
pub struct SpendCredentialData {
|
||||
funds: Coin,
|
||||
blinded_serial_number: String,
|
||||
@@ -43,7 +43,7 @@ pub enum SpendCredentialStatus {
|
||||
Spent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct SpendCredential {
|
||||
funds: Coin,
|
||||
blinded_serial_number: String,
|
||||
@@ -74,7 +74,7 @@ impl SpendCredential {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct PagedSpendCredentialResponse {
|
||||
pub spend_credentials: Vec<SpendCredential>,
|
||||
pub per_page: usize,
|
||||
@@ -95,7 +95,7 @@ impl PagedSpendCredentialResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct SpendCredentialResponse {
|
||||
pub spend_credential: Option<SpendCredential>,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ pub type Nonce = u32;
|
||||
|
||||
// define this type explicitly for [hopefully] better usability
|
||||
// (so you wouldn't need to worry about whether you should use bytes, bs58, etc.)
|
||||
#[derive(Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
pub struct MessageSignature(Vec<u8>);
|
||||
|
||||
impl MessageSignature {
|
||||
|
||||
@@ -6,6 +6,8 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cw4 = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
schemars = "0.8"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use cosmwasm_schema::{cw_serde, QueryResponses};
|
||||
use cw4::Member;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
/// The admin is the only account that can update the group state.
|
||||
/// Omit it to make the group immutable.
|
||||
@@ -15,8 +9,7 @@ pub struct InstantiateMsg {
|
||||
pub members: Vec<Member>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Change the admin
|
||||
UpdateAdmin { admin: Option<String> },
|
||||
@@ -32,23 +25,24 @@ pub enum ExecuteMsg {
|
||||
RemoveHook { addr: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cw_serde]
|
||||
#[derive(QueryResponses)]
|
||||
pub enum QueryMsg {
|
||||
/// Return AdminResponse
|
||||
#[returns(cw_controllers::AdminResponse)]
|
||||
Admin {},
|
||||
/// Return TotalWeightResponse
|
||||
TotalWeight {},
|
||||
/// Returns MembersListResponse
|
||||
#[returns(cw4::TotalWeightResponse)]
|
||||
TotalWeight { at_height: Option<u64> },
|
||||
#[returns(cw4::MemberListResponse)]
|
||||
ListMembers {
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
/// Returns MemberResponse
|
||||
#[returns(cw4::MemberResponse)]
|
||||
Member {
|
||||
addr: String,
|
||||
at_height: Option<u64>,
|
||||
},
|
||||
/// Shows all registered hooks. Returns HooksResponse.
|
||||
/// Shows all registered hooks.
|
||||
#[returns(cw_controllers::HooksResponse)]
|
||||
Hooks {},
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ pub fn generate_owner_storage_subkey(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
|
||||
pub struct Delegation {
|
||||
/// Address of the owner of this delegation.
|
||||
pub owner: Addr,
|
||||
@@ -114,7 +114,7 @@ impl Delegation {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct PagedMixNodeDelegationsResponse {
|
||||
pub delegations: Vec<Delegation>,
|
||||
pub start_next_after: Option<OwnerProxySubKey>,
|
||||
@@ -129,7 +129,7 @@ impl PagedMixNodeDelegationsResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct PagedDelegatorDelegationsResponse {
|
||||
pub delegations: Vec<Delegation>,
|
||||
pub start_next_after: Option<(MixId, OwnerProxySubKey)>,
|
||||
@@ -147,7 +147,7 @@ impl PagedDelegatorDelegationsResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct MixNodeDelegationResponse {
|
||||
pub delegation: Option<Delegation>,
|
||||
pub mixnode_still_bonded: bool,
|
||||
@@ -162,7 +162,7 @@ impl MixNodeDelegationResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct PagedAllDelegationsResponse {
|
||||
pub delegations: Vec<Delegation>,
|
||||
pub start_next_after: Option<StorageKey>,
|
||||
|
||||
@@ -23,7 +23,7 @@ pub struct Gateway {
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct GatewayBond {
|
||||
pub pledge_amount: Coin,
|
||||
pub owner: Addr,
|
||||
@@ -132,7 +132,7 @@ impl GatewayConfigUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct PagedGatewayResponse {
|
||||
pub nodes: Vec<GatewayBond>,
|
||||
pub per_page: usize,
|
||||
@@ -153,13 +153,13 @@ impl PagedGatewayResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct GatewayOwnershipResponse {
|
||||
pub address: Addr,
|
||||
pub gateway: Option<GatewayBond>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct GatewayBondResponse {
|
||||
pub identity: IdentityKey,
|
||||
pub gateway: Option<GatewayBond>,
|
||||
|
||||
@@ -489,7 +489,7 @@ impl CurrentIntervalResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct PendingEpochEventsResponse {
|
||||
pub seconds_until_executable: i64,
|
||||
pub events: Vec<PendingEpochEvent>,
|
||||
@@ -510,7 +510,7 @@ impl PendingEpochEventsResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct PendingIntervalEventsResponse {
|
||||
pub seconds_until_executable: i64,
|
||||
pub events: Vec<PendingIntervalEvent>,
|
||||
|
||||
@@ -33,7 +33,7 @@ impl RewardedSetNodeStatus {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct MixNodeDetails {
|
||||
pub bond_information: MixNodeBond,
|
||||
pub rewarding_details: MixNodeRewarding,
|
||||
@@ -86,7 +86,7 @@ impl MixNodeDetails {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct MixNodeRewarding {
|
||||
/// Information provided by the operator that influence the cost function.
|
||||
pub cost_params: MixNodeCostParams,
|
||||
@@ -465,7 +465,7 @@ impl MixNodeRewarding {
|
||||
}
|
||||
|
||||
// operator information + data assigned by the contract(s)
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct MixNodeBond {
|
||||
/// Unique id assigned to the bonded mixnode.
|
||||
pub mix_id: MixId,
|
||||
@@ -559,7 +559,7 @@ pub struct MixNode {
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct MixNodeCostParams {
|
||||
pub profit_margin_percent: Percent,
|
||||
|
||||
@@ -686,7 +686,7 @@ impl MixNodeConfigUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct PagedMixnodeBondsResponse {
|
||||
pub nodes: Vec<MixNodeBond>,
|
||||
pub per_page: usize,
|
||||
@@ -703,7 +703,7 @@ impl PagedMixnodeBondsResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct PagedMixnodesDetailsResponse {
|
||||
pub nodes: Vec<MixNodeDetails>,
|
||||
pub per_page: usize,
|
||||
@@ -745,19 +745,19 @@ impl PagedUnbondedMixnodesResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct MixOwnershipResponse {
|
||||
pub address: Addr,
|
||||
pub mixnode_details: Option<MixNodeDetails>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct MixnodeDetailsResponse {
|
||||
pub mix_id: MixId,
|
||||
pub mixnode_details: Option<MixNodeDetails>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)]
|
||||
pub struct MixnodeRewardingDetailsResponse {
|
||||
pub mix_id: MixId,
|
||||
pub rewarding_details: Option<MixNodeRewarding>,
|
||||
|
||||
@@ -7,19 +7,19 @@ use crate::{BlockHeight, EpochEventId, IntervalEventId, MixId};
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PendingEpochEvent {
|
||||
pub id: EpochEventId,
|
||||
pub event: PendingEpochEventData,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PendingEpochEventData {
|
||||
pub created_at: BlockHeight,
|
||||
pub kind: PendingEpochEventKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum PendingEpochEventKind {
|
||||
// can't just pass the `Delegation` struct here as it's impossible to determine
|
||||
// `cumulative_reward_ratio` ahead of time
|
||||
@@ -68,19 +68,19 @@ impl From<(EpochEventId, PendingEpochEventData)> for PendingEpochEvent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PendingIntervalEvent {
|
||||
pub id: IntervalEventId,
|
||||
pub event: PendingIntervalEventData,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PendingIntervalEventData {
|
||||
pub created_at: BlockHeight,
|
||||
pub kind: PendingIntervalEventKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum PendingIntervalEventKind {
|
||||
ChangeMixCostParams {
|
||||
mix_id: MixId,
|
||||
|
||||
@@ -35,7 +35,7 @@ pub struct RewardDistribution {
|
||||
pub delegates: Decimal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct PendingRewardResponse {
|
||||
pub amount_staked: Option<Coin>,
|
||||
pub amount_earned: Option<Coin>,
|
||||
@@ -46,7 +46,7 @@ pub struct PendingRewardResponse {
|
||||
pub mixnode_still_fully_bonded: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct EstimatedCurrentEpochRewardResponse {
|
||||
pub original_stake: Option<Coin>,
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ pub type EpochEventId = u32;
|
||||
pub type IntervalEventId = u32;
|
||||
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, PartialEq, Eq)]
|
||||
pub struct LayerAssignment {
|
||||
mix_id: MixId,
|
||||
layer: Layer,
|
||||
@@ -119,7 +119,7 @@ impl Index<Layer> for LayerDistribution {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
pub struct ContractState {
|
||||
pub owner: Addr, // only the owner account can update state
|
||||
pub rewarding_validator_address: Addr,
|
||||
@@ -131,7 +131,7 @@ pub struct ContractState {
|
||||
pub params: ContractStateParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
pub struct ContractStateParams {
|
||||
/// Minimum amount a delegator must stake in orders for his delegation to get accepted.
|
||||
pub minimum_mixnode_delegation: Option<Coin>,
|
||||
|
||||
@@ -9,6 +9,8 @@ edition = "2021"
|
||||
cw-utils = { workspace = true }
|
||||
cw3 = { workspace = true }
|
||||
cw4 = { workspace= true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
schemars = "0.8"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::StdError;
|
||||
use cw_utils::ThresholdError;
|
||||
use cw3::DepositError;
|
||||
use cw_utils::{PaymentError, ThresholdError};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -17,9 +18,6 @@ pub enum ContractError {
|
||||
#[error("Group contract invalid address '{addr}'")]
|
||||
InvalidGroup { addr: String },
|
||||
|
||||
#[error("Coconut bandwidth contract address not found")]
|
||||
InvalidCoconutBandwidth {},
|
||||
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized {},
|
||||
|
||||
@@ -43,4 +41,10 @@ pub enum ContractError {
|
||||
|
||||
#[error("Cannot close completed or passed proposals")]
|
||||
WrongCloseStatus {},
|
||||
|
||||
#[error("{0}")]
|
||||
Payment(#[from] PaymentError),
|
||||
|
||||
#[error("{0}")]
|
||||
Deposit(#[from] DepositError),
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod error;
|
||||
pub mod msg;
|
||||
pub mod state;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use cosmwasm_schema::{cw_serde, QueryResponses};
|
||||
use cosmwasm_std::{CosmosMsg, Empty};
|
||||
use cw3::Vote;
|
||||
use cw3::{UncheckedDepositInfo, Vote};
|
||||
use cw4::MemberChangedHookMsg;
|
||||
use cw_utils::{Duration, Expiration, Threshold};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
|
||||
use crate::state::Executor;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
// this is the group contract that contains the member list
|
||||
pub group_addr: String,
|
||||
@@ -17,11 +17,15 @@ pub struct InstantiateMsg {
|
||||
pub coconut_dkg_contract_address: String,
|
||||
pub threshold: Threshold,
|
||||
pub max_voting_period: Duration,
|
||||
// who is able to execute passed proposals
|
||||
// None means that anyone can execute
|
||||
pub executor: Option<Executor>,
|
||||
/// The cost of creating a proposal (if any).
|
||||
pub proposal_deposit: Option<UncheckedDepositInfo>,
|
||||
}
|
||||
|
||||
// TODO: add some T variants? Maybe good enough as fixed Empty for now
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
Propose {
|
||||
title: String,
|
||||
@@ -45,41 +49,44 @@ pub enum ExecuteMsg {
|
||||
}
|
||||
|
||||
// We can also add this as a cw3 extension
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cw_serde]
|
||||
#[derive(QueryResponses)]
|
||||
pub enum QueryMsg {
|
||||
/// Return ThresholdResponse
|
||||
#[returns(cw_utils::ThresholdResponse)]
|
||||
Threshold {},
|
||||
/// Returns ProposalResponse
|
||||
#[returns(cw3::ProposalResponse)]
|
||||
Proposal { proposal_id: u64 },
|
||||
/// Returns ProposalListResponse
|
||||
#[returns(cw3::ProposalListResponse)]
|
||||
ListProposals {
|
||||
start_after: Option<u64>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
/// Returns ProposalListResponse
|
||||
#[returns(cw3::ProposalListResponse)]
|
||||
ReverseProposals {
|
||||
start_before: Option<u64>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
/// Returns VoteResponse
|
||||
#[returns(cw3::VoteResponse)]
|
||||
Vote { proposal_id: u64, voter: String },
|
||||
/// Returns VoteListResponse
|
||||
#[returns(cw3::VoteListResponse)]
|
||||
ListVotes {
|
||||
proposal_id: u64,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
/// Returns VoterInfo
|
||||
#[returns(cw3::VoterResponse)]
|
||||
Voter { address: String },
|
||||
/// Returns VoterListResponse
|
||||
#[returns(cw3::VoterListResponse)]
|
||||
ListVoters {
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
},
|
||||
/// Gets the current configuration.
|
||||
#[returns(crate::state::Config)]
|
||||
Config {},
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
pub coconut_bandwidth_address: String,
|
||||
pub coconut_dkg_address: String,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, QuerierWrapper};
|
||||
use cw3::DepositInfo;
|
||||
use cw4::Cw4Contract;
|
||||
use cw_storage_plus::Item;
|
||||
use cw_utils::{Duration, Threshold};
|
||||
|
||||
use crate::error::ContractError;
|
||||
|
||||
/// Defines who is able to execute proposals once passed
|
||||
#[cw_serde]
|
||||
pub enum Executor {
|
||||
/// Any member of the voting group, even with 0 points
|
||||
Member,
|
||||
/// Only the given address
|
||||
Only(Addr),
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct Config {
|
||||
pub threshold: Threshold,
|
||||
pub max_voting_period: Duration,
|
||||
// Total weight and voters are queried from this contract
|
||||
pub group_addr: Cw4Contract,
|
||||
pub coconut_bandwidth_addr: Addr,
|
||||
pub coconut_dkg_addr: Addr,
|
||||
// who is able to execute passed proposals
|
||||
// None means that anyone can execute
|
||||
pub executor: Option<Executor>,
|
||||
/// The price, if any, of creating a new proposal.
|
||||
pub proposal_deposit: Option<DepositInfo>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
// Executor can be set in 3 ways:
|
||||
// - Member: any member of the voting group is authorized
|
||||
// - Only: only passed address is authorized
|
||||
// - None: Everyone are authorized
|
||||
pub fn authorize(&self, querier: &QuerierWrapper, sender: &Addr) -> Result<(), ContractError> {
|
||||
if let Some(executor) = &self.executor {
|
||||
match executor {
|
||||
Executor::Member => {
|
||||
self.group_addr
|
||||
.is_member(querier, sender, None)?
|
||||
.ok_or(ContractError::Unauthorized {})?;
|
||||
}
|
||||
Executor::Only(addr) => {
|
||||
if addr != sender {
|
||||
return Err(ContractError::Unauthorized {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// unique items
|
||||
pub const CONFIG: Item<Config> = Item::new("config");
|
||||
@@ -28,7 +28,7 @@ pub enum Period {
|
||||
After,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
pub struct PledgeData {
|
||||
pub amount: Coin,
|
||||
pub block_time: Timestamp,
|
||||
@@ -49,7 +49,7 @@ impl PledgeData {
|
||||
}
|
||||
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
pub enum PledgeCap {
|
||||
Percent(Percent),
|
||||
Absolute(Uint128), // This has to be in unym
|
||||
@@ -77,7 +77,7 @@ impl Default for PledgeCap {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
pub struct OriginalVestingResponse {
|
||||
pub amount: Coin,
|
||||
pub number_of_periods: usize,
|
||||
|
||||
@@ -55,7 +55,7 @@ impl VestingSpecification {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecuteMsg {
|
||||
// Families
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] }
|
||||
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" }
|
||||
cosmrs = { workspace = true }
|
||||
thiserror = "1.0"
|
||||
|
||||
# I guess temporarily until we get serde support in coconut up and running
|
||||
|
||||
@@ -6,8 +6,8 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bip32 = "0.3.0"
|
||||
k256 = "0.10.4"
|
||||
bip32 = "0.4.0"
|
||||
k256 = { workspace = true }
|
||||
ledger-transport = "0.10.0"
|
||||
ledger-transport-hid = "0.10.0"
|
||||
thiserror = "1"
|
||||
@@ -20,7 +20,7 @@ url = "2.2"
|
||||
ts-rs = "6.1.2"
|
||||
|
||||
cosmwasm-std = { workspace = true }
|
||||
cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" }
|
||||
cosmrs = { workspace = true }
|
||||
|
||||
nym-validator-client = { path = "../../common/client-libs/validator-client", features = [
|
||||
"nyxd-client",
|
||||
|
||||
Generated
+235
-103
@@ -241,8 +241,8 @@ dependencies = [
|
||||
"cosmwasm-storage",
|
||||
"cw-controllers",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw-utils 1.0.1",
|
||||
"cw3",
|
||||
"cw3-flex-multisig",
|
||||
"cw4",
|
||||
@@ -260,9 +260,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.7.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3"
|
||||
checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
@@ -272,11 +272,11 @@ checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b"
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-crypto"
|
||||
version = "1.0.0"
|
||||
version = "1.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eb0afef2325df81aadbf9be1233f522ed8f6e91df870c764bc44cca2b1415bd"
|
||||
checksum = "75836a10cb9654c54e77ee56da94d592923092a10b369cdb0dbd56acefc16340"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"digest 0.10.7",
|
||||
"ed25519-zebra",
|
||||
"k256",
|
||||
"rand_core 0.6.4",
|
||||
@@ -285,45 +285,62 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-derive"
|
||||
version = "1.0.0"
|
||||
version = "1.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b36e527620a2a3e00e46b6e731ab6c9b68d11069c986f7d7be8eba79ef081a4"
|
||||
checksum = "1c9f7f0e51bfc7295f7b2664fe8513c966428642aa765dad8a74acdab5e0c773"
|
||||
dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-schema"
|
||||
version = "1.0.0"
|
||||
version = "1.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "772e80bbad231a47a2068812b723a1ff81dd4a0d56c9391ac748177bea3a61da"
|
||||
checksum = "0f00b363610218eea83f24bbab09e1a7c3920b79f068334fdfcc62f6129ef9fc"
|
||||
dependencies = [
|
||||
"cosmwasm-schema-derive",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-schema-derive"
|
||||
version = "1.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae38f909b2822d32b275c9e2db9728497aa33ffe67dd463bc67c6a3b7092785c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-std"
|
||||
version = "1.0.0"
|
||||
version = "1.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "875994993c2082a6fcd406937bf0fca21c349e4a624f3810253a14fa83a3a195"
|
||||
checksum = "a49b85345e811c8e80ec55d0d091e4fcb4f00f97ab058f9be5f614c444a730cb"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"cosmwasm-crypto",
|
||||
"cosmwasm-derive",
|
||||
"derivative",
|
||||
"forward_ref",
|
||||
"hex",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
"serde-json-wasm 0.5.1",
|
||||
"sha2 0.10.6",
|
||||
"thiserror",
|
||||
"uint",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-storage"
|
||||
version = "1.0.0"
|
||||
version = "1.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d18403b07304d15d304dad11040d45bbcaf78d603b4be3fb5e2685c16f9229b5"
|
||||
checksum = "a3737a3aac48f5ed883b5b73bfb731e77feebd8fc6b43419844ec2971072164d"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"serde",
|
||||
@@ -389,9 +406,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.3.2"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21"
|
||||
checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef"
|
||||
dependencies = [
|
||||
"generic-array 0.14.6",
|
||||
"rand_core 0.6.4",
|
||||
@@ -454,13 +471,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cw-controllers"
|
||||
version = "0.13.4"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f0bc6019b4d3d81e11f5c384bcce7173e2210bd654d75c6c9668e12cca05dfa"
|
||||
checksum = "91440ce8ec4f0642798bc8c8cb6b9b53c1926c6dadaf0eed267a5145cd529071"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw-utils 1.0.1",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
@@ -468,17 +486,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cw-multi-test"
|
||||
version = "0.13.4"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3f9a8ab7c3c29ec93cb7a39ce4b14a05e053153b4a17ef7cf2246af1b7c087e"
|
||||
checksum = "2a18afd2e201221c6d72a57f0886ef2a22151bbc9e6db7af276fde8a91081042"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-std",
|
||||
"cosmwasm-storage",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw-utils 1.0.1",
|
||||
"derivative",
|
||||
"itertools",
|
||||
"k256",
|
||||
"prost",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -487,9 +505,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cw-storage-plus"
|
||||
version = "0.13.4"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "648b1507290bbc03a8d88463d7cd9b04b1fa0155e5eef366c4fa052b9caaac7a"
|
||||
checksum = "d9b6f91c0b94481a3e9ef1ceb183c37d00764f8751e39b45fc09f4d9b970d469"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw-storage-plus"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053a5083c258acd68386734f428a5a171b29f7d733151ae83090c6fcc9417ffa"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"schemars",
|
||||
@@ -498,50 +527,117 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cw-utils"
|
||||
version = "0.13.4"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b"
|
||||
checksum = "d6a84c6c1c0acc3616398eba50783934bd6c964bad6974241eaee3460c8f5b26"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw2 0.16.0",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw-utils"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw2 1.0.1",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw2"
|
||||
version = "0.13.4"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1"
|
||||
checksum = "91398113b806f4d2a8d5f8d05684704a20ffd5968bf87e3473e1973710b884ad"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw-storage-plus 0.16.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw2"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fb70cee2cf0b4a8ff7253e6bc6647107905e8eb37208f87d54f67810faa62f8"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw20"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91666da6c7b40c8dd5ff94df655a28114efc10c79b70b4d06f13c31e37d60609"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-utils 1.0.1",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw20-base"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f79314264ffc46b7658ee30caccc1540f14b9119568264bc02817f79c6f989a9"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus 0.16.0",
|
||||
"cw-utils 0.16.0",
|
||||
"cw2 1.0.1",
|
||||
"cw20",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw3"
|
||||
version = "0.13.4"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe19462a7f644ba60c19d3443cb90d00c50d9b6b3b0a3a7fca93df8261af979b"
|
||||
checksum = "2fe0b587008aa221cd2a2579a21990a28c4347dc53ad43167c68ad765f5b6efa"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-utils",
|
||||
"cw-utils 1.0.1",
|
||||
"cw20",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw3-fixed-multisig"
|
||||
version = "0.13.4"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df54aa54c13f405ec4ab36b6217538bc957d439eee58f89312db05a79caf6706"
|
||||
checksum = "e9e2415adb201e5e89dab34edf59d7dc166bc558526de009a49ae66276c9119a"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw-utils 1.0.1",
|
||||
"cw2 1.0.1",
|
||||
"cw3",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -550,14 +646,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cw3-flex-multisig"
|
||||
version = "0.13.1"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw-utils 1.0.1",
|
||||
"cw2 1.0.1",
|
||||
"cw20",
|
||||
"cw20-base",
|
||||
"cw3",
|
||||
"cw3-fixed-multisig",
|
||||
"cw4",
|
||||
@@ -565,31 +662,31 @@ dependencies = [
|
||||
"nym-group-contract-common",
|
||||
"nym-multisig-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw4"
|
||||
version = "0.13.4"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0acc3549d5ce11c6901b3a676f2e2628684722197054d97cd0101ea174ed5cbd"
|
||||
checksum = "2c236e0bae02ce97e89235a681dd0e07d099524b369f1ef908d704db3e6b049b"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw4-group"
|
||||
version = "0.13.4"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw-utils 1.0.1",
|
||||
"cw2 1.0.1",
|
||||
"cw4",
|
||||
"nym-group-contract-common",
|
||||
"schemars",
|
||||
@@ -599,11 +696,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.5.1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c"
|
||||
checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -654,9 +752,9 @@ checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.13.4"
|
||||
version = "0.14.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9"
|
||||
checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c"
|
||||
dependencies = [
|
||||
"der",
|
||||
"elliptic-curve",
|
||||
@@ -683,7 +781,7 @@ dependencies = [
|
||||
"ed25519",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"sha2",
|
||||
"sha2 0.9.9",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -698,7 +796,7 @@ dependencies = [
|
||||
"hex",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"sha2",
|
||||
"sha2 0.9.9",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -710,16 +808,18 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.11.12"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25b477563c2bfed38a3b7a60964c49e058b2510ad3f12ba3483fd8f62c2306d6"
|
||||
checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"der",
|
||||
"digest 0.10.7",
|
||||
"ff",
|
||||
"generic-array 0.14.6",
|
||||
"group",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sec1",
|
||||
"subtle 2.4.1",
|
||||
@@ -778,9 +878,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.11.1"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924"
|
||||
checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.4.1",
|
||||
@@ -974,9 +1074,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.11.0"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89"
|
||||
checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core 0.6.4",
|
||||
@@ -1020,7 +1120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"hmac",
|
||||
"hmac 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1033,6 +1133,15 @@ dependencies = [
|
||||
"digest 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
@@ -1123,15 +1232,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "k256"
|
||||
version = "0.10.4"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19c3a5e0a0b8450278feda242592512e09f61c72e018b8cd5c859482802daf2d"
|
||||
checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"sec1",
|
||||
"sha2",
|
||||
"sha2 0.10.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1267,7 +1375,7 @@ dependencies = [
|
||||
"cosmwasm-std",
|
||||
"cosmwasm-storage",
|
||||
"cw-controllers",
|
||||
"cw-storage-plus",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"nym-coconut-bandwidth-contract-common",
|
||||
"nym-multisig-contract-common",
|
||||
"schemars",
|
||||
@@ -1293,7 +1401,7 @@ dependencies = [
|
||||
"cosmwasm-storage",
|
||||
"cw-controllers",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw4",
|
||||
"cw4-group",
|
||||
"lazy_static",
|
||||
@@ -1310,7 +1418,7 @@ name = "nym-coconut-dkg-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"cw-utils",
|
||||
"cw-utils 1.0.1",
|
||||
"nym-contracts-common",
|
||||
"nym-multisig-contract-common",
|
||||
"schemars",
|
||||
@@ -1347,6 +1455,8 @@ dependencies = [
|
||||
name = "nym-group-contract-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cw-controllers",
|
||||
"cw4",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -1361,8 +1471,8 @@ dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cosmwasm-storage",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw2 1.0.1",
|
||||
"nym-contracts-common",
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract-common",
|
||||
@@ -1387,7 +1497,7 @@ dependencies = [
|
||||
"nym-contracts-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
"serde-json-wasm 0.4.1",
|
||||
"serde_repr",
|
||||
"thiserror",
|
||||
"time",
|
||||
@@ -1397,8 +1507,10 @@ dependencies = [
|
||||
name = "nym-multisig-contract-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-utils",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw-utils 1.0.1",
|
||||
"cw3",
|
||||
"cw4",
|
||||
"schemars",
|
||||
@@ -1414,9 +1526,9 @@ dependencies = [
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw-utils 1.0.1",
|
||||
"cw2 1.0.1",
|
||||
"nym-contracts-common",
|
||||
"nym-name-service-common",
|
||||
"rand 0.8.5",
|
||||
@@ -1468,9 +1580,9 @@ dependencies = [
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw-utils 1.0.1",
|
||||
"cw2 1.0.1",
|
||||
"nym-contracts-common",
|
||||
"nym-service-provider-directory-common",
|
||||
"semver",
|
||||
@@ -1505,8 +1617,8 @@ dependencies = [
|
||||
"cosmwasm-crypto",
|
||||
"cosmwasm-derive",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"cw-storage-plus 1.0.1",
|
||||
"cw2 1.0.1",
|
||||
"hex",
|
||||
"nym-contracts-common",
|
||||
"nym-mixnet-contract-common",
|
||||
@@ -1580,13 +1692,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0"
|
||||
checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1812,12 +1923,12 @@ checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.1.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525"
|
||||
checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb"
|
||||
dependencies = [
|
||||
"crypto-bigint",
|
||||
"hmac",
|
||||
"hmac 0.12.1",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1926,10 +2037,11 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1"
|
||||
checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"der",
|
||||
"generic-array 0.14.6",
|
||||
"pkcs8",
|
||||
@@ -1961,6 +2073,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-json-wasm"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.160"
|
||||
@@ -2019,12 +2140,23 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "1.4.0"
|
||||
name = "sha2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788"
|
||||
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "1.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
@@ -2052,20 +2184,20 @@ dependencies = [
|
||||
"curve25519-dalek",
|
||||
"digest 0.9.0",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"hmac 0.11.0",
|
||||
"lioness",
|
||||
"log",
|
||||
"rand 0.7.3",
|
||||
"rand_distr",
|
||||
"sha2",
|
||||
"sha2 0.9.9",
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.5.4"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27"
|
||||
checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
|
||||
+14
-13
@@ -32,16 +32,17 @@ incremental = false
|
||||
overflow-checks = true
|
||||
|
||||
[workspace.dependencies]
|
||||
cosmwasm-crypto = "=1.0.0"
|
||||
cosmwasm-derive = "=1.0.0"
|
||||
cosmwasm-schema = "=1.0.0"
|
||||
cosmwasm-std = "=1.0.0"
|
||||
cosmwasm-storage = "=1.0.0"
|
||||
cw-controllers = "=0.13.4"
|
||||
cw-multi-test = "=0.13.4"
|
||||
cw-storage-plus = "=0.13.4"
|
||||
cw-utils = "=0.13.4"
|
||||
cw2 = "=0.13.4"
|
||||
cw3 = "=0.13.4"
|
||||
cw3-fixed-multisig = "=0.13.4"
|
||||
cw4 = "=0.13.4"
|
||||
cosmwasm-crypto = "=1.2.5"
|
||||
cosmwasm-derive = "=1.2.5"
|
||||
cosmwasm-schema = "=1.2.5"
|
||||
cosmwasm-std = "=1.2.5"
|
||||
cosmwasm-storage = "=1.2.5"
|
||||
cw-controllers = "=1.0.1"
|
||||
cw-multi-test = "=0.16.4"
|
||||
cw-storage-plus = "=1.0.1"
|
||||
cw-utils = "=1.0.1"
|
||||
cw2 = "=1.0.1"
|
||||
cw3 = "=1.0.1"
|
||||
cw3-fixed-multisig = "=1.0.1"
|
||||
cw4 = "=1.0.1"
|
||||
cw20 = "=1.0.1"
|
||||
|
||||
@@ -30,7 +30,7 @@ impl<'a> IndexList<ContractVKShare> for VkShareIndex<'a> {
|
||||
pub(crate) fn vk_shares<'a>() -> IndexedMap<'a, VKShareKey<'a>, ContractVKShare, VkShareIndex<'a>> {
|
||||
let indexes = VkShareIndex {
|
||||
epoch_id: MultiIndex::new(
|
||||
|d| d.epoch_id,
|
||||
|_pk, d| d.epoch_id,
|
||||
VK_SHARES_PK_NAMESPACE,
|
||||
VK_SHARES_EPOCH_ID_IDX_NAMESPACE,
|
||||
),
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::{entry_point, Addr, Coin, DepsMut, Empty, Env, Response};
|
||||
use cw3_flex_multisig::state::CONFIG;
|
||||
use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper};
|
||||
use nym_multisig_contract_common::error::ContractError;
|
||||
use nym_multisig_contract_common::state::CONFIG;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ fn spend_credential_creates_proposal() {
|
||||
threshold: Threshold::AbsolutePercentage {
|
||||
percentage: Decimal::from_ratio(2u128, 3u128),
|
||||
},
|
||||
executor: None,
|
||||
proposal_deposit: None,
|
||||
max_voting_period: Duration::Height(1000),
|
||||
coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(),
|
||||
coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(),
|
||||
|
||||
@@ -52,6 +52,8 @@ fn dkg_proposal() {
|
||||
threshold: Threshold::AbsolutePercentage {
|
||||
percentage: Decimal::from_ratio(1u128, 1u128),
|
||||
},
|
||||
executor: None,
|
||||
proposal_deposit: None,
|
||||
max_voting_period: Duration::Time(1000),
|
||||
coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(),
|
||||
coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(),
|
||||
|
||||
@@ -520,7 +520,7 @@ mod tests {
|
||||
.delegations
|
||||
.iter()
|
||||
.filter(|d| d.proxy.is_some())
|
||||
.all(|d| d.proxy.as_ref().unwrap() == &vesting_contract));
|
||||
.all(|d| d.proxy.as_ref().unwrap() == vesting_contract));
|
||||
|
||||
// now make sure that if we do it in paged manner, we'll get exactly the same result
|
||||
let per_page = Some(15);
|
||||
|
||||
@@ -27,12 +27,12 @@ impl<'a> IndexList<Delegation> for DelegationIndex<'a> {
|
||||
pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, DelegationIndex<'a>> {
|
||||
let indexes = DelegationIndex {
|
||||
owner: MultiIndex::new(
|
||||
|d| d.owner.clone(),
|
||||
|_pk, d| d.owner.clone(),
|
||||
DELEGATION_PK_NAMESPACE,
|
||||
DELEGATION_OWNER_IDX_NAMESPACE,
|
||||
),
|
||||
mixnode: MultiIndex::new(
|
||||
|d| d.mix_id,
|
||||
|_pk, d| d.mix_id,
|
||||
DELEGATION_PK_NAMESPACE,
|
||||
DELEGATION_MIXNODE_IDX_NAMESPACE,
|
||||
),
|
||||
|
||||
@@ -39,12 +39,12 @@ pub(crate) fn unbonded_mixnodes<'a>(
|
||||
) -> IndexedMap<'a, MixId, UnbondedMixnode, UnbondedMixnodeIndex<'a>> {
|
||||
let indexes = UnbondedMixnodeIndex {
|
||||
owner: MultiIndex::new(
|
||||
|d| d.owner.clone(),
|
||||
|_pk, d| d.owner.clone(),
|
||||
UNBONDED_MIXNODES_PK_NAMESPACE,
|
||||
UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE,
|
||||
),
|
||||
identity_key: MultiIndex::new(
|
||||
|d| d.identity_key.clone(),
|
||||
|_pk, d| d.identity_key.clone(),
|
||||
UNBONDED_MIXNODES_PK_NAMESPACE,
|
||||
UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE,
|
||||
),
|
||||
|
||||
@@ -185,7 +185,7 @@ pub(crate) fn _try_withdraw_operator_reward(
|
||||
// we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract
|
||||
// otherwise, we don't care
|
||||
let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?;
|
||||
if proxy == &vesting_contract {
|
||||
if proxy == vesting_contract {
|
||||
let msg = VestingContractExecuteMsg::TrackReward {
|
||||
amount: reward.clone(),
|
||||
address: owner.clone().into_string(),
|
||||
@@ -271,7 +271,7 @@ pub(crate) fn _try_withdraw_delegator_reward(
|
||||
// we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract
|
||||
// otherwise, we don't care
|
||||
let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?;
|
||||
if proxy == &vesting_contract {
|
||||
if proxy == vesting_contract {
|
||||
let msg = VestingContractExecuteMsg::TrackReward {
|
||||
amount: reward.clone(),
|
||||
address: owner.clone().into_string(),
|
||||
|
||||
@@ -298,7 +298,7 @@ pub(crate) fn ensure_is_authorized(
|
||||
sender: &Addr,
|
||||
storage: &dyn Storage,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
if sender != &crate::mixnet_contract_settings::storage::rewarding_validator_address(storage)? {
|
||||
if sender != crate::mixnet_contract_settings::storage::rewarding_validator_address(storage)? {
|
||||
return Err(MixnetContractError::Unauthorized);
|
||||
}
|
||||
Ok(())
|
||||
@@ -309,7 +309,7 @@ pub(crate) fn ensure_can_advance_epoch(
|
||||
storage: &dyn Storage,
|
||||
) -> Result<EpochStatus, MixnetContractError> {
|
||||
let epoch_status = crate::interval::storage::current_epoch_status(storage)?;
|
||||
if sender != &epoch_status.being_advanced_by {
|
||||
if sender != epoch_status.being_advanced_by {
|
||||
// well, we know we're going to throw an error now,
|
||||
// but we might as well also check if we're even a validator
|
||||
// to return a possibly better error message
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "cw3-flex-multisig"
|
||||
version = "0.13.1"
|
||||
version = "1.0.0"
|
||||
authors = ["Ethan Frey <ethanfrey@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "Implementing cw3 with multiple voting patterns and dynamic groups"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/CosmWasm/cw-plus"
|
||||
@@ -13,6 +13,7 @@ documentation = "https://docs.cosmwasm.com"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
backtraces = ["cosmwasm-std/backtraces"]
|
||||
# use library feature to disable all instantiate/execute/query exports
|
||||
library = []
|
||||
|
||||
@@ -22,15 +23,15 @@ cw2 = { workspace = true }
|
||||
cw3 = { workspace = true }
|
||||
cw3-fixed-multisig = { workspace = true, features = ["library"] }
|
||||
cw4 = { workspace = true }
|
||||
cw20 = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
schemars = "0.8.1"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
|
||||
nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-multisig-contract-common = { path= "../../../common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
|
||||
[dev-dependencies]
|
||||
cosmwasm-schema = { version = "1.0.0" }
|
||||
cw4-group = { path = "../cw4-group", version = "0.13.4" }
|
||||
cw4-group = { path = "../cw4-group", version = "1.0.0" }
|
||||
cw-multi-test = { workspace = true }
|
||||
cw20-base = "1.0.0"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,25 @@
|
||||
/*!
|
||||
This builds on [`cw3_fixed_multisig`] with a more
|
||||
powerful implementation of the [cw3 spec](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw3/README.md).
|
||||
It is a multisig contract that is backed by a
|
||||
[cw4 (group)](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw4/README.md) contract, which independently
|
||||
maintains the voter set.
|
||||
|
||||
This provides 2 main advantages:
|
||||
|
||||
* You can create two different multisigs with different voting thresholds
|
||||
backed by the same group. Thus, you can have a 50% vote, and a 67% vote
|
||||
that always use the same voter set, but can take other actions.
|
||||
* TODO: It allows dynamic multisig groups.
|
||||
|
||||
|
||||
In addition to the dynamic voting set, the main difference with the native
|
||||
Cosmos SDK multisig, is that it aggregates the signatures on chain, with
|
||||
visible proposals (like `x/gov` in the Cosmos SDK), rather than requiring
|
||||
signers to share signatures off chain.
|
||||
|
||||
For more information on this contract, please check out the
|
||||
[README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw3-flex-multisig/README.md).
|
||||
*/
|
||||
|
||||
pub mod contract;
|
||||
pub mod state;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
use cosmwasm_std::Addr;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use cw4::Cw4Contract;
|
||||
use cw_storage_plus::Item;
|
||||
use cw_utils::{Duration, Threshold};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
|
||||
pub struct Config {
|
||||
pub threshold: Threshold,
|
||||
pub max_voting_period: Duration,
|
||||
// Total weight and voters are queried from this contract
|
||||
pub group_addr: Cw4Contract,
|
||||
pub coconut_bandwidth_addr: Addr,
|
||||
pub coconut_dkg_addr: Addr,
|
||||
}
|
||||
|
||||
// unique items
|
||||
pub const CONFIG: Item<Config> = Item::new("config");
|
||||
@@ -1,5 +1,5 @@
|
||||
[alias]
|
||||
wasm = "build --release --target wasm32-unknown-unknown"
|
||||
wasm-debug = "build --target wasm32-unknown-unknown"
|
||||
wasm = "build --release --lib --target wasm32-unknown-unknown"
|
||||
wasm-debug = "build --lib --target wasm32-unknown-unknown"
|
||||
unit-test = "test --lib"
|
||||
schema = "run --example schema"
|
||||
schema = "run --bin schema"
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "cw4-group"
|
||||
version = "0.13.4"
|
||||
version = "1.0.0"
|
||||
authors = ["Ethan Frey <ethanfrey@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "Simple cw4 implementation of group membership controlled by admin "
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/CosmWasm/cw-plus"
|
||||
@@ -20,6 +20,8 @@ exclude = [
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
# for more explicit tests, cargo test --features=backtraces
|
||||
backtraces = ["cosmwasm-std/backtraces"]
|
||||
# use library feature to disable all instantiate/execute/query exports
|
||||
library = []
|
||||
|
||||
@@ -31,10 +33,8 @@ cw2 = { workspace = true }
|
||||
cw4 = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
schemars = "0.8.1"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
thiserror = { version = "1.0.23" }
|
||||
|
||||
[dev-dependencies]
|
||||
cosmwasm-schema = { workspace = true }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use cosmwasm_std::entry_point;
|
||||
use cosmwasm_std::{
|
||||
attr, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult,
|
||||
SubMsg,
|
||||
SubMsg, Uint64,
|
||||
};
|
||||
use cw2::set_contract_version;
|
||||
use cw4::{
|
||||
@@ -13,6 +13,7 @@ use cw_storage_plus::Bound;
|
||||
use cw_utils::maybe_addr;
|
||||
|
||||
use crate::error::ContractError;
|
||||
use crate::helpers::validate_unique_members;
|
||||
use crate::state::{ADMIN, HOOKS, MEMBERS, TOTAL};
|
||||
use nym_group_contract_common::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
|
||||
|
||||
@@ -39,21 +40,25 @@ pub fn instantiate(
|
||||
pub fn create(
|
||||
mut deps: DepsMut,
|
||||
admin: Option<String>,
|
||||
members: Vec<Member>,
|
||||
mut members: Vec<Member>,
|
||||
height: u64,
|
||||
) -> Result<(), ContractError> {
|
||||
validate_unique_members(&mut members)?;
|
||||
let members = members; // let go of mutability
|
||||
|
||||
let admin_addr = admin
|
||||
.map(|admin| deps.api.addr_validate(&admin))
|
||||
.transpose()?;
|
||||
ADMIN.set(deps.branch(), admin_addr)?;
|
||||
|
||||
let mut total = 0u64;
|
||||
let mut total = Uint64::zero();
|
||||
for member in members.into_iter() {
|
||||
total += member.weight;
|
||||
let member_weight = Uint64::from(member.weight);
|
||||
total = total.checked_add(member_weight)?;
|
||||
let member_addr = deps.api.addr_validate(&member.addr)?;
|
||||
MEMBERS.save(deps.storage, &member_addr, &member.weight, height)?;
|
||||
MEMBERS.save(deps.storage, &member_addr, &member_weight.u64(), height)?;
|
||||
}
|
||||
TOTAL.save(deps.storage, &total)?;
|
||||
TOTAL.save(deps.storage, &total.u64(), height)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -115,20 +120,23 @@ pub fn update_members(
|
||||
deps: DepsMut,
|
||||
height: u64,
|
||||
sender: Addr,
|
||||
to_add: Vec<Member>,
|
||||
mut to_add: Vec<Member>,
|
||||
to_remove: Vec<String>,
|
||||
) -> Result<MemberChangedHookMsg, ContractError> {
|
||||
validate_unique_members(&mut to_add)?;
|
||||
let to_add = to_add; // let go of mutability
|
||||
|
||||
ADMIN.assert_admin(deps.as_ref(), &sender)?;
|
||||
|
||||
let mut total = TOTAL.load(deps.storage)?;
|
||||
let mut total = Uint64::from(TOTAL.load(deps.storage)?);
|
||||
let mut diffs: Vec<MemberDiff> = vec![];
|
||||
|
||||
// add all new members and update total
|
||||
for add in to_add.into_iter() {
|
||||
let add_addr = deps.api.addr_validate(&add.addr)?;
|
||||
MEMBERS.update(deps.storage, &add_addr, height, |old| -> StdResult<_> {
|
||||
total -= old.unwrap_or_default();
|
||||
total += add.weight;
|
||||
total = total.checked_sub(Uint64::from(old.unwrap_or_default()))?;
|
||||
total = total.checked_add(Uint64::from(add.weight))?;
|
||||
diffs.push(MemberDiff::new(add.addr, old, Some(add.weight)));
|
||||
Ok(add.weight)
|
||||
})?;
|
||||
@@ -140,12 +148,12 @@ pub fn update_members(
|
||||
// Only process this if they were actually in the list before
|
||||
if let Some(weight) = old {
|
||||
diffs.push(MemberDiff::new(remove, Some(weight), None));
|
||||
total -= weight;
|
||||
total = total.checked_sub(Uint64::from(weight))?;
|
||||
MEMBERS.remove(deps.storage, &remove_addr, height)?;
|
||||
}
|
||||
}
|
||||
|
||||
TOTAL.save(deps.storage, &total)?;
|
||||
TOTAL.save(deps.storage, &total.u64(), height)?;
|
||||
Ok(MemberChangedHookMsg { diffs })
|
||||
}
|
||||
|
||||
@@ -157,20 +165,26 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
|
||||
at_height: height,
|
||||
} => to_binary(&query_member(deps, addr, height)?),
|
||||
QueryMsg::ListMembers { start_after, limit } => {
|
||||
to_binary(&list_members(deps, start_after, limit)?)
|
||||
to_binary(&query_list_members(deps, start_after, limit)?)
|
||||
}
|
||||
QueryMsg::TotalWeight { at_height: height } => {
|
||||
to_binary(&query_total_weight(deps, height)?)
|
||||
}
|
||||
QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?),
|
||||
QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?),
|
||||
QueryMsg::Hooks {} => to_binary(&HOOKS.query_hooks(deps)?),
|
||||
}
|
||||
}
|
||||
|
||||
fn query_total_weight(deps: Deps) -> StdResult<TotalWeightResponse> {
|
||||
let weight = TOTAL.load(deps.storage)?;
|
||||
pub fn query_total_weight(deps: Deps, height: Option<u64>) -> StdResult<TotalWeightResponse> {
|
||||
let weight = match height {
|
||||
Some(h) => TOTAL.may_load_at_height(deps.storage, h),
|
||||
None => TOTAL.may_load(deps.storage),
|
||||
}?
|
||||
.unwrap_or_default();
|
||||
Ok(TotalWeightResponse { weight })
|
||||
}
|
||||
|
||||
fn query_member(deps: Deps, addr: String, height: Option<u64>) -> StdResult<MemberResponse> {
|
||||
pub fn query_member(deps: Deps, addr: String, height: Option<u64>) -> StdResult<MemberResponse> {
|
||||
let addr = deps.api.addr_validate(&addr)?;
|
||||
let weight = match height {
|
||||
Some(h) => MEMBERS.may_load_at_height(deps.storage, &addr, h),
|
||||
@@ -183,7 +197,7 @@ fn query_member(deps: Deps, addr: String, height: Option<u64>) -> StdResult<Memb
|
||||
const MAX_LIMIT: u32 = 30;
|
||||
const DEFAULT_LIMIT: u32 = 10;
|
||||
|
||||
fn list_members(
|
||||
pub fn query_list_members(
|
||||
deps: Deps,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
@@ -205,352 +219,3 @@ fn list_members(
|
||||
|
||||
Ok(MemberListResponse { members })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
|
||||
use cosmwasm_std::{from_slice, Api, OwnedDeps, Querier, Storage};
|
||||
use cw4::{member_key, TOTAL_KEY};
|
||||
use cw_controllers::{AdminError, HookError};
|
||||
|
||||
const INIT_ADMIN: &str = "juan";
|
||||
const USER1: &str = "somebody";
|
||||
const USER2: &str = "else";
|
||||
const USER3: &str = "funny";
|
||||
|
||||
fn do_instantiate(deps: DepsMut) {
|
||||
let msg = InstantiateMsg {
|
||||
admin: Some(INIT_ADMIN.into()),
|
||||
members: vec![
|
||||
Member {
|
||||
addr: USER1.into(),
|
||||
weight: 11,
|
||||
},
|
||||
Member {
|
||||
addr: USER2.into(),
|
||||
weight: 6,
|
||||
},
|
||||
],
|
||||
};
|
||||
let info = mock_info("creator", &[]);
|
||||
instantiate(deps, mock_env(), info, msg).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proper_instantiation() {
|
||||
let mut deps = mock_dependencies();
|
||||
do_instantiate(deps.as_mut());
|
||||
|
||||
// it worked, let's query the state
|
||||
let res = ADMIN.query_admin(deps.as_ref()).unwrap();
|
||||
assert_eq!(Some(INIT_ADMIN.into()), res.admin);
|
||||
|
||||
let res = query_total_weight(deps.as_ref()).unwrap();
|
||||
assert_eq!(17, res.weight);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_member_queries() {
|
||||
let mut deps = mock_dependencies();
|
||||
do_instantiate(deps.as_mut());
|
||||
|
||||
let member1 = query_member(deps.as_ref(), USER1.into(), None).unwrap();
|
||||
assert_eq!(member1.weight, Some(11));
|
||||
|
||||
let member2 = query_member(deps.as_ref(), USER2.into(), None).unwrap();
|
||||
assert_eq!(member2.weight, Some(6));
|
||||
|
||||
let member3 = query_member(deps.as_ref(), USER3.into(), None).unwrap();
|
||||
assert_eq!(member3.weight, None);
|
||||
|
||||
let members = list_members(deps.as_ref(), None, None).unwrap();
|
||||
assert_eq!(members.members.len(), 2);
|
||||
// TODO: assert the set is proper
|
||||
}
|
||||
|
||||
fn assert_users<S: Storage, A: Api, Q: Querier>(
|
||||
deps: &OwnedDeps<S, A, Q>,
|
||||
user1_weight: Option<u64>,
|
||||
user2_weight: Option<u64>,
|
||||
user3_weight: Option<u64>,
|
||||
height: Option<u64>,
|
||||
) {
|
||||
let member1 = query_member(deps.as_ref(), USER1.into(), height).unwrap();
|
||||
assert_eq!(member1.weight, user1_weight);
|
||||
|
||||
let member2 = query_member(deps.as_ref(), USER2.into(), height).unwrap();
|
||||
assert_eq!(member2.weight, user2_weight);
|
||||
|
||||
let member3 = query_member(deps.as_ref(), USER3.into(), height).unwrap();
|
||||
assert_eq!(member3.weight, user3_weight);
|
||||
|
||||
// this is only valid if we are not doing a historical query
|
||||
if height.is_none() {
|
||||
// compute expected metrics
|
||||
let weights = vec![user1_weight, user2_weight, user3_weight];
|
||||
let sum: u64 = weights.iter().map(|x| x.unwrap_or_default()).sum();
|
||||
let count = weights.iter().filter(|x| x.is_some()).count();
|
||||
|
||||
// TODO: more detailed compare?
|
||||
let members = list_members(deps.as_ref(), None, None).unwrap();
|
||||
assert_eq!(count, members.members.len());
|
||||
|
||||
let total = query_total_weight(deps.as_ref()).unwrap();
|
||||
assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_new_remove_old_member() {
|
||||
let mut deps = mock_dependencies();
|
||||
do_instantiate(deps.as_mut());
|
||||
|
||||
// add a new one and remove existing one
|
||||
let add = vec![Member {
|
||||
addr: USER3.into(),
|
||||
weight: 15,
|
||||
}];
|
||||
let remove = vec![USER1.into()];
|
||||
|
||||
// non-admin cannot update
|
||||
let height = mock_env().block.height;
|
||||
let err = update_members(
|
||||
deps.as_mut(),
|
||||
height + 5,
|
||||
Addr::unchecked(USER1),
|
||||
add.clone(),
|
||||
remove.clone(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(err, AdminError::NotAdmin {}.into());
|
||||
|
||||
// Test the values from instantiate
|
||||
assert_users(&deps, Some(11), Some(6), None, None);
|
||||
// Note all values were set at height, the beginning of that block was all None
|
||||
assert_users(&deps, None, None, None, Some(height));
|
||||
// This will get us the values at the start of the block after instantiate (expected initial values)
|
||||
assert_users(&deps, Some(11), Some(6), None, Some(height + 1));
|
||||
|
||||
// admin updates properly
|
||||
update_members(
|
||||
deps.as_mut(),
|
||||
height + 10,
|
||||
Addr::unchecked(INIT_ADMIN),
|
||||
add,
|
||||
remove,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// updated properly
|
||||
assert_users(&deps, None, Some(6), Some(15), None);
|
||||
|
||||
// snapshot still shows old value
|
||||
assert_users(&deps, Some(11), Some(6), None, Some(height + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_old_remove_new_member() {
|
||||
// add will over-write and remove have no effect
|
||||
let mut deps = mock_dependencies();
|
||||
do_instantiate(deps.as_mut());
|
||||
|
||||
// add a new one and remove existing one
|
||||
let add = vec![Member {
|
||||
addr: USER1.into(),
|
||||
weight: 4,
|
||||
}];
|
||||
let remove = vec![USER3.into()];
|
||||
|
||||
// admin updates properly
|
||||
let height = mock_env().block.height;
|
||||
update_members(
|
||||
deps.as_mut(),
|
||||
height,
|
||||
Addr::unchecked(INIT_ADMIN),
|
||||
add,
|
||||
remove,
|
||||
)
|
||||
.unwrap();
|
||||
assert_users(&deps, Some(4), Some(6), None, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_and_remove_same_member() {
|
||||
// add will over-write and remove have no effect
|
||||
let mut deps = mock_dependencies();
|
||||
do_instantiate(deps.as_mut());
|
||||
|
||||
// USER1 is updated and remove in the same call, we should remove this an add member3
|
||||
let add = vec![
|
||||
Member {
|
||||
addr: USER1.into(),
|
||||
weight: 20,
|
||||
},
|
||||
Member {
|
||||
addr: USER3.into(),
|
||||
weight: 5,
|
||||
},
|
||||
];
|
||||
let remove = vec![USER1.into()];
|
||||
|
||||
// admin updates properly
|
||||
let height = mock_env().block.height;
|
||||
update_members(
|
||||
deps.as_mut(),
|
||||
height,
|
||||
Addr::unchecked(INIT_ADMIN),
|
||||
add,
|
||||
remove,
|
||||
)
|
||||
.unwrap();
|
||||
assert_users(&deps, None, Some(6), Some(5), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_remove_hooks() {
|
||||
// add will over-write and remove have no effect
|
||||
let mut deps = mock_dependencies();
|
||||
do_instantiate(deps.as_mut());
|
||||
|
||||
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
|
||||
assert!(hooks.hooks.is_empty());
|
||||
|
||||
let contract1 = String::from("hook1");
|
||||
let contract2 = String::from("hook2");
|
||||
|
||||
let add_msg = ExecuteMsg::AddHook {
|
||||
addr: contract1.clone(),
|
||||
};
|
||||
|
||||
// non-admin cannot add hook
|
||||
let user_info = mock_info(USER1, &[]);
|
||||
let err = execute(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
user_info.clone(),
|
||||
add_msg.clone(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into());
|
||||
|
||||
// admin can add it, and it appears in the query
|
||||
let admin_info = mock_info(INIT_ADMIN, &[]);
|
||||
let _ = execute(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
admin_info.clone(),
|
||||
add_msg.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
|
||||
assert_eq!(hooks.hooks, vec![contract1.clone()]);
|
||||
|
||||
// cannot remove a non-registered contract
|
||||
let remove_msg = ExecuteMsg::RemoveHook {
|
||||
addr: contract2.clone(),
|
||||
};
|
||||
let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), remove_msg).unwrap_err();
|
||||
assert_eq!(err, HookError::HookNotRegistered {}.into());
|
||||
|
||||
// add second contract
|
||||
let add_msg2 = ExecuteMsg::AddHook {
|
||||
addr: contract2.clone(),
|
||||
};
|
||||
let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap();
|
||||
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
|
||||
assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]);
|
||||
|
||||
// cannot re-add an existing contract
|
||||
let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err();
|
||||
assert_eq!(err, HookError::HookAlreadyRegistered {}.into());
|
||||
|
||||
// non-admin cannot remove
|
||||
let remove_msg = ExecuteMsg::RemoveHook { addr: contract1 };
|
||||
let err = execute(deps.as_mut(), mock_env(), user_info, remove_msg.clone()).unwrap_err();
|
||||
assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into());
|
||||
|
||||
// remove the original
|
||||
let _ = execute(deps.as_mut(), mock_env(), admin_info, remove_msg).unwrap();
|
||||
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
|
||||
assert_eq!(hooks.hooks, vec![contract2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hooks_fire() {
|
||||
let mut deps = mock_dependencies();
|
||||
do_instantiate(deps.as_mut());
|
||||
|
||||
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
|
||||
assert!(hooks.hooks.is_empty());
|
||||
|
||||
let contract1 = String::from("hook1");
|
||||
let contract2 = String::from("hook2");
|
||||
|
||||
// register 2 hooks
|
||||
let admin_info = mock_info(INIT_ADMIN, &[]);
|
||||
let add_msg = ExecuteMsg::AddHook {
|
||||
addr: contract1.clone(),
|
||||
};
|
||||
let add_msg2 = ExecuteMsg::AddHook {
|
||||
addr: contract2.clone(),
|
||||
};
|
||||
for msg in vec![add_msg, add_msg2] {
|
||||
let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap();
|
||||
}
|
||||
|
||||
// make some changes - add 3, remove 2, and update 1
|
||||
// USER1 is updated and remove in the same call, we should remove this an add member3
|
||||
let add = vec![
|
||||
Member {
|
||||
addr: USER1.into(),
|
||||
weight: 20,
|
||||
},
|
||||
Member {
|
||||
addr: USER3.into(),
|
||||
weight: 5,
|
||||
},
|
||||
];
|
||||
let remove = vec![USER2.into()];
|
||||
let msg = ExecuteMsg::UpdateMembers { remove, add };
|
||||
|
||||
// admin updates properly
|
||||
assert_users(&deps, Some(11), Some(6), None, None);
|
||||
let res = execute(deps.as_mut(), mock_env(), admin_info, msg).unwrap();
|
||||
assert_users(&deps, Some(20), None, Some(5), None);
|
||||
|
||||
// ensure 2 messages for the 2 hooks
|
||||
assert_eq!(res.messages.len(), 2);
|
||||
// same order as in the message (adds first, then remove)
|
||||
let diffs = vec![
|
||||
MemberDiff::new(USER1, Some(11), Some(20)),
|
||||
MemberDiff::new(USER3, None, Some(5)),
|
||||
MemberDiff::new(USER2, Some(6), None),
|
||||
];
|
||||
let hook_msg = MemberChangedHookMsg { diffs };
|
||||
let msg1 = SubMsg::new(hook_msg.clone().into_cosmos_msg(contract1).unwrap());
|
||||
let msg2 = SubMsg::new(hook_msg.into_cosmos_msg(contract2).unwrap());
|
||||
assert_eq!(res.messages, vec![msg1, msg2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_queries_work() {
|
||||
// add will over-write and remove have no effect
|
||||
let mut deps = mock_dependencies();
|
||||
do_instantiate(deps.as_mut());
|
||||
|
||||
// get total from raw key
|
||||
let total_raw = deps.storage.get(TOTAL_KEY.as_bytes()).unwrap();
|
||||
let total: u64 = from_slice(&total_raw).unwrap();
|
||||
assert_eq!(17, total);
|
||||
|
||||
// get member votes from raw key
|
||||
let member2_raw = deps.storage.get(&member_key(USER2)).unwrap();
|
||||
let member2: u64 = from_slice(&member2_raw).unwrap();
|
||||
assert_eq!(6, member2);
|
||||
|
||||
// and execute misses
|
||||
let member3_raw = deps.storage.get(&member_key(USER3));
|
||||
assert_eq!(None, member3_raw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
use cosmwasm_std::StdError;
|
||||
use cosmwasm_std::{OverflowError, StdError};
|
||||
use thiserror::Error;
|
||||
|
||||
use cw_controllers::{AdminError, HookError};
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum ContractError {
|
||||
#[error(transparent)]
|
||||
#[error("{0}")]
|
||||
Std(#[from] StdError),
|
||||
|
||||
#[error(transparent)]
|
||||
#[error("{0}")]
|
||||
Hook(#[from] HookError),
|
||||
|
||||
#[error(transparent)]
|
||||
#[error("{0}")]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
#[error("{0}")]
|
||||
Overflow(#[from] OverflowError),
|
||||
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized {},
|
||||
|
||||
#[error("Message contained duplicate member: {member}")]
|
||||
DuplicateMember { member: String },
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{to_binary, Addr, CosmosMsg, StdResult, WasmMsg};
|
||||
use cw4::{Cw4Contract, Member};
|
||||
|
||||
use crate::ContractError;
|
||||
use nym_group_contract_common::msg::ExecuteMsg;
|
||||
|
||||
/// Cw4GroupContract is a wrapper around Cw4Contract that provides a lot of helpers
|
||||
/// for working with cw4-group contracts.
|
||||
///
|
||||
/// It extends Cw4Contract to add the extra calls from cw4-group.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[cw_serde]
|
||||
pub struct Cw4GroupContract(pub Cw4Contract);
|
||||
|
||||
impl Deref for Cw4GroupContract {
|
||||
@@ -41,3 +41,17 @@ impl Cw4GroupContract {
|
||||
self.encode_msg(msg)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sorts the slice and verifies all member addresses are unique.
|
||||
pub fn validate_unique_members(members: &mut [Member]) -> Result<(), ContractError> {
|
||||
members.sort_by(|a, b| a.addr.cmp(&b.addr));
|
||||
for (a, b) in members.iter().zip(members.iter().skip(1)) {
|
||||
if a.addr == b.addr {
|
||||
return Err(ContractError::DuplicateMember {
|
||||
member: a.addr.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
/*!
|
||||
This is a basic implementation of the [cw4 spec](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw4/README.md).
|
||||
It fulfills all elements of the spec, including the raw query lookups,
|
||||
and it designed to be used as a backing storage for
|
||||
[cw3 compliant contracts](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw3/README.md).
|
||||
|
||||
It stores a set of members along with an admin, and allows the admin to
|
||||
update the state. Raw queries (intended for cross-contract queries)
|
||||
can check a given member address and the total weight. Smart queries (designed
|
||||
for client API) can do the same, and also query the admin address as well as
|
||||
paginate over all members.
|
||||
|
||||
For more information on this contract, please check out the
|
||||
[README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw4-group/README.md).
|
||||
*/
|
||||
|
||||
pub mod contract;
|
||||
pub mod error;
|
||||
pub mod helpers;
|
||||
pub mod state;
|
||||
|
||||
pub use crate::error::ContractError;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
use cosmwasm_std::Addr;
|
||||
use cw4::TOTAL_KEY;
|
||||
use cw4::{
|
||||
MEMBERS_CHANGELOG, MEMBERS_CHECKPOINTS, MEMBERS_KEY, TOTAL_KEY, TOTAL_KEY_CHANGELOG,
|
||||
TOTAL_KEY_CHECKPOINTS,
|
||||
};
|
||||
use cw_controllers::{Admin, Hooks};
|
||||
use cw_storage_plus::{Item, SnapshotMap, Strategy};
|
||||
use cw_storage_plus::{SnapshotItem, SnapshotMap, Strategy};
|
||||
|
||||
pub const ADMIN: Admin = Admin::new("admin");
|
||||
pub const HOOKS: Hooks = Hooks::new("cw4-hooks");
|
||||
|
||||
pub const TOTAL: Item<u64> = Item::new(TOTAL_KEY);
|
||||
|
||||
pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new(
|
||||
cw4::MEMBERS_KEY,
|
||||
cw4::MEMBERS_CHECKPOINTS,
|
||||
cw4::MEMBERS_CHANGELOG,
|
||||
pub const TOTAL: SnapshotItem<u64> = SnapshotItem::new(
|
||||
TOTAL_KEY,
|
||||
TOTAL_KEY_CHECKPOINTS,
|
||||
TOTAL_KEY_CHANGELOG,
|
||||
Strategy::EveryBlock,
|
||||
);
|
||||
|
||||
pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new(
|
||||
MEMBERS_KEY,
|
||||
MEMBERS_CHECKPOINTS,
|
||||
MEMBERS_CHANGELOG,
|
||||
Strategy::EveryBlock,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
|
||||
use cosmwasm_std::{from_slice, Addr, Api, DepsMut, OwnedDeps, Querier, Storage, SubMsg};
|
||||
use cw4::{member_key, Member, MemberChangedHookMsg, MemberDiff, TOTAL_KEY};
|
||||
use cw_controllers::{AdminError, HookError};
|
||||
|
||||
use crate::contract::{
|
||||
execute, instantiate, query_list_members, query_member, query_total_weight, update_members,
|
||||
};
|
||||
use crate::state::{ADMIN, HOOKS};
|
||||
use crate::ContractError;
|
||||
use nym_group_contract_common::msg::{ExecuteMsg, InstantiateMsg};
|
||||
|
||||
const INIT_ADMIN: &str = "juan";
|
||||
const USER1: &str = "somebody";
|
||||
const USER2: &str = "else";
|
||||
const USER3: &str = "funny";
|
||||
|
||||
fn set_up(deps: DepsMut) {
|
||||
let msg = InstantiateMsg {
|
||||
admin: Some(INIT_ADMIN.into()),
|
||||
members: vec![
|
||||
Member {
|
||||
addr: USER1.into(),
|
||||
weight: 11,
|
||||
},
|
||||
Member {
|
||||
addr: USER2.into(),
|
||||
weight: 6,
|
||||
},
|
||||
],
|
||||
};
|
||||
let info = mock_info("creator", &[]);
|
||||
instantiate(deps, mock_env(), info, msg).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proper_instantiation() {
|
||||
let mut deps = mock_dependencies();
|
||||
set_up(deps.as_mut());
|
||||
|
||||
// it worked, let's query the state
|
||||
let res = ADMIN.query_admin(deps.as_ref()).unwrap();
|
||||
assert_eq!(Some(INIT_ADMIN.into()), res.admin);
|
||||
|
||||
let res = query_total_weight(deps.as_ref(), None).unwrap();
|
||||
assert_eq!(17, res.weight);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_member_queries() {
|
||||
let mut deps = mock_dependencies();
|
||||
set_up(deps.as_mut());
|
||||
|
||||
let member1 = query_member(deps.as_ref(), USER1.into(), None).unwrap();
|
||||
assert_eq!(member1.weight, Some(11));
|
||||
|
||||
let member2 = query_member(deps.as_ref(), USER2.into(), None).unwrap();
|
||||
assert_eq!(member2.weight, Some(6));
|
||||
|
||||
let member3 = query_member(deps.as_ref(), USER3.into(), None).unwrap();
|
||||
assert_eq!(member3.weight, None);
|
||||
|
||||
let members = query_list_members(deps.as_ref(), None, None).unwrap();
|
||||
assert_eq!(members.members.len(), 2);
|
||||
// TODO: assert the set is proper
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_members_instantiation() {
|
||||
let mut deps = mock_dependencies();
|
||||
|
||||
let msg = InstantiateMsg {
|
||||
admin: Some(INIT_ADMIN.into()),
|
||||
members: vec![
|
||||
Member {
|
||||
addr: USER1.into(),
|
||||
weight: 5,
|
||||
},
|
||||
Member {
|
||||
addr: USER2.into(),
|
||||
weight: 6,
|
||||
},
|
||||
Member {
|
||||
addr: USER1.into(),
|
||||
weight: 6,
|
||||
},
|
||||
],
|
||||
};
|
||||
let info = mock_info("creator", &[]);
|
||||
let err = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
ContractError::DuplicateMember {
|
||||
member: USER1.to_string()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_members_execution() {
|
||||
let mut deps = mock_dependencies();
|
||||
|
||||
set_up(deps.as_mut());
|
||||
|
||||
let add = vec![
|
||||
Member {
|
||||
addr: USER3.into(),
|
||||
weight: 15,
|
||||
},
|
||||
Member {
|
||||
addr: USER3.into(),
|
||||
weight: 11,
|
||||
},
|
||||
];
|
||||
|
||||
let height = mock_env().block.height;
|
||||
let err = update_members(
|
||||
deps.as_mut(),
|
||||
height + 5,
|
||||
Addr::unchecked(INIT_ADMIN),
|
||||
add,
|
||||
vec![],
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
ContractError::DuplicateMember {
|
||||
member: USER3.to_string()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_users<S: Storage, A: Api, Q: Querier>(
|
||||
deps: &OwnedDeps<S, A, Q>,
|
||||
user1_weight: Option<u64>,
|
||||
user2_weight: Option<u64>,
|
||||
user3_weight: Option<u64>,
|
||||
height: Option<u64>,
|
||||
) {
|
||||
let member1 = query_member(deps.as_ref(), USER1.into(), height).unwrap();
|
||||
assert_eq!(member1.weight, user1_weight);
|
||||
|
||||
let member2 = query_member(deps.as_ref(), USER2.into(), height).unwrap();
|
||||
assert_eq!(member2.weight, user2_weight);
|
||||
|
||||
let member3 = query_member(deps.as_ref(), USER3.into(), height).unwrap();
|
||||
assert_eq!(member3.weight, user3_weight);
|
||||
|
||||
// this is only valid if we are not doing a historical query
|
||||
if height.is_none() {
|
||||
// compute expected metrics
|
||||
let weights = vec![user1_weight, user2_weight, user3_weight];
|
||||
let sum: u64 = weights.iter().map(|x| x.unwrap_or_default()).sum();
|
||||
let count = weights.iter().filter(|x| x.is_some()).count();
|
||||
|
||||
// TODO: more detailed compare?
|
||||
let members = query_list_members(deps.as_ref(), None, None).unwrap();
|
||||
assert_eq!(count, members.members.len());
|
||||
|
||||
let total = query_total_weight(deps.as_ref(), None).unwrap();
|
||||
assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_new_remove_old_member() {
|
||||
let mut deps = mock_dependencies();
|
||||
set_up(deps.as_mut());
|
||||
|
||||
// add a new one and remove existing one
|
||||
let add = vec![Member {
|
||||
addr: USER3.into(),
|
||||
weight: 15,
|
||||
}];
|
||||
let remove = vec![USER1.into()];
|
||||
|
||||
// non-admin cannot update
|
||||
let height = mock_env().block.height;
|
||||
let err = update_members(
|
||||
deps.as_mut(),
|
||||
height + 5,
|
||||
Addr::unchecked(USER1),
|
||||
add.clone(),
|
||||
remove.clone(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(err, AdminError::NotAdmin {}.into());
|
||||
|
||||
// Test the values from instantiate
|
||||
assert_users(&deps, Some(11), Some(6), None, None);
|
||||
// Note all values were set at height, the beginning of that block was all None
|
||||
assert_users(&deps, None, None, None, Some(height));
|
||||
// This will get us the values at the start of the block after instantiate (expected initial values)
|
||||
assert_users(&deps, Some(11), Some(6), None, Some(height + 1));
|
||||
|
||||
// admin updates properly
|
||||
update_members(
|
||||
deps.as_mut(),
|
||||
height + 10,
|
||||
Addr::unchecked(INIT_ADMIN),
|
||||
add,
|
||||
remove,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// updated properly
|
||||
assert_users(&deps, None, Some(6), Some(15), None);
|
||||
|
||||
// snapshot still shows old value
|
||||
assert_users(&deps, Some(11), Some(6), None, Some(height + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_old_remove_new_member() {
|
||||
// add will over-write and remove have no effect
|
||||
let mut deps = mock_dependencies();
|
||||
set_up(deps.as_mut());
|
||||
|
||||
// add a new one and remove existing one
|
||||
let add = vec![Member {
|
||||
addr: USER1.into(),
|
||||
weight: 4,
|
||||
}];
|
||||
let remove = vec![USER3.into()];
|
||||
|
||||
// admin updates properly
|
||||
let height = mock_env().block.height;
|
||||
update_members(
|
||||
deps.as_mut(),
|
||||
height,
|
||||
Addr::unchecked(INIT_ADMIN),
|
||||
add,
|
||||
remove,
|
||||
)
|
||||
.unwrap();
|
||||
assert_users(&deps, Some(4), Some(6), None, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_and_remove_same_member() {
|
||||
// add will over-write and remove have no effect
|
||||
let mut deps = mock_dependencies();
|
||||
set_up(deps.as_mut());
|
||||
|
||||
// USER1 is updated and remove in the same call, we should remove this an add member3
|
||||
let add = vec![
|
||||
Member {
|
||||
addr: USER1.into(),
|
||||
weight: 20,
|
||||
},
|
||||
Member {
|
||||
addr: USER3.into(),
|
||||
weight: 5,
|
||||
},
|
||||
];
|
||||
let remove = vec![USER1.into()];
|
||||
|
||||
// admin updates properly
|
||||
let height = mock_env().block.height;
|
||||
update_members(
|
||||
deps.as_mut(),
|
||||
height,
|
||||
Addr::unchecked(INIT_ADMIN),
|
||||
add,
|
||||
remove,
|
||||
)
|
||||
.unwrap();
|
||||
assert_users(&deps, None, Some(6), Some(5), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_remove_hooks() {
|
||||
// add will over-write and remove have no effect
|
||||
let mut deps = mock_dependencies();
|
||||
set_up(deps.as_mut());
|
||||
|
||||
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
|
||||
assert!(hooks.hooks.is_empty());
|
||||
|
||||
let contract1 = String::from("hook1");
|
||||
let contract2 = String::from("hook2");
|
||||
|
||||
let add_msg = ExecuteMsg::AddHook {
|
||||
addr: contract1.clone(),
|
||||
};
|
||||
|
||||
// non-admin cannot add hook
|
||||
let user_info = mock_info(USER1, &[]);
|
||||
let err = execute(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
user_info.clone(),
|
||||
add_msg.clone(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into());
|
||||
|
||||
// admin can add it, and it appears in the query
|
||||
let admin_info = mock_info(INIT_ADMIN, &[]);
|
||||
let _ = execute(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
admin_info.clone(),
|
||||
add_msg.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
|
||||
assert_eq!(hooks.hooks, vec![contract1.clone()]);
|
||||
|
||||
// cannot remove a non-registered contract
|
||||
let remove_msg = ExecuteMsg::RemoveHook {
|
||||
addr: contract2.clone(),
|
||||
};
|
||||
let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), remove_msg).unwrap_err();
|
||||
assert_eq!(err, HookError::HookNotRegistered {}.into());
|
||||
|
||||
// add second contract
|
||||
let add_msg2 = ExecuteMsg::AddHook {
|
||||
addr: contract2.clone(),
|
||||
};
|
||||
let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap();
|
||||
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
|
||||
assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]);
|
||||
|
||||
// cannot re-add an existing contract
|
||||
let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err();
|
||||
assert_eq!(err, HookError::HookAlreadyRegistered {}.into());
|
||||
|
||||
// non-admin cannot remove
|
||||
let remove_msg = ExecuteMsg::RemoveHook { addr: contract1 };
|
||||
let err = execute(deps.as_mut(), mock_env(), user_info, remove_msg.clone()).unwrap_err();
|
||||
assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into());
|
||||
|
||||
// remove the original
|
||||
let _ = execute(deps.as_mut(), mock_env(), admin_info, remove_msg).unwrap();
|
||||
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
|
||||
assert_eq!(hooks.hooks, vec![contract2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hooks_fire() {
|
||||
let mut deps = mock_dependencies();
|
||||
set_up(deps.as_mut());
|
||||
|
||||
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
|
||||
assert!(hooks.hooks.is_empty());
|
||||
|
||||
let contract1 = String::from("hook1");
|
||||
let contract2 = String::from("hook2");
|
||||
|
||||
// register 2 hooks
|
||||
let admin_info = mock_info(INIT_ADMIN, &[]);
|
||||
let add_msg = ExecuteMsg::AddHook {
|
||||
addr: contract1.clone(),
|
||||
};
|
||||
let add_msg2 = ExecuteMsg::AddHook {
|
||||
addr: contract2.clone(),
|
||||
};
|
||||
for msg in vec![add_msg, add_msg2] {
|
||||
let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap();
|
||||
}
|
||||
|
||||
// make some changes - add 3, remove 2, and update 1
|
||||
// USER1 is updated and remove in the same call, we should remove this an add member3
|
||||
let add = vec![
|
||||
Member {
|
||||
addr: USER1.into(),
|
||||
weight: 20,
|
||||
},
|
||||
Member {
|
||||
addr: USER3.into(),
|
||||
weight: 5,
|
||||
},
|
||||
];
|
||||
let remove = vec![USER2.into()];
|
||||
let msg = ExecuteMsg::UpdateMembers { remove, add };
|
||||
|
||||
// admin updates properly
|
||||
assert_users(&deps, Some(11), Some(6), None, None);
|
||||
let res = execute(deps.as_mut(), mock_env(), admin_info, msg).unwrap();
|
||||
assert_users(&deps, Some(20), None, Some(5), None);
|
||||
|
||||
// ensure 2 messages for the 2 hooks
|
||||
assert_eq!(res.messages.len(), 2);
|
||||
// same order as in the message (adds first, then remove)
|
||||
// order of added users is not guaranteed to be preserved
|
||||
let diffs = vec![
|
||||
MemberDiff::new(USER3, None, Some(5)),
|
||||
MemberDiff::new(USER1, Some(11), Some(20)),
|
||||
MemberDiff::new(USER2, Some(6), None),
|
||||
];
|
||||
let hook_msg = MemberChangedHookMsg { diffs };
|
||||
let msg1 = SubMsg::new(hook_msg.clone().into_cosmos_msg(contract1).unwrap());
|
||||
let msg2 = SubMsg::new(hook_msg.into_cosmos_msg(contract2).unwrap());
|
||||
dbg!(&res.messages);
|
||||
dbg!(&msg1);
|
||||
dbg!(&msg2);
|
||||
assert_eq!(res.messages, vec![msg1, msg2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_queries_work() {
|
||||
// add will over-write and remove have no effect
|
||||
let mut deps = mock_dependencies();
|
||||
set_up(deps.as_mut());
|
||||
|
||||
// get total from raw key
|
||||
let total_raw = deps.storage.get(TOTAL_KEY.as_bytes()).unwrap();
|
||||
let total: u64 = from_slice(&total_raw).unwrap();
|
||||
assert_eq!(17, total);
|
||||
|
||||
// get member votes from raw key
|
||||
let member2_raw = deps.storage.get(&member_key(USER2)).unwrap();
|
||||
let member2: u64 = from_slice(&member2_raw).unwrap();
|
||||
assert_eq!(6, member2);
|
||||
|
||||
// and execute misses
|
||||
let member3_raw = deps.storage.get(&member_key(USER3));
|
||||
assert_eq!(None, member3_raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_at_height() {
|
||||
let mut deps = mock_dependencies();
|
||||
set_up(deps.as_mut());
|
||||
|
||||
let height = mock_env().block.height;
|
||||
|
||||
// Test the values from instantiate
|
||||
let total = query_total_weight(deps.as_ref(), None).unwrap();
|
||||
assert_eq!(17, total.weight);
|
||||
// Note all values were set at height, the beginning of that block was all None
|
||||
let total = query_total_weight(deps.as_ref(), Some(height)).unwrap();
|
||||
assert_eq!(0, total.weight);
|
||||
// This will get us the values at the start of the block after instantiate (expected initial values)
|
||||
let total = query_total_weight(deps.as_ref(), Some(height + 1)).unwrap();
|
||||
assert_eq!(17, total.weight);
|
||||
}
|
||||
@@ -30,12 +30,12 @@ fn names<'a>() -> IndexedMap<'a, NameId, RegisteredName, NameIndex<'a>> {
|
||||
let indexes = NameIndex {
|
||||
name: UniqueIndex::new(|d| d.name.to_string(), NAMES_NAME_IDX_NAMESPACE),
|
||||
address: MultiIndex::new(
|
||||
|d| d.address.to_string(),
|
||||
|_pk, d| d.address.to_string(),
|
||||
NAMES_PK_NAMESPACE,
|
||||
NAMES_ADDRESS_IDX_NAMESPACE,
|
||||
),
|
||||
owner: MultiIndex::new(
|
||||
|d| d.owner.clone(),
|
||||
|_pk, d| d.owner.clone(),
|
||||
NAMES_PK_NAMESPACE,
|
||||
NAMES_OWNER_IDX_NAMESPACE,
|
||||
),
|
||||
|
||||
@@ -26,12 +26,12 @@ impl<'a> IndexList<Service> for ServiceIndex<'a> {
|
||||
fn services<'a>() -> IndexedMap<'a, ServiceId, Service, ServiceIndex<'a>> {
|
||||
let indexes = ServiceIndex {
|
||||
nym_address: MultiIndex::new(
|
||||
|d| d.nym_address.to_string(),
|
||||
|_pk, d| d.nym_address.to_string(),
|
||||
SERVICES_PK_NAMESPACE,
|
||||
SERVICES_NYM_ADDRESS_IDX_NAMESPACE,
|
||||
),
|
||||
announcer: MultiIndex::new(
|
||||
|d| d.announcer.clone(),
|
||||
|_pk, d| d.announcer.clone(),
|
||||
SERVICES_PK_NAMESPACE,
|
||||
SERVICES_ANNOUNCER_IDX_NAMESPACE,
|
||||
),
|
||||
|
||||
@@ -25,7 +25,7 @@ fn generate_storage_key(storage: &mut dyn Storage) -> Result<AccountStorageKey,
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
pub struct Account {
|
||||
pub owner_address: Addr,
|
||||
pub staking_address: Option<Addr>,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
[package]
|
||||
name = "ephemera"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "ephemera"
|
||||
path = "bin/main.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4"
|
||||
anyhow = { version = "1.0.66", features = ["backtrace"] }
|
||||
array-bytes = "6.0.0"
|
||||
async-trait = "0.1.59"
|
||||
asynchronous-codec = "0.6.1"
|
||||
blake2 = "0.10.6"
|
||||
bs58 = "0.4.0"
|
||||
bytes = "1.3.0"
|
||||
cfg-if = "1.0.0"
|
||||
chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.0.32", features = ["derive"] }
|
||||
config = { version = "0.13", default-features = false, features = ["toml"] }
|
||||
digest = "0.10.6"
|
||||
dirs = "5.0.0"
|
||||
futures = "0.3.18"
|
||||
futures-util = "0.3.25"
|
||||
lazy_static = "1.4.0"
|
||||
libp2p = { version = "0.51.3", default-features = false, features = ["dns", "gossipsub", "kad", "macros", "noise", "request-response", "serde", "tcp", "tokio", "yamux"] }
|
||||
libp2p-identity = "0.1.0"
|
||||
log = "0.4.14"
|
||||
lru = "0.10.0"
|
||||
pretty_env_logger = "0.4"
|
||||
refinery = { version = "0.8.7", features = ["rusqlite"], optional = true }
|
||||
reqwest = { version = "0.11.6", features = ["json"] }
|
||||
rocksdb = { version = "0.20.1", optional = true }
|
||||
rusqlite = { version = "0.27.0", features = ["bundled"], optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_derive = "1.0.149"
|
||||
serde_json = "1.0.91"
|
||||
thiserror = "1.0.37"
|
||||
tokio = { version = "1", features = ["macros", "net","rt-multi-thread"] }
|
||||
tokio-tungstenite = "0.18.0"
|
||||
tokio-util = { version = "0.7.4", features = ["full"] }
|
||||
toml = "0.7.0"
|
||||
unsigned-varint = "0.7.1"
|
||||
utoipa = { version = "3.0.1", features = ["actix_extras"] }
|
||||
utoipa-swagger-ui = { version = "3.0.2", features = ["actix-web"] }
|
||||
uuid = { version = "1.2.2", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5.0"
|
||||
rand = "0.8.5"
|
||||
|
||||
[features]
|
||||
default = ["sqlite_storage"]
|
||||
rocksdb_storage = ["rocksdb"]
|
||||
sqlite_storage = ["rusqlite", "refinery"]
|
||||
@@ -0,0 +1,142 @@
|
||||
# Ephemera - reliable broadcast protocol implementation
|
||||
|
||||
Ephemera does reliable broadcast for blocks.
|
||||
|
||||
## Short Overview
|
||||
|
||||
All Ephemera nodes accept messages submitted by clients. Node then gossips these to other nodes in the cluster. After certain interval,
|
||||
a node collects messages and produces a block. Then it does reliable broadcast for the block with other nodes in the cluster.
|
||||
|
||||
Ephemera doesn't have the concept of (decentralised) leader at the moment. It's up to an _Application_ to decide which block to use.
|
||||
For example in case of Nym-Api, it is the first block submitted to a "Smart Contract".
|
||||
|
||||
At the same time, the purpose of blocks is to reach consensus about which messages are included. It's just that Ephemera doesn't make the final decision,
|
||||
instead it leaves that to an _Application_.
|
||||
|
||||
## Main concepts
|
||||
|
||||
- **Node** - a single instance of Ephemera.
|
||||
- **Cluster** - a set of nodes participating in reliable broadcast.
|
||||
- **EphemeraMessage** - a message submitted by a client.
|
||||
- **Block** - a set of messages collected by a node.
|
||||
- **Application(ABCI)** - a trait which Ephemera users implement to accept messages and blocks.
|
||||
- check_tx
|
||||
- check_block
|
||||
- accept_block
|
||||
|
||||
## How to run
|
||||
|
||||
[README](../scripts/README.md)
|
||||
|
||||
## HTTP API
|
||||
|
||||
See [Rust](src/api/http/mod.rs)
|
||||
|
||||
### Endpoints
|
||||
|
||||
**NODE**
|
||||
- `/ephemera/node/health`
|
||||
- `/ephemera/node/config`
|
||||
|
||||
**BLOCKS**
|
||||
- `/ephemera/broadcast/block/{hash}`
|
||||
- `/ephemera/broadcast/block/height/{height}`
|
||||
- `/ephemera/broadcast/blocks/last`
|
||||
- `/ephemera/broadcast/block/certificates/{hash}`
|
||||
- `/ephemera/broadcast/block/broadcast_info/{hash}`
|
||||
|
||||
**GROUP**
|
||||
- `/ephemera/broadcast/group/info`
|
||||
|
||||
**MESSAGES**
|
||||
- `/ephemera/broadcast/submit_message`
|
||||
|
||||
**DHT**
|
||||
- `/ephemera/dht/query/{key}`
|
||||
- `/ephemera/dht/store`
|
||||
|
||||
## Rust API
|
||||
|
||||
Almost identical to HTTP API.
|
||||
|
||||
See [Rust](src/api/mod.rs)
|
||||
|
||||
## Application(Ephemera ABCI)
|
||||
|
||||
Cosmos style ABCI application hook
|
||||
- `check_tx`
|
||||
- `check_block`
|
||||
- `deliver_block`
|
||||
|
||||
See [Rust](src/api/application.rs)
|
||||
|
||||
## Examples
|
||||
|
||||
### Ephemera HTTP and WS external interfaces example/tests
|
||||
|
||||
See [README.md](../examples/http-ws-sync/README.md)
|
||||
|
||||
### Nym Api simulation
|
||||
|
||||
See [README.md](../examples/nym-api/README.md)
|
||||
|
||||
### http API example/tests
|
||||
|
||||
See [README.md](../examples/cluster-http-api/README.md)
|
||||
|
||||
### Membership over HTTP API example/tests
|
||||
|
||||
See [README.md](../examples/members_provider_http/README.md)
|
||||
|
||||
## About reliable broadcast and consensus
|
||||
|
||||
In blockchain technology blocks have two main purposes:
|
||||
1. To maintain chain of blocks, so that the validity of each block can be cryptographically verified by the previous blocks
|
||||
2. As a unit of consensus, each block contains a set of transactions/messages/actions that are agreed upon by
|
||||
the network. This set of transactions is chosen from the global set of all possible transactions that are pending.
|
||||
We call the set of transactions in a block consensus because the set of nodes trying to achieve global shared state
|
||||
agreed on this particular set of transactions.
|
||||
|
||||
Ephemera is not a blockchain. But it uses blocks to agree on the set of transactions in a block.
|
||||
But at the same time it doesn't behave like a blockchain consensus algorithm.
|
||||
We may say that it allows each application that uses Ephemera to "propose" something what can be
|
||||
afterwards to be used to achieve consensus.
|
||||
|
||||
### In Summary
|
||||
|
||||
1. Ephemera provides functionality to reach agreement on a single value between a set of nodes.
|
||||
2. Ephemera also provides the concept of a block, which application can take advantage of to reach consensus externally.
|
||||
|
||||
### Reliable broadcast, consensus and blocks
|
||||
|
||||
In distributed systems(including byzantine), we try to solve the problem of reaching to a commons state between a set of nodes.
|
||||
|
||||
One way to define this problem is using the following properties:
|
||||
1.
|
||||
1) Agreement: All nodes agree on the same value.(TODO clarify)
|
||||
2) Consensus: All nodes agree on the same value.(TODO clarify)
|
||||
2. Validity: All nodes agree on a value that is valid.
|
||||
3. Termination: All nodes eventually agree on a value.
|
||||
|
||||
Reliable broadcast ensures the properties of 1.1 and 1.2. It's left to a particular consensus algorithm to ensure the termination property.
|
||||
|
||||
One important feature of consensus in blockchain is that it guarantees total ordering of transactions.
|
||||
Reliable broadcast with blocks helps to ensure this total ordering.
|
||||
|
||||
### Ephemera specific properties
|
||||
|
||||
Because Ephemera doesn't use the idea of leader, we can say that it solves consensus partially.
|
||||
It allows each instance to create a block. And then it's up to an application to decide which block to use.
|
||||
|
||||
Also, as it doesn't implement a full consensus algorithm, it doesn't ensure the termination.
|
||||
There's no algorithm in place what tries to reach a consensus about a single block globally and sequentially
|
||||
in time.
|
||||
|
||||
When a block contains a single message, then it's semantically equivalent to a reliable broadcast.
|
||||
|
||||
But when a block contains multiple messages, then it can be part of a consensus process. Except that in Ephemera each node
|
||||
can create a block. To achieve consensus in a more traditional sense, it needs an application help if more strict
|
||||
consensus is required.
|
||||
|
||||
For example, Nym-Api allows each node to create a block but uses external coordinator(a smart contract)
|
||||
to decide which block to use.
|
||||
@@ -0,0 +1,12 @@
|
||||
use clap::Parser;
|
||||
|
||||
use ephemera::cli::Cli;
|
||||
use ephemera::logging;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
logging::init();
|
||||
|
||||
Cli::parse().execute().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
block_hash TEXT NOT NULL UNIQUE,
|
||||
height TEXT NOT NULL UNIQUE,
|
||||
block BLOB NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS block_certificates (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
block_hash TEXT NOT NULL UNIQUE,
|
||||
certificates BLOB NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS block_broadcast_group (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
block_hash TEXT NOT NULL UNIQUE,
|
||||
members BLOB NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS block_merkle_tree (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
block_hash TEXT NOT NULL UNIQUE,
|
||||
merkle_tree BLOB NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,104 @@
|
||||
use log::trace;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::api::types::{ApiBlock, ApiEphemeraMessage};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RemoveMessages {
|
||||
/// Remove all messages from the mempool
|
||||
All,
|
||||
/// Remove only inclued messages from the mempool
|
||||
Selected(Vec<ApiEphemeraMessage>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CheckBlockResult {
|
||||
/// Accept the block
|
||||
Accept,
|
||||
/// Reject the block with a reason.
|
||||
Reject,
|
||||
/// Reject the block and remove messages from the mempool
|
||||
RejectAndRemoveMessages(RemoveMessages),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CheckBlockResponse {
|
||||
pub accept: bool,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
//Just a placeholder for now
|
||||
#[error("ApplicationError: {0}")]
|
||||
Application(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Cosmos style ABCI application hook
|
||||
///
|
||||
/// These functions should be relatively fast, as they are called synchronously by Ephemera main loop.
|
||||
pub trait Application {
|
||||
/// It's called when receiving a new message from network before adding it to the mempool.
|
||||
/// It's up to the application to decide whether the message is valid or not.
|
||||
/// Basic check could be for example signature verification.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `message` - message to be checked
|
||||
///
|
||||
/// # Returns
|
||||
/// * `true` - if the message is valid
|
||||
/// * `false` - if the message is invalid
|
||||
///
|
||||
/// # Errors
|
||||
/// * `Error::General` - if there was an error during validation
|
||||
fn check_tx(&self, message: ApiEphemeraMessage) -> Result<bool>;
|
||||
|
||||
/// Ephemera produces new blocks with configured interval.
|
||||
/// Application can decide whether to accept the block or not.
|
||||
/// For example, if the block doesn't contain any transactions, it can be rejected.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `block` - block to be checked
|
||||
///
|
||||
/// # Returns
|
||||
/// * `CheckBlockResult::Accept` - if the block is valid
|
||||
/// * `CheckBlockResult::Reject` - if the block is invalid
|
||||
/// * `CheckBlockResult::RejectAndRemoveMessages` - if the block is invalid and some messages should be removed from the mempool
|
||||
///
|
||||
/// # Errors
|
||||
/// * `Error::General` - if there was an error during validation
|
||||
fn check_block(&self, block: &ApiBlock) -> Result<CheckBlockResult>;
|
||||
|
||||
/// Deliver Block is called after block is confirmed by Ephemera and persisted to the storage.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `block` - block to be delivered
|
||||
///
|
||||
/// # Errors
|
||||
/// * `Error::General` - if there was an error during validation
|
||||
fn deliver_block(&self, block: ApiBlock) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Dummy application which doesn't do any validation.
|
||||
/// Might be useful for testing.
|
||||
#[derive(Default)]
|
||||
pub struct Dummy;
|
||||
|
||||
impl Application for Dummy {
|
||||
fn check_tx(&self, tx: ApiEphemeraMessage) -> Result<bool> {
|
||||
trace!("check_tx: {tx:?}");
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn check_block(&self, block: &ApiBlock) -> Result<CheckBlockResult> {
|
||||
trace!("accept_block: {block:?}");
|
||||
Ok(CheckBlockResult::Accept)
|
||||
}
|
||||
|
||||
fn deliver_block(&self, block: ApiBlock) -> Result<()> {
|
||||
trace!("deliver_block: {block:?}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::api::types::{ApiBlockBroadcastInfo, ApiBroadcastInfo, ApiHealth};
|
||||
use crate::ephemera_api::{
|
||||
ApiBlock, ApiCertificate, ApiDhtQueryRequest, ApiDhtQueryResponse, ApiDhtStoreRequest,
|
||||
ApiEphemeraConfig, ApiEphemeraMessage, ApiVerifyMessageInBlock,
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Internal(#[from] reqwest::Error),
|
||||
#[error("Unexpected response: {status} {body}")]
|
||||
UnexpectedResponse {
|
||||
status: reqwest::StatusCode,
|
||||
body: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Client to interact with the node over HTTP api.
|
||||
pub struct Client {
|
||||
pub(crate) client: reqwest::Client,
|
||||
pub(crate) url: String,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a http new client.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `url` - The url of the node api endpoint.
|
||||
#[must_use]
|
||||
pub fn new(url: String) -> Self {
|
||||
let client = reqwest::Client::new();
|
||||
Self { client, url }
|
||||
}
|
||||
|
||||
/// Create a new client.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `url` - The url of the node api endpoint.
|
||||
/// * `timeout_sec` - Request timeout in seconds.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the client cannot be created.
|
||||
#[must_use]
|
||||
pub fn new_with_timeout(url: String, timeout_sec: u64) -> Self {
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.timeout(Duration::from_secs(timeout_sec))
|
||||
.build()
|
||||
.unwrap();
|
||||
Self { client, url }
|
||||
}
|
||||
|
||||
/// Get the health of the node.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use ephemera::ephemera_api::{Client, ApiHealth};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
///async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = Client::new("http://localhost:7000".to_string());
|
||||
/// let health = client.health().await.unwrap();
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Returns
|
||||
/// * [`ApiHealth`] - The health of the node.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn health(&self) -> Result<ApiHealth> {
|
||||
self.query("ephemera/node/health").await
|
||||
}
|
||||
|
||||
/// Get the block by hash.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use ephemera::ephemera_api::{ApiBlock, Client};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
///async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = Client::new("http://localhost:7000".to_string());
|
||||
/// let block = client.get_block_by_hash("9D2LaY17rbnxfgKUbvcsJ5cB2BRHEd8fPJwsBnDHNGBX").await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Returns
|
||||
/// * Option<[`ApiBlock`]> - The block.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn get_block_by_hash(&self, hash: &str) -> Result<Option<ApiBlock>> {
|
||||
let url = format!("ephemera/broadcast/block/{hash}",);
|
||||
self.query_optional(&url).await
|
||||
}
|
||||
|
||||
/// Get the block certificates by hash.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use ephemera::ephemera_api::{ApiCertificate, Client};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = Client::new("http://localhost:7000".to_string());
|
||||
/// let certificates = client.get_block_certificates("9D2LaY17rbnxfgKUbvcsJ5cB2BRHEd8fPJwsBnDHNGBX").await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `hash` - The hash of the block.
|
||||
///
|
||||
/// # Returns
|
||||
/// * Option<Vec<[`ApiCertificate`]>> - The block certificates.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn get_block_certificates(&self, hash: &str) -> Result<Option<Vec<ApiCertificate>>> {
|
||||
let url = format!("ephemera/broadcast/block/certificates/{hash}",);
|
||||
self.query_optional(&url).await
|
||||
}
|
||||
|
||||
/// Get the block by height.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use ephemera::ephemera_api::{ApiBlock, Client};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = Client::new("http://localhost:7000/".to_string());
|
||||
/// let block = client.get_block_by_height(1).await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `height` - The height of the block.
|
||||
///
|
||||
/// # Returns
|
||||
/// * Option<[`ApiBlock`]> - The block.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn get_block_by_height(&self, height: u64) -> Result<Option<ApiBlock>> {
|
||||
let url = format!("ephemera/broadcast/block/height/{height}",);
|
||||
self.query_optional(&url).await
|
||||
}
|
||||
|
||||
/// Get the last block.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use ephemera::ephemera_api::{ApiBlock, Client};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = Client::new("http://localhost:7000/".to_string());
|
||||
/// let block = client.get_last_block().await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Returns
|
||||
/// * [`ApiBlock`] - The last block.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn get_last_block(&self) -> Result<ApiBlock> {
|
||||
self.query("ephemera/broadcast/blocks/last").await
|
||||
}
|
||||
|
||||
/// Get the node configuration.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use ephemera::ephemera_api::{ApiEphemeraConfig, Client};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = Client::new("http://localhost:7000/".to_string());
|
||||
/// let config = client.get_ephemera_config().await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Returns
|
||||
/// * [`ApiEphemeraConfig`] - The node configuration.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn get_ephemera_config(&self) -> Result<ApiEphemeraConfig> {
|
||||
self.query("ephemera/node/config").await
|
||||
}
|
||||
|
||||
/// Submit a message to the node.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use ephemera::ephemera_api::{ApiEphemeraMessage, Client};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = Client::new("http://localhost:7000/".to_string());
|
||||
/// let message = unimplemented!("See how to create a ApiEphemeraMessage");
|
||||
/// client.submit_message(message).await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `message` - The message to submit.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn submit_message(&self, message: ApiEphemeraMessage) -> Result<()> {
|
||||
let url = format!("{}/{}", self.url, "ephemera/broadcast/submit_message");
|
||||
let response = self.client.post(&url).json(&message).send().await?;
|
||||
if response.status().is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::UnexpectedResponse {
|
||||
status: response.status(),
|
||||
body: response.text().await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
///Store Key Value pair in the DHT.
|
||||
///
|
||||
/// # Example
|
||||
///```no_run
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// use ephemera::ephemera_api::Client;
|
||||
/// let client = Client::new("http://localhost:7000/".to_string());
|
||||
/// let request = unimplemented!("See how to create a ApiDhtStoreRequest");
|
||||
/// client.store_in_dht(request).await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request` - Key Value pair to store.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn store_in_dht(&self, request: ApiDhtStoreRequest) -> Result<()> {
|
||||
let url = format!("{}/{}", self.url, "ephemera/dht/store");
|
||||
let response = self.client.post(&url).json(&request).send().await?;
|
||||
if response.status().is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::UnexpectedResponse {
|
||||
status: response.status(),
|
||||
body: response.text().await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
///Store Key Value pair in the DHT.
|
||||
///
|
||||
/// # Example
|
||||
///```no_run
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// use ephemera::ephemera_api::Client;
|
||||
/// let client = Client::new("http://localhost:7000/".to_string());
|
||||
/// let key = &[1, 2, 3];
|
||||
/// let value = &[4, 5, 6];
|
||||
/// client.store_in_dht_key_value(key, value).await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
/// # Arguments
|
||||
/// * `key` - Key to use to store the value.
|
||||
/// * `value` - Value to store.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn store_in_dht_key_value(&self, key: &[u8], value: &[u8]) -> Result<()> {
|
||||
let request = ApiDhtStoreRequest::new(key, value);
|
||||
self.store_in_dht(request).await
|
||||
}
|
||||
|
||||
/// Query the DHT for a given key.
|
||||
///
|
||||
/// # Example
|
||||
///```no_run
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// use ephemera::ephemera_api::{ApiDhtQueryRequest, Client};
|
||||
/// let client = Client::new("http://localhost:7000/".to_string());
|
||||
/// let request = ApiDhtQueryRequest::new(&[1, 2, 3]);
|
||||
/// let response = client.query_dht(request).await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request` - Key to query.
|
||||
///
|
||||
/// # Returns
|
||||
/// * Option<[`ApiDhtQueryResponse`]> - The value stored in the DHT for the given key.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn query_dht(
|
||||
&self,
|
||||
request: ApiDhtQueryRequest,
|
||||
) -> Result<Option<ApiDhtQueryResponse>> {
|
||||
let url = format!("ephemera/dht/query/{}", request.key_encoded());
|
||||
self.query_optional(&url).await
|
||||
}
|
||||
|
||||
/// Query the DHT for a given key.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// use ephemera::ephemera_api::Client;
|
||||
/// let client = Client::new("http://localhost:7000/".to_string());
|
||||
/// let key = &[1, 2, 3];
|
||||
/// let response = client.query_dht_key(key).await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - Key to query.
|
||||
///
|
||||
/// # Returns
|
||||
/// * Option<[`ApiDhtQueryResponse`]> - The value stored in the DHT for the given key.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn query_dht_key(&self, key: &[u8]) -> Result<Option<ApiDhtQueryResponse>> {
|
||||
let request = ApiDhtQueryRequest::new(key);
|
||||
self.query_dht(request).await
|
||||
}
|
||||
|
||||
/// Get broadcast group info.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use ephemera::ephemera_api::Client;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = Client::new("http://localhost:7000/".to_string());
|
||||
/// let info = client.broadcast_info().await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Returns
|
||||
/// * [`ApiBroadcastInfo`] - The broadcast group info.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn broadcast_info(&self) -> Result<ApiBroadcastInfo> {
|
||||
self.query("ephemera/broadcast/group/info").await
|
||||
}
|
||||
|
||||
/// Get block broadcast info
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use ephemera::ephemera_api::Client;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = Client::new("http://localhost:7000/".to_string());
|
||||
/// let info = client.get_block_broadcast_info("hash").await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
///```
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `hash` - Hash of the block to query.
|
||||
///
|
||||
/// # Returns
|
||||
/// * Some([`ApiBlockBroadcastInfo`]) - The block broadcast info.
|
||||
/// * None - If the block is not found.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn get_block_broadcast_info(
|
||||
&self,
|
||||
hash: &str,
|
||||
) -> Result<Option<ApiBlockBroadcastInfo>> {
|
||||
let url = format!("ephemera/broadcast/block/broadcast_info/{hash}",);
|
||||
self.query_optional(&url).await
|
||||
}
|
||||
|
||||
/// Verifies if given message is in block identified by block hash
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use ephemera::ephemera_api::Client;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let client = Client::new("http://localhost:7000/".to_string());
|
||||
/// let is_in_block = client.verify_message_in_block("block_hash", "message_hash", 0).await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `block_hash` - Hash of the block to query.
|
||||
/// * `message_hash` - Hash of the message to query.
|
||||
/// * `message_index` - Index of the message in the block.
|
||||
///
|
||||
/// # Returns
|
||||
/// * bool - True if the message is in the block, false otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// If the request fails.
|
||||
pub async fn verify_message_in_block(
|
||||
&self,
|
||||
block_hash: &str,
|
||||
message_hash: &str,
|
||||
message_index: usize,
|
||||
) -> Result<bool> {
|
||||
let request = ApiVerifyMessageInBlock::new(
|
||||
block_hash.to_string(),
|
||||
message_hash.to_string(),
|
||||
message_index,
|
||||
);
|
||||
|
||||
let url = format!("{}/{}", self.url, "ephemera/messages/verify");
|
||||
let response = self.client.post(&url).json(&request).send().await?;
|
||||
if response.status().is_success() {
|
||||
let body = response.json::<bool>().await?;
|
||||
Ok(body)
|
||||
} else {
|
||||
Err(Error::UnexpectedResponse {
|
||||
status: response.status(),
|
||||
body: response.text().await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_optional<T: for<'de> serde::Deserialize<'de>>(
|
||||
&self,
|
||||
path: &str,
|
||||
) -> Result<Option<T>> {
|
||||
let url = format!("{}/{}", self.url, path);
|
||||
match self.client.get(&url).send().await {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
let body = response.json::<T>().await?;
|
||||
Ok(Some(body))
|
||||
} else if response.status() == reqwest::StatusCode::NOT_FOUND {
|
||||
Ok(None)
|
||||
} else {
|
||||
return Err(Error::UnexpectedResponse {
|
||||
status: response.status(),
|
||||
body: response.text().await?,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn query<T: for<'de> serde::Deserialize<'de>>(&self, path: &str) -> Result<T> {
|
||||
let url = format!("{}/{}", self.url, path);
|
||||
match self.client.get(&url).send().await {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
let body = response.json::<T>().await?;
|
||||
Ok(body)
|
||||
} else {
|
||||
Err(Error::UnexpectedResponse {
|
||||
status: response.status(),
|
||||
body: response.text().await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use actix_web::{dev::Server, http::KeepAlive, web::Data, App, HttpServer};
|
||||
use log::info;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
use crate::api::CommandExecutor;
|
||||
use crate::core::builder::NodeInfo;
|
||||
|
||||
pub(crate) mod client;
|
||||
pub(crate) mod query;
|
||||
pub(crate) mod submit;
|
||||
|
||||
/// Starts the HTTP server.
|
||||
pub(crate) fn init(node_info: &NodeInfo, api: CommandExecutor) -> anyhow::Result<Server> {
|
||||
print_startup_messages(node_info);
|
||||
|
||||
let server = HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(Data::new(api.clone()))
|
||||
.service(query::health)
|
||||
.service(query::block_by_hash)
|
||||
.service(query::block_certificates)
|
||||
.service(query::block_by_height)
|
||||
.service(query::block_broadcast_group)
|
||||
.service(query::last_block)
|
||||
.service(query::node_config)
|
||||
.service(query::query_dht)
|
||||
.service(query::broadcast_info)
|
||||
.service(submit::submit_message)
|
||||
.service(submit::store_in_dht)
|
||||
.service(submit::verify_message_in_block)
|
||||
.service(swagger_ui())
|
||||
})
|
||||
.keep_alive(KeepAlive::Os)
|
||||
.bind((node_info.ip.as_str(), node_info.initial_config.http.port))?
|
||||
.run();
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
/// Builds the Swagger UI.
|
||||
///
|
||||
/// Note that all routes you want Swagger docs for must be in the `paths` annotation.
|
||||
fn swagger_ui() -> SwaggerUi {
|
||||
use crate::api::types;
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
query::health,
|
||||
query::block_by_hash,
|
||||
query::block_certificates,
|
||||
query::block_by_height,
|
||||
query::last_block,
|
||||
query::block_broadcast_group,
|
||||
query::node_config,
|
||||
query::query_dht,
|
||||
query::broadcast_info,
|
||||
submit::submit_message,
|
||||
submit::store_in_dht,
|
||||
submit::verify_message_in_block
|
||||
),
|
||||
components(schemas(
|
||||
types::ApiBlock,
|
||||
types::ApiEphemeraMessage,
|
||||
types::ApiCertificate,
|
||||
types::ApiSignature,
|
||||
types::ApiPublicKey,
|
||||
types::ApiHealth,
|
||||
types::HealthStatus,
|
||||
types::ApiEphemeraConfig,
|
||||
types::ApiDhtStoreRequest,
|
||||
types::ApiDhtQueryRequest,
|
||||
types::ApiDhtQueryResponse,
|
||||
types::ApiBroadcastInfo,
|
||||
types::ApiVerifyMessageInBlock,
|
||||
))
|
||||
)]
|
||||
struct ApiDoc;
|
||||
SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-doc/openapi.json", ApiDoc::openapi())
|
||||
}
|
||||
|
||||
/// Prints messages saying which ports HTTP is running on, and some helpful pointers
|
||||
/// `OpenAPI` and `Swagger UI` endpoints.
|
||||
fn print_startup_messages(info: &NodeInfo) {
|
||||
let http_root = info.api_address_http();
|
||||
info!("Server running on {}", http_root);
|
||||
info!("Swagger UI: {}/swagger-ui/", http_root);
|
||||
info!("OpenAPI spec is at: {}/api-doc/openapi.json", http_root);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use actix_web::{get, web, HttpResponse, Responder};
|
||||
use log::error;
|
||||
|
||||
use crate::{
|
||||
api::{types::ApiHealth, types::HealthStatus::Healthy, CommandExecutor},
|
||||
ephemera_api::{ApiDhtQueryRequest, ApiDhtQueryResponse},
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
responses(
|
||||
(status = 200, description = "Endpoint to check if the server is running")),
|
||||
)]
|
||||
#[get("/ephemera/node/health")]
|
||||
#[allow(clippy::unused_async)]
|
||||
pub(crate) async fn health() -> impl Responder {
|
||||
HttpResponse::Ok().json(ApiHealth { status: Healthy })
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
responses(
|
||||
(status = 200, description = "Get current broadcast group"),
|
||||
(status = 500, description = "Server failed to process request")),
|
||||
)]
|
||||
#[get("/ephemera/broadcast/group/info")]
|
||||
pub(crate) async fn broadcast_info(api: web::Data<CommandExecutor>) -> impl Responder {
|
||||
match api.get_broadcast_info().await {
|
||||
Ok(group) => HttpResponse::Ok().json(group),
|
||||
Err(err) => {
|
||||
error!("Failed to get current broadcast group: {err}",);
|
||||
HttpResponse::InternalServerError().json("Server failed to process request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
responses(
|
||||
(status = 200, description = "GET block by hash"),
|
||||
(status = 404, description = "Block not found"),
|
||||
(status = 500, description = "Server failed to process request")),
|
||||
params(("hash", description = "Block hash")),
|
||||
)]
|
||||
#[get("/ephemera/broadcast/block/{hash}")]
|
||||
pub(crate) async fn block_by_hash(
|
||||
hash: web::Path<String>,
|
||||
api: web::Data<CommandExecutor>,
|
||||
) -> impl Responder {
|
||||
match api.get_block_by_id(hash.into_inner()).await {
|
||||
Ok(Some(block)) => HttpResponse::Ok().json(block),
|
||||
Ok(_) => HttpResponse::NotFound().json("Block not found"),
|
||||
Err(err) => {
|
||||
error!("Failed to get block by hash: {err}",);
|
||||
HttpResponse::InternalServerError().json("Server failed to process request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
responses(
|
||||
(status = 200, description = "Get block signatures"),
|
||||
(status = 404, description = "Certificates not found"),
|
||||
(status = 500, description = "Server failed to process request")),
|
||||
params(("hash", description = "Block hash")),
|
||||
)]
|
||||
#[get("/ephemera/broadcast/block/certificates/{hash}")]
|
||||
pub(crate) async fn block_certificates(
|
||||
hash: web::Path<String>,
|
||||
api: web::Data<CommandExecutor>,
|
||||
) -> impl Responder {
|
||||
let id = hash.into_inner();
|
||||
match api.get_block_certificates(id.clone()).await {
|
||||
Ok(Some(signatures)) => HttpResponse::Ok().json(signatures),
|
||||
Ok(_) => HttpResponse::NotFound().json("Certificates not found"),
|
||||
Err(err) => {
|
||||
error!("Failed to get signatures {err}",);
|
||||
HttpResponse::InternalServerError().json("Server failed to process request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
responses(
|
||||
(status = 200, description = "Get block by height"),
|
||||
(status = 404, description = "Block not found"),
|
||||
(status = 500, description = "Server failed to process request")),
|
||||
params(("height", description = "Block height")),
|
||||
)]
|
||||
#[get("/ephemera/broadcast/block/height/{height}")]
|
||||
pub(crate) async fn block_by_height(
|
||||
height: web::Path<u64>,
|
||||
api: web::Data<CommandExecutor>,
|
||||
) -> impl Responder {
|
||||
match api.get_block_by_height(height.into_inner()).await {
|
||||
Ok(Some(block)) => HttpResponse::Ok().json(block),
|
||||
Ok(_) => HttpResponse::NotFound().json("Block not found"),
|
||||
Err(err) => {
|
||||
error!("Failed to get block {err}",);
|
||||
HttpResponse::InternalServerError().json("Server failed to process request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
responses(
|
||||
(status = 200, description = "Get last block"),
|
||||
(status = 500, description = "Server failed to process request")),
|
||||
)]
|
||||
//Need to use plural(blocks), otherwise overlaps with block_by_id route
|
||||
#[get("/ephemera/broadcast/blocks/last")]
|
||||
pub(crate) async fn last_block(api: web::Data<CommandExecutor>) -> impl Responder {
|
||||
match api.get_last_block().await {
|
||||
Ok(block) => HttpResponse::Ok().json(block),
|
||||
Err(err) => {
|
||||
error!("Failed to get block {err}",);
|
||||
HttpResponse::InternalServerError().json("Server failed to process request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
responses(
|
||||
(status = 200, description = "Get block broadcast group"),
|
||||
(status = 404, description = "Block not found"),
|
||||
(status = 500, description = "Server failed to process request")),
|
||||
params(("hash", description = "Block hash")),
|
||||
)]
|
||||
#[get("/ephemera/broadcast/block/broadcast_info/{hash}")]
|
||||
pub(crate) async fn block_broadcast_group(
|
||||
hash: web::Path<String>,
|
||||
api: web::Data<CommandExecutor>,
|
||||
) -> impl Responder {
|
||||
let hash = hash.into_inner();
|
||||
match api.get_block_broadcast_info(hash).await {
|
||||
Ok(Some(group)) => HttpResponse::Ok().json(group),
|
||||
Ok(_) => HttpResponse::NotFound().json("Block not found"),
|
||||
Err(err) => {
|
||||
error!("Failed to get block broadcast group {err}",);
|
||||
HttpResponse::InternalServerError().json("Server failed to process request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
responses(
|
||||
(status = 200, description = "Get node config"),
|
||||
(status = 500, description = "Server failed to process request")),
|
||||
)]
|
||||
#[get("/ephemera/node/config")]
|
||||
pub(crate) async fn node_config(api: web::Data<CommandExecutor>) -> impl Responder {
|
||||
match api.get_node_config().await {
|
||||
Ok(config) => HttpResponse::Ok().json(config),
|
||||
Err(err) => {
|
||||
error!("Failed to get node config {err}",);
|
||||
HttpResponse::InternalServerError().json("Server failed to process request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
responses(
|
||||
(status = 200, description = "Query dht"),
|
||||
(status = 500, description = "Server failed to process request")),
|
||||
params(("query", description = "Dht query")),
|
||||
)]
|
||||
#[get("/ephemera/dht/query/{key}")]
|
||||
pub(crate) async fn query_dht(
|
||||
api: web::Data<CommandExecutor>,
|
||||
key: web::Path<String>,
|
||||
) -> impl Responder {
|
||||
let key = ApiDhtQueryRequest::parse_key(key.into_inner().as_str());
|
||||
|
||||
match api.query_dht(key).await {
|
||||
Ok(Some((key, value))) => {
|
||||
let response = ApiDhtQueryResponse::new(key, value);
|
||||
HttpResponse::Ok().json(response)
|
||||
}
|
||||
Ok(_) => HttpResponse::NotFound().json("Not found"),
|
||||
Err(err) => {
|
||||
error!("Failed to query dht {err}",);
|
||||
HttpResponse::InternalServerError().json("Server failed to process request")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use actix_web::{post, web, HttpResponse};
|
||||
use log::{debug, error};
|
||||
|
||||
use crate::api::types::ApiVerifyMessageInBlock;
|
||||
use crate::api::{
|
||||
types::{ApiDhtStoreRequest, ApiEphemeraMessage},
|
||||
ApiError, CommandExecutor,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
request_body = ApiEphemeraMessage,
|
||||
responses(
|
||||
(status = 200, description = "Send a message to an Ephemera node which will be broadcast to the network"),
|
||||
(status = 500, description = "Server failed to process request")),
|
||||
params(("message", description = "Message to send"))
|
||||
)]
|
||||
#[post("/ephemera/broadcast/submit_message")]
|
||||
pub(crate) async fn submit_message(
|
||||
message: web::Json<ApiEphemeraMessage>,
|
||||
api: web::Data<CommandExecutor>,
|
||||
) -> HttpResponse {
|
||||
match api.send_ephemera_message(message.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().json("Message submitted"),
|
||||
Err(err) => {
|
||||
if let ApiError::DuplicateMessage = err {
|
||||
debug!("Message already submitted {err:?}");
|
||||
HttpResponse::BadRequest().json("Message already submitted")
|
||||
} else {
|
||||
error!("Error submitting message: {}", err);
|
||||
HttpResponse::InternalServerError().json("Server failed to process request")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
request_body = ApiDhtStoreRequest,
|
||||
responses(
|
||||
(status = 200, description = "Request to store a value in the DHT"),
|
||||
(status = 500, description = "Server failed to process request")),
|
||||
params(
|
||||
("request", description = "Dht store request")
|
||||
)
|
||||
)]
|
||||
#[post("/ephemera/dht/store")]
|
||||
pub(crate) async fn store_in_dht(
|
||||
request: web::Json<ApiDhtStoreRequest>,
|
||||
api: web::Data<CommandExecutor>,
|
||||
) -> HttpResponse {
|
||||
let request = request.into_inner();
|
||||
|
||||
let key = request.key();
|
||||
let value = request.value();
|
||||
|
||||
match api.store_in_dht(key, value).await {
|
||||
Ok(_) => HttpResponse::Ok().json("Store request submitted"),
|
||||
Err(err) => {
|
||||
error!("Error storing in dht: {}", err);
|
||||
HttpResponse::InternalServerError().json("Server failed to process request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
request_body = ApiVerifyMessageInBlock,
|
||||
responses(
|
||||
(status = 200, description = "Verifies if given message is in block identified by block hash.\
|
||||
Returns true if message is in block, false otherwise. False can also mean that block or message \
|
||||
does not exist in that block."),
|
||||
(status = 500, description = "Server failed to process request")),
|
||||
params(
|
||||
("request", description = "Verify message request")
|
||||
)
|
||||
)]
|
||||
#[post("/ephemera/messages/verify")]
|
||||
pub(crate) async fn verify_message_in_block(
|
||||
request: web::Json<ApiVerifyMessageInBlock>,
|
||||
api: web::Data<CommandExecutor>,
|
||||
) -> HttpResponse {
|
||||
let request = request.into_inner();
|
||||
match api.verify_message_in_block(request).await {
|
||||
Ok(valid) => HttpResponse::Ok().json(valid),
|
||||
Err(err) => {
|
||||
error!("Error verifying message: {}", err);
|
||||
HttpResponse::InternalServerError().json("Server failed to process request")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
//! # Ephemera API
|
||||
//!
|
||||
//! This module contains all the types and functions available as part of Ephemera public API.
|
||||
//!
|
||||
//! This API is also available over HTTP.
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use log::{error, trace};
|
||||
use tokio::sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
oneshot,
|
||||
};
|
||||
|
||||
use crate::api::types::{
|
||||
ApiBlock, ApiBlockBroadcastInfo, ApiBroadcastInfo, ApiCertificate, ApiEphemeraConfig,
|
||||
ApiEphemeraMessage, ApiError, ApiVerifyMessageInBlock,
|
||||
};
|
||||
|
||||
pub(crate) mod application;
|
||||
pub(crate) mod http;
|
||||
pub(crate) mod types;
|
||||
|
||||
/// Kademlia DHT key
|
||||
pub(crate) type DhtKey = Vec<u8>;
|
||||
|
||||
/// Kademlia DHT value
|
||||
pub(crate) type DhtValue = Vec<u8>;
|
||||
|
||||
/// Kademlia DHT key/value pair
|
||||
pub(crate) type DhtKV = (DhtKey, DhtValue);
|
||||
|
||||
pub(crate) type Result<T> = std::result::Result<T, ApiError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ToEphemeraApiCmd {
|
||||
SubmitEphemeraMessage(Box<ApiEphemeraMessage>, oneshot::Sender<Result<()>>),
|
||||
QueryBlockByHeight(u64, oneshot::Sender<Result<Option<ApiBlock>>>),
|
||||
QueryBlockByHash(String, oneshot::Sender<Result<Option<ApiBlock>>>),
|
||||
QueryLastBlock(oneshot::Sender<Result<ApiBlock>>),
|
||||
QueryBlockCertificates(String, oneshot::Sender<Result<Option<Vec<ApiCertificate>>>>),
|
||||
QueryDht(DhtKey, oneshot::Sender<Result<Option<DhtKV>>>),
|
||||
StoreInDht(DhtKey, DhtValue, oneshot::Sender<Result<()>>),
|
||||
QueryEphemeraConfig(oneshot::Sender<Result<ApiEphemeraConfig>>),
|
||||
QueryBroadcastGroup(oneshot::Sender<Result<ApiBroadcastInfo>>),
|
||||
QueryBlockBroadcastInfo(
|
||||
String,
|
||||
oneshot::Sender<Result<Option<ApiBlockBroadcastInfo>>>,
|
||||
),
|
||||
VerifyMessageInBlock(String, String, usize, oneshot::Sender<Result<bool>>),
|
||||
}
|
||||
|
||||
impl Display for ToEphemeraApiCmd {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ToEphemeraApiCmd::SubmitEphemeraMessage(message, _) => {
|
||||
write!(f, "SubmitEphemeraMessage({message})",)
|
||||
}
|
||||
ToEphemeraApiCmd::QueryBlockByHeight(height, _) => {
|
||||
write!(f, "QueryBlockByHeight({height})",)
|
||||
}
|
||||
ToEphemeraApiCmd::QueryBlockByHash(hash, _) => write!(f, "QueryBlockByHash({hash})",),
|
||||
ToEphemeraApiCmd::QueryLastBlock(_) => write!(f, "QueryLastBlock"),
|
||||
ToEphemeraApiCmd::QueryBlockCertificates(id, _) => {
|
||||
write!(f, "QueryBlockSignatures{id}")
|
||||
}
|
||||
ToEphemeraApiCmd::QueryDht(_, _) => {
|
||||
write!(f, "QueryDht")
|
||||
}
|
||||
ToEphemeraApiCmd::StoreInDht(_, _, _) => {
|
||||
write!(f, "StoreInDht")
|
||||
}
|
||||
ToEphemeraApiCmd::QueryEphemeraConfig(_) => {
|
||||
write!(f, "EphemeraConfig")
|
||||
}
|
||||
ToEphemeraApiCmd::QueryBroadcastGroup(_) => {
|
||||
write!(f, "BroadcastGroup")
|
||||
}
|
||||
ToEphemeraApiCmd::QueryBlockBroadcastInfo(hash, ..) => {
|
||||
write!(f, "BlockBroadcastInfo({hash})")
|
||||
}
|
||||
ToEphemeraApiCmd::VerifyMessageInBlock(block_id, message_id, height, _) => {
|
||||
write!(
|
||||
f,
|
||||
"VerifyMessageInBlock({block_id}, {message_id}, {height})",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ApiListener {
|
||||
pub(crate) messages_rcv: Receiver<ToEphemeraApiCmd>,
|
||||
}
|
||||
|
||||
impl ApiListener {
|
||||
pub(crate) fn new(messages_rcv: Receiver<ToEphemeraApiCmd>) -> Self {
|
||||
Self { messages_rcv }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CommandExecutor {
|
||||
pub(crate) commands_channel: Sender<ToEphemeraApiCmd>,
|
||||
}
|
||||
|
||||
impl CommandExecutor {
|
||||
pub(crate) fn new() -> (CommandExecutor, ApiListener) {
|
||||
let (commands_channel, signed_messages_rcv) = channel(100);
|
||||
let api_listener = ApiListener::new(signed_messages_rcv);
|
||||
let api = CommandExecutor { commands_channel };
|
||||
(api, api_listener)
|
||||
}
|
||||
|
||||
/// Returns block with given id if it exists
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `block_id` - Block id
|
||||
///
|
||||
/// # Returns
|
||||
/// * `ApiBlock` - Block
|
||||
///
|
||||
/// # Errors
|
||||
/// * `ApiError::InternalError` - If there is an internal error
|
||||
pub async fn get_block_by_id(&self, block_id: String) -> Result<Option<ApiBlock>> {
|
||||
trace!("get_block_by_id({:?})", block_id);
|
||||
self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockByHash(block_id, tx))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns block with given height if it exists
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `height` - Block height
|
||||
///
|
||||
/// # Returns
|
||||
/// * `ApiBlock` - Block
|
||||
///
|
||||
/// # Errors
|
||||
/// * `ApiError::InternalError` - If there is an internal error
|
||||
pub async fn get_block_by_height(&self, height: u64) -> Result<Option<ApiBlock>> {
|
||||
trace!("get_block_by_height({:?})", height);
|
||||
self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockByHeight(height, tx))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns last block. Which has maximum height and is stored in database
|
||||
///
|
||||
/// # Returns
|
||||
/// * `ApiBlock` - Last block
|
||||
///
|
||||
/// # Errors
|
||||
/// * `ApiError::InternalError` - If there is an internal error
|
||||
pub async fn get_last_block(&self) -> Result<ApiBlock> {
|
||||
trace!("get_last_block()");
|
||||
self.send_and_wait_response(ToEphemeraApiCmd::QueryLastBlock)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns signatures for given block id
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `block_hash` - Block id
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Vec<ApiCertificate>` - Certificates
|
||||
///
|
||||
/// # Errors
|
||||
/// * `ApiError::InternalError` - If there is an internal error
|
||||
pub async fn get_block_certificates(
|
||||
&self,
|
||||
block_hash: String,
|
||||
) -> Result<Option<Vec<ApiCertificate>>> {
|
||||
trace!("get_block_certificates({block_hash:?})",);
|
||||
self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockCertificates(block_hash, tx))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Queries DHT for given key
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - DHT key
|
||||
///
|
||||
/// # Errors
|
||||
/// * `ApiError::InternalError` - If there is an internal error
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Some((key, value))` - If key is found
|
||||
/// * `None` - If key is not found
|
||||
pub async fn query_dht(&self, key: DhtKey) -> Result<Option<(DhtKey, DhtValue)>> {
|
||||
trace!("get_dht({key:?})");
|
||||
//TODO: this needs timeout(somewhere around dht query functionality)
|
||||
self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryDht(key, tx))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Stores given key-value pair in DHT
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - DHT key
|
||||
/// * `value` - DHT value
|
||||
///
|
||||
/// # Errors
|
||||
/// * `ApiError::InternalError` - If there is an internal error
|
||||
pub async fn store_in_dht(&self, key: DhtKey, value: DhtValue) -> Result<()> {
|
||||
trace!("store_in_dht({key:?}, {value:?})");
|
||||
self.send_and_wait_response(|tx| ToEphemeraApiCmd::StoreInDht(key, value, tx))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns node configuration
|
||||
///
|
||||
/// # Returns
|
||||
/// * `ApiEphemeraConfig` - Node configuration
|
||||
///
|
||||
/// # Errors
|
||||
/// * `ApiError::InternalError` - If there is an internal error
|
||||
pub async fn get_node_config(&self) -> Result<ApiEphemeraConfig> {
|
||||
trace!("get_node_config()");
|
||||
self.send_and_wait_response(ToEphemeraApiCmd::QueryEphemeraConfig)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns broadcast group
|
||||
///
|
||||
/// # Errors
|
||||
/// * `ApiError::InternalError` - If there is an internal error
|
||||
///
|
||||
/// # Return
|
||||
/// * `ApiBroadcastInfo` - Broadcast group
|
||||
pub async fn get_broadcast_info(&self) -> Result<ApiBroadcastInfo> {
|
||||
trace!("get_broadcast_group()");
|
||||
self.send_and_wait_response(ToEphemeraApiCmd::QueryBroadcastGroup)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns block broadcast info.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `block_hash` - Block hash
|
||||
///
|
||||
/// # Return
|
||||
/// * `ApiBlockBroadcastInfo` - Block broadcast info
|
||||
///
|
||||
/// # Errors
|
||||
/// * `ApiError::InternalError` - If there is an internal error
|
||||
pub async fn get_block_broadcast_info(
|
||||
&self,
|
||||
block_hash: String,
|
||||
) -> Result<Option<ApiBlockBroadcastInfo>> {
|
||||
trace!("get_broadcast_group()");
|
||||
self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockBroadcastInfo(block_hash, tx))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a message to Ephemera which should then be included in mempool and broadcast to all peers
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `message` - Message to be sent
|
||||
///
|
||||
/// # Errors
|
||||
/// * `ApiError::InternalError` - If there is an internal error
|
||||
pub async fn send_ephemera_message(&self, message: ApiEphemeraMessage) -> Result<()> {
|
||||
trace!("send_ephemera_message({message})",);
|
||||
self.send_and_wait_response(|tx| {
|
||||
ToEphemeraApiCmd::SubmitEphemeraMessage(message.into(), tx)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Verifies if given message is in block identified by block hash
|
||||
/// Returns true if message is in block, false otherwise. False can also mean that block or message
|
||||
/// does not exist.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request` - Message and block hash
|
||||
///
|
||||
/// # Errors
|
||||
/// * `ApiError::InternalError` - If there is an internal error
|
||||
pub async fn verify_message_in_block(&self, request: ApiVerifyMessageInBlock) -> Result<bool> {
|
||||
trace!("verify_message_in_block({request})",);
|
||||
let block_hash = request.block_hash;
|
||||
let message_hash = request.message_hash;
|
||||
let index = request.message_index;
|
||||
self.send_and_wait_response(|tx| {
|
||||
ToEphemeraApiCmd::VerifyMessageInBlock(block_hash, message_hash, index, tx)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_and_wait_response<F, R>(&self, f: F) -> Result<R>
|
||||
where
|
||||
F: FnOnce(oneshot::Sender<Result<R>>) -> ToEphemeraApiCmd,
|
||||
R: Send + 'static,
|
||||
{
|
||||
let (tx, rcv) = oneshot::channel();
|
||||
let cmd = f(tx);
|
||||
if let Err(e) = self.commands_channel.send(cmd).await {
|
||||
error!("Failed to send command to Ephemera: {e:?}",);
|
||||
return Err(ApiError::Internal(
|
||||
"Failed to receive response from Ephemera".to_string(),
|
||||
));
|
||||
}
|
||||
rcv.await.map_err(|e| {
|
||||
error!("Failed to receive response from Ephemera: {e:?}",);
|
||||
ApiError::Internal("Failed to receive response from Ephemera".to_string())
|
||||
})?
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
//! This module contains all the types that are used in the API.
|
||||
//!
|
||||
//! - `ApiEphemeraMessage`
|
||||
//! - `RawApiEphemeraMessage`
|
||||
//! - `ApiBlock`
|
||||
//! - `ApiCertificate`
|
||||
//! - `Health`
|
||||
//! - `ApiError`
|
||||
//! - `ApiEphemeraConfig`
|
||||
//! - `ApiDhtQueryRequest`
|
||||
//! - `ApiDhtQueryResponse`
|
||||
//! - `ApiDhtStoreRequest`
|
||||
//! - `ApiBroadcastInfo`
|
||||
//! - `ApiBlockBroadcastInfo`
|
||||
//! - `ApiVerifyMessageInBlock`
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
|
||||
use array_bytes::{bytes2hex, hex2bytes};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::peer::{PeerId, ToPeerId};
|
||||
use crate::utilities::codec::{Codec, DecodingError, EncodingError, EphemeraCodec};
|
||||
use crate::{
|
||||
block::types::{block::Block, block::BlockHeader, message::EphemeraMessage},
|
||||
codec::{Decode, Encode},
|
||||
crypto::{Keypair, PublicKey},
|
||||
ephemera_api,
|
||||
utilities::{
|
||||
crypto::{Certificate, Signature},
|
||||
time::EphemeraTime,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ApiError {
|
||||
#[error("Application rejected ephemera message")]
|
||||
ApplicationRejectedMessage,
|
||||
#[error("Duplicate message")]
|
||||
DuplicateMessage,
|
||||
#[error("Invalid hash: {0}")]
|
||||
InvalidHash(String),
|
||||
#[error("ApplicationError: {0}")]
|
||||
Application(#[from] ephemera_api::ApplicationError),
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
/// # Ephemera message.
|
||||
///
|
||||
/// A message submitted to an Ephemera node will be gossiped to other nodes.
|
||||
/// And will be eventually included in a Ephemera block.
|
||||
///
|
||||
/// It needs to signed by the sender. The signature is included in the certificate.
|
||||
///
|
||||
/// The fields of the message what are signed:
|
||||
/// - timestamp
|
||||
/// - label
|
||||
/// - data
|
||||
///
|
||||
/// Currently it's up provided [`ephemera_api::application::Application`] to verify the signature.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiEphemeraMessage {
|
||||
/// The timestamp of the message.
|
||||
pub timestamp: u64,
|
||||
/// The label of the message. It can be used to identify the type of a message for example.
|
||||
pub label: String,
|
||||
/// The data of the message. It is application specific.
|
||||
pub data: Vec<u8>,
|
||||
/// The certificate of the message. All messages are required to be signed.
|
||||
pub certificate: ApiCertificate,
|
||||
}
|
||||
|
||||
impl ApiEphemeraMessage {
|
||||
#[must_use]
|
||||
pub fn new(raw_message: RawApiEphemeraMessage, certificate: ApiCertificate) -> Self {
|
||||
Self {
|
||||
timestamp: raw_message.timestamp,
|
||||
label: raw_message.label,
|
||||
data: raw_message.data,
|
||||
certificate,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the message hash.
|
||||
///
|
||||
/// # Errors
|
||||
/// - If internal hash function fails.
|
||||
pub fn hash(&self) -> anyhow::Result<String> {
|
||||
let em = EphemeraMessage::from(self.clone());
|
||||
let hash = em.hash_with_default_hasher()?.to_string();
|
||||
Ok(hash)
|
||||
}
|
||||
}
|
||||
|
||||
/// `RawApiEphemeraMessage` contains the fields of the `ApiEphemeraMessage` that are signed.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct RawApiEphemeraMessage {
|
||||
/// The timestamp of the message. It's initialized when the message is created.
|
||||
/// It uses UTC time.
|
||||
pub timestamp: u64,
|
||||
/// The label of the message. It can be used to identify the type of a message without decoding full data.
|
||||
pub label: String,
|
||||
/// The data of the message. It is application specific.
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl RawApiEphemeraMessage {
|
||||
#[must_use]
|
||||
pub fn new(label: String, data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
timestamp: EphemeraTime::now(),
|
||||
label,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
/// Signs the message with the given keypair.
|
||||
///
|
||||
/// # Signing example
|
||||
///
|
||||
/// ```
|
||||
/// use ephemera::codec::Encode;
|
||||
/// use ephemera::crypto::{EphemeraKeypair, EphemeraPublicKey, Keypair};
|
||||
/// use ephemera::ephemera_api::{ApiEphemeraMessage, RawApiEphemeraMessage};
|
||||
///
|
||||
/// let keypair = Keypair::generate(None);
|
||||
/// let raw_message = RawApiEphemeraMessage::new("test".to_string(), vec![]);
|
||||
///
|
||||
/// let signed_message:ApiEphemeraMessage = raw_message.sign(&keypair).unwrap();
|
||||
///
|
||||
/// assert_eq!(signed_message.certificate.public_key, keypair.public_key().into());
|
||||
///
|
||||
/// let bytes = raw_message.encode().unwrap();
|
||||
/// assert!(keypair.public_key().verify(&bytes, &signed_message.certificate.signature.into()));
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// - If the message can't be encoded.
|
||||
/// - If the message can't be signed.
|
||||
pub fn sign(&self, keypair: &Keypair) -> anyhow::Result<ApiEphemeraMessage> {
|
||||
let certificate = Certificate::prepare(keypair, &self)?;
|
||||
let message = ApiEphemeraMessage::new(self.clone(), certificate.into());
|
||||
Ok(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ApiBlockHeader {
|
||||
/// The timestamp of the block. It's initialized when the block is created.
|
||||
/// It uses UTC time.
|
||||
pub timestamp: u64,
|
||||
/// The PeerId of the block producer instance.
|
||||
pub creator: PeerId,
|
||||
/// The height of the block.
|
||||
pub height: u64,
|
||||
/// The hash of the current block.
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiBlock {
|
||||
pub header: ApiBlockHeader,
|
||||
pub messages: Vec<ApiEphemeraMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ApiRawBlock {
|
||||
pub(crate) header: ApiBlockHeader,
|
||||
pub(crate) messages: Vec<ApiEphemeraMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiCertificate {
|
||||
pub signature: ApiSignature,
|
||||
pub public_key: ApiPublicKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiSignature(pub(crate) Signature);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiPublicKey(pub(crate) PublicKey);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiEphemeraConfig {
|
||||
/// The address of the node. It's the address what Ephemera instance uses to communicate with other nodes.
|
||||
pub protocol_address: String,
|
||||
/// The HTTP API address of the node.
|
||||
pub api_address: String,
|
||||
/// The WebSocket address of the node.
|
||||
pub websocket_address: String,
|
||||
/// Node's public key.
|
||||
///
|
||||
/// # Converting to string and back example
|
||||
/// ```
|
||||
/// use ephemera::crypto::{EphemeraKeypair, Keypair, PublicKey};
|
||||
///
|
||||
/// let keypair = Keypair::generate(None);
|
||||
/// let public_key = keypair.public_key().to_string();
|
||||
///
|
||||
/// let from_str = public_key.parse::<PublicKey>().unwrap();
|
||||
///
|
||||
/// assert_eq!(keypair.public_key(), from_str);
|
||||
/// ```
|
||||
pub public_key: String,
|
||||
/// True if the node is a block producer. It's a configuration option.
|
||||
pub block_producer: bool,
|
||||
/// The interval of block creation in seconds. It's a configuration option.
|
||||
pub block_creation_interval_sec: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiDhtQueryRequest {
|
||||
/// The key to query for in hex format.
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiDhtQueryResponse {
|
||||
/// The key that was queried for in hex format.
|
||||
key: String,
|
||||
/// The value that was stored under the queried key in hex format.
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub enum HealthStatus {
|
||||
Healthy,
|
||||
Unhealthy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiHealth {
|
||||
pub(crate) status: HealthStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiDhtStoreRequest {
|
||||
/// The key to store the value under in hex format.
|
||||
key: String,
|
||||
/// The value to store in hex format.
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiBroadcastInfo {
|
||||
/// The PeerId of the local node.
|
||||
pub local_peer_id: PeerId,
|
||||
/// The list of the current members of the network.
|
||||
pub current_members: HashSet<PeerId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiBlockBroadcastInfo {
|
||||
pub local_peer_id: PeerId,
|
||||
pub broadcast_group: Vec<PeerId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ApiVerifyMessageInBlock {
|
||||
pub block_hash: String,
|
||||
pub message_hash: String,
|
||||
pub message_index: usize,
|
||||
}
|
||||
|
||||
impl ApiVerifyMessageInBlock {
|
||||
#[must_use]
|
||||
pub fn new(block_hash: String, message_hash: String, message_index: usize) -> Self {
|
||||
Self {
|
||||
block_hash,
|
||||
message_hash,
|
||||
message_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ApiVerifyMessageInBlock {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{{ block_hash: {}, message_hash: {} }}",
|
||||
self.block_hash, self.message_hash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiBlockBroadcastInfo {
|
||||
pub(crate) fn new(local_peer_id: PeerId, broadcast_group: Vec<PeerId>) -> Self {
|
||||
Self {
|
||||
local_peer_id,
|
||||
broadcast_group,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiBroadcastInfo {
|
||||
pub(crate) fn new(current_members: HashSet<PeerId>, local_peer_id: PeerId) -> Self {
|
||||
Self {
|
||||
local_peer_id,
|
||||
current_members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ApiBroadcastInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let current_members = self.current_members.iter().map(ToString::to_string);
|
||||
write!(
|
||||
f,
|
||||
"{{ local_peer_id: {}, current_members: {current_members:?} }}",
|
||||
self.local_peer_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ApiEphemeraMessage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"ApiEphemeraMessage(timestamp: {}, label: {})",
|
||||
self.timestamp, self.label,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiEphemeraMessage> for RawApiEphemeraMessage {
|
||||
fn from(message: ApiEphemeraMessage) -> Self {
|
||||
RawApiEphemeraMessage {
|
||||
timestamp: message.timestamp,
|
||||
label: message.label,
|
||||
data: message.data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiEphemeraMessage> for EphemeraMessage {
|
||||
fn from(message: ApiEphemeraMessage) -> Self {
|
||||
Self {
|
||||
timestamp: message.timestamp,
|
||||
label: message.label,
|
||||
data: message.data,
|
||||
certificate: message.certificate.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for RawApiEphemeraMessage {
|
||||
type Output = Self;
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
|
||||
Codec::decode(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for RawApiEphemeraMessage {
|
||||
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
Codec::encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for &RawApiEphemeraMessage {
|
||||
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
Codec::encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ApiBlockHeader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"ApiBlockHeader(timestamp: {}, creator: {}, height: {}, hash: {})",
|
||||
self.timestamp, self.creator, self.height, self.hash,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiBlock {
|
||||
#[must_use]
|
||||
pub fn as_raw_block(&self) -> ApiRawBlock {
|
||||
ApiRawBlock {
|
||||
header: self.header.clone(),
|
||||
messages: self.messages.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn message_count(&self) -> usize {
|
||||
self.messages.len()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn hash(&self) -> String {
|
||||
self.header.hash.clone()
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// - If the block is invalid.
|
||||
/// - If the block's certificate is invalid.
|
||||
/// - If the block's certificate is not signed by the block's creator.
|
||||
pub fn verify(&self, certificate: &ApiCertificate) -> Result<bool, ApiError> {
|
||||
let block: Block = self.clone().try_into()?;
|
||||
let valid = block.verify(&(certificate.clone()).into()).map_err(|e| {
|
||||
error!("Failed to verify block: {}", e);
|
||||
ApiError::Internal("Failed to verify block certificate".to_string())
|
||||
})?;
|
||||
Ok(valid)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ApiBlock {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"ApiBlock(header: {}, message_count: {})",
|
||||
self.header,
|
||||
self.message_count()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiRawBlock {
|
||||
pub fn new(header: ApiBlockHeader, messages: Vec<ApiEphemeraMessage>) -> Self {
|
||||
Self { header, messages }
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiCertificate {
|
||||
/// # Errors
|
||||
/// - `EncodingError` if the message cannot be encoded.
|
||||
/// - `KeyPairError` if the message cannot be signed.
|
||||
pub fn prepare<D: Encode>(key_pair: &Keypair, data: &D) -> anyhow::Result<Self> {
|
||||
Certificate::prepare(key_pair, data).map(Into::into)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// -`EncodingError` if the message cannot be encoded.
|
||||
pub fn verify<D: Encode>(&self, data: &D) -> anyhow::Result<bool> {
|
||||
let certificate: Certificate = (self.clone()).into();
|
||||
Certificate::verify(&certificate, data)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EphemeraMessage> for ApiEphemeraMessage {
|
||||
fn from(ephemera_message: EphemeraMessage) -> Self {
|
||||
Self {
|
||||
timestamp: ephemera_message.timestamp,
|
||||
label: ephemera_message.label,
|
||||
data: ephemera_message.data,
|
||||
certificate: ApiCertificate {
|
||||
signature: ephemera_message.certificate.signature.into(),
|
||||
public_key: ephemera_message.certificate.public_key.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Certificate> for ApiCertificate {
|
||||
fn from(signature: Certificate) -> Self {
|
||||
Self {
|
||||
signature: signature.signature.into(),
|
||||
public_key: signature.public_key.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiCertificate> for Certificate {
|
||||
fn from(value: ApiCertificate) -> Self {
|
||||
Certificate {
|
||||
signature: value.signature.into(),
|
||||
public_key: value.public_key.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Block> for &ApiBlock {
|
||||
fn from(block: &Block) -> Self {
|
||||
let api_block: ApiBlock = block.clone().into();
|
||||
Box::leak(Box::new(api_block))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signature> for ApiSignature {
|
||||
fn from(signature: Signature) -> Self {
|
||||
Self(signature)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiSignature> for Signature {
|
||||
fn from(signature: ApiSignature) -> Self {
|
||||
signature.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiPublicKey {
|
||||
pub fn peer_id(&self) -> String {
|
||||
self.0.peer_id().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicKey> for ApiPublicKey {
|
||||
fn from(public_key: PublicKey) -> Self {
|
||||
Self(public_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiPublicKey> for PublicKey {
|
||||
fn from(public_key: ApiPublicKey) -> Self {
|
||||
public_key.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Block> for ApiBlock {
|
||||
fn from(block: Block) -> Self {
|
||||
Self {
|
||||
header: ApiBlockHeader {
|
||||
timestamp: block.header.timestamp,
|
||||
creator: block.header.creator,
|
||||
height: block.header.height,
|
||||
hash: block.header.hash.to_string(),
|
||||
},
|
||||
messages: block.messages.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ApiBlock> for Block {
|
||||
type Error = ApiError;
|
||||
|
||||
fn try_from(api_block: ApiBlock) -> Result<Self, ApiError> {
|
||||
let messages: Vec<EphemeraMessage> = api_block
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<EphemeraMessage>>();
|
||||
Ok(Self {
|
||||
header: BlockHeader {
|
||||
timestamp: api_block.header.timestamp,
|
||||
creator: api_block.header.creator,
|
||||
height: api_block.header.height,
|
||||
hash: api_block.header.hash.parse().map_err(|e| {
|
||||
error!("Failed to parse block hash: {}", e);
|
||||
ApiError::Internal("Failed to parse block hash".to_string())
|
||||
})?,
|
||||
},
|
||||
messages,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiDhtStoreRequest {
|
||||
#[must_use]
|
||||
pub fn new(key: &[u8], value: &[u8]) -> Self {
|
||||
let key = bytes2hex("0x", key);
|
||||
let value = bytes2hex("0x", value);
|
||||
Self { key, value }
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
#[must_use]
|
||||
pub fn key(&self) -> Vec<u8> {
|
||||
//We can unwrap here because the key is always valid.
|
||||
hex2bytes(&self.key).unwrap()
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
#[must_use]
|
||||
pub fn value(&self) -> Vec<u8> {
|
||||
//We can unwrap here because the value is always valid.
|
||||
hex2bytes(&self.value).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiDhtQueryRequest {
|
||||
#[must_use]
|
||||
pub fn new(key: &[u8]) -> Self {
|
||||
let key = bytes2hex("0x", key);
|
||||
Self { key }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn key_encoded(&self) -> String {
|
||||
self.key.clone()
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
#[must_use]
|
||||
pub fn key(&self) -> Vec<u8> {
|
||||
//We can unwrap here because the value is always valid.
|
||||
hex2bytes(&self.key).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_key(key: &str) -> Vec<u8> {
|
||||
hex2bytes(key).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiDhtQueryResponse {
|
||||
pub(crate) fn new(key: Vec<u8>, value: Vec<u8>) -> Self {
|
||||
let key = bytes2hex("0x", key);
|
||||
let value = bytes2hex("0x", value);
|
||||
Self { key, value }
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
#[must_use]
|
||||
pub fn key(&self) -> Vec<u8> {
|
||||
//We can unwrap here because the key is always valid.
|
||||
hex2bytes(&self.key).unwrap()
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
#[must_use]
|
||||
pub fn value(&self) -> Vec<u8> {
|
||||
//We can unwrap here because the value is always valid.
|
||||
hex2bytes(&self.value).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::crypto::EphemeraKeypair;
|
||||
use crate::crypto::Keypair;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_message_sign_ok() {
|
||||
let message_signing_keypair = Keypair::generate(None);
|
||||
|
||||
let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]);
|
||||
let signed_message = message
|
||||
.sign(&message_signing_keypair)
|
||||
.expect("Failed to sign message");
|
||||
|
||||
let certificate = signed_message.certificate;
|
||||
|
||||
assert!(certificate.verify(&message).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_sign_fail() {
|
||||
let message_signing_keypair = Keypair::generate(None);
|
||||
|
||||
let message = RawApiEphemeraMessage::new("test1".to_string(), vec![1, 2, 3]);
|
||||
let signed_message = message
|
||||
.sign(&message_signing_keypair)
|
||||
.expect("Failed to sign message");
|
||||
|
||||
let certificate = signed_message.certificate;
|
||||
|
||||
let modified_message = RawApiEphemeraMessage::new("test2".to_string(), vec![1, 2, 3]);
|
||||
assert!(!certificate.verify(&modified_message).unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use std::collections::HashSet;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::block::manager::State;
|
||||
use crate::peer::ToPeerId;
|
||||
use crate::{
|
||||
block::{
|
||||
manager::{BlockChainState, BlockManager},
|
||||
message_pool::MessagePool,
|
||||
producer::BlockProducer,
|
||||
types::block::Block,
|
||||
},
|
||||
broadcast::signing::BlockSigner,
|
||||
config::BlockManagerConfiguration,
|
||||
crypto::Keypair,
|
||||
storage::EphemeraDatabase,
|
||||
};
|
||||
|
||||
pub(crate) struct BlockManagerBuilder {
|
||||
config: BlockManagerConfiguration,
|
||||
block_producer: BlockProducer,
|
||||
keypair: Arc<Keypair>,
|
||||
}
|
||||
|
||||
impl BlockManagerBuilder {
|
||||
pub(crate) fn new(config: BlockManagerConfiguration, keypair: Arc<Keypair>) -> Self {
|
||||
let block_producer = BlockProducer::new(keypair.peer_id());
|
||||
Self {
|
||||
config,
|
||||
block_producer,
|
||||
keypair,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build<D: EphemeraDatabase + ?Sized>(
|
||||
self,
|
||||
storage: &mut D,
|
||||
) -> anyhow::Result<BlockManager> {
|
||||
let mut most_recent_block = storage.get_last_block()?;
|
||||
if most_recent_block.is_none() {
|
||||
//Although Ephemera is not a blockchain(chain of historically dependent blocks),
|
||||
//it's helpful to have some sort of notion of progress in time. So we use the concept of height.
|
||||
//The genesis block helps to define the start of it.
|
||||
|
||||
info!("No last block found in database. Creating genesis block.");
|
||||
|
||||
let genesis_block = Block::new_genesis_block(self.block_producer.peer_id);
|
||||
storage.store_block(&genesis_block, HashSet::new(), HashSet::new())?;
|
||||
most_recent_block = Some(genesis_block);
|
||||
}
|
||||
|
||||
let last_created_block = most_recent_block.expect("Block should be present");
|
||||
debug!("Most recent block: {:?}", last_created_block);
|
||||
|
||||
let block_signer = BlockSigner::new(self.keypair.clone());
|
||||
let message_pool = MessagePool::new();
|
||||
let block_chain_state = BlockChainState::new(last_created_block);
|
||||
let block_creation_interval =
|
||||
tokio::time::interval(Duration::from_secs(self.config.creation_interval_sec));
|
||||
|
||||
Ok(BlockManager {
|
||||
config: self.config,
|
||||
block_producer: self.block_producer,
|
||||
block_signer,
|
||||
message_pool,
|
||||
block_chain_state,
|
||||
state: State::Paused,
|
||||
backoff: None,
|
||||
block_creation_interval,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,688 @@
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::task::Poll;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
num::NonZeroUsize,
|
||||
pin::Pin,
|
||||
task,
|
||||
task::Poll::{Pending, Ready},
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use futures::Stream;
|
||||
use futures_util::FutureExt;
|
||||
use log::{debug, error, info, trace};
|
||||
use lru::LruCache;
|
||||
use thiserror::Error;
|
||||
use tokio::time;
|
||||
use tokio::time::{Instant, Interval};
|
||||
|
||||
use crate::network::PeerId;
|
||||
use crate::peer::ToPeerId;
|
||||
use crate::{
|
||||
api::application::RemoveMessages,
|
||||
block::{
|
||||
message_pool::MessagePool,
|
||||
producer::BlockProducer,
|
||||
types::{block::Block, message::EphemeraMessage},
|
||||
},
|
||||
broadcast::signing::BlockSigner,
|
||||
config::BlockManagerConfiguration,
|
||||
utilities::{crypto::Certificate, hash::Hash},
|
||||
};
|
||||
|
||||
pub(crate) type Result<T> = std::result::Result<T, BlockManagerError>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum BlockManagerError {
|
||||
#[error("Message is already in pool: {0}")]
|
||||
DuplicateMessage(String),
|
||||
//Just a placeholder for now
|
||||
#[error("BlockManagerError: {0}")]
|
||||
BlockManager(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// It helps to use atomic state management for new blocks.
|
||||
pub(crate) struct BlockChainState {
|
||||
pub(crate) last_blocks: LruCache<Hash, Block>,
|
||||
/// Last block that we created.
|
||||
/// It's not Option because we always have genesis block
|
||||
last_produced_block: Option<Block>,
|
||||
/// Last block that we accepted
|
||||
/// It's not Option because we always have genesis block
|
||||
last_committed_block: Block,
|
||||
}
|
||||
|
||||
impl BlockChainState {
|
||||
pub(crate) fn new(last_committed_block: Block) -> Self {
|
||||
Self {
|
||||
//1000 is just a "big enough".
|
||||
last_blocks: LruCache::new(NonZeroUsize::new(1000).unwrap()),
|
||||
last_produced_block: None,
|
||||
last_committed_block,
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_last_produced_block_as_committed(&mut self) {
|
||||
self.last_committed_block = self
|
||||
.last_produced_block
|
||||
.take()
|
||||
.expect("Block should be present");
|
||||
}
|
||||
|
||||
fn is_last_produced_block(&self, hash: Hash) -> bool {
|
||||
match self.last_produced_block.as_ref() {
|
||||
Some(block) => block.get_hash() == hash,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_last_produced_block_is_pending(&self) -> bool {
|
||||
self.last_produced_block.is_some()
|
||||
}
|
||||
|
||||
fn next_block_height(&self) -> u64 {
|
||||
self.last_committed_block.get_height() + 1
|
||||
}
|
||||
|
||||
fn remove_last_produced_block(&mut self) -> Block {
|
||||
self.last_produced_block
|
||||
.take()
|
||||
.expect("Block should be present")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum State {
|
||||
Paused,
|
||||
Running,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BackOffInterval {
|
||||
/// Maximum number of attempts before this backoff expires.
|
||||
maximum_times: u32,
|
||||
/// Number of attempts that have been made so far.
|
||||
nr_of_attempts: u32,
|
||||
/// Backoff rate. Previous delay is multiplied by this rate to get next delay.
|
||||
backoff_rate: u32,
|
||||
/// Delay between before next attempt.
|
||||
delay: Interval,
|
||||
}
|
||||
|
||||
impl BackOffInterval {
|
||||
fn new(maximum_times: u32, backoff_rate: u32, initial_wait: Duration) -> Self {
|
||||
let delay = time::interval_at(Instant::now() + initial_wait, initial_wait);
|
||||
Self {
|
||||
maximum_times,
|
||||
nr_of_attempts: 0,
|
||||
backoff_rate,
|
||||
delay,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_expired(&self) -> bool {
|
||||
self.nr_of_attempts >= self.maximum_times
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for BackOffInterval {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Self::Output> {
|
||||
if self.nr_of_attempts >= self.maximum_times {
|
||||
debug!("Backoff expired after {} attempts", self.nr_of_attempts);
|
||||
return Pending;
|
||||
}
|
||||
|
||||
match Pin::new(&mut self.delay).poll_tick(cx) {
|
||||
Ready(_) => {
|
||||
self.nr_of_attempts += 1;
|
||||
let next_tick = Instant::now()
|
||||
+ self.delay.period() * self.backoff_rate.pow(self.nr_of_attempts);
|
||||
debug!("Backoff attempt: {}", self.nr_of_attempts);
|
||||
self.delay = time::interval_at(next_tick, self.delay.period());
|
||||
Ready(())
|
||||
}
|
||||
Pending => Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct BlockManager {
|
||||
pub(crate) config: BlockManagerConfiguration,
|
||||
/// Block producer. Simple helper that creates blocks
|
||||
pub(crate) block_producer: BlockProducer,
|
||||
/// Message pool. Contains all messages that we received from the network and not included in any(committed) block yet.
|
||||
pub(crate) message_pool: MessagePool,
|
||||
/// Delay between block creation attempts.
|
||||
pub(crate) block_creation_interval: Interval,
|
||||
/// Backoff between block creation attempts. When `last_produced_block` is not committed during
|
||||
/// certain time window, and normal delay is not passed yet, we use backoff delay to try again.
|
||||
pub(crate) backoff: Option<BackOffInterval>,
|
||||
/// Signs and verifies blocks
|
||||
pub(crate) block_signer: BlockSigner,
|
||||
/// State management for new blocks
|
||||
pub(crate) block_chain_state: BlockChainState,
|
||||
/// Current state of the block manager
|
||||
pub(crate) state: State,
|
||||
}
|
||||
|
||||
impl BlockManager {
|
||||
pub(crate) fn on_new_message(&mut self, msg: EphemeraMessage) -> Result<()> {
|
||||
trace!("Message received: {:?}", msg);
|
||||
|
||||
let message_hash = msg.hash_with_default_hasher()?;
|
||||
if self.message_pool.contains(&message_hash) {
|
||||
return Err(BlockManagerError::DuplicateMessage(
|
||||
message_hash.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
self.message_pool.add_message(msg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn on_block(
|
||||
&mut self,
|
||||
sender: &PeerId,
|
||||
block: &Block,
|
||||
certificate: &Certificate,
|
||||
) -> Result<()> {
|
||||
let hash = block.hash_with_default_hasher()?;
|
||||
|
||||
trace!(
|
||||
"Received block: {:?} from peer {sender:?}",
|
||||
block.get_hash()
|
||||
);
|
||||
|
||||
//Reject blocks with invalid hash
|
||||
if block.header.hash != hash {
|
||||
return Err(anyhow!("Block hash is invalid: {} != {hash}", block.header.hash).into());
|
||||
}
|
||||
|
||||
//Block signer should be also its sender
|
||||
let signer_peer_id = certificate.public_key.peer_id();
|
||||
if *sender != signer_peer_id {
|
||||
return Err(anyhow!(
|
||||
"Block signer is not the block sender: {sender:?} != {signer_peer_id:?}",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
//Verify that block signature is valid
|
||||
if self.block_signer.verify_block(block, certificate).is_err() {
|
||||
return Err(anyhow!("Block signature is invalid: {hash}").into());
|
||||
}
|
||||
|
||||
self.block_chain_state.last_blocks.put(hash, block.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn sign_block(&mut self, block: &Block) -> Result<Certificate> {
|
||||
let hash = block.hash_with_default_hasher()?;
|
||||
|
||||
trace!("Signing block: {block}");
|
||||
|
||||
let certificate = self.block_signer.sign_block(block, &hash)?;
|
||||
|
||||
trace!("Block certificate: {certificate:?}",);
|
||||
|
||||
Ok(certificate)
|
||||
}
|
||||
|
||||
pub(crate) fn on_application_rejected_block(
|
||||
&mut self,
|
||||
messages_to_remove: RemoveMessages,
|
||||
) -> Result<()> {
|
||||
debug!("Application rejected last created block");
|
||||
|
||||
let last_produced_block = self.block_chain_state.remove_last_produced_block();
|
||||
match messages_to_remove {
|
||||
RemoveMessages::All => {
|
||||
let messages = last_produced_block
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
debug!("Removing block messages from pool: all: {messages:?}",);
|
||||
self.message_pool.remove_messages(&messages)?;
|
||||
}
|
||||
RemoveMessages::Selected(messages) => {
|
||||
debug!("Removing block messages from pool: selected: {messages:?}",);
|
||||
let messages = messages.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
self.message_pool.remove_messages(messages.as_slice())?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// After a block gets committed, clear up mempool from its messages
|
||||
pub(crate) fn on_block_committed(&mut self, block: &Block) -> Result<()> {
|
||||
info!("Block committed: {}", block);
|
||||
|
||||
let hash = &block.header.hash;
|
||||
|
||||
if !self.block_chain_state.is_last_produced_block(*hash) {
|
||||
let last_produced_block = self
|
||||
.block_chain_state
|
||||
.last_produced_block
|
||||
.as_ref()
|
||||
.expect("Last produced block should be present");
|
||||
log::error!(
|
||||
"Received unexpected committed block: {hash}, was expecting: {}",
|
||||
last_produced_block.get_hash()
|
||||
);
|
||||
panic!("Received committed block which isn't last produced block, this is a bug!");
|
||||
}
|
||||
|
||||
match self.message_pool.remove_messages(&block.messages) {
|
||||
Ok(_) => {
|
||||
self.block_chain_state
|
||||
.mark_last_produced_block_as_committed();
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(anyhow!("Failed to remove messages from mempool: {}", e).into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_block_by_hash(&mut self, block_id: &Hash) -> Option<Block> {
|
||||
self.block_chain_state.last_blocks.get(block_id).cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn get_block_certificates(&mut self, hash: &Hash) -> Option<&HashSet<Certificate>> {
|
||||
self.block_signer.get_block_certificates(hash)
|
||||
}
|
||||
|
||||
pub(crate) fn stop(&mut self) {
|
||||
debug!("Stopping block creation");
|
||||
self.state = State::Paused;
|
||||
self.backoff = None;
|
||||
}
|
||||
|
||||
pub(crate) fn start(&mut self) {
|
||||
if !self.config.producer {
|
||||
return;
|
||||
}
|
||||
if let State::Running = self.state {
|
||||
return;
|
||||
}
|
||||
debug!("Starting block creation");
|
||||
self.state = State::Running;
|
||||
self.block_creation_interval =
|
||||
tokio::time::interval(Duration::from_secs(self.config.creation_interval_sec));
|
||||
}
|
||||
}
|
||||
|
||||
//Produces blocks at a predefined interval.
|
||||
//If blocks will be actually broadcast depends on the application.
|
||||
impl Stream for BlockManager {
|
||||
type Item = (Block, Certificate);
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context) -> Poll<Option<Self::Item>> {
|
||||
//Optionally it is possible to turn off block production and let the node behave just as voter.
|
||||
//For example for testing purposes.
|
||||
if !self.config.producer {
|
||||
return Pending;
|
||||
}
|
||||
|
||||
//It is dynamically turned off when node is not part of most recent broadcast group.
|
||||
if let State::Paused = self.state {
|
||||
return Pending;
|
||||
}
|
||||
|
||||
let is_previous_pending = self.block_chain_state.is_last_produced_block_is_pending();
|
||||
if !is_previous_pending {
|
||||
self.backoff = None;
|
||||
}
|
||||
|
||||
if self.block_creation_interval.poll_tick(cx).is_pending() {
|
||||
if let Some(mut backoff) = self.backoff.take() {
|
||||
if backoff.is_expired() {
|
||||
return Pending;
|
||||
}
|
||||
if backoff.poll_unpin(cx).is_pending() {
|
||||
self.backoff = Some(backoff);
|
||||
return Pending;
|
||||
}
|
||||
self.backoff = Some(backoff);
|
||||
} else {
|
||||
return Pending;
|
||||
}
|
||||
} else {
|
||||
self.backoff = None;
|
||||
}
|
||||
|
||||
//If backoff is expired and we still don't have previous block committed
|
||||
let repeat_previous = is_previous_pending && self.config.repeat_last_block_messages;
|
||||
|
||||
let pending_messages = if repeat_previous {
|
||||
let block = self
|
||||
.block_chain_state
|
||||
.last_produced_block
|
||||
.clone()
|
||||
.expect("Block should be present");
|
||||
|
||||
//Use only previous block messages but create new block with new timestamp.
|
||||
debug!("Producing block with previous messages");
|
||||
block.messages
|
||||
} else {
|
||||
debug!("Producing block with new messages");
|
||||
self.message_pool.get_messages()
|
||||
};
|
||||
|
||||
let new_height = self.block_chain_state.next_block_height();
|
||||
let created_block = self
|
||||
.block_producer
|
||||
.create_block(new_height, pending_messages);
|
||||
|
||||
if let Ok(block) = created_block {
|
||||
info!("Created block: {}", block);
|
||||
|
||||
let hash = block.get_hash();
|
||||
self.block_chain_state.last_produced_block = Some(block.clone());
|
||||
self.block_chain_state.last_blocks.put(hash, block.clone());
|
||||
|
||||
let certificate = self
|
||||
.block_signer
|
||||
.sign_block(&block, &hash)
|
||||
.expect("Failed to sign block");
|
||||
|
||||
if self.backoff.is_none() {
|
||||
let backoff = BackOffInterval::new(100, 2, Duration::from_secs(10));
|
||||
self.backoff = Some(backoff);
|
||||
}
|
||||
|
||||
Ready(Some((block, certificate)))
|
||||
} else {
|
||||
error!("Error producing block: {:?}", created_block);
|
||||
Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
use crate::crypto::{EphemeraKeypair, Keypair};
|
||||
use crate::ephemera_api::RawApiEphemeraMessage;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_message() {
|
||||
let (mut manager, _) = block_manager_with_defaults();
|
||||
|
||||
let signed_message = message("test");
|
||||
let hash = signed_message.hash_with_default_hasher().unwrap();
|
||||
|
||||
manager.on_new_message(signed_message).unwrap();
|
||||
|
||||
assert!(manager.message_pool.contains(&hash));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_duplicate_message() {
|
||||
let (mut manager, _) = block_manager_with_defaults();
|
||||
|
||||
let signed_message = message("test");
|
||||
|
||||
manager.on_new_message(signed_message.clone()).unwrap();
|
||||
|
||||
assert_matches!(
|
||||
manager.on_new_message(signed_message),
|
||||
Err(BlockManagerError::DuplicateMessage(_))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_accept_valid_block() {
|
||||
let (mut manager, peer_id) = block_manager_with_defaults();
|
||||
|
||||
let block = block();
|
||||
let certificate = manager.sign_block(&block).unwrap();
|
||||
|
||||
let result = manager.on_block(&peer_id, &block, &certificate);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_invalid_sender() {
|
||||
let (mut manager, _) = block_manager_with_defaults();
|
||||
|
||||
let block = block();
|
||||
let certificate = manager.sign_block(&block).unwrap();
|
||||
|
||||
let invalid_peer_id = PeerId::from_public_key(&Keypair::generate(None).public_key());
|
||||
let result = manager.on_block(&invalid_peer_id, &block, &certificate);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_invalid_hash() {
|
||||
let (mut manager, peer_id) = block_manager_with_defaults();
|
||||
|
||||
let mut block = block();
|
||||
let certificate = manager.sign_block(&block).unwrap();
|
||||
|
||||
block.header.hash = Hash::new([0; 32]);
|
||||
let result = manager.on_block(&peer_id, &block, &certificate);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_invalid_signature() {
|
||||
let (mut manager, peer_id) = block_manager_with_defaults();
|
||||
|
||||
let correct_block = block();
|
||||
let fake_block = block();
|
||||
|
||||
let fake_certificate = manager.sign_block(&fake_block).unwrap();
|
||||
let correct_certificate = manager.sign_block(&correct_block).unwrap();
|
||||
|
||||
let result = manager.on_block(&peer_id, &correct_block, &fake_certificate);
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = manager.on_block(&peer_id, &fake_block, &correct_certificate);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_next_block_empty() {
|
||||
let (mut manager, _) = block_manager_with_defaults();
|
||||
|
||||
let (block, _) = manager.next().await.unwrap();
|
||||
assert_eq!(block.header.height, 1);
|
||||
assert!(block.messages.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_next_block_with_message() {
|
||||
let (mut manager, _) = block_manager_with_defaults();
|
||||
|
||||
let signed_message = message("test");
|
||||
manager.on_new_message(signed_message).unwrap();
|
||||
|
||||
match manager.next().await {
|
||||
Some((block, _)) => {
|
||||
assert_eq!(block.header.height, 1);
|
||||
assert_eq!(block.messages.len(), 1);
|
||||
}
|
||||
None => {
|
||||
panic!("No block produced");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_next_block_previous_not_committed_repeat() {
|
||||
let (mut manager, _) = block_manager_with_defaults();
|
||||
|
||||
let signed_message = message("test");
|
||||
manager.on_new_message(signed_message).unwrap();
|
||||
|
||||
let (block1, _) = manager.next().await.unwrap();
|
||||
|
||||
let signed_message = message("test");
|
||||
manager.on_new_message(signed_message).unwrap();
|
||||
|
||||
let (block2, _) = manager.next().await.unwrap();
|
||||
|
||||
assert_eq!(block1.messages.len(), block2.messages.len());
|
||||
assert_eq!(block1.header.height, block2.header.height);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_next_block_previous_not_committed_repeat_false() {
|
||||
let config = BlockManagerConfiguration::new(true, 0, false);
|
||||
let (mut manager, _) = block_manager_with_config(config);
|
||||
|
||||
let signed_message = message("test");
|
||||
manager.on_new_message(signed_message).unwrap();
|
||||
|
||||
let (block1, _) = manager.next().await.unwrap();
|
||||
|
||||
let signed_message = message("test");
|
||||
manager.on_new_message(signed_message).unwrap();
|
||||
|
||||
let (block2, _) = manager.next().await.unwrap();
|
||||
|
||||
assert_eq!(block1.messages.len(), 1);
|
||||
assert_eq!(block2.messages.len(), 2);
|
||||
//We create new block but don't leave gap
|
||||
assert_eq!(block1.header.height, block2.header.height);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_on_committed_with_correct_pending_block() {
|
||||
let (mut manager, _) = block_manager_with_defaults();
|
||||
|
||||
let signed_message = message("test");
|
||||
manager.on_new_message(signed_message).unwrap();
|
||||
|
||||
let (block, _) = manager.next().await.unwrap();
|
||||
|
||||
let result = manager.on_block_committed(&block);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert!(manager.message_pool.get_messages().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[should_panic]
|
||||
async fn test_on_committed_with_invalid_pending_block() {
|
||||
let (mut manager, _) = block_manager_with_defaults();
|
||||
|
||||
let signed_message = message("test");
|
||||
manager.on_new_message(signed_message).unwrap();
|
||||
|
||||
manager.next().await.unwrap();
|
||||
|
||||
//Create invalid block
|
||||
let wrong_block = block();
|
||||
|
||||
//This shouldn't remove messages from the pool
|
||||
manager.on_block_committed(&wrong_block).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn application_rejected_messages_all() {
|
||||
let (mut manager, _) = block_manager_with_defaults();
|
||||
|
||||
//Add messages to pool
|
||||
let signed_message = message("test");
|
||||
manager.on_new_message(signed_message).unwrap();
|
||||
|
||||
let signed_message = message("test");
|
||||
manager.on_new_message(signed_message).unwrap();
|
||||
|
||||
//Produce new block
|
||||
manager.next().await.unwrap();
|
||||
|
||||
//Application Rejects the block with ALL messages
|
||||
manager
|
||||
.on_application_rejected_block(RemoveMessages::All)
|
||||
.unwrap();
|
||||
|
||||
assert!(manager.message_pool.get_messages().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn application_rejected_messages_selected() {
|
||||
let (mut manager, _) = block_manager_with_defaults();
|
||||
|
||||
//Add messages to pool
|
||||
let signed_message1 = message("test");
|
||||
manager.on_new_message(signed_message1.clone()).unwrap();
|
||||
|
||||
let signed_message2 = message("test");
|
||||
manager.on_new_message(signed_message2.clone()).unwrap();
|
||||
|
||||
//Produce new block
|
||||
manager.next().await.unwrap();
|
||||
|
||||
//Application Rejects the block with ALL messages
|
||||
manager
|
||||
.on_application_rejected_block(RemoveMessages::Selected(vec![signed_message2.into()]))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(manager.message_pool.get_messages().len(), 1);
|
||||
let message = manager
|
||||
.message_pool
|
||||
.get_messages()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
assert_eq!(message, signed_message1);
|
||||
}
|
||||
|
||||
fn block_manager_with_defaults() -> (BlockManager, PeerId) {
|
||||
let config = BlockManagerConfiguration::new(true, 0, true);
|
||||
block_manager_with_config(config)
|
||||
}
|
||||
|
||||
fn block_manager_with_config(config: BlockManagerConfiguration) -> (BlockManager, PeerId) {
|
||||
let keypair: Arc<Keypair> = Keypair::generate(None).into();
|
||||
let peer_id = keypair.public_key().peer_id();
|
||||
let genesis_block = Block::new_genesis_block(peer_id);
|
||||
let block_chain_state = BlockChainState::new(genesis_block);
|
||||
(
|
||||
BlockManager {
|
||||
config,
|
||||
block_producer: BlockProducer::new(peer_id),
|
||||
message_pool: MessagePool::new(),
|
||||
block_creation_interval: tokio::time::interval(Duration::from_millis(1)),
|
||||
backoff: None,
|
||||
block_signer: BlockSigner::new(keypair),
|
||||
block_chain_state,
|
||||
state: State::Running,
|
||||
},
|
||||
peer_id,
|
||||
)
|
||||
}
|
||||
|
||||
fn block() -> Block {
|
||||
let keypair: Arc<Keypair> = Keypair::generate(None).into();
|
||||
let peer_id = keypair.public_key().peer_id();
|
||||
let mut producer = BlockProducer::new(peer_id);
|
||||
producer.create_block(1, vec![]).unwrap()
|
||||
}
|
||||
|
||||
fn message(label: &str) -> EphemeraMessage {
|
||||
let message1 = RawApiEphemeraMessage::new(label.into(), vec![1, 2, 3]);
|
||||
let keypair = Keypair::generate(None);
|
||||
let signed_message1 = message1.sign(&keypair).expect("Failed to sign message");
|
||||
signed_message1.into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Message pool for Ephemera messages
|
||||
//!
|
||||
//! It stores pending Ephemera messages which will be added to a future block.
|
||||
//! It doesn't have any other logic than just storing messages.
|
||||
//!
|
||||
//! It's up to the user provided [`crate::ephemera_api::Application::check_tx`] to decide which messages to include.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use log::{trace, warn};
|
||||
|
||||
use crate::block::types::message::EphemeraMessage;
|
||||
use crate::utilities::hash::Hash;
|
||||
|
||||
pub(crate) struct MessagePool {
|
||||
pending_messages: HashMap<Hash, EphemeraMessage>,
|
||||
}
|
||||
|
||||
impl MessagePool {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
pending_messages: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn contains(&self, hash: &Hash) -> bool {
|
||||
self.pending_messages.contains_key(hash)
|
||||
}
|
||||
|
||||
pub(super) fn add_message(&mut self, msg: EphemeraMessage) -> anyhow::Result<()> {
|
||||
trace!("Adding message to pool: {:?}", msg);
|
||||
|
||||
let msg_hash = msg.hash_with_default_hasher()?;
|
||||
|
||||
self.pending_messages.insert(msg_hash, msg);
|
||||
|
||||
trace!("Message pool size: {:?}", self.pending_messages.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn remove_messages(&mut self, messages: &[EphemeraMessage]) -> anyhow::Result<()> {
|
||||
trace!(
|
||||
"Mempool size before removing messages {}",
|
||||
self.pending_messages.len()
|
||||
);
|
||||
for msg in messages {
|
||||
let hash = msg.hash_with_default_hasher()?;
|
||||
if self.pending_messages.remove(&hash).is_none() {
|
||||
warn!("Message not found in pool: {:?}", msg);
|
||||
}
|
||||
}
|
||||
trace!(
|
||||
"Mempool size after removing messages {}",
|
||||
self.pending_messages.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a `Vec` of all `EphemeraMessage`s in the message pool.
|
||||
/// The message pool is not cleared.
|
||||
pub(super) fn get_messages(&self) -> Vec<EphemeraMessage> {
|
||||
self.pending_messages.values().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::block::message_pool::MessagePool;
|
||||
use crate::block::types::message::EphemeraMessage;
|
||||
use crate::crypto::{EphemeraKeypair, Keypair};
|
||||
use crate::ephemera_api::RawApiEphemeraMessage;
|
||||
|
||||
#[test]
|
||||
fn test_add_remove() {
|
||||
let keypair = Keypair::generate(None);
|
||||
|
||||
let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]);
|
||||
let signed_message = message.sign(&keypair).expect("Failed to sign message");
|
||||
let signed_message: EphemeraMessage = signed_message.into();
|
||||
|
||||
let mut pool = MessagePool::new();
|
||||
pool.add_message(signed_message.clone()).unwrap();
|
||||
pool.remove_messages(&[signed_message]).unwrap();
|
||||
|
||||
assert_eq!(pool.get_messages().len(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//! # Block manager
|
||||
//!
|
||||
//! Block manager is quite simple. It keeps pending messages in memory and puts all of them into a block
|
||||
//! at predefined intervals. That's all it does.
|
||||
//!
|
||||
//! If the block actually will be broadcast or not is decided by the application. If not, it will produce next block with
|
||||
//! the same messages plus the new ones.
|
||||
//!
|
||||
//! When application shuts down, pending messages are lost.
|
||||
//!
|
||||
//! When a block gets accepted by reliable broadcast then Block Manager will remove all messages included in the block from the
|
||||
//! pending messages queue.
|
||||
//!
|
||||
//! # Synchronization and duplicate messages in sequence of blocks
|
||||
//!
|
||||
//! When previous block hasn't been accepted yet, then the next block will contain the same messages as the previous one.
|
||||
//! One way to solve this is that an application itself keeps track of duplicate messages and discards them if necessary.
|
||||
//!
|
||||
//! But it seems a reasonable assumption that in general duplicate messages are unwanted. Therefore, Ephemera solves this
|
||||
//! by dropping previous blocks which get Finalised/Committed after a new block has been created.
|
||||
|
||||
pub(crate) mod builder;
|
||||
pub(crate) mod manager;
|
||||
pub(crate) mod message_pool;
|
||||
pub(crate) mod producer;
|
||||
pub(crate) mod types;
|
||||
@@ -0,0 +1,92 @@
|
||||
use crate::block::{
|
||||
types::block::{Block, RawBlock, RawBlockHeader},
|
||||
types::message::EphemeraMessage,
|
||||
};
|
||||
use crate::peer::PeerId;
|
||||
use log::trace;
|
||||
|
||||
pub(crate) struct BlockProducer {
|
||||
pub(crate) peer_id: PeerId,
|
||||
}
|
||||
|
||||
impl BlockProducer {
|
||||
pub(super) fn new(peer_id: PeerId) -> Self {
|
||||
Self { peer_id }
|
||||
}
|
||||
|
||||
pub(super) fn create_block(
|
||||
&mut self,
|
||||
height: u64,
|
||||
pending_messages: Vec<EphemeraMessage>,
|
||||
) -> anyhow::Result<Block> {
|
||||
trace!("Pending messages for new block: {:?}", pending_messages);
|
||||
let block = self.new_block(height, pending_messages)?;
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
fn new_block(&self, height: u64, mut messages: Vec<EphemeraMessage>) -> anyhow::Result<Block> {
|
||||
//Ordering is fundamental for block hash. Simple sort is fine for now.
|
||||
messages.sort();
|
||||
|
||||
let raw_header = RawBlockHeader::new(self.peer_id, height);
|
||||
let raw_block = RawBlock::new(raw_header, messages);
|
||||
|
||||
//Better idea is probably combine header hash with Merkle tree root hash
|
||||
let block_hash = raw_block.hash_with_default_hasher()?;
|
||||
|
||||
let block = Block::new(raw_block, block_hash);
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::crypto::{EphemeraKeypair, Keypair};
|
||||
use crate::ephemera_api::RawApiEphemeraMessage;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_produce_block() {
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
let mut block_producer = BlockProducer::new(peer_id);
|
||||
|
||||
let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]);
|
||||
let signed_message = message
|
||||
.sign(&Keypair::generate(None))
|
||||
.expect("Failed to sign message");
|
||||
let signed_message1: EphemeraMessage = signed_message.into();
|
||||
|
||||
let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]);
|
||||
let signed_message = message
|
||||
.sign(&Keypair::generate(None))
|
||||
.expect("Failed to sign message");
|
||||
let signed_message2: EphemeraMessage = signed_message.into();
|
||||
|
||||
let messages = vec![signed_message1.clone(), signed_message2.clone()];
|
||||
|
||||
let block = block_producer.create_block(1, messages).unwrap();
|
||||
|
||||
assert_eq!(block.header.height, 1);
|
||||
assert_eq!(block.header.creator, peer_id);
|
||||
assert_eq!(block.messages.len(), 2);
|
||||
|
||||
//Nondeterministic because of timestamp
|
||||
match signed_message1.cmp(&signed_message2) {
|
||||
Ordering::Less => {
|
||||
assert_eq!(block.messages[0], signed_message1);
|
||||
assert_eq!(block.messages[1], signed_message2);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
assert_eq!(block.messages[0], signed_message2);
|
||||
assert_eq!(block.messages[1], signed_message1);
|
||||
}
|
||||
|
||||
Ordering::Equal => {
|
||||
panic!("Messages are equal");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::utilities::merkle::MerkleTree;
|
||||
use crate::{
|
||||
block::types::message::EphemeraMessage,
|
||||
codec::{Decode, Encode},
|
||||
crypto::Keypair,
|
||||
peer::PeerId,
|
||||
utilities::{
|
||||
codec::{Codec, DecodingError, EncodingError, EphemeraCodec},
|
||||
crypto::Certificate,
|
||||
hash::{EphemeraHash, EphemeraHasher},
|
||||
hash::{Hash, Hasher},
|
||||
time::EphemeraTime,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) struct BlockHeader {
|
||||
pub(crate) timestamp: u64,
|
||||
pub(crate) creator: PeerId,
|
||||
pub(crate) height: u64,
|
||||
pub(crate) hash: Hash,
|
||||
}
|
||||
|
||||
impl BlockHeader {
|
||||
pub(crate) fn new(raw_header: &RawBlockHeader, hash: Hash) -> Self {
|
||||
Self {
|
||||
timestamp: raw_header.timestamp,
|
||||
creator: raw_header.creator,
|
||||
height: raw_header.height,
|
||||
hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for BlockHeader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let hash = &self.hash;
|
||||
let time = self.timestamp;
|
||||
let creator = &self.creator;
|
||||
let height = self.height;
|
||||
write!(
|
||||
f,
|
||||
"hash: {hash}, timestamp: {time}, creator: {creator}, height: {height}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for BlockHeader {
|
||||
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
Codec::encode(&self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for BlockHeader {
|
||||
type Output = Self;
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
|
||||
Codec::decode(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl EphemeraHash for BlockHeader {
|
||||
fn hash<H: EphemeraHasher>(&self, state: &mut H) -> anyhow::Result<()> {
|
||||
let bytes = Codec::encode(&self)?;
|
||||
state.update(&bytes);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub(crate) struct RawBlockHeader {
|
||||
pub(crate) timestamp: u64,
|
||||
pub(crate) creator: PeerId,
|
||||
pub(crate) height: u64,
|
||||
}
|
||||
|
||||
impl RawBlockHeader {
|
||||
pub(crate) fn new(creator: PeerId, height: u64) -> Self {
|
||||
Self {
|
||||
timestamp: EphemeraTime::now(),
|
||||
creator,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result<Hash> {
|
||||
let mut hasher = Hasher::default();
|
||||
self.hash(&mut hasher)?;
|
||||
let header_hash = hasher.finish().into();
|
||||
Ok(header_hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RawBlockHeader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let creator = &self.creator;
|
||||
let height = self.height;
|
||||
write!(f, "creator: {creator}, height: {height}",)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockHeader> for RawBlockHeader {
|
||||
fn from(block_header: BlockHeader) -> Self {
|
||||
Self {
|
||||
timestamp: block_header.timestamp,
|
||||
creator: block_header.creator,
|
||||
height: block_header.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) struct Block {
|
||||
pub(crate) header: BlockHeader,
|
||||
pub(crate) messages: Vec<EphemeraMessage>,
|
||||
}
|
||||
|
||||
impl Block {
|
||||
pub(crate) fn new(raw_block: RawBlock, block_hash: Hash) -> Self {
|
||||
let header = BlockHeader::new(&raw_block.header, block_hash);
|
||||
Self {
|
||||
header,
|
||||
messages: raw_block.messages,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_hash(&self) -> Hash {
|
||||
self.header.hash
|
||||
}
|
||||
|
||||
pub(crate) fn get_height(&self) -> u64 {
|
||||
self.header.height
|
||||
}
|
||||
|
||||
pub(crate) fn new_genesis_block(creator: PeerId) -> Self {
|
||||
let mut block = Self {
|
||||
header: BlockHeader {
|
||||
timestamp: EphemeraTime::now(),
|
||||
creator,
|
||||
height: 0,
|
||||
hash: Hash::new([0; 32]),
|
||||
},
|
||||
messages: Vec::new(),
|
||||
};
|
||||
|
||||
let hash = block
|
||||
.hash_with_default_hasher()
|
||||
.expect("Failed to hash genesis block");
|
||||
block.header.hash = hash;
|
||||
block
|
||||
}
|
||||
|
||||
pub(crate) fn sign(&self, keypair: &Keypair) -> anyhow::Result<Certificate> {
|
||||
let raw_block: RawBlock = self.clone().into();
|
||||
let certificate = Certificate::prepare(keypair, &raw_block)?;
|
||||
Ok(certificate)
|
||||
}
|
||||
|
||||
pub(crate) fn verify(&self, certificate: &Certificate) -> anyhow::Result<bool> {
|
||||
let raw_block: RawBlock = self.clone().into();
|
||||
certificate.verify(&raw_block)
|
||||
}
|
||||
|
||||
pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result<Hash> {
|
||||
let raw_block: RawBlock = self.clone().into();
|
||||
raw_block.hash_with_default_hasher()
|
||||
}
|
||||
|
||||
pub(crate) fn merkle_tree(&self) -> anyhow::Result<MerkleTree> {
|
||||
merkle_tree(&self.messages)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Block {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let header = &self.header;
|
||||
write!(f, "{header}, nr of messages: {}", self.messages.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for Block {
|
||||
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
Codec::encode(&self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for Block {
|
||||
type Output = Block;
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
|
||||
Codec::decode(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub(crate) struct RawBlock {
|
||||
pub(crate) header: RawBlockHeader,
|
||||
pub(crate) messages: Vec<EphemeraMessage>,
|
||||
}
|
||||
|
||||
impl RawBlock {
|
||||
pub(crate) fn new(header: RawBlockHeader, messages: Vec<EphemeraMessage>) -> Self {
|
||||
Self { header, messages }
|
||||
}
|
||||
|
||||
pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result<Hash> {
|
||||
let header_hash = self.header.hash_with_default_hasher()?;
|
||||
let merkle_root = merkle_tree(&self.messages)?.root_hash();
|
||||
let block_hash = Hasher::digest(&[header_hash.inner(), merkle_root.inner()].concat());
|
||||
Ok(block_hash.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Block> for RawBlock {
|
||||
fn from(block: Block) -> Self {
|
||||
Self {
|
||||
header: block.header.into(),
|
||||
messages: block.messages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for RawBlockHeader {
|
||||
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
Codec::encode(&self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for RawBlockHeader {
|
||||
type Output = RawBlockHeader;
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
|
||||
Codec::decode(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for RawBlock {
|
||||
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
Codec::encode(&self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for RawBlock {
|
||||
type Output = RawBlock;
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
|
||||
Codec::decode(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl EphemeraHash for RawBlockHeader {
|
||||
fn hash<H: EphemeraHasher>(&self, state: &mut H) -> anyhow::Result<()> {
|
||||
state.update(&self.encode()?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl EphemeraHash for RawBlock {
|
||||
fn hash<H: EphemeraHasher>(&self, state: &mut H) -> anyhow::Result<()> {
|
||||
self.header.hash(state)?;
|
||||
for message in &self.messages {
|
||||
message.hash(state)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn merkle_tree(messages: &[EphemeraMessage]) -> anyhow::Result<MerkleTree> {
|
||||
let message_hashes = messages
|
||||
.iter()
|
||||
.map(EphemeraMessage::hash_with_default_hasher)
|
||||
.collect::<anyhow::Result<Vec<Hash>>>()?;
|
||||
let merkle_tree = MerkleTree::build_tree(&message_hashes);
|
||||
Ok(merkle_tree)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::block::types::message::RawEphemeraMessage;
|
||||
use crate::crypto::EphemeraKeypair;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_block_hash_no_messages() {
|
||||
let block = Block::new_genesis_block(PeerId::random());
|
||||
let block_hash = block.hash_with_default_hasher().unwrap();
|
||||
assert_eq!(block_hash, block.get_hash());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_hash_with_messages() {
|
||||
let messages = create_ephemera_messages(10);
|
||||
let message_hashes = messages
|
||||
.iter()
|
||||
.map(EphemeraMessage::hash_with_default_hasher)
|
||||
.collect::<anyhow::Result<Vec<Hash>>>()
|
||||
.unwrap();
|
||||
|
||||
let raw_block = RawBlock::new(RawBlockHeader::new(PeerId::random(), 0), messages);
|
||||
let block_hash = raw_block.hash_with_default_hasher().unwrap();
|
||||
|
||||
let header_hash = raw_block.header.hash_with_default_hasher().unwrap();
|
||||
let merkle_root = MerkleTree::build_tree(&message_hashes).root_hash();
|
||||
let expected_block_hash =
|
||||
Hasher::digest(&[header_hash.inner(), merkle_root.inner()].concat());
|
||||
|
||||
assert_eq!(block_hash, expected_block_hash.into());
|
||||
}
|
||||
|
||||
fn create_ephemera_messages(n: usize) -> Vec<EphemeraMessage> {
|
||||
let keypair = Keypair::generate(None);
|
||||
let mut messages = Vec::new();
|
||||
for i in 0..n {
|
||||
let label = format!("test {i}",);
|
||||
let message = RawEphemeraMessage::new(label, vec![0; 32]);
|
||||
let certificate = Certificate::prepare(&keypair, &message).unwrap();
|
||||
let ephemera_message = EphemeraMessage::new(message, certificate);
|
||||
messages.push(ephemera_message);
|
||||
}
|
||||
messages
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::utilities::codec::{Codec, DecodingError, EncodingError, EphemeraCodec};
|
||||
use crate::{
|
||||
codec::{Decode, Encode},
|
||||
utilities::{
|
||||
crypto::Certificate,
|
||||
hash::{EphemeraHash, EphemeraHasher},
|
||||
hash::{Hash, Hasher},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
|
||||
pub(crate) struct EphemeraMessage {
|
||||
///Timestamp of the message
|
||||
pub(crate) timestamp: u64,
|
||||
///Application specific logical identifier of the message
|
||||
pub(crate) label: String,
|
||||
///Application specific data
|
||||
pub(crate) data: Vec<u8>,
|
||||
///Signature of the raw message
|
||||
pub(crate) certificate: Certificate,
|
||||
}
|
||||
|
||||
impl EphemeraMessage {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new(raw_message: RawEphemeraMessage, certificate: Certificate) -> Self {
|
||||
Self {
|
||||
timestamp: raw_message.timestamp,
|
||||
label: raw_message.label,
|
||||
data: raw_message.data,
|
||||
certificate,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result<Hash> {
|
||||
let mut hasher = Hasher::default();
|
||||
self.hash(&mut hasher)?;
|
||||
let hash = hasher.finish().into();
|
||||
Ok(hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for EphemeraMessage {
|
||||
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
Codec::encode(&self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for EphemeraMessage {
|
||||
type Output = Self;
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
|
||||
Codec::decode(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl EphemeraHash for EphemeraMessage {
|
||||
fn hash<H: EphemeraHasher>(&self, state: &mut H) -> anyhow::Result<()> {
|
||||
state.update(&self.encode()?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw message represents all the data what will be signed.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub(crate) struct RawEphemeraMessage {
|
||||
pub(crate) timestamp: u64,
|
||||
pub(crate) label: String,
|
||||
pub(crate) data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl RawEphemeraMessage {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new(label: String, data: Vec<u8>) -> Self {
|
||||
use crate::utilities::time::EphemeraTime;
|
||||
Self {
|
||||
timestamp: EphemeraTime::now(),
|
||||
label,
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EphemeraMessage> for RawEphemeraMessage {
|
||||
fn from(message: EphemeraMessage) -> Self {
|
||||
Self {
|
||||
timestamp: message.timestamp,
|
||||
label: message.label,
|
||||
data: message.data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for RawEphemeraMessage {
|
||||
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
Codec::encode(&self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod block;
|
||||
pub(crate) mod message;
|
||||
@@ -0,0 +1,320 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use log::{debug, trace};
|
||||
use lru::LruCache;
|
||||
|
||||
use crate::broadcast::bracha::quorum::BrachaMessageType;
|
||||
use crate::peer::PeerId;
|
||||
use crate::{
|
||||
block::types::block::Block,
|
||||
broadcast::{
|
||||
bracha::quorum::Quorum,
|
||||
MessageType::{Echo, Vote},
|
||||
ProtocolContext, RawRbMsg,
|
||||
},
|
||||
utilities::hash::Hash,
|
||||
};
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum BroadcastResponse {
|
||||
Broadcast(RawRbMsg),
|
||||
Deliver(Hash),
|
||||
Drop(Hash),
|
||||
}
|
||||
|
||||
pub(crate) struct Broadcaster {
|
||||
/// Local peer id
|
||||
local_peer_id: PeerId,
|
||||
/// We keep a context for each block we are processing.
|
||||
contexts: LruCache<Hash, ProtocolContext>,
|
||||
/// Current cluster size
|
||||
cluster_size: usize,
|
||||
}
|
||||
|
||||
impl Broadcaster {
|
||||
pub fn new(peer_id: PeerId) -> Broadcaster {
|
||||
Broadcaster {
|
||||
//At any given time we are processing in parallel about n messages, where n is the number of peers in the group.
|
||||
//This is just large enough buffer.
|
||||
contexts: LruCache::new(NonZeroUsize::new(1000).unwrap()),
|
||||
cluster_size: 0,
|
||||
local_peer_id: peer_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_broadcast(&mut self, block: Block) -> anyhow::Result<BroadcastResponse> {
|
||||
debug!("Starting broadcast for new block {:?}", block.get_hash());
|
||||
self.handle(&RawRbMsg::new(block, self.local_peer_id))
|
||||
}
|
||||
|
||||
pub(crate) fn handle(&mut self, rb_msg: &RawRbMsg) -> anyhow::Result<BroadcastResponse> {
|
||||
trace!("Processing new broadcast message: {:?}", rb_msg);
|
||||
|
||||
let block = rb_msg.block();
|
||||
let hash = block.hash_with_default_hasher()?;
|
||||
|
||||
let ctx = self.contexts.get_or_insert(hash, || {
|
||||
ProtocolContext::new(hash, self.local_peer_id, Quorum::new(self.cluster_size))
|
||||
});
|
||||
|
||||
if ctx.delivered {
|
||||
trace!("Block {hash:?} already delivered");
|
||||
return Ok(BroadcastResponse::Drop(hash));
|
||||
}
|
||||
|
||||
match rb_msg.message_type {
|
||||
Echo(_) => {
|
||||
trace!("Processing ECHO {:?}", rb_msg.id);
|
||||
Ok(self.process_echo(rb_msg, hash))
|
||||
}
|
||||
Vote(_) => {
|
||||
trace!("Processing VOTE {:?}", rb_msg.id);
|
||||
Ok(self.process_vote(rb_msg, hash))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_echo(&mut self, rb_msg: &RawRbMsg, hash: Hash) -> BroadcastResponse {
|
||||
let ctx = self.contexts.get_mut(&hash).expect("Context not found");
|
||||
|
||||
if self.local_peer_id != rb_msg.original_sender {
|
||||
trace!("Adding echo from {:?}", rb_msg.original_sender);
|
||||
ctx.add_echo(rb_msg.original_sender);
|
||||
}
|
||||
|
||||
if !ctx.echoed() {
|
||||
ctx.add_echo(self.local_peer_id);
|
||||
|
||||
trace!("Sending echo reply for {hash:?}",);
|
||||
return BroadcastResponse::Broadcast(
|
||||
rb_msg.echo_reply(self.local_peer_id, rb_msg.block()),
|
||||
);
|
||||
}
|
||||
|
||||
if !ctx.voted()
|
||||
&& ctx
|
||||
.quorum
|
||||
.check_threshold(ctx, BrachaMessageType::Echo)
|
||||
.is_vote()
|
||||
{
|
||||
ctx.add_vote(self.local_peer_id);
|
||||
|
||||
trace!("Sending vote reply for {hash:?}",);
|
||||
return BroadcastResponse::Broadcast(
|
||||
rb_msg.vote_reply(self.local_peer_id, rb_msg.block()),
|
||||
);
|
||||
}
|
||||
|
||||
BroadcastResponse::Drop(hash)
|
||||
}
|
||||
|
||||
fn process_vote(&mut self, rb_msg: &RawRbMsg, hash: Hash) -> BroadcastResponse {
|
||||
let block = rb_msg.block();
|
||||
let ctx = self.contexts.get_mut(&hash).expect("Context not found");
|
||||
|
||||
if self.local_peer_id != rb_msg.original_sender {
|
||||
trace!("Adding vote from {:?}", rb_msg.original_sender);
|
||||
ctx.add_vote(rb_msg.original_sender);
|
||||
}
|
||||
|
||||
if ctx
|
||||
.quorum
|
||||
.check_threshold(ctx, BrachaMessageType::Vote)
|
||||
.is_vote()
|
||||
{
|
||||
ctx.add_vote(self.local_peer_id);
|
||||
|
||||
trace!("Sending vote reply for {hash:?}",);
|
||||
return BroadcastResponse::Broadcast(rb_msg.vote_reply(self.local_peer_id, block));
|
||||
}
|
||||
|
||||
if ctx
|
||||
.quorum
|
||||
.check_threshold(ctx, BrachaMessageType::Vote)
|
||||
.is_deliver()
|
||||
{
|
||||
trace!("Commit complete for {:?}", rb_msg.id);
|
||||
|
||||
ctx.delivered = true;
|
||||
|
||||
return BroadcastResponse::Deliver(hash);
|
||||
}
|
||||
|
||||
BroadcastResponse::Drop(hash)
|
||||
}
|
||||
|
||||
pub(crate) fn group_updated(&mut self, size: usize) {
|
||||
self.cluster_size = size;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
//1.make sure before voting enough echo messages are received
|
||||
//2.make sure before delivering enough vote messages are received
|
||||
//a)Either f + 1
|
||||
//b)Or n - f
|
||||
|
||||
//3.make sure that duplicate messages doesn't have impact
|
||||
|
||||
//4. "Ideally" make sure that when group changes, the ongoing broadcast can deal with it
|
||||
|
||||
use std::iter;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use crate::broadcast::bracha::broadcast::BroadcastResponse;
|
||||
use crate::peer::PeerId;
|
||||
use crate::utilities::hash::Hash;
|
||||
use crate::{
|
||||
block::types::block::{Block, RawBlock, RawBlockHeader},
|
||||
broadcast::{self, bracha::broadcast::Broadcaster, RawRbMsg},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_state_transitions_from_start_to_end() {
|
||||
let peers: Vec<PeerId> = iter::repeat_with(PeerId::random).take(10).collect();
|
||||
let local_peer_id = peers[0];
|
||||
let block_creator_peer_id = peers[1];
|
||||
|
||||
let mut broadcaster = Broadcaster::new(local_peer_id);
|
||||
broadcaster.group_updated(peers.len());
|
||||
|
||||
let (block_hash, block) = create_block(block_creator_peer_id);
|
||||
|
||||
//After this echo set contains local and block creator(msg sender)
|
||||
receive_echo_first_message(&mut broadcaster, &block, block_creator_peer_id);
|
||||
|
||||
let ctx = broadcaster.contexts.get(&block_hash).unwrap();
|
||||
assert_eq!(ctx.echo.len(), 2);
|
||||
assert!(ctx.echoed());
|
||||
assert!(!ctx.voted());
|
||||
|
||||
receive_nr_of_echo_messages_below_vote_threshold(&mut broadcaster, &block, &peers[2..6]);
|
||||
|
||||
let ctx = broadcaster.contexts.get(&block_hash).unwrap();
|
||||
assert_eq!(ctx.echo.len(), 6);
|
||||
assert!(ctx.echoed());
|
||||
assert!(!ctx.voted());
|
||||
|
||||
receive_echo_threshold_message(&mut broadcaster, &block, *peers.get(7).unwrap());
|
||||
|
||||
let ctx = broadcaster.contexts.get(&block_hash).unwrap();
|
||||
assert_eq!(ctx.echo.len(), 7);
|
||||
assert_eq!(ctx.vote.len(), 1);
|
||||
assert!(ctx.echoed());
|
||||
assert!(ctx.voted());
|
||||
|
||||
receive_nr_of_vote_messages_below_deliver_threshold(&mut broadcaster, &block, &peers[2..7]);
|
||||
|
||||
let ctx = broadcaster.contexts.get(&block_hash).unwrap();
|
||||
assert_eq!(ctx.echo.len(), 7);
|
||||
assert_eq!(ctx.vote.len(), 6);
|
||||
assert!(ctx.echoed());
|
||||
assert!(ctx.voted());
|
||||
|
||||
receive_threshold_vote_message_for_deliver(
|
||||
&mut broadcaster,
|
||||
&block,
|
||||
*peers.get(8).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
fn receive_threshold_vote_message_for_deliver(
|
||||
broadcaster: &mut Broadcaster,
|
||||
block: &Block,
|
||||
peer_id: PeerId,
|
||||
) {
|
||||
let rb_msg = RawRbMsg::new(block.clone(), PeerId::random());
|
||||
let rb_msg = rb_msg.vote_reply(peer_id, block.clone());
|
||||
|
||||
let response = handle_double(broadcaster, &rb_msg);
|
||||
|
||||
assert_matches!(response, BroadcastResponse::Deliver(_));
|
||||
}
|
||||
|
||||
fn receive_nr_of_echo_messages_below_vote_threshold(
|
||||
broadcaster: &mut Broadcaster,
|
||||
block: &Block,
|
||||
peers: &[PeerId],
|
||||
) {
|
||||
for peer_id in peers {
|
||||
let rb_msg = RawRbMsg::new(block.clone(), *peer_id);
|
||||
|
||||
let response = handle_double(broadcaster, &rb_msg);
|
||||
|
||||
assert_matches!(response, BroadcastResponse::Drop(_));
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_nr_of_vote_messages_below_deliver_threshold(
|
||||
broadcaster: &mut Broadcaster,
|
||||
block: &Block,
|
||||
peers: &[PeerId],
|
||||
) {
|
||||
for peer_id in peers {
|
||||
let rb_msg = RawRbMsg::new(block.clone(), PeerId::random());
|
||||
let rb_msg = rb_msg.vote_reply(*peer_id, block.clone());
|
||||
|
||||
let response = handle_double(broadcaster, &rb_msg);
|
||||
assert_matches!(response, BroadcastResponse::Drop(_));
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_echo_first_message(
|
||||
broadcaster: &mut Broadcaster,
|
||||
block: &Block,
|
||||
block_creator: PeerId,
|
||||
) {
|
||||
let rb_msg = RawRbMsg::new(block.clone(), block_creator);
|
||||
let response = handle_double(broadcaster, &rb_msg);
|
||||
|
||||
assert_matches!(
|
||||
response,
|
||||
BroadcastResponse::Broadcast(RawRbMsg {
|
||||
id: _,
|
||||
request_id: _,
|
||||
original_sender: _,
|
||||
timestamp: _,
|
||||
message_type: broadcast::MessageType::Echo(_),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn receive_echo_threshold_message(
|
||||
broadcaster: &mut Broadcaster,
|
||||
block: &Block,
|
||||
peer_id: PeerId,
|
||||
) {
|
||||
let rb_msg = RawRbMsg::new(block.clone(), peer_id);
|
||||
|
||||
let response = handle_double(broadcaster, &rb_msg);
|
||||
assert_matches!(
|
||||
response,
|
||||
BroadcastResponse::Broadcast(RawRbMsg {
|
||||
id: _,
|
||||
request_id: _,
|
||||
original_sender: _,
|
||||
timestamp: _,
|
||||
message_type: broadcast::MessageType::Vote(_),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn create_block(block_creator_peer_id: PeerId) -> (Hash, Block) {
|
||||
let header = RawBlockHeader::new(block_creator_peer_id, 0);
|
||||
let raw_block = RawBlock::new(header, vec![]);
|
||||
let block_hash = raw_block.hash_with_default_hasher().unwrap();
|
||||
let block = Block::new(raw_block, block_hash);
|
||||
(block_hash, block)
|
||||
}
|
||||
|
||||
//make sure that duplicate messages doesn't have impact
|
||||
fn handle_double(broadcaster: &mut Broadcaster, rb_msg: &RawRbMsg) -> BroadcastResponse {
|
||||
let response = broadcaster.handle(rb_msg).unwrap();
|
||||
broadcaster.handle(rb_msg).unwrap();
|
||||
response
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod broadcast;
|
||||
pub(crate) mod quorum;
|
||||
@@ -0,0 +1,254 @@
|
||||
use log::trace;
|
||||
|
||||
use crate::broadcast::{MessageType, ProtocolContext};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum BrachaMessageType {
|
||||
Echo,
|
||||
Vote,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum BrachaAction {
|
||||
Vote,
|
||||
Deliver,
|
||||
Ignore,
|
||||
}
|
||||
|
||||
impl BrachaAction {
|
||||
pub(crate) fn is_vote(self) -> bool {
|
||||
matches!(self, BrachaAction::Vote)
|
||||
}
|
||||
|
||||
pub(crate) fn is_deliver(self) -> bool {
|
||||
matches!(self, BrachaAction::Deliver)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MessageType> for BrachaMessageType {
|
||||
fn from(message_type: MessageType) -> Self {
|
||||
match message_type {
|
||||
MessageType::Echo(_) => BrachaMessageType::Echo,
|
||||
MessageType::Vote(_) => BrachaMessageType::Vote,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_FAULTY_RATIO: f64 = 1.0 / 3.0;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct Quorum {
|
||||
pub(crate) cluster_size: usize,
|
||||
pub(crate) max_faulty_nodes: usize,
|
||||
}
|
||||
|
||||
impl Quorum {
|
||||
pub fn new(cluster_size: usize) -> Self {
|
||||
let max_faulty_nodes = Quorum::max_faulty_nodes(cluster_size);
|
||||
Self {
|
||||
cluster_size,
|
||||
max_faulty_nodes,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_threshold(
|
||||
&self,
|
||||
ctx: &ProtocolContext,
|
||||
phase: BrachaMessageType,
|
||||
) -> BrachaAction {
|
||||
if self.cluster_size == 0 {
|
||||
return BrachaAction::Ignore;
|
||||
}
|
||||
|
||||
match phase {
|
||||
BrachaMessageType::Echo => {
|
||||
if ctx.echo.len() >= self.cluster_size - self.max_faulty_nodes {
|
||||
trace!(
|
||||
"Echo threshold reached: Echoed:{} / Threshold:{} for Block:{}",
|
||||
ctx.echo.len(),
|
||||
self.cluster_size - self.max_faulty_nodes,
|
||||
ctx.hash
|
||||
);
|
||||
BrachaAction::Vote
|
||||
} else {
|
||||
trace!(
|
||||
"Echo threshold not reached: Echoed:{} / Threshold:{} for Block:{}",
|
||||
ctx.echo.len(),
|
||||
self.cluster_size - self.max_faulty_nodes,
|
||||
ctx.hash
|
||||
);
|
||||
BrachaAction::Ignore
|
||||
}
|
||||
}
|
||||
BrachaMessageType::Vote => {
|
||||
if !ctx.voted() {
|
||||
// f + 1 votes are enough to send our vote
|
||||
if ctx.vote.len() >= self.max_faulty_nodes {
|
||||
trace!(
|
||||
"Vote send threshold reached: Voted:{} / Threshold:{} for Block:{}",
|
||||
ctx.vote.len(),
|
||||
self.max_faulty_nodes + 1,
|
||||
ctx.hash
|
||||
);
|
||||
return BrachaAction::Vote;
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.voted() {
|
||||
// n-f votes are enough to deliver the value
|
||||
if ctx.vote.len() >= self.cluster_size - self.max_faulty_nodes {
|
||||
trace!(
|
||||
"Deliver threshold reached: Voted:{} / Threshold:{} for Block:{}",
|
||||
ctx.vote.len(),
|
||||
self.cluster_size - self.max_faulty_nodes,
|
||||
ctx.hash
|
||||
);
|
||||
return BrachaAction::Deliver;
|
||||
}
|
||||
}
|
||||
|
||||
trace!(
|
||||
"Vote threshold not reached: Voted:{} / Threshold:{} for Block:{}",
|
||||
ctx.vote.len(),
|
||||
self.max_faulty_nodes + 1,
|
||||
ctx.hash
|
||||
);
|
||||
BrachaAction::Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cluster_size_info(cluster_size: usize) -> String {
|
||||
let max_faulty_nodes = Quorum::max_faulty_nodes(cluster_size);
|
||||
format!("Cluster size: {cluster_size} / Max faulty nodes: {max_faulty_nodes}",)
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_possible_truncation
|
||||
)]
|
||||
fn max_faulty_nodes(cluster_size: usize) -> usize {
|
||||
(cluster_size as f64 * MAX_FAULTY_RATIO).floor() as usize
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::broadcast::{
|
||||
bracha::quorum::{BrachaAction, BrachaMessageType, Quorum},
|
||||
ProtocolContext,
|
||||
};
|
||||
use crate::peer::PeerId;
|
||||
|
||||
#[test]
|
||||
fn test_max_faulty_nodes() {
|
||||
let quorum = Quorum::new(10);
|
||||
assert_eq!(quorum.max_faulty_nodes, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_threshold_from_n_minus_f_peers() {
|
||||
let quorum = Quorum::new(10);
|
||||
|
||||
let ctx = ctx_with_nr_echoes(0);
|
||||
assert_eq!(
|
||||
quorum.check_threshold(&ctx, BrachaMessageType::Echo),
|
||||
BrachaAction::Ignore
|
||||
);
|
||||
|
||||
let ctx = ctx_with_nr_echoes(3);
|
||||
assert_eq!(
|
||||
quorum.check_threshold(&ctx, BrachaMessageType::Echo),
|
||||
BrachaAction::Ignore
|
||||
);
|
||||
|
||||
let ctx = ctx_with_nr_echoes(8);
|
||||
assert_eq!(
|
||||
quorum.check_threshold(&ctx, BrachaMessageType::Echo),
|
||||
BrachaAction::Vote
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vote_threshold_from_f_plus_one_peers() {
|
||||
let quorum = Quorum::new(10);
|
||||
|
||||
let ctx = ctx_with_nr_votes(0, None);
|
||||
assert_eq!(
|
||||
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
|
||||
BrachaAction::Ignore
|
||||
);
|
||||
|
||||
let ctx = ctx_with_nr_votes(2, None);
|
||||
assert_eq!(
|
||||
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
|
||||
BrachaAction::Ignore
|
||||
);
|
||||
|
||||
let ctx = ctx_with_nr_votes(5, None);
|
||||
assert_eq!(
|
||||
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
|
||||
BrachaAction::Vote
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deliver_threshold_from_n_minus_f_peers() {
|
||||
let quorum = Quorum::new(10);
|
||||
|
||||
let local_peer_id = PeerId::random();
|
||||
let ctx = ctx_with_nr_votes(0, local_peer_id.into());
|
||||
assert_eq!(
|
||||
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
|
||||
BrachaAction::Ignore
|
||||
);
|
||||
|
||||
let ctx = ctx_with_nr_votes(3, local_peer_id.into());
|
||||
assert_eq!(
|
||||
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
|
||||
BrachaAction::Ignore
|
||||
);
|
||||
|
||||
let ctx = ctx_with_nr_votes(7, local_peer_id.into());
|
||||
assert_eq!(
|
||||
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
|
||||
BrachaAction::Deliver
|
||||
);
|
||||
}
|
||||
|
||||
fn ctx_with_nr_echoes(n: usize) -> ProtocolContext {
|
||||
let mut ctx = ProtocolContext {
|
||||
local_peer_id: PeerId::random(),
|
||||
hash: [0; 32].into(),
|
||||
echo: HashSet::default(),
|
||||
vote: HashSet::default(),
|
||||
quorum: Quorum::new(10),
|
||||
delivered: false,
|
||||
};
|
||||
for _ in 0..n {
|
||||
ctx.echo.insert(PeerId::random());
|
||||
}
|
||||
ctx
|
||||
}
|
||||
|
||||
fn ctx_with_nr_votes(n: usize, local_peer_id: Option<PeerId>) -> ProtocolContext {
|
||||
let mut ctx = ProtocolContext {
|
||||
local_peer_id: local_peer_id.unwrap_or(PeerId::random()),
|
||||
hash: [0; 32].into(),
|
||||
echo: HashSet::default(),
|
||||
vote: HashSet::default(),
|
||||
quorum: Quorum::new(10),
|
||||
delivered: false,
|
||||
};
|
||||
for _ in 0..n {
|
||||
ctx.vote.insert(PeerId::random());
|
||||
}
|
||||
if let Some(id) = local_peer_id {
|
||||
ctx.vote.insert(id);
|
||||
}
|
||||
ctx
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
use std::collections::HashSet;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use log::warn;
|
||||
use lru::LruCache;
|
||||
|
||||
use crate::peer::PeerId;
|
||||
use crate::utilities::hash::Hash;
|
||||
|
||||
pub(crate) struct BroadcastGroup {
|
||||
/// The id of current group. Incremented every time a new snapshot is added.
|
||||
pub(crate) current_id: u64,
|
||||
/// A cache of the group snapshots.
|
||||
pub(crate) snapshots: LruCache<u64, HashSet<PeerId>>,
|
||||
/// A cache of the groups for each block.
|
||||
pub(crate) broadcast_groups: LruCache<Hash, u64>,
|
||||
}
|
||||
|
||||
impl BroadcastGroup {
|
||||
pub(crate) fn new() -> BroadcastGroup {
|
||||
let mut snapshots = LruCache::new(NonZeroUsize::new(100).unwrap());
|
||||
snapshots.put(0, HashSet::new());
|
||||
BroadcastGroup {
|
||||
current_id: 0,
|
||||
snapshots,
|
||||
broadcast_groups: LruCache::new(NonZeroUsize::new(100).unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_snapshot(&mut self, snapshot: HashSet<PeerId>) {
|
||||
self.current_id += 1;
|
||||
self.snapshots.put(self.current_id, snapshot);
|
||||
}
|
||||
|
||||
pub(crate) fn is_member(&mut self, id: u64, peer_id: &PeerId) -> bool {
|
||||
self.snapshots
|
||||
.get(&id)
|
||||
.map_or(false, |s| s.contains(peer_id))
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&mut self) -> bool {
|
||||
self.snapshots
|
||||
.get(&self.current_id)
|
||||
.map_or(true, HashSet::is_empty)
|
||||
}
|
||||
|
||||
// Returns empty snapshots(inserted in 'new' fn) if we haven't received any yet.
|
||||
pub(crate) fn current(&mut self) -> &HashSet<PeerId> {
|
||||
self.snapshots
|
||||
.get(&self.current_id)
|
||||
.expect("Current group should always exist")
|
||||
}
|
||||
|
||||
// Checks if creator and sender are part of the expected group.
|
||||
// If we see hash first time, it checks against the current group. And if check passes, it
|
||||
// associates the hash with the current group.
|
||||
pub(crate) fn check_membership(
|
||||
&mut self,
|
||||
hash: Hash,
|
||||
block_creator: &PeerId,
|
||||
message_sender: &PeerId,
|
||||
) -> bool {
|
||||
//We see this block first time
|
||||
if !self.broadcast_groups.contains(&hash) {
|
||||
//This can happen at startup for example when node is not ready yet(caught up with the network)
|
||||
if self.is_empty() {
|
||||
warn!(
|
||||
"Received new block {:?} but current group is empty, rejecting the block",
|
||||
hash
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//Make sure that the sender peer_id and block peer_id are part of the block initial group
|
||||
//1. If the block is new, the group is the current one
|
||||
//2. If the block is old, the group is the one that was used when the block was created
|
||||
|
||||
//It's needed to make sure that
|
||||
//1. The peer is authenticated(part of the network)
|
||||
//2. Block processing is consistent regarding the group across rounds
|
||||
|
||||
let membership_id = *self.broadcast_groups.get(&hash).unwrap_or(&self.current_id);
|
||||
|
||||
//Node is excluded from group for some reason(for example health checks failed)
|
||||
if !self.is_member(membership_id, message_sender) {
|
||||
warn!(
|
||||
"Received new block {} but sender {} is not part of the current group",
|
||||
hash, message_sender
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Node is excluded from group for some reason(for example health checks failed)
|
||||
if !self.is_member(membership_id, block_creator) {
|
||||
warn!(
|
||||
"Received new block {} but sender {} is not part of the current group",
|
||||
hash, message_sender
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
self.broadcast_groups.put(hash, membership_id);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn get_group_by_block_hash(&mut self, hash: Hash) -> Option<&HashSet<PeerId>> {
|
||||
let membership_id = *self.broadcast_groups.get(&hash)?;
|
||||
self.snapshots.get(&membership_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::broadcast::group::BroadcastGroup;
|
||||
use crate::peer::PeerId;
|
||||
use crate::utilities::hash::Hash;
|
||||
|
||||
#[test]
|
||||
fn test_no_snapshot() {
|
||||
let group = BroadcastGroup::new();
|
||||
assert_eq!(group.current_id, 0);
|
||||
//Including initial default snapshot
|
||||
assert_eq!(group.snapshots.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_snapshots() {
|
||||
let (mut group, snapshots) = group_with_snapshots(10);
|
||||
assert_eq!(group.current_id, 10);
|
||||
//Including initial default snapshot
|
||||
assert_eq!(group.snapshots.len(), 11);
|
||||
|
||||
for (i, sn) in snapshots
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, sn)| ((i + 1) as u64, sn))
|
||||
{
|
||||
let gsn = group.snapshots.get(&i).unwrap();
|
||||
assert_eq!(sn, gsn);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_membership_empty_group() {
|
||||
let mut group = BroadcastGroup::new();
|
||||
let hash = Hash::new([0; 32]);
|
||||
assert!(!group.check_membership(hash, &PeerId::random(), &PeerId::random()));
|
||||
assert!(!group.broadcast_groups.contains(&hash));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_membership_creator_nor_sender_not_member() {
|
||||
let (mut group, _snapshots) = group_with_snapshots(1);
|
||||
assert!(!group.check_membership(Hash::new([0; 32]), &PeerId::random(), &PeerId::random()));
|
||||
assert!(!group.broadcast_groups.contains(&Hash::new([0; 32])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_membership_creator_not_member() {
|
||||
let (mut group, snapshots) = group_with_snapshots(1);
|
||||
let sender = snapshots[0].clone().into_iter().next().unwrap();
|
||||
|
||||
let hash = Hash::new([0; 32]);
|
||||
assert!(!group.check_membership(hash, &PeerId::random(), &sender));
|
||||
assert!(!group.broadcast_groups.contains(&hash));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_membership_sender_not_member() {
|
||||
let (mut group, snapshots) = group_with_snapshots(1);
|
||||
let creator = snapshots[0].clone().into_iter().next().unwrap();
|
||||
let hash = Hash::new([0; 32]);
|
||||
assert!(!group.check_membership(hash, &creator, &PeerId::random()));
|
||||
assert!(!group.broadcast_groups.contains(&hash));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_snapshot_membership_both_are_members() {
|
||||
let (mut group, snapshots) = group_with_snapshots(1);
|
||||
let creator = snapshots[0].clone().into_iter().next().unwrap();
|
||||
let sender = creator;
|
||||
let hash = Hash::new([0; 32]);
|
||||
assert!(group.check_membership(hash, &creator, &sender));
|
||||
assert!(group.broadcast_groups.contains(&hash));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_snapshot_membership_of_current_snapshot() {
|
||||
let (mut group, snapshots) = group_with_snapshots(2);
|
||||
let current_snapshot = snapshots[1].clone();
|
||||
let creator = current_snapshot.into_iter().next().unwrap();
|
||||
let sender = creator;
|
||||
|
||||
let hash = Hash::new([0; 32]);
|
||||
assert!(group.check_membership(hash, &creator, &sender));
|
||||
assert!(group.broadcast_groups.contains(&hash));
|
||||
|
||||
//Remove the current snapshot
|
||||
group.snapshots.pop(&group.current_id);
|
||||
|
||||
//Membership should fail
|
||||
assert!(!group.check_membership(hash, &creator, &sender));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_snapshot_membership_of_previous_snapshot() {
|
||||
let mut group = BroadcastGroup::new();
|
||||
let first_snapshot = create_snapshot();
|
||||
group.add_snapshot(first_snapshot.clone());
|
||||
|
||||
let creator = first_snapshot.into_iter().next().unwrap();
|
||||
let sender = creator;
|
||||
let hash = Hash::new([0; 32]);
|
||||
assert!(group.check_membership(hash, &creator, &sender));
|
||||
assert!(group.broadcast_groups.contains(&hash));
|
||||
|
||||
//Add second snapshot
|
||||
group.add_snapshot(create_snapshot());
|
||||
|
||||
//Remove the current snapshot
|
||||
group.snapshots.pop(&group.current_id);
|
||||
|
||||
//Membership should still pass
|
||||
assert!(group.broadcast_groups.contains(&hash));
|
||||
assert!(group.check_membership(hash, &creator, &sender));
|
||||
}
|
||||
|
||||
fn group_with_snapshots(count: usize) -> (BroadcastGroup, Vec<HashSet<PeerId>>) {
|
||||
let mut group = BroadcastGroup::new();
|
||||
let mut snapshots = Vec::new();
|
||||
for _ in 0..count {
|
||||
let snapshot = create_snapshot();
|
||||
snapshots.push(snapshot.clone());
|
||||
group.add_snapshot(snapshot);
|
||||
}
|
||||
(group, snapshots)
|
||||
}
|
||||
|
||||
fn create_snapshot() -> HashSet<PeerId> {
|
||||
let mut snapshot = HashSet::new();
|
||||
let peer_id = PeerId::random();
|
||||
snapshot.insert(peer_id);
|
||||
snapshot
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
//! Simple reliable broadcast protocol(Bracha) implementation
|
||||
//!
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::broadcast::bracha::quorum::Quorum;
|
||||
use crate::{
|
||||
block::types::block::Block,
|
||||
peer::PeerId,
|
||||
utilities::{
|
||||
crypto::Certificate,
|
||||
hash::Hash,
|
||||
id::{EphemeraId, EphemeraIdentifier},
|
||||
time::EphemeraTime,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) mod bracha;
|
||||
pub(crate) mod group;
|
||||
pub(crate) mod signing;
|
||||
|
||||
/// Context keeps the broadcast state for a block
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ProtocolContext {
|
||||
pub(crate) local_peer_id: PeerId,
|
||||
/// Block hash
|
||||
pub(crate) hash: Hash,
|
||||
/// Peers that sent prepare message(this peer included)
|
||||
pub(crate) echo: HashSet<PeerId>,
|
||||
/// Peers that sent commit message(this peer included)
|
||||
pub(crate) vote: HashSet<PeerId>,
|
||||
/// Quorum logic for Bracha protocol
|
||||
pub(crate) quorum: Quorum,
|
||||
/// Flag indicating if the message was delivered to the client
|
||||
pub(crate) delivered: bool,
|
||||
}
|
||||
|
||||
impl ProtocolContext {
|
||||
pub(crate) fn new(hash: Hash, local_peer_id: PeerId, quorum: Quorum) -> ProtocolContext {
|
||||
ProtocolContext {
|
||||
local_peer_id,
|
||||
hash,
|
||||
echo: HashSet::new(),
|
||||
vote: HashSet::new(),
|
||||
quorum,
|
||||
delivered: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_echo(&mut self, peer: PeerId) {
|
||||
self.echo.insert(peer);
|
||||
}
|
||||
|
||||
fn add_vote(&mut self, peer: PeerId) {
|
||||
self.vote.insert(peer);
|
||||
}
|
||||
|
||||
fn echoed(&self) -> bool {
|
||||
self.echo.contains(&self.local_peer_id)
|
||||
}
|
||||
|
||||
fn voted(&self) -> bool {
|
||||
self.vote.contains(&self.local_peer_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) struct RbMsg {
|
||||
///Unique id of the message which stays the same throughout the protocol
|
||||
pub(crate) id: EphemeraId,
|
||||
///Distinct id of the message which changes when the message is rebroadcast
|
||||
pub(crate) request_id: EphemeraId,
|
||||
///Id of the peer that CREATED the message(not necessarily the one that sent it, with gossip it can come through a different peer)
|
||||
pub(crate) original_sender: PeerId,
|
||||
///When the message was created by the sender.
|
||||
pub(crate) timestamp: u64,
|
||||
///Current phase of the protocol(Echo, Vote)
|
||||
pub(crate) phase: MessageType,
|
||||
///Signature of the message
|
||||
pub(crate) certificate: Certificate,
|
||||
}
|
||||
|
||||
impl RbMsg {
|
||||
pub(crate) fn new(raw: RawRbMsg, signature: Certificate) -> RbMsg {
|
||||
RbMsg {
|
||||
id: raw.id,
|
||||
request_id: raw.request_id,
|
||||
original_sender: raw.original_sender,
|
||||
timestamp: raw.timestamp,
|
||||
phase: raw.message_type,
|
||||
certificate: signature,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn block(&self) -> &Block {
|
||||
match &self.phase {
|
||||
MessageType::Echo(block) | MessageType::Vote(block) => block,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) struct RawRbMsg {
|
||||
pub(crate) id: EphemeraId,
|
||||
pub(crate) request_id: EphemeraId,
|
||||
pub(crate) original_sender: PeerId,
|
||||
pub(crate) timestamp: u64,
|
||||
pub(crate) message_type: MessageType,
|
||||
}
|
||||
|
||||
impl RawRbMsg {
|
||||
pub(crate) fn new(block: Block, original_sender: PeerId) -> RawRbMsg {
|
||||
RawRbMsg {
|
||||
id: EphemeraId::generate(),
|
||||
request_id: EphemeraId::generate(),
|
||||
original_sender,
|
||||
timestamp: EphemeraTime::now(),
|
||||
message_type: MessageType::Echo(block),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn block(&self) -> Block {
|
||||
match &self.message_type {
|
||||
MessageType::Echo(block) | MessageType::Vote(block) => block.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reply(&self, local_id: PeerId, phase: MessageType) -> Self {
|
||||
RawRbMsg {
|
||||
id: self.id.clone(),
|
||||
request_id: EphemeraId::generate(),
|
||||
original_sender: local_id,
|
||||
timestamp: EphemeraTime::now(),
|
||||
message_type: phase,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn echo_reply(&self, local_id: PeerId, data: Block) -> Self {
|
||||
self.reply(local_id, MessageType::Echo(data))
|
||||
}
|
||||
|
||||
pub(crate) fn vote_reply(&self, local_id: PeerId, data: Block) -> Self {
|
||||
self.reply(local_id, MessageType::Vote(data))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RbMsg> for RawRbMsg {
|
||||
fn from(msg: RbMsg) -> Self {
|
||||
RawRbMsg {
|
||||
id: msg.id,
|
||||
request_id: msg.request_id,
|
||||
original_sender: msg.original_sender,
|
||||
timestamp: msg.timestamp,
|
||||
message_type: msg.phase,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RbMsg {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"[id: {}, peer: {}, block: {}, phase: {:?}]",
|
||||
self.id,
|
||||
self.original_sender,
|
||||
self.block().get_hash(),
|
||||
self.phase
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for RbMsg {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"[id: {}, peer: {}, block: {}, phase: {:?}]",
|
||||
self.id,
|
||||
self.original_sender,
|
||||
self.block().get_hash(),
|
||||
self.phase
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) enum MessageType {
|
||||
Echo(Block),
|
||||
Vote(Block),
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
use std::collections::HashSet;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::trace;
|
||||
use lru::LruCache;
|
||||
|
||||
use crate::{
|
||||
block::types::block::{Block, RawBlock},
|
||||
crypto::Keypair,
|
||||
utilities::{codec::Encode, crypto::Certificate, crypto::EphemeraPublicKey, hash::Hash},
|
||||
};
|
||||
|
||||
pub(crate) struct BlockSigner {
|
||||
/// All signatures of the last blocks that we received from the network(+ our own)
|
||||
verified_signatures: LruCache<Hash, HashSet<Certificate>>,
|
||||
/// Our own keypair
|
||||
signing_keypair: Arc<Keypair>,
|
||||
}
|
||||
|
||||
impl BlockSigner {
|
||||
pub fn new(keypair: Arc<Keypair>) -> Self {
|
||||
Self {
|
||||
verified_signatures: LruCache::new(NonZeroUsize::new(1000).unwrap()),
|
||||
signing_keypair: keypair,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_block_certificates(
|
||||
&mut self,
|
||||
block_id: &Hash,
|
||||
) -> Option<&HashSet<Certificate>> {
|
||||
self.verified_signatures.get(block_id)
|
||||
}
|
||||
|
||||
pub(crate) fn sign_block(&mut self, block: &Block, hash: &Hash) -> anyhow::Result<Certificate> {
|
||||
trace!("Signing block: {:?}", block);
|
||||
|
||||
let certificate = block.sign(self.signing_keypair.as_ref())?;
|
||||
self.add_certificate(hash, certificate.clone());
|
||||
Ok(certificate)
|
||||
}
|
||||
|
||||
/// This verification is part of reliable broadcast and verifies only the
|
||||
/// signature of the sender.
|
||||
pub(crate) fn verify_block(
|
||||
&mut self,
|
||||
block: &Block,
|
||||
certificate: &Certificate,
|
||||
) -> anyhow::Result<()> {
|
||||
trace!("Verifying block: {block:?} against certificate {certificate:?}");
|
||||
|
||||
let raw_block: RawBlock = (*block).clone().into();
|
||||
let raw_block = raw_block.encode()?;
|
||||
|
||||
if certificate
|
||||
.public_key
|
||||
.verify(&raw_block, &certificate.signature)
|
||||
{
|
||||
self.add_certificate(&block.header.hash, certificate.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
anyhow::bail!("Invalid block certificate");
|
||||
}
|
||||
}
|
||||
|
||||
fn add_certificate(&mut self, hash: &Hash, certificate: Certificate) {
|
||||
trace!("Adding certificate to block: {}", hash);
|
||||
self.verified_signatures
|
||||
.get_or_insert_mut(*hash, HashSet::new)
|
||||
.insert(certificate);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::block::types::block::RawBlockHeader;
|
||||
use crate::block::types::message::{EphemeraMessage, RawEphemeraMessage};
|
||||
use crate::crypto::EphemeraKeypair;
|
||||
use crate::peer::ToPeerId;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sign_verify_block_ok() {
|
||||
let mut signer = BlockSigner::new(Arc::new(Keypair::generate(None)));
|
||||
|
||||
let message_signing_keypair = Keypair::generate(None);
|
||||
|
||||
let block = new_block(&message_signing_keypair, "label1");
|
||||
let hash = block.hash_with_default_hasher().unwrap();
|
||||
|
||||
let certificate = signer.sign_block(&block, &hash).unwrap();
|
||||
|
||||
assert!(signer.verify_block(&block, &certificate).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_signatures_cached_correctly() {
|
||||
let mut signer = BlockSigner::new(Arc::new(Keypair::generate(None)));
|
||||
|
||||
let block = new_block(&Keypair::generate(None), "label1");
|
||||
let hash = block.hash_with_default_hasher().unwrap();
|
||||
|
||||
//Signed by node 1
|
||||
let certificate1 = block.sign(&Keypair::generate(None)).unwrap();
|
||||
signer.verify_block(&block, &certificate1).unwrap();
|
||||
//Signed by node 2
|
||||
let certificate2 = block.sign(&Keypair::generate(None)).unwrap();
|
||||
signer.verify_block(&block, &certificate2).unwrap();
|
||||
|
||||
let block_certificates = signer.get_block_certificates(&hash).unwrap();
|
||||
assert_eq!(block_certificates.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_verify_block_fail() {
|
||||
let mut signer = BlockSigner::new(Arc::new(Keypair::generate(None)));
|
||||
let message_signing_keypair = Keypair::generate(None);
|
||||
|
||||
let block = new_block(&message_signing_keypair, "label1");
|
||||
let certificate = block.sign(&message_signing_keypair).unwrap();
|
||||
|
||||
let modified_block = new_block(&message_signing_keypair, "label2");
|
||||
|
||||
assert!(signer.verify_block(&modified_block, &certificate).is_err());
|
||||
}
|
||||
|
||||
fn new_block(keypair: &Keypair, message_label: &str) -> Block {
|
||||
let peer_id = keypair.public_key().peer_id();
|
||||
|
||||
let raw_ephemera_message =
|
||||
RawEphemeraMessage::new(message_label.to_string(), "payload".as_bytes().to_vec());
|
||||
|
||||
let message_certificate = Certificate::prepare(keypair, &raw_ephemera_message).unwrap();
|
||||
let messages = vec![EphemeraMessage::new(
|
||||
raw_ephemera_message,
|
||||
message_certificate,
|
||||
)];
|
||||
|
||||
let raw_block_header = RawBlockHeader::new(peer_id, 0);
|
||||
let raw_block = RawBlock::new(raw_block_header, messages);
|
||||
|
||||
let block_hash = raw_block
|
||||
.hash_with_default_hasher()
|
||||
.expect("Hashing failed");
|
||||
|
||||
Block::new(raw_block, block_hash)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::Parser;
|
||||
use log::{error, info};
|
||||
use toml::{Table, Value};
|
||||
|
||||
use crate::config::Configuration;
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct UpdateConfigCmd {
|
||||
#[clap(long)]
|
||||
pub config_path: String,
|
||||
#[clap(long)]
|
||||
pub property: String,
|
||||
#[clap(long)]
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl UpdateConfigCmd {
|
||||
/// # Panics
|
||||
/// Panics if the config file does not exist.
|
||||
pub fn execute(self) {
|
||||
let path: PathBuf = self.config_path.clone().into();
|
||||
match Configuration::try_load(path.clone()) {
|
||||
Ok(_) => {
|
||||
info!("Updating config: {:?}", self);
|
||||
|
||||
let toml_str = fs::read_to_string(path.clone()).unwrap();
|
||||
let table = toml_str.parse::<Table>().unwrap();
|
||||
|
||||
let keys = self.property.split('.').collect::<Vec<&str>>();
|
||||
|
||||
if !table.contains_key(keys[0]) {
|
||||
println!("Key '{}' does not exist", keys[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut visitor = ConfigVisitor::new(keys, self.value);
|
||||
let table = visitor.process(table);
|
||||
|
||||
let toml_str = toml::to_string(&table).unwrap();
|
||||
fs::write(path, toml_str).unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error loading configuration file: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigVisitor<'a> {
|
||||
keys: Vec<&'a str>,
|
||||
value: String,
|
||||
in_array: bool,
|
||||
data_in_array: Vec<Value>,
|
||||
}
|
||||
|
||||
impl<'a> ConfigVisitor<'a> {
|
||||
fn new(keys: Vec<&'a str>, value: String) -> Self {
|
||||
Self {
|
||||
keys,
|
||||
value,
|
||||
in_array: false,
|
||||
data_in_array: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn process(&mut self, table: Table) -> Table {
|
||||
let key = self.keys[0];
|
||||
let value = table.get(key).unwrap();
|
||||
self.process_value(table.clone(), value.clone(), 0)
|
||||
}
|
||||
|
||||
fn process_value(&mut self, mut table: Table, value: Value, key_index: usize) -> Table {
|
||||
let root_key = self.keys[key_index];
|
||||
match value {
|
||||
Value::String(str) => {
|
||||
println!("Old value: {str}",);
|
||||
let value = String::from(&self.value);
|
||||
println!("New value: {value}",);
|
||||
table.insert(root_key.to_string(), Value::String(value));
|
||||
}
|
||||
Value::Integer(i) => {
|
||||
println!("Old value: {i}",);
|
||||
let value = i64::from_str(&self.value).unwrap();
|
||||
println!("New value: {value}",);
|
||||
table.insert(root_key.to_string(), Value::Integer(value));
|
||||
}
|
||||
Value::Float(f) => {
|
||||
println!("Old value: {f}",);
|
||||
let value = f64::from_str(&self.value).unwrap();
|
||||
println!("New value: {value}",);
|
||||
table.insert(root_key.to_string(), Value::Float(value));
|
||||
}
|
||||
Value::Boolean(b) => {
|
||||
println!("Old value: {b}",);
|
||||
let value = bool::from_str(&self.value).unwrap();
|
||||
println!("New value: {value}",);
|
||||
table.insert(root_key.to_string(), Value::Boolean(value));
|
||||
}
|
||||
Value::Datetime(_) => {
|
||||
println!("Datetime not supported");
|
||||
}
|
||||
Value::Array(ar) => {
|
||||
self.in_array = true;
|
||||
for value in ar {
|
||||
self.process_value(table.clone(), value, key_index);
|
||||
}
|
||||
table.remove(root_key);
|
||||
table.insert(
|
||||
root_key.to_string(),
|
||||
Value::Array(self.data_in_array.clone()),
|
||||
);
|
||||
self.data_in_array.clear();
|
||||
self.in_array = false;
|
||||
}
|
||||
Value::Table(t) => {
|
||||
let value = t.get(self.keys[key_index + 1]).cloned().unwrap();
|
||||
let updated_table = self.process_value(t, value, key_index + 1);
|
||||
if self.in_array {
|
||||
self.data_in_array.push(updated_table.into());
|
||||
return table.clone();
|
||||
}
|
||||
table.insert(root_key.to_string(), updated_table.into());
|
||||
}
|
||||
}
|
||||
table
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use crate::crypto::{EphemeraKeypair, Keypair};
|
||||
use clap::Parser;
|
||||
|
||||
use crate::utilities::crypto::EphemeraPublicKey;
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct GenerateKeypairCmd;
|
||||
|
||||
impl GenerateKeypairCmd {
|
||||
pub fn execute() {
|
||||
let keypair = Keypair::generate(None);
|
||||
println!("Keypair: {:>5}", keypair.to_base58());
|
||||
println!("Public key: {:>5}", keypair.public_key().to_base58());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
use clap::{Args, Parser};
|
||||
|
||||
use crate::config::{
|
||||
BlockManagerConfiguration, Configuration, DatabaseConfiguration, HttpConfiguration,
|
||||
Libp2pConfiguration, MembershipKind as ConfigMembershipKind, NodeConfiguration,
|
||||
WebsocketConfiguration,
|
||||
};
|
||||
use crate::crypto::{EphemeraKeypair, Keypair};
|
||||
|
||||
//network settings
|
||||
const DEFAULT_LISTEN_ADDRESS: &str = "127.0.0.1";
|
||||
const DEFAULT_LISTEN_PORT: &str = "3000";
|
||||
|
||||
//libp2p settings
|
||||
const DEFAULT_MESSAGES_TOPIC_NAME: &str = "nym-ephemera-proposed";
|
||||
const DEFAULT_HEARTBEAT_INTERVAL_SEC: u64 = 1;
|
||||
|
||||
#[derive(Args)]
|
||||
#[group(required = true, multiple = false)]
|
||||
pub struct MembershipKind {
|
||||
/// Requires the threshold of peers returned by membership provider to be online
|
||||
#[clap(long)]
|
||||
threshold: Option<f64>,
|
||||
/// Requires that all peers returned by membership provider peers to be online
|
||||
#[clap(long)]
|
||||
all: bool,
|
||||
/// Membership is just all online peers from the set returned by membership provider
|
||||
#[clap(long)]
|
||||
any: bool,
|
||||
}
|
||||
|
||||
impl From<MembershipKind> for ConfigMembershipKind {
|
||||
fn from(kind: MembershipKind) -> Self {
|
||||
match (kind.threshold, kind.all, kind.any) {
|
||||
//FIXME: use threshold value
|
||||
(Some(_), false, false) => ConfigMembershipKind::Threshold,
|
||||
(None, true, false) => ConfigMembershipKind::AllOnline,
|
||||
(None, false, true) => ConfigMembershipKind::AnyOnline,
|
||||
_ => panic!("Invalid membership kind"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Cmd {
|
||||
/// Name of the node
|
||||
#[arg(long, default_value = "default")]
|
||||
pub node_name: String,
|
||||
#[clap(long, default_value = DEFAULT_LISTEN_ADDRESS)]
|
||||
/// The IP address to listen on
|
||||
pub ip: String,
|
||||
/// The port which Ephemera uses for peer to peer communication
|
||||
#[clap(long, default_value = DEFAULT_LISTEN_PORT)]
|
||||
pub protocol_port: u16,
|
||||
/// The port which Ephemera listens on for websocket subscriptions
|
||||
#[clap(long)]
|
||||
pub websocket_port: u16,
|
||||
/// The port which Ephemera listens on for http api
|
||||
#[clap(long)]
|
||||
pub http_api_port: u16,
|
||||
/// Either this node produces blocks or not
|
||||
#[clap(long, default_value_t = true)]
|
||||
pub block_producer: bool,
|
||||
/// At which interval to produce blocks
|
||||
#[clap(long, default_value_t = 30)]
|
||||
pub block_creation_interval_sec: u64,
|
||||
/// When next block is created before preious one is finished, should we repeat it with the same messages
|
||||
#[clap(long, default_value_t = false)]
|
||||
pub repeat_last_block_messages: bool,
|
||||
/// The interval at which Ephemera requests the list of members
|
||||
#[clap(long, default_value_t = 60 * 60)]
|
||||
pub members_provider_delay_sec: u64,
|
||||
/// A rule how to choose members based on their online status
|
||||
#[command(flatten)]
|
||||
pub membership_kind: MembershipKind,
|
||||
}
|
||||
|
||||
impl Cmd {
|
||||
/// # Panics
|
||||
/// Panics if the config file already exists.
|
||||
pub fn execute(self) {
|
||||
assert!(
|
||||
Configuration::try_load_from_home_dir(&self.node_name).is_err(),
|
||||
"Configuration file already exists: {}",
|
||||
self.node_name
|
||||
);
|
||||
|
||||
let path = Configuration::ephemera_root_dir()
|
||||
.unwrap()
|
||||
.join(&self.node_name);
|
||||
println!("Creating ephemera node configuration in: {path:?}",);
|
||||
|
||||
let db_dir = path.join("db");
|
||||
let rocksdb_path = db_dir.join("rocksdb");
|
||||
let sqlite_path = db_dir.join("ephemera.sqlite");
|
||||
std::fs::create_dir_all(&rocksdb_path).unwrap();
|
||||
std::fs::File::create(&sqlite_path).unwrap();
|
||||
|
||||
let keypair = Keypair::generate(None);
|
||||
let private_key = keypair.to_base58();
|
||||
|
||||
let configuration = Configuration {
|
||||
node: NodeConfiguration {
|
||||
ip: self.ip,
|
||||
private_key,
|
||||
},
|
||||
libp2p: Libp2pConfiguration {
|
||||
port: self.protocol_port,
|
||||
ephemera_msg_topic_name: DEFAULT_MESSAGES_TOPIC_NAME.to_string(),
|
||||
heartbeat_interval_sec: DEFAULT_HEARTBEAT_INTERVAL_SEC,
|
||||
members_provider_delay_sec: self.members_provider_delay_sec,
|
||||
membership_kind: self.membership_kind.into(),
|
||||
},
|
||||
storage: DatabaseConfiguration {
|
||||
rocksdb_path: rocksdb_path.as_os_str().to_str().unwrap().to_string(),
|
||||
sqlite_path: sqlite_path.as_os_str().to_str().unwrap().to_string(),
|
||||
create_if_not_exists: true,
|
||||
},
|
||||
websocket: WebsocketConfiguration {
|
||||
port: self.websocket_port,
|
||||
},
|
||||
http: HttpConfiguration {
|
||||
port: self.http_api_port,
|
||||
},
|
||||
block_manager: BlockManagerConfiguration {
|
||||
producer: self.block_producer,
|
||||
creation_interval_sec: self.block_creation_interval_sec,
|
||||
repeat_last_block_messages: self.repeat_last_block_messages,
|
||||
},
|
||||
};
|
||||
|
||||
if let Err(err) = configuration.try_write_home_dir(&self.node_name) {
|
||||
eprintln!("Error creating configuration file: {err:?}",);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use crate::cli::crypto::GenerateKeypairCmd;
|
||||
use clap::Parser;
|
||||
|
||||
pub mod config;
|
||||
mod crypto;
|
||||
pub mod init;
|
||||
pub mod peers;
|
||||
pub mod run_node;
|
||||
|
||||
pub const PEERS_CONFIG_FILE: &str = "peers.toml";
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command()]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub subcommand: Subcommand,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
pub enum Subcommand {
|
||||
InitConfig(init::Cmd),
|
||||
InitLocalPeersConfig(peers::CreateLocalPeersConfiguration),
|
||||
RunNode(run_node::RunExternalNodeCmd),
|
||||
GenerateKeypair(crypto::GenerateKeypairCmd),
|
||||
UpdateConfig(config::UpdateConfigCmd),
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
/// # Errors
|
||||
/// Returns an error if the subcommand fails.
|
||||
pub async fn execute(self) -> anyhow::Result<()> {
|
||||
match self.subcommand {
|
||||
Subcommand::InitConfig(init) => {
|
||||
init.execute();
|
||||
}
|
||||
Subcommand::InitLocalPeersConfig(add_local_peers) => {
|
||||
add_local_peers.execute();
|
||||
}
|
||||
Subcommand::RunNode(run_node) => run_node.execute().await?,
|
||||
Subcommand::GenerateKeypair(_) => {
|
||||
GenerateKeypairCmd::execute();
|
||||
}
|
||||
Subcommand::UpdateConfig(update_config) => {
|
||||
update_config.execute();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! This is used to create local peers configuration file. Useful for local cluster development.
|
||||
|
||||
use crate::cli::PEERS_CONFIG_FILE;
|
||||
use clap::Parser;
|
||||
|
||||
use crate::config::Configuration;
|
||||
use crate::crypto::{EphemeraKeypair, EphemeraPublicKey, Keypair};
|
||||
use crate::membership::PeerSetting;
|
||||
use crate::network::members::ConfigPeers;
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct CreateLocalPeersConfiguration;
|
||||
|
||||
impl CreateLocalPeersConfiguration {
|
||||
/// # Panics
|
||||
/// Panics if the configuration file cannot be written.
|
||||
pub fn execute(self) {
|
||||
let peers = Self::from_ephemera_dev_cluster_conf().unwrap();
|
||||
let config_peers = ConfigPeers::new(peers);
|
||||
|
||||
let peers_conf_path = Configuration::ephemera_root_dir()
|
||||
.unwrap()
|
||||
.join(PEERS_CONFIG_FILE);
|
||||
|
||||
config_peers.try_write(peers_conf_path).unwrap();
|
||||
}
|
||||
|
||||
//LOCAL DEV CLUSTER ONLY
|
||||
//Get peers from dev Ephemera cluster config files
|
||||
pub(crate) fn from_ephemera_dev_cluster_conf() -> anyhow::Result<Vec<PeerSetting>> {
|
||||
let ephemera_root_dir = Configuration::ephemera_root_dir().unwrap();
|
||||
|
||||
let mut peers = vec![];
|
||||
|
||||
let home_dir = std::fs::read_dir(ephemera_root_dir)?;
|
||||
for entry in home_dir {
|
||||
let path = entry?.path();
|
||||
if path.is_dir() {
|
||||
let node_name = path.file_name().unwrap().to_str().unwrap();
|
||||
|
||||
if !node_name.starts_with("node") {
|
||||
continue;
|
||||
}
|
||||
|
||||
println!("Reading peer info config from node {node_name}",);
|
||||
|
||||
let conf = Configuration::try_load_from_home_dir(node_name)
|
||||
.unwrap_or_else(|_| panic!("Error loading configuration for node {node_name}"));
|
||||
|
||||
let node_info = conf.node;
|
||||
|
||||
let keypair = bs58::decode(&node_info.private_key).into_vec().unwrap();
|
||||
let keypair = Keypair::from_bytes(&keypair).unwrap();
|
||||
|
||||
let peer = PeerSetting {
|
||||
name: node_name.to_string(),
|
||||
address: format!("/ip4/{}/tcp/{}", node_info.ip, conf.libp2p.port),
|
||||
public_key: keypair.public_key().to_base58(),
|
||||
};
|
||||
peers.push(peer);
|
||||
|
||||
println!("Loaded config for node {node_name}",);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(peers)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use futures_util::future::Either;
|
||||
use log::{info, trace};
|
||||
use reqwest::Url;
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
|
||||
use crate::ephemera_api::ApplicationResult;
|
||||
use crate::utilities::codec::{Codec, EphemeraCodec};
|
||||
use crate::{
|
||||
api::application::CheckBlockResult,
|
||||
cli::PEERS_CONFIG_FILE,
|
||||
config::Configuration,
|
||||
crypto::EphemeraKeypair,
|
||||
crypto::Keypair,
|
||||
ephemera_api::{ApiBlock, ApiEphemeraMessage, Application, Dummy, RawApiEphemeraMessage},
|
||||
membership::HttpMembersProvider,
|
||||
network::members::ConfigMembersProvider,
|
||||
EphemeraStarterInit,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HttpMembersProviderArg {
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
impl FromStr for HttpMembersProviderArg {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(HttpMembersProviderArg { url: s.parse()? })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct RunExternalNodeCmd {
|
||||
#[clap(short, long)]
|
||||
pub config_file: String,
|
||||
#[clap(short, long)]
|
||||
pub peers_config: String,
|
||||
}
|
||||
|
||||
impl RunExternalNodeCmd {
|
||||
/// # Errors
|
||||
/// If the members provider cannot be created.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the ephemera cannot be created.
|
||||
pub async fn execute(&self) -> anyhow::Result<()> {
|
||||
let ephemera_conf = match Configuration::try_load(self.config_file.clone()) {
|
||||
Ok(conf) => conf,
|
||||
Err(err) => anyhow::bail!("Error loading configuration file: {err:?}"),
|
||||
};
|
||||
|
||||
let members_provider = Self::config_members_provider_with_path(self.peers_config.clone())?;
|
||||
let ephemera = EphemeraStarterInit::new(ephemera_conf.clone())
|
||||
.unwrap()
|
||||
.with_application(Dummy)
|
||||
.with_members_provider(members_provider)?
|
||||
.build();
|
||||
|
||||
let mut ephemera_shutdown = ephemera.ephemera_handle.shutdown.clone();
|
||||
let mut ephemera_shutdown_internal = ephemera_shutdown.clone();
|
||||
|
||||
let ephemera_handle = tokio::spawn(ephemera.run());
|
||||
|
||||
let shutdown = async {
|
||||
let mut stream_int = signal(SignalKind::interrupt()).unwrap();
|
||||
let mut stream_term = signal(SignalKind::terminate()).unwrap();
|
||||
tokio::select! {
|
||||
_ = stream_int.recv() => {
|
||||
ephemera_shutdown.shutdown().expect("Failed to shutdown");
|
||||
}
|
||||
_ = stream_term.recv() => {
|
||||
ephemera_shutdown.shutdown().expect("Failed to shutdown");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match futures::future::select(Box::pin(shutdown), ephemera_handle).await {
|
||||
Either::Left((_, ephemera_exited)) => {
|
||||
info!("Shutdown signal received, shutting down");
|
||||
ephemera_exited.await.unwrap();
|
||||
info!("Shutdown complete");
|
||||
}
|
||||
Either::Right((_, _)) => {
|
||||
info!("Ephemera failed, trying graceful shutdown...");
|
||||
ephemera_shutdown_internal
|
||||
.shutdown()
|
||||
.expect("Failed to shutdown");
|
||||
info!("Shutdown complete");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn config_members_provider() -> anyhow::Result<ConfigMembersProvider> {
|
||||
let peers_conf_path = Configuration::ephemera_root_dir()
|
||||
.unwrap()
|
||||
.join(PEERS_CONFIG_FILE);
|
||||
|
||||
let peers_conf = match ConfigMembersProvider::init(peers_conf_path) {
|
||||
Ok(conf) => conf,
|
||||
Err(err) => anyhow::bail!("Error loading peers file: {err:?}"),
|
||||
};
|
||||
Ok(peers_conf)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn config_members_provider_with_path(
|
||||
peers_conf_path: String,
|
||||
) -> anyhow::Result<ConfigMembersProvider> {
|
||||
let peers_conf = match ConfigMembersProvider::init(peers_conf_path) {
|
||||
Ok(conf) => conf,
|
||||
Err(err) => anyhow::bail!("Error loading peers file: {err:?}"),
|
||||
};
|
||||
Ok(peers_conf)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn http_members_provider(url: String) -> HttpMembersProvider {
|
||||
HttpMembersProvider::new(url)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SignatureVerificationApplication {
|
||||
keypair: Arc<Keypair>,
|
||||
}
|
||||
|
||||
impl SignatureVerificationApplication {
|
||||
#[must_use]
|
||||
pub fn new(keypair: Arc<Keypair>) -> Self {
|
||||
Self { keypair }
|
||||
}
|
||||
|
||||
pub(crate) fn verify_message(&self, msg: ApiEphemeraMessage) -> anyhow::Result<()> {
|
||||
let signature = msg.certificate.clone();
|
||||
let raw_message: RawApiEphemeraMessage = msg.into();
|
||||
let encoded_message = Codec::encode(&raw_message)?;
|
||||
if self
|
||||
.keypair
|
||||
.verify(&encoded_message, &signature.signature.into())
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
anyhow::bail!("Invalid signature")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Application for SignatureVerificationApplication {
|
||||
fn check_tx(&self, tx: ApiEphemeraMessage) -> ApplicationResult<bool> {
|
||||
trace!("SignatureVerificationApplicationHook::check_tx");
|
||||
self.verify_message(tx)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn check_block(&self, _block: &ApiBlock) -> ApplicationResult<CheckBlockResult> {
|
||||
Ok(CheckBlockResult::Accept)
|
||||
}
|
||||
|
||||
fn deliver_block(&self, _block: ApiBlock) -> ApplicationResult<()> {
|
||||
trace!("SignatureVerificationApplicationHook::deliver_block");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
//! Configuration for Ephemeris node. It contains mandatory settings for a node to start.
|
||||
//!
|
||||
//! Default location for the configuration file is `~/.ephemera/ephemera.toml`.
|
||||
//! Or relative to a node specific directory `~/.ephemera/<node_name>/ephemera.toml`.
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use config::ConfigError;
|
||||
use log::{error, info};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
//TODO - validate configuration at load time
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct Configuration {
|
||||
/// Configuration related to node instance identity
|
||||
pub node: NodeConfiguration,
|
||||
/// Configuration for libp2p network
|
||||
pub libp2p: Libp2pConfiguration,
|
||||
/// Configuration for Ephemera embedded database
|
||||
pub storage: DatabaseConfiguration,
|
||||
/// Configuration for websocket
|
||||
pub websocket: WebsocketConfiguration,
|
||||
/// Configuration for embedded http API server
|
||||
pub http: HttpConfiguration,
|
||||
/// Configuration related to block creation
|
||||
pub block_manager: BlockManagerConfiguration,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct NodeConfiguration {
|
||||
/// Node IP shared by all Ephemera services like libp2p, websocket, http.
|
||||
/// If separate IP/DNS is needed for each service, it should be configured outside of Ephemera.
|
||||
pub ip: String,
|
||||
//FIXME: dev only
|
||||
/// If private key is stored in configuration as plain text, read it from here.
|
||||
/// Private key is mandatory for a node to be able to function in the network.
|
||||
/// It is used to signe protocol messages and identify node in the network.
|
||||
pub private_key: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MembershipKind {
|
||||
/// Mandatory minimum membership size is defined by threshold of all peers returned by membership provider.
|
||||
/// Threshold value is defined the ratio of peers that need to be available.
|
||||
/// For example, if the threshold is 0.5, then at least 50% of the peers need to be available.
|
||||
Threshold,
|
||||
/// Mandatory minimum membership size is all peers who are online.
|
||||
AnyOnline,
|
||||
/// Mandatory minimum membership size is all peers returned by membership provider.
|
||||
AllOnline,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct Libp2pConfiguration {
|
||||
/// Port to listen on for libp2p internal connections
|
||||
pub port: u16,
|
||||
/// Gossipsub topic to gossip Ephemera messages between peers. Ephemera listens messages
|
||||
/// only from this topic. Invalid topic configuration means that Ephemera is not able to
|
||||
/// reach messages from other peers.
|
||||
pub ephemera_msg_topic_name: String,
|
||||
/// Gossipsub interval to check its mesh health.
|
||||
pub heartbeat_interval_sec: u64,
|
||||
/// How often Ephemera checks its membership rendezvous endpoint. It's configurable with second granularity.
|
||||
/// But in general it's up to the user and depends how rendezvous endpoint is configured.
|
||||
///
|
||||
/// Ephemera uses rendezvous endpoint as authority to tell which nodes are authorized to participate.
|
||||
/// So it should be configured and implemented in a manner that nodes always have the most up to date and
|
||||
/// accurate information.
|
||||
pub members_provider_delay_sec: u64,
|
||||
/// Defines how the actual membership is decided. See `[ephemera:]` for more details.
|
||||
pub membership_kind: MembershipKind,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct DatabaseConfiguration {
|
||||
/// Path to the RocksDb database directory
|
||||
pub rocksdb_path: String,
|
||||
/// Path to the SQLite database file
|
||||
pub sqlite_path: String,
|
||||
/// If to create database if it does not exist
|
||||
pub create_if_not_exists: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct WebsocketConfiguration {
|
||||
/// Port to listen on for WebSocket subscriptions.
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct HttpConfiguration {
|
||||
/// Port to listen on for HTTP API requests
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct BlockManagerConfiguration {
|
||||
/// By default every node is block producer.
|
||||
///
|
||||
/// But in trusted settings it might be useful configure only one or selected nodes to be block producer.
|
||||
/// The rest of nodes still participate in the message gossiping and reliable broadcast.
|
||||
pub producer: bool,
|
||||
/// Interval in seconds between block creation
|
||||
/// Blocks are "proposed" at this interval.
|
||||
pub creation_interval_sec: u64,
|
||||
/// Ephemera creates blocks at fixed interval and doesn't have any consensus algorithm to make progress
|
||||
/// if the most recent block fails to go through reliable broadcast.
|
||||
/// This flag tells what to do when at next interval previous block is not yet delivered.
|
||||
///
|
||||
/// If true, Ephemera will repeat messages from the previous block. Otherwise it will take all messages
|
||||
/// from mempool as normally.
|
||||
pub repeat_last_block_messages: bool,
|
||||
}
|
||||
|
||||
impl BlockManagerConfiguration {
|
||||
pub fn new(producer: bool, creation_interval_sec: u64, repeat_last_block: bool) -> Self {
|
||||
BlockManagerConfiguration {
|
||||
producer,
|
||||
creation_interval_sec,
|
||||
repeat_last_block_messages: repeat_last_block,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// This is returned if configuration file exists and user tries to create new one.
|
||||
#[error("Configuration file exists: '{0}'")]
|
||||
Exists(String),
|
||||
/// This is returned if configuration file does not exist and user tries to load it.
|
||||
#[error("Configuration file does not exists: '{0}'")]
|
||||
NotFound(String),
|
||||
#[error("Configuration file does not exist")]
|
||||
/// This is returned if IoError happens during configuration file read/write.
|
||||
IoError(#[from] std::io::Error),
|
||||
/// This is returned if configuration file is invalid.
|
||||
#[error("Configuration file is invalid: '{0}'")]
|
||||
InvalidFormat(String),
|
||||
/// This is returned if configuration file path is invalid.
|
||||
#[error("Configuration file path is invalid: '{0}'")]
|
||||
InvalidPath(String),
|
||||
/// Technical error happens during parsing.
|
||||
#[error("{}", .0)]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<ConfigError> for Error {
|
||||
fn from(err: ConfigError) -> Self {
|
||||
match err {
|
||||
ConfigError::NotFound(err) => Error::NotFound(err),
|
||||
ConfigError::PathParse(err) => {
|
||||
Error::InvalidPath(format!("Invalid path to configuration file: {err:?}",))
|
||||
}
|
||||
ConfigError::FileParse { uri, cause } => {
|
||||
Error::InvalidFormat(format!("Invalid configuration file: {uri:?}: {cause:?}",))
|
||||
}
|
||||
_ => Error::Other(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const EPHEMERA_DIR_NAME: &str = ".ephemera";
|
||||
const EPHEMERA_CONFIG_FILE: &str = "ephemera.toml";
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl Configuration {
|
||||
/// Tries to read Ephemera node configuration file (`ephemera.toml`) from the given path.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `path` - Path to the configuration file.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the configuration file does not exist or is invalid.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use ephemera::configuration::Configuration;
|
||||
///
|
||||
/// let config = Configuration::try_load("/ephemera/ephemera.toml");
|
||||
/// ```
|
||||
pub fn try_load<P: Into<PathBuf>>(path: P) -> Result<Configuration> {
|
||||
let buf = path.into();
|
||||
log::debug!("Loading configuration from {:?}", buf);
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::File::from(buf))
|
||||
.build()
|
||||
.map_err(Error::from)?;
|
||||
|
||||
config.try_deserialize().map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Tries to read Ephemera node configuration from default
|
||||
/// default Ephemera configuration directory(`~.ephemera`). Full path resolves
|
||||
/// as `~/.ephemera/<node_name>/<file>`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `node_name` - Name of the node.
|
||||
/// * `file` - Name of the configuration file.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the configuration file does not exist or is invalid.
|
||||
pub fn try_load_node_from_home_dir(node_name: &str, file: &str) -> Result<Configuration> {
|
||||
let file_path = Self::ephemera_node_dir(node_name)?.join(file);
|
||||
Configuration::try_load(file_path)
|
||||
}
|
||||
|
||||
/// Tries to read Ephemera node configuration from default Ephemera configuration directory(`~.ephemera`).
|
||||
/// Full path resolves as `~/.ephemera/<node_name>/ephemera.toml`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `node_name` - Name of the node.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the configuration file does not exist or is invalid.
|
||||
pub fn try_load_from_home_dir(node_name: &str) -> Result<Configuration> {
|
||||
let file_path = Configuration::ephemera_config_file_home(node_name)?;
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::File::from(file_path))
|
||||
.build()
|
||||
.map_err(Error::from)?;
|
||||
|
||||
config.try_deserialize().map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Tries to write(create) Ephemera node configuration file (`ephemera.toml`) relative to default
|
||||
/// Ephemera configuration directory(`~.ephemera`). Full path resolves as `~/.ephemera/<node_name>/ephemera.toml`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `node_name` - Name of the node.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the configuration file already exists.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the configuration file cannot be written.
|
||||
pub fn try_write_home_dir(&self, node_name: &str) -> Result<()> {
|
||||
let conf_path = Configuration::ephemera_node_dir(node_name)?;
|
||||
if !conf_path.exists() {
|
||||
std::fs::create_dir_all(conf_path)?;
|
||||
}
|
||||
|
||||
let file_path = Configuration::ephemera_config_file_home(node_name)?;
|
||||
if file_path.exists() {
|
||||
return Err(Error::Exists(file_path.to_str().unwrap().to_string()));
|
||||
}
|
||||
|
||||
self.write(&file_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to write(update) Ephemera node configuration file (`ephemera.toml`) relative to default
|
||||
/// Ephemera configuration directory(`~.ephemera`). Full path resolves as `~/.ephemera/<node_name>/ephemera.toml`.
|
||||
/// If the file does not exist, update will be refused.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `node_name` - Name of the node.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the configuration file does not exist.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the configuration file cannot be written.
|
||||
pub fn try_update_home_dir(&self, node_name: &str) -> Result<()> {
|
||||
let file_path = Configuration::ephemera_config_file_home(node_name)?;
|
||||
if !file_path.exists() {
|
||||
error!(
|
||||
"Configuration file does not exist {}",
|
||||
file_path.to_str().unwrap()
|
||||
);
|
||||
return Err(Error::NotFound(file_path.to_str().unwrap().to_string()));
|
||||
}
|
||||
self.write(&file_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns node configuration file path relative to default Ephemera configuration directory(`~.ephemera`).
|
||||
/// Full path resolves as `~/.ephemera/<node_name>/ephemera.toml`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `node_name` - Name of the node.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the configuration file path cannot be resolved.
|
||||
pub fn ephemera_config_file_home(node_name: &str) -> Result<PathBuf> {
|
||||
Ok(Self::ephemera_node_dir(node_name)?.join(EPHEMERA_CONFIG_FILE))
|
||||
}
|
||||
|
||||
/// Returns default Ephemera configuration directory(`~.ephemera`).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the configuration directory cannot be resolved.
|
||||
pub fn ephemera_root_dir() -> Result<PathBuf> {
|
||||
dirs::home_dir()
|
||||
.map(|home| home.join(EPHEMERA_DIR_NAME))
|
||||
.ok_or(Error::Other("Could not find home directory".to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn ephemera_node_dir(node_name: &str) -> Result<PathBuf> {
|
||||
Ok(Self::ephemera_root_dir()?.join(node_name))
|
||||
}
|
||||
|
||||
fn write(&self, file_path: &PathBuf) -> Result<()> {
|
||||
//TODO: use toml or config crate, not both
|
||||
let config = toml::to_string(&self).map_err(|e| {
|
||||
Error::InvalidFormat(format!("Failed to serialize configuration: {e}",))
|
||||
})?;
|
||||
|
||||
let config = format!(
|
||||
"#This file is generated by cli and automatically overwritten every time when cli is run\n{config}",
|
||||
);
|
||||
|
||||
if file_path.exists() {
|
||||
info!("Updating configuration file: '{}'", file_path.display());
|
||||
} else {
|
||||
info!("Writing configuration to file: '{}'", file_path.display());
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::create(file_path)?;
|
||||
file.write_all(config.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use log::{debug, error, trace};
|
||||
use lru::LruCache;
|
||||
use tokio::sync::oneshot::Sender;
|
||||
|
||||
use crate::api::types::{ApiBlockBroadcastInfo, ApiBroadcastInfo};
|
||||
use crate::api::{DhtKV, DhtKey, DhtValue};
|
||||
use crate::ephemera_api::ApiEphemeraMessage;
|
||||
use crate::peer::ToPeerId;
|
||||
use crate::{
|
||||
api::{
|
||||
self,
|
||||
application::Application,
|
||||
types::{ApiBlock, ApiCertificate, ApiError},
|
||||
ToEphemeraApiCmd,
|
||||
},
|
||||
block::{manager::BlockManagerError, types::message},
|
||||
crypto::EphemeraKeypair,
|
||||
ephemera_api::ApiEphemeraConfig,
|
||||
network::libp2p::ephemera_sender::EphemeraEvent,
|
||||
Ephemera,
|
||||
};
|
||||
|
||||
type DhtPendingQueryReply = Sender<Result<Option<(Vec<u8>, Vec<u8>)>, ApiError>>;
|
||||
|
||||
pub(crate) struct ApiCmdProcessor {
|
||||
pub(crate) dht_query_cache: LruCache<Vec<u8>, Vec<DhtPendingQueryReply>>,
|
||||
}
|
||||
|
||||
impl ApiCmdProcessor {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
dht_query_cache: LruCache::new(NonZeroUsize::new(1000).unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn process_api_requests<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
cmd: ToEphemeraApiCmd,
|
||||
) -> api::Result<()> {
|
||||
trace!("Processing API request: {:?}", cmd);
|
||||
match cmd {
|
||||
ToEphemeraApiCmd::SubmitEphemeraMessage(api_msg, reply) => {
|
||||
// Ask application to decide if we should accept this message.
|
||||
Self::submit_message(ephemera, api_msg, reply).await?;
|
||||
}
|
||||
|
||||
ToEphemeraApiCmd::QueryBlockByHash(block_hash, reply) => {
|
||||
Self::query_block_by_hash(ephemera, &block_hash, reply).await;
|
||||
}
|
||||
|
||||
ToEphemeraApiCmd::QueryBlockByHeight(height, reply) => {
|
||||
Self::query_block_by_height(ephemera, height, reply).await;
|
||||
}
|
||||
|
||||
ToEphemeraApiCmd::QueryLastBlock(reply) => {
|
||||
Self::query_last_block(ephemera, reply).await;
|
||||
}
|
||||
|
||||
ToEphemeraApiCmd::QueryBlockCertificates(block_id, reply) => {
|
||||
Self::query_block_certificates(ephemera, &block_id, reply).await;
|
||||
}
|
||||
|
||||
ToEphemeraApiCmd::QueryDht(key, reply) => {
|
||||
Self::query_dht(ephemera, key, reply).await;
|
||||
}
|
||||
|
||||
ToEphemeraApiCmd::StoreInDht(key, value, reply) => {
|
||||
Self::store_in_dht(ephemera, key, value, reply).await;
|
||||
}
|
||||
|
||||
ToEphemeraApiCmd::QueryEphemeraConfig(reply) => {
|
||||
Self::ephemera_config(ephemera, reply);
|
||||
}
|
||||
|
||||
ToEphemeraApiCmd::QueryBroadcastGroup(reply) => {
|
||||
Self::broadcast_group(ephemera, reply);
|
||||
}
|
||||
ToEphemeraApiCmd::QueryBlockBroadcastInfo(hash, reply) => {
|
||||
Self::query_block_broadcast_info(ephemera, &hash, reply).await;
|
||||
}
|
||||
ToEphemeraApiCmd::VerifyMessageInBlock(block_hash, message_hash, index, reply) => {
|
||||
Self::verify_message_in_block(ephemera, block_hash, message_hash, index, reply)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn broadcast_group<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
reply: Sender<api::Result<ApiBroadcastInfo>>,
|
||||
) {
|
||||
let group_peers = ephemera.broadcast_group.current();
|
||||
|
||||
let bc = ApiBroadcastInfo::new(group_peers.clone(), ephemera.node_info.peer_id);
|
||||
reply
|
||||
.send(Ok(bc))
|
||||
.expect("Error sending BroadcastGroup response to api");
|
||||
}
|
||||
|
||||
fn ephemera_config<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
reply: Sender<api::Result<ApiEphemeraConfig>>,
|
||||
) {
|
||||
let node_info = ephemera.node_info.clone();
|
||||
let api_config = ApiEphemeraConfig {
|
||||
protocol_address: node_info.protocol_address(),
|
||||
api_address: node_info.api_address_http(),
|
||||
websocket_address: node_info.ws_address_ws(),
|
||||
public_key: node_info.keypair.public_key().to_string(),
|
||||
block_producer: node_info.initial_config.block_manager.producer,
|
||||
block_creation_interval_sec: node_info
|
||||
.initial_config
|
||||
.block_manager
|
||||
.creation_interval_sec,
|
||||
};
|
||||
reply
|
||||
.send(Ok(api_config))
|
||||
.expect("Error sending EphemeraConfig response to api");
|
||||
}
|
||||
|
||||
async fn store_in_dht<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
key: DhtKey,
|
||||
value: DhtValue,
|
||||
reply: Sender<api::Result<()>>,
|
||||
) {
|
||||
let response = match ephemera
|
||||
.to_network
|
||||
.send_ephemera_event(EphemeraEvent::StoreInDht { key, value })
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
error!("Error sending StoreInDht to network: {:?}", err);
|
||||
Err(ApiError::Internal("Failed to store in DHT".to_string()))
|
||||
}
|
||||
};
|
||||
reply
|
||||
.send(response)
|
||||
.expect("Error sending StoreInDht response to api");
|
||||
}
|
||||
|
||||
async fn query_dht<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
key: DhtKey,
|
||||
reply: Sender<api::Result<Option<DhtKV>>>,
|
||||
) {
|
||||
match ephemera
|
||||
.to_network
|
||||
.send_ephemera_event(EphemeraEvent::QueryDht { key: key.clone() })
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
//Save the reply channel in a map and send the reply when we get the response from the network
|
||||
ephemera
|
||||
.api_cmd_processor
|
||||
.dht_query_cache
|
||||
.get_or_insert_mut(key, Vec::new)
|
||||
.push(reply);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error sending QueryDht to network: {:?}", err);
|
||||
reply
|
||||
.send(Err(ApiError::Internal("Failed to query DHT".to_string())))
|
||||
.expect("Error sending QueryDht response to api");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async fn query_block_certificates<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
block_id: &str,
|
||||
reply: Sender<api::Result<Option<Vec<ApiCertificate>>>>,
|
||||
) {
|
||||
let response = match ephemera
|
||||
.storage
|
||||
.lock()
|
||||
.await
|
||||
.get_block_certificates(block_id)
|
||||
{
|
||||
Ok(signatures) => {
|
||||
let certificates = signatures.map(|s| {
|
||||
s.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<ApiCertificate>>()
|
||||
});
|
||||
Ok(certificates)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error querying block certificates: {:?}", err);
|
||||
Err(ApiError::Internal(
|
||||
"Failed to query block certificates".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
reply
|
||||
.send(response)
|
||||
.expect("Error sending QueryBlockSignatures response to api");
|
||||
}
|
||||
|
||||
async fn query_last_block<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
reply: Sender<api::Result<ApiBlock>>,
|
||||
) {
|
||||
let response = match ephemera.storage.lock().await.get_last_block() {
|
||||
Ok(Some(block)) => Ok(block.into()),
|
||||
Ok(None) => Err(ApiError::Internal(
|
||||
"No blocks found, this is a bug!".to_string(),
|
||||
)),
|
||||
Err(err) => {
|
||||
error!("Error querying last block: {:?}", err);
|
||||
Err(ApiError::Internal("Failed to query last block".to_string()))
|
||||
}
|
||||
};
|
||||
reply
|
||||
.send(response)
|
||||
.expect("Error sending QueryLastBlock response to api");
|
||||
}
|
||||
|
||||
async fn query_block_by_height<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
height: u64,
|
||||
reply: Sender<api::Result<Option<ApiBlock>>>,
|
||||
) {
|
||||
let response = match ephemera.storage.lock().await.get_block_by_height(height) {
|
||||
Ok(Some(block)) => {
|
||||
let api_block: ApiBlock = block.into();
|
||||
Ok(api_block.into())
|
||||
}
|
||||
Ok(None) => Ok(None),
|
||||
Err(err) => {
|
||||
error!("Error querying block by height: {:?}", err);
|
||||
Err(ApiError::Internal(
|
||||
"Failed to query block by height".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
reply
|
||||
.send(response)
|
||||
.expect("Error sending QueryBlockByHeight response to api");
|
||||
}
|
||||
|
||||
async fn query_block_by_hash<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
block_hash: &str,
|
||||
reply: Sender<api::Result<Option<ApiBlock>>>,
|
||||
) {
|
||||
let response = match ephemera.storage.lock().await.get_block_by_hash(block_hash) {
|
||||
Ok(Some(block)) => {
|
||||
let api_block: ApiBlock = block.into();
|
||||
Ok(api_block.into())
|
||||
}
|
||||
Ok(None) => Ok(None),
|
||||
Err(err) => {
|
||||
error!("Error querying block by id: {:?}", err);
|
||||
Err(ApiError::Internal(
|
||||
"Failed to query block by id".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
reply
|
||||
.send(response)
|
||||
.expect("Error sending QueryBlockByHash response to api");
|
||||
}
|
||||
|
||||
async fn submit_message<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
api_msg: Box<ApiEphemeraMessage>,
|
||||
reply: Sender<api::Result<()>>,
|
||||
) -> api::Result<()> {
|
||||
let response = match ephemera.application.check_tx(*api_msg.clone()) {
|
||||
Ok(true) => {
|
||||
trace!("Application accepted ephemera message: {:?}", api_msg);
|
||||
|
||||
// Send to BlockManager to verify it and put into memory pool
|
||||
let ephemera_msg: message::EphemeraMessage = (*api_msg).into();
|
||||
match ephemera.block_manager.on_new_message(ephemera_msg.clone()) {
|
||||
Ok(_) => {
|
||||
//Gossip to network for other nodes to receive
|
||||
match ephemera
|
||||
.to_network
|
||||
.send_ephemera_event(EphemeraEvent::EphemeraMessage(
|
||||
ephemera_msg.into(),
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
error!("Error sending EphemeraMessage to network: {:?}", err);
|
||||
Err(ApiError::Internal("Failed to submit message".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => match err {
|
||||
BlockManagerError::DuplicateMessage(_) => Err(ApiError::DuplicateMessage),
|
||||
BlockManagerError::BlockManager(err) => {
|
||||
error!("Error submitting message to block manager: {:?}", err);
|
||||
Err(ApiError::Internal("Failed to submit message".to_string()))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
debug!("Application rejected ephemera message: {:?}", api_msg);
|
||||
Err(ApiError::ApplicationRejectedMessage)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Application rejected ephemera message: {:?}", err);
|
||||
Err(ApiError::Application(err))
|
||||
}
|
||||
};
|
||||
reply
|
||||
.send(response)
|
||||
.expect("Error sending SubmitEphemeraMessage response to api");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn query_block_broadcast_info<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
block_id: &str,
|
||||
reply: Sender<api::Result<Option<ApiBlockBroadcastInfo>>>,
|
||||
) {
|
||||
let response = match ephemera
|
||||
.storage
|
||||
.lock()
|
||||
.await
|
||||
.get_block_broadcast_group(block_id)
|
||||
{
|
||||
Ok(Some(peers)) => {
|
||||
let local_peer = ephemera.node_info.keypair.peer_id();
|
||||
Ok(Some(ApiBlockBroadcastInfo::new(local_peer, peers)))
|
||||
}
|
||||
Ok(None) => Ok(None),
|
||||
Err(err) => {
|
||||
error!("Error querying block broadcast info: {:?}", err);
|
||||
Err(ApiError::Internal(
|
||||
"Failed to query block broadcast info".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
reply
|
||||
.send(response)
|
||||
.expect("Error sending QueryBlockBroadcastGroup response to api");
|
||||
}
|
||||
async fn verify_message_in_block<A: Application>(
|
||||
ephemera: &mut Ephemera<A>,
|
||||
block_hash: String,
|
||||
message_hash: String,
|
||||
index: usize,
|
||||
reply: Sender<api::Result<bool>>,
|
||||
) {
|
||||
let message_hash_hash = message_hash.parse();
|
||||
if message_hash_hash.is_err() {
|
||||
reply
|
||||
.send(Err(ApiError::InvalidHash(
|
||||
"Failed to parse message hash".to_string(),
|
||||
)))
|
||||
.expect("Error sending VerifyMessageInBlock response to api");
|
||||
return;
|
||||
}
|
||||
|
||||
let message_hash_hash = message_hash_hash.unwrap();
|
||||
|
||||
let storage = ephemera.storage.lock().await;
|
||||
match storage.get_block_merkle_tree(&block_hash) {
|
||||
Ok(Some(tree)) => {
|
||||
let result = tree.verify_leaf_at_index(message_hash_hash, index);
|
||||
reply
|
||||
.send(Ok(result))
|
||||
.expect("Error sending VerifyMessageInBlock response to api");
|
||||
}
|
||||
Ok(None) => {
|
||||
reply
|
||||
.send(Ok(false))
|
||||
.expect("Error sending VerifyMessageInBlock response to api");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error querying block merkle tree: {:?}", err);
|
||||
reply
|
||||
.send(Err(ApiError::Internal(
|
||||
"Failed to verify message".to_string(),
|
||||
)))
|
||||
.expect("Error sending VerifyMessageInBlock response to api");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
use std::fmt::Display;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_util::FutureExt;
|
||||
use log::{debug, error, info};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::core::shutdown::Shutdown;
|
||||
#[cfg(feature = "rocksdb_storage")]
|
||||
use crate::storage::rocksdb::RocksDbStorage;
|
||||
#[cfg(feature = "sqlite_storage")]
|
||||
use crate::storage::sqlite::SqliteStorage;
|
||||
use crate::{
|
||||
api::{application::Application, http, ApiListener, CommandExecutor},
|
||||
block::{builder::BlockManagerBuilder, manager::BlockManager},
|
||||
broadcast::bracha::broadcast::Broadcaster,
|
||||
broadcast::group::BroadcastGroup,
|
||||
config::Configuration,
|
||||
core::{
|
||||
api_cmd::ApiCmdProcessor,
|
||||
shutdown::{Handle, ShutdownManager},
|
||||
},
|
||||
crypto::Keypair,
|
||||
membership,
|
||||
membership::PeerInfo,
|
||||
network::libp2p::{
|
||||
ephemera_sender::EphemeraToNetworkSender, network_sender::NetCommunicationReceiver,
|
||||
swarm_network::SwarmNetwork,
|
||||
},
|
||||
peer::{PeerId, ToPeerId},
|
||||
storage::EphemeraDatabase,
|
||||
utilities::crypto::key_manager::KeyManager,
|
||||
websocket::ws_manager::{WsManager, WsMessageBroadcaster},
|
||||
Ephemera,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct NodeInfo {
|
||||
pub(crate) ip: String,
|
||||
pub(crate) protocol_port: u16,
|
||||
pub(crate) http_port: u16,
|
||||
pub(crate) ws_port: u16,
|
||||
pub(crate) peer_id: PeerId,
|
||||
pub(crate) keypair: Arc<Keypair>,
|
||||
pub(crate) initial_config: Configuration,
|
||||
}
|
||||
|
||||
impl NodeInfo {
|
||||
pub(crate) fn new(config: Configuration) -> anyhow::Result<Self> {
|
||||
let keypair = KeyManager::read_keypair_from_str(&config.node.private_key)?;
|
||||
let info = Self {
|
||||
ip: config.node.ip.clone(),
|
||||
protocol_port: config.libp2p.port,
|
||||
http_port: config.http.port,
|
||||
ws_port: config.websocket.port,
|
||||
peer_id: keypair.peer_id(),
|
||||
keypair,
|
||||
initial_config: config,
|
||||
};
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
pub(crate) fn protocol_address(&self) -> String {
|
||||
format!("/ip4/{}/tcp/{}", self.ip, self.protocol_port)
|
||||
}
|
||||
|
||||
pub(crate) fn api_address_http(&self) -> String {
|
||||
format!("http://{}:{}", self.ip, self.http_port)
|
||||
}
|
||||
|
||||
pub(crate) fn ws_address_ws(&self) -> String {
|
||||
format!("ws://{}:{}", self.ip, self.ws_port)
|
||||
}
|
||||
|
||||
pub(crate) fn ws_address_ip_port(&self) -> String {
|
||||
format!("{}:{}", self.ip, self.ws_port)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NodeInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"NodeInfo {{ ip: {}, protocol_port: {}, http_port: {}, ws_port: {}, peer_id: {}, keypair: {} }}",
|
||||
self.ip,
|
||||
self.protocol_port,
|
||||
self.http_port,
|
||||
self.ws_port,
|
||||
self.peer_id,
|
||||
self.keypair
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EphemeraHandle {
|
||||
/// Ephemera API
|
||||
pub api: CommandExecutor,
|
||||
/// Allows to send shutdown signal to the node
|
||||
pub shutdown: Handle,
|
||||
}
|
||||
|
||||
pub struct EphemeraStarterInit {
|
||||
config: Configuration,
|
||||
node_info: NodeInfo,
|
||||
broadcaster: Broadcaster,
|
||||
api_listener: ApiListener,
|
||||
api: CommandExecutor,
|
||||
}
|
||||
|
||||
impl EphemeraStarterInit {
|
||||
/// Initialize Ephemera builder
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `config` - [Configuration]
|
||||
///
|
||||
/// # Returns
|
||||
/// [`EphemeraStarterInit`]
|
||||
///
|
||||
/// # Errors
|
||||
/// * If the node configuration is invalid
|
||||
pub fn new(config: Configuration) -> anyhow::Result<Self> {
|
||||
let instance_info = NodeInfo::new(config.clone())?;
|
||||
let broadcaster = Broadcaster::new(instance_info.peer_id);
|
||||
let (api, api_listener) = CommandExecutor::new();
|
||||
|
||||
let builder = EphemeraStarterInit {
|
||||
config,
|
||||
node_info: instance_info,
|
||||
broadcaster,
|
||||
api_listener,
|
||||
api,
|
||||
};
|
||||
Ok(builder)
|
||||
}
|
||||
|
||||
pub fn with_application<A: Application>(
|
||||
self,
|
||||
application: A,
|
||||
) -> EphemeraStarterWithApplication<A> {
|
||||
EphemeraStarterWithApplication {
|
||||
init: self,
|
||||
application,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ServiceInfo {
|
||||
ws_message_broadcast: Option<WsMessageBroadcaster>,
|
||||
from_network: Option<NetCommunicationReceiver>,
|
||||
to_network: Option<EphemeraToNetworkSender>,
|
||||
}
|
||||
|
||||
pub struct EphemeraStarterWithApplication<A: Application> {
|
||||
init: EphemeraStarterInit,
|
||||
application: A,
|
||||
}
|
||||
|
||||
impl<A: Application> EphemeraStarterWithApplication<A> {
|
||||
/// Initialize Ephemera with the given application.
|
||||
/// It also tries to open the database connection.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `application` - [Application] to be used
|
||||
///
|
||||
/// # Returns
|
||||
/// [`EphemeraStarterWithApplication`]
|
||||
///
|
||||
/// # Errors
|
||||
/// * If the node configuration is invalid or the database connection cannot be opened
|
||||
pub fn with_members_provider<
|
||||
P: Future<Output = membership::Result<Vec<PeerInfo>>> + Send + Unpin + 'static,
|
||||
>(
|
||||
mut self,
|
||||
provider: P,
|
||||
) -> anyhow::Result<EphemeraStarterWithProvider<A>> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "sqlite_storage")] {
|
||||
let mut storage = self.connect_sqlite()?;
|
||||
debug!("Connected to sqlite database")
|
||||
} else if #[cfg(feature = "rocksdb_storage")] {
|
||||
let mut storage = self.connect_rocksdb()?;
|
||||
debug!("Connected to rocksdb database")
|
||||
} else {
|
||||
compile_error!("Must enable either sqlite or rocksdb feature");
|
||||
}
|
||||
}
|
||||
|
||||
let block_manager = self.init_block_manager(&mut storage)?;
|
||||
|
||||
let (mut shutdown_manager, shutdown_handle) = ShutdownManager::init();
|
||||
|
||||
let mut service_data = ServiceInfo::default();
|
||||
let services = self.init_services(&mut service_data, &mut shutdown_manager, provider)?;
|
||||
|
||||
Ok(EphemeraStarterWithProvider {
|
||||
with_application: self,
|
||||
block_manager: Some(block_manager),
|
||||
service_data,
|
||||
services,
|
||||
storage: Some(Box::new(storage)),
|
||||
shutdown_manager: Some(shutdown_manager),
|
||||
shutdown_handle: Some(shutdown_handle),
|
||||
})
|
||||
}
|
||||
|
||||
//allocate database connection
|
||||
#[cfg(feature = "rocksdb_storage")]
|
||||
fn connect_rocksdb(&self) -> anyhow::Result<RocksDbStorage> {
|
||||
info!("Opening database...");
|
||||
RocksDbStorage::open(self.init.config.storage.clone())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open database: {}", e))
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite_storage")]
|
||||
fn connect_sqlite(&mut self) -> anyhow::Result<SqliteStorage> {
|
||||
info!("Opening database...");
|
||||
SqliteStorage::open(self.init.config.storage.clone())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open database: {}", e))
|
||||
}
|
||||
|
||||
fn init_block_manager<D: EphemeraDatabase + ?Sized>(
|
||||
&mut self,
|
||||
db: &mut D,
|
||||
) -> anyhow::Result<BlockManager> {
|
||||
let block_manager_configuration = self.init.config.block_manager.clone();
|
||||
let keypair = self.init.node_info.keypair.clone();
|
||||
let builder = BlockManagerBuilder::new(block_manager_configuration, keypair);
|
||||
builder.build(db)
|
||||
}
|
||||
|
||||
fn init_services<
|
||||
P: Future<Output = membership::Result<Vec<PeerInfo>>> + Send + Unpin + 'static,
|
||||
>(
|
||||
&mut self,
|
||||
service_data: &mut ServiceInfo,
|
||||
shutdown_manager: &mut ShutdownManager,
|
||||
provider: P,
|
||||
) -> anyhow::Result<Vec<BoxFuture<'static, anyhow::Result<()>>>> {
|
||||
let services = vec![
|
||||
self.init_libp2p(service_data, shutdown_manager.subscribe(), provider)?,
|
||||
self.init_http(shutdown_manager.subscribe())?,
|
||||
self.init_websocket(service_data, shutdown_manager.subscribe()),
|
||||
];
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
fn init_websocket(
|
||||
&mut self,
|
||||
service_data: &mut ServiceInfo,
|
||||
mut shutdown: Shutdown,
|
||||
) -> BoxFuture<'static, anyhow::Result<()>> {
|
||||
let (mut websocket, ws_message_broadcast) =
|
||||
WsManager::new(self.init.node_info.ws_address_ip_port());
|
||||
|
||||
service_data.ws_message_broadcast = Some(ws_message_broadcast);
|
||||
|
||||
async move {
|
||||
websocket.listen().await?;
|
||||
|
||||
tokio::select! {
|
||||
_ = shutdown.shutdown_signal_rcv.recv() => {
|
||||
info!("Shutting down websocket manager");
|
||||
}
|
||||
ws_stopped = websocket.run() => {
|
||||
match ws_stopped {
|
||||
Ok(_) => info!("Websocket stopped unexpectedly"),
|
||||
Err(e) => error!("Websocket stopped with error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Websocket task finished");
|
||||
Ok(())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn init_http(
|
||||
&mut self,
|
||||
mut shutdown: Shutdown,
|
||||
) -> anyhow::Result<BoxFuture<'static, anyhow::Result<()>>> {
|
||||
let http = http::init(&self.init.node_info, self.init.api.clone())?;
|
||||
|
||||
let fut = async move {
|
||||
let server_handle = http.handle();
|
||||
tokio::select! {
|
||||
_ = shutdown.shutdown_signal_rcv.recv() => {
|
||||
info!("Shutting down http server");
|
||||
server_handle.stop(true).await;
|
||||
}
|
||||
http_stopped = http => {
|
||||
match http_stopped {
|
||||
Ok(_) => info!("Http server stopped unexpectedly"),
|
||||
Err(e) => error!("Http server stopped with error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Http task finished");
|
||||
Ok(())
|
||||
}
|
||||
.boxed();
|
||||
Ok(fut)
|
||||
}
|
||||
|
||||
fn init_libp2p<
|
||||
P: Future<Output = membership::Result<Vec<PeerInfo>>> + Send + Unpin + 'static,
|
||||
>(
|
||||
&mut self,
|
||||
service_data: &mut ServiceInfo,
|
||||
mut shutdown: Shutdown,
|
||||
provider: P,
|
||||
) -> anyhow::Result<BoxFuture<'static, anyhow::Result<()>>> {
|
||||
info!("Starting network...",);
|
||||
|
||||
let (mut network, from_network, to_network) =
|
||||
SwarmNetwork::new(self.init.node_info.clone(), provider)?;
|
||||
|
||||
service_data.from_network = Some(from_network);
|
||||
service_data.to_network = Some(to_network);
|
||||
|
||||
let libp2p = async move {
|
||||
network.listen()?;
|
||||
|
||||
tokio::select! {
|
||||
_ = shutdown.shutdown_signal_rcv.recv() => {
|
||||
info!("Shutting down network");
|
||||
}
|
||||
nw_stopped = network.start() => {
|
||||
match nw_stopped {
|
||||
Ok(_) => info!("Network stopped unexpectedly"),
|
||||
Err(e) => error!("Network stopped with error: {e}",),
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Network task finished");
|
||||
Ok(())
|
||||
}
|
||||
.boxed();
|
||||
Ok(libp2p)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EphemeraStarterWithProvider<A>
|
||||
where
|
||||
A: Application + 'static,
|
||||
{
|
||||
with_application: EphemeraStarterWithApplication<A>,
|
||||
block_manager: Option<BlockManager>,
|
||||
service_data: ServiceInfo,
|
||||
storage: Option<Box<dyn EphemeraDatabase>>,
|
||||
services: Vec<BoxFuture<'static, anyhow::Result<()>>>,
|
||||
shutdown_manager: Option<ShutdownManager>,
|
||||
shutdown_handle: Option<Handle>,
|
||||
}
|
||||
|
||||
impl<A> EphemeraStarterWithProvider<A>
|
||||
where
|
||||
A: Application + 'static,
|
||||
{
|
||||
pub fn build(self) -> Ephemera<A> {
|
||||
self.ephemera()
|
||||
}
|
||||
|
||||
fn ephemera(mut self) -> Ephemera<A> {
|
||||
let ephemera_handle = EphemeraHandle {
|
||||
api: self.with_application.init.api,
|
||||
shutdown: self.shutdown_handle.take().unwrap(),
|
||||
};
|
||||
|
||||
let node_info = self.with_application.init.node_info;
|
||||
let application = self.with_application.application;
|
||||
let block_manager = self.block_manager.expect("Block manager not initialized");
|
||||
let broadcaster = self.with_application.init.broadcaster;
|
||||
let from_network = self
|
||||
.service_data
|
||||
.from_network
|
||||
.expect("From network not initialized");
|
||||
let to_network = self
|
||||
.service_data
|
||||
.to_network
|
||||
.expect("To network not initialized");
|
||||
let storage = self.storage.expect("Storage not initialized");
|
||||
let ws_message_broadcast = self
|
||||
.service_data
|
||||
.ws_message_broadcast
|
||||
.expect("WS message broadcast not initialized");
|
||||
let api_listener = self.with_application.init.api_listener;
|
||||
let shutdown_manager = self
|
||||
.shutdown_manager
|
||||
.expect("Shutdown manager not initialized");
|
||||
let services = self.services;
|
||||
|
||||
Ephemera {
|
||||
node_info,
|
||||
block_manager,
|
||||
broadcaster,
|
||||
from_network,
|
||||
to_network,
|
||||
broadcast_group: BroadcastGroup::new(),
|
||||
storage: Arc::new(Mutex::new(storage)),
|
||||
ws_message_broadcast,
|
||||
api_listener,
|
||||
api_cmd_processor: ApiCmdProcessor::new(),
|
||||
application: application.into(),
|
||||
ephemera_handle,
|
||||
shutdown_manager,
|
||||
services,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_util::StreamExt;
|
||||
use log::{debug, error, info, trace};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::broadcast::bracha::quorum::Quorum;
|
||||
use crate::storage::DatabaseError;
|
||||
use crate::{
|
||||
api::{application::Application, application::CheckBlockResult, ApiListener},
|
||||
block::{manager::BlockManager, types::block::Block},
|
||||
broadcast::{
|
||||
bracha::broadcast::BroadcastResponse, bracha::broadcast::Broadcaster,
|
||||
group::BroadcastGroup, RbMsg,
|
||||
},
|
||||
core::{
|
||||
api_cmd::ApiCmdProcessor,
|
||||
builder::{EphemeraHandle, NodeInfo},
|
||||
shutdown::ShutdownManager,
|
||||
},
|
||||
network::{
|
||||
libp2p::network_sender::GroupChangeEvent,
|
||||
libp2p::{
|
||||
ephemera_sender::{EphemeraEvent, EphemeraToNetworkSender},
|
||||
network_sender::{NetCommunicationReceiver, NetworkEvent},
|
||||
},
|
||||
},
|
||||
storage::EphemeraDatabase,
|
||||
utilities::crypto::Certificate,
|
||||
websocket::ws_manager::WsMessageBroadcaster,
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
enum EphemeraCoreError {
|
||||
#[error("DatabaseFailure: {0}")]
|
||||
DatabaseFailure(DatabaseError),
|
||||
//Just a placeholder now
|
||||
#[error("EphemeraCore: {0}")]
|
||||
EphemeraCore(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, EphemeraCoreError>;
|
||||
|
||||
pub struct Ephemera<A: Application> {
|
||||
/// Node info
|
||||
pub(crate) node_info: NodeInfo,
|
||||
|
||||
/// Block manager responsibility includes:
|
||||
/// - Block production and signing
|
||||
/// - Block verification for externally received blocks
|
||||
/// - Message verification sent by clients and gossiped other nodes
|
||||
pub(crate) block_manager: BlockManager,
|
||||
|
||||
/// Broadcaster is making sure that blocks are deterministically agreed by all nodes.
|
||||
pub(crate) broadcaster: Broadcaster,
|
||||
|
||||
/// A component which receives messages from network.
|
||||
pub(crate) from_network: NetCommunicationReceiver,
|
||||
|
||||
/// A component which sends messages to network.
|
||||
pub(crate) to_network: EphemeraToNetworkSender,
|
||||
|
||||
/// A component which keeps track of broadcast group over time.
|
||||
pub(crate) broadcast_group: BroadcastGroup,
|
||||
|
||||
/// A component which has mutable access to database.
|
||||
pub(crate) storage: Arc<Mutex<Box<dyn EphemeraDatabase>>>,
|
||||
|
||||
/// A component which broadcasts messages to websocket clients.
|
||||
pub(crate) ws_message_broadcast: WsMessageBroadcaster,
|
||||
|
||||
/// A component which listens API requests.
|
||||
pub(crate) api_listener: ApiListener,
|
||||
|
||||
/// A component which processes API requests.
|
||||
pub(crate) api_cmd_processor: ApiCmdProcessor,
|
||||
|
||||
/// An implementation of Application trait. Provides callbacks to broadcast.
|
||||
pub(crate) application: Arc<A>,
|
||||
|
||||
///Interface to external Rust code
|
||||
pub(crate) ephemera_handle: EphemeraHandle,
|
||||
|
||||
/// A component which handles shutdown.
|
||||
pub(crate) shutdown_manager: ShutdownManager,
|
||||
|
||||
/// A list of services which are running in background.
|
||||
pub(crate) services: Vec<BoxFuture<'static, anyhow::Result<()>>>,
|
||||
}
|
||||
|
||||
impl<A: Application> Ephemera<A> {
|
||||
///Provides external api for Rust code to interact with ephemera node.
|
||||
#[must_use]
|
||||
pub fn handle(&self) -> EphemeraHandle {
|
||||
self.ephemera_handle.clone()
|
||||
}
|
||||
|
||||
/// Main loop of ephemera node.
|
||||
/// 1. Block manager generates new blocks or receives blocks from network.
|
||||
/// 2. Run reliable broadcast.
|
||||
/// 3. Process reliable broadcast result.
|
||||
/// 4. Process http api request
|
||||
/// 5. Process rust api request
|
||||
/// 6. Publish(gossip) messages to network
|
||||
/// 7. Publish blocks to network
|
||||
/// 8. Broadcast messages to websocket clients
|
||||
pub async fn run(mut self) {
|
||||
info!("Starting ephemera services");
|
||||
for service in self.services.drain(..) {
|
||||
let handle = tokio::spawn(service);
|
||||
self.shutdown_manager.add_handle(handle);
|
||||
}
|
||||
|
||||
info!("Starting ephemera main loop");
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// GENERATING NEW BLOCKS
|
||||
Some((new_block, certificate)) = self.block_manager.next() => {
|
||||
if let Err(err) = self.process_new_local_block(new_block, certificate).await{
|
||||
error!("Error processing new block: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
// PROCESSING NETWORK EVENTS
|
||||
Some(net_event) = self.from_network.net_event_rcv.recv() => {
|
||||
if let Err(err) = self.process_network_event(net_event).await{
|
||||
error!("Error processing network event: {:?}", err);
|
||||
if let EphemeraCoreError::DatabaseFailure(_) = err {
|
||||
info!("Database failure. Shutting down ephemera");
|
||||
self.shutdown_manager.stop().await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//PROCESSING EXTERNAL API REQUESTS
|
||||
api = self.api_listener.messages_rcv.recv() => {
|
||||
match api {
|
||||
Some(api_msg) => {
|
||||
if let Err(err) = ApiCmdProcessor::process_api_requests(&mut self, api_msg).await{
|
||||
error!("Error processing api request: {:?}", err);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!("Error: Api listener channel closed");
|
||||
//TODO: handle shutdown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//PROCESSING SHUTDOWN REQUEST
|
||||
_ = self.shutdown_manager.external_shutdown.recv() => {
|
||||
info!("Shutting down ephemera");
|
||||
self.shutdown_manager.stop().await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Ephemera main loop finished");
|
||||
}
|
||||
|
||||
async fn process_network_event(&mut self, net_event: NetworkEvent) -> Result<()> {
|
||||
trace!("New network event: {:?}", net_event);
|
||||
|
||||
match net_event {
|
||||
NetworkEvent::EphemeraMessage(em) => {
|
||||
let api_msg = (*em.clone()).into();
|
||||
trace!("New ephemera message from network: {:?}", api_msg);
|
||||
|
||||
//Only Application checks if messages are valid(possibly message origin).
|
||||
//For messages we don't check if sender belongs to group.
|
||||
|
||||
// Ask application to decide if we should accept this message.
|
||||
match self.application.check_tx(api_msg) {
|
||||
Ok(true) => {
|
||||
trace!("Application accepted message: {:?}", em);
|
||||
|
||||
// Send to BlockManager to store in mempool.
|
||||
if let Err(err) = self.block_manager.on_new_message(*em) {
|
||||
error!("Error sending signed message to block manager: {:?}", err);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
trace!("Application rejected message: {:?}", em);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Application check_tx failed: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
NetworkEvent::BroadcastMessage(rb_msg) => {
|
||||
self.process_block_from_network(*rb_msg).await?;
|
||||
}
|
||||
NetworkEvent::GroupUpdate(event) => {
|
||||
self.process_group_update(event);
|
||||
}
|
||||
NetworkEvent::QueryDhtResponse { key, value } => {
|
||||
match self.api_cmd_processor.dht_query_cache.pop(&key) {
|
||||
Some(replies) => {
|
||||
for reply in replies {
|
||||
let response = Ok(Some((key.clone(), value.clone())));
|
||||
if let Err(err) = reply.send(response) {
|
||||
error!("Error sending dht query response: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
trace!(
|
||||
"No dht query cache found for key: {:?}",
|
||||
String::from_utf8(key)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_group_update(&mut self, event: GroupChangeEvent) {
|
||||
match event {
|
||||
GroupChangeEvent::PeersUpdated(peers) => {
|
||||
info!("New group: {:?}", peers);
|
||||
info!("{}", Quorum::cluster_size_info(peers.len()));
|
||||
self.broadcaster.group_updated(peers.len());
|
||||
self.broadcast_group.add_snapshot(peers);
|
||||
self.block_manager.start();
|
||||
}
|
||||
GroupChangeEvent::LocalPeerRemoved(peers) | GroupChangeEvent::NotEnoughPeers(peers) => {
|
||||
info!("New group: {:?}", peers);
|
||||
info!("Group update: Local peer removed or not enough peers");
|
||||
self.broadcaster.group_updated(0);
|
||||
self.broadcast_group.add_snapshot(peers);
|
||||
self.block_manager.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_new_local_block(
|
||||
&mut self,
|
||||
new_block: Block,
|
||||
certificate: Certificate,
|
||||
) -> Result<()> {
|
||||
debug!("New block from block manager: {:?}", new_block.get_hash());
|
||||
|
||||
let hash = new_block.header.hash;
|
||||
let block_creator = &self.node_info.peer_id;
|
||||
let sender = &self.node_info.peer_id;
|
||||
|
||||
// Check if block matches group membership.
|
||||
if !self
|
||||
.broadcast_group
|
||||
.check_membership(hash, block_creator, sender)
|
||||
{
|
||||
debug!("Membership check rejected block: {:?}", new_block);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
//Ephemera ABCI
|
||||
match self.application.check_block(&new_block.clone().into()) {
|
||||
Ok(response) => match response {
|
||||
CheckBlockResult::Accept => {
|
||||
debug!("Application accepted new block: {hash:?}",);
|
||||
}
|
||||
CheckBlockResult::Reject => {
|
||||
debug!("Application rejected block: {hash:?}",);
|
||||
return Ok(());
|
||||
}
|
||||
CheckBlockResult::RejectAndRemoveMessages(messages_to_remove) => {
|
||||
debug!("Application rejected block: {:?}", messages_to_remove);
|
||||
self.block_manager
|
||||
.on_application_rejected_block(messages_to_remove)
|
||||
.map_err(|err| {
|
||||
anyhow!("Error rejecting block from block manager: {:?}", err)
|
||||
})?;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
return Err(anyhow!("Application check_block failed: {:?}", err).into());
|
||||
}
|
||||
}
|
||||
|
||||
//Block manager generated new block that nobody hasn't seen yet.
|
||||
//We start reliable broadcaster protocol to broadcaster it to other nodes.
|
||||
match self.broadcaster.new_broadcast(new_block) {
|
||||
Ok(resp) => {
|
||||
if let BroadcastResponse::Broadcast(msg) = resp {
|
||||
trace!("Broadcasting new block: {:?}", msg);
|
||||
|
||||
let rb_msg = RbMsg::new(msg, certificate);
|
||||
self.to_network
|
||||
.send_ephemera_event(EphemeraEvent::ProtocolMessage(rb_msg.into()))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error starting new broadcast: {:?}", err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//TODO: should we accept more blocks(certificates) from peers after its committed?
|
||||
async fn process_block_from_network(&mut self, msg: RbMsg) -> Result<()> {
|
||||
let msg_id = msg.id.clone();
|
||||
let block = msg.block();
|
||||
let block_creator = &block.header.creator;
|
||||
let sender = &msg.original_sender;
|
||||
let hash = block.header.hash;
|
||||
let certificate = msg.certificate.clone();
|
||||
|
||||
trace!("New broadcast message from network: {:?}", msg);
|
||||
|
||||
if !self
|
||||
.broadcast_group
|
||||
.check_membership(hash, block_creator, sender)
|
||||
{
|
||||
return Err(anyhow!("Block doesn't match broacast group").into());
|
||||
}
|
||||
|
||||
if let Err(err) = self.block_manager.on_block(sender, block, &certificate) {
|
||||
return Err(anyhow!("Error sending block to block manager: {:?}", err).into());
|
||||
}
|
||||
let raw_mgs = msg.into();
|
||||
match self.broadcaster.handle(&raw_mgs) {
|
||||
Ok(resp) => {
|
||||
match resp {
|
||||
BroadcastResponse::Broadcast(msg) => {
|
||||
trace!("Broadcasting block to network: {:?}", msg);
|
||||
|
||||
match self.block_manager.sign_block(&msg.block()) {
|
||||
Ok(certificate) => {
|
||||
let rb_msg = RbMsg::new(msg, certificate);
|
||||
self.to_network
|
||||
.send_ephemera_event(EphemeraEvent::ProtocolMessage(
|
||||
rb_msg.into(),
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(anyhow!("Error signing block: {:?}", err).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
BroadcastResponse::Deliver(hash) => {
|
||||
trace!("Block broadcast complete: {hash:?}",);
|
||||
let block = self.block_manager.get_block_by_hash(&hash);
|
||||
match block {
|
||||
Some(block) => {
|
||||
if block.header.creator == self.node_info.peer_id {
|
||||
info!("Block committed, ready to deliver...: {hash:?}",);
|
||||
|
||||
//BlockManager
|
||||
self.block_manager.on_block_committed(&block).map_err(|e| {
|
||||
anyhow!(
|
||||
"Error: BlockManager failed to process block: {e:?}",
|
||||
)
|
||||
})?;
|
||||
|
||||
//Save to database
|
||||
let certificates = self
|
||||
.block_manager
|
||||
.get_block_certificates(&block.header.hash)
|
||||
.ok_or(anyhow!(
|
||||
"Error: Block certificates not found for block: {hash:?}"
|
||||
))?;
|
||||
let members = self
|
||||
.broadcast_group
|
||||
.get_group_by_block_hash(block.get_hash())
|
||||
.ok_or(anyhow!(
|
||||
"Error: Group not found for block: {hash:?}"
|
||||
))?;
|
||||
|
||||
if let Err(e) = self.storage.lock().await.store_block(
|
||||
&block,
|
||||
certificates.clone(),
|
||||
members.clone(),
|
||||
) {
|
||||
return Err(EphemeraCoreError::DatabaseFailure(e));
|
||||
}
|
||||
|
||||
// It is open question how much Application `deliver_block` failure should affect
|
||||
// continuing with next block.
|
||||
//Application(ABCI)
|
||||
self.application
|
||||
.deliver_block(Into::into(block.clone()))
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Error: Deliver block to Application failed: {e:?}",
|
||||
)
|
||||
})?;
|
||||
|
||||
//WS
|
||||
self.ws_message_broadcast.send_block(&block)?;
|
||||
info!("Block broadcast complete: {hash:?}",);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(
|
||||
anyhow!("Error: Block not found in block manager").into()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
BroadcastResponse::Drop(hash) => {
|
||||
trace!("Ignoring broadcast message {:?}[block {:?}]", msg_id, hash);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error handling broadcast message: {:?}", err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub(crate) mod api_cmd;
|
||||
pub(crate) mod builder;
|
||||
pub(crate) mod ephemera;
|
||||
pub(crate) mod shutdown;
|
||||
@@ -0,0 +1,84 @@
|
||||
use log::info;
|
||||
|
||||
use tokio::sync::mpsc::error::SendError;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
pub(crate) struct ShutdownManager {
|
||||
pub(crate) shutdown_tx: broadcast::Sender<()>,
|
||||
pub(crate) _shutdown_rcv: broadcast::Receiver<()>,
|
||||
pub(crate) external_shutdown: mpsc::UnboundedReceiver<()>,
|
||||
handles: Vec<JoinHandle<anyhow::Result<()>>>,
|
||||
}
|
||||
|
||||
pub(crate) struct Shutdown {
|
||||
pub(crate) shutdown_signal_rcv: broadcast::Receiver<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Handle {
|
||||
pub(crate) external_shutdown: mpsc::UnboundedSender<()>,
|
||||
pub(crate) shutdown_started: bool,
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
/// Shutdown the node.
|
||||
/// This will send a shutdown signal to all tasks and wait for them to finish.
|
||||
///
|
||||
/// # Errors
|
||||
/// This will return an error if shutdown signal can't be sent.
|
||||
///
|
||||
/// # Panics
|
||||
/// This will panic if shutdown signal can't be sent.
|
||||
pub fn shutdown(&mut self) -> Result<(), SendError<()>> {
|
||||
self.shutdown_started = true;
|
||||
self.external_shutdown.send(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ShutdownManager {
|
||||
pub(crate) fn init() -> (ShutdownManager, Handle) {
|
||||
let (shutdown_tx, shutdown_rcv) = broadcast::channel(1);
|
||||
let (external_tx, external_rcv) = mpsc::unbounded_channel();
|
||||
let shutdown_handle = Handle {
|
||||
external_shutdown: external_tx,
|
||||
shutdown_started: false,
|
||||
};
|
||||
let manager = Self {
|
||||
shutdown_tx,
|
||||
_shutdown_rcv: shutdown_rcv,
|
||||
external_shutdown: external_rcv,
|
||||
handles: vec![],
|
||||
};
|
||||
(manager, shutdown_handle)
|
||||
}
|
||||
|
||||
pub async fn stop(self) {
|
||||
info!("Starting Ephemera shutdown");
|
||||
self.shutdown_tx.send(()).unwrap();
|
||||
info!("Waiting for tasks to finish");
|
||||
for (i, handle) in self
|
||||
.handles
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, h)| (i + 1, h))
|
||||
{
|
||||
match handle.await.unwrap() {
|
||||
Ok(_) => info!("Task {i} finished successfully"),
|
||||
Err(e) => info!("Task {i} finished with error: {e}",),
|
||||
}
|
||||
}
|
||||
info!("All tasks finished");
|
||||
}
|
||||
|
||||
pub(crate) fn subscribe(&self) -> Shutdown {
|
||||
let shutdown = self.shutdown_tx.subscribe();
|
||||
Shutdown {
|
||||
shutdown_signal_rcv: shutdown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_handle(&mut self, handle: JoinHandle<anyhow::Result<()>>) {
|
||||
self.handles.push(handle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
//! # Ephemera Node
|
||||
|
||||
//! An Ephemera node does reliable broadcast of inbound messages to all other Ephemera nodes in the cluster.
|
||||
//!
|
||||
//! Each node has a unique ID, and each message is signed by the node that first received it.
|
||||
//! Messages are then re-broadcast and re-signed by all other nodes in the cluster.
|
||||
//!
|
||||
//! At the end of the process, each message is signed by every node in the cluster, and each node has also
|
||||
//! signed all messages that were broadcast by other nodes. This means that nodes are unable to repudiate messages
|
||||
//! once they are seen and signed, so there is a strong guarantee of message integrity within the cluster.
|
||||
//!
|
||||
//! # Why would I want this?
|
||||
//!
|
||||
//! Let's say you have blockchain system that needs to ship large amounts of information around, but the information
|
||||
//! is relatively short-lived. You could use a blockchain to store the information, but that would be expensive,
|
||||
//! slow, and wasteful. Instead, you could use Ephemera to broadcast the information to all nodes in the cluster,
|
||||
//! and then store only a cryptographic commitment in the blockchain's data store.
|
||||
//!
|
||||
//! Ephemera nodes then keep messages around for inspection in a data availability layer (accessible over HTTP)
|
||||
//! so that interested parties can verify correctness. Ephemeral information can then be automatically discarded
|
||||
//! once it's no longer useful.
|
||||
//!
|
||||
//! This gives very similar guarantees to a blockchain, but without incurring the permanent storage costs.
|
||||
//!
|
||||
//! Note that it *requires* a blockchain to be present.
|
||||
|
||||
//'Denying' everything and allowing exceptions seems better than other way around.
|
||||
#![deny(clippy::pedantic)]
|
||||
|
||||
// PUBLIC MODULES
|
||||
|
||||
pub use crate::core::builder::{
|
||||
EphemeraStarterInit, EphemeraStarterWithApplication, EphemeraStarterWithProvider,
|
||||
};
|
||||
pub use crate::core::ephemera::Ephemera;
|
||||
pub use crate::core::shutdown::Handle as ShutdownHandle;
|
||||
|
||||
/// Ephemera API. Public interface and types.
|
||||
pub mod ephemera_api {
|
||||
pub use crate::api::{
|
||||
application::{
|
||||
Application, CheckBlockResult, Dummy, Error as ApplicationError, RemoveMessages,
|
||||
Result as ApplicationResult,
|
||||
},
|
||||
http::client::{Client, Error as HttpClientError, Result as HttpClientResult},
|
||||
types::{
|
||||
ApiBlock, ApiBlockBroadcastInfo, ApiBroadcastInfo, ApiCertificate, ApiDhtQueryRequest,
|
||||
ApiDhtQueryResponse, ApiDhtStoreRequest, ApiEphemeraConfig, ApiEphemeraMessage,
|
||||
ApiError, ApiHealth, ApiVerifyMessageInBlock, RawApiEphemeraMessage,
|
||||
},
|
||||
CommandExecutor,
|
||||
};
|
||||
}
|
||||
|
||||
/// Peer identification
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub mod peer {
|
||||
pub use super::network::{PeerId, PeerIdError, ToPeerId};
|
||||
}
|
||||
|
||||
/// Ephemera membership. How to find other nodes in the cluster.
|
||||
pub mod membership {
|
||||
pub use super::network::members::{
|
||||
ConfigMembersProvider, DummyMembersProvider, HttpMembersProvider, JsonPeerInfo, PeerInfo,
|
||||
PeerSetting, Result,
|
||||
};
|
||||
}
|
||||
|
||||
/// Ephemera keypair and public key
|
||||
pub mod crypto {
|
||||
pub use super::utilities::crypto::{
|
||||
EphemeraKeypair, EphemeraPublicKey, KeyPairError, Keypair, PublicKey,
|
||||
};
|
||||
}
|
||||
|
||||
/// Ephemera codec to encode and decode messages
|
||||
pub mod codec {
|
||||
pub use super::utilities::codec::{Decode, Encode};
|
||||
}
|
||||
|
||||
/// Ephemera node configuration
|
||||
pub mod configuration {
|
||||
pub use super::config::Configuration;
|
||||
}
|
||||
|
||||
/// Ephemera CLI. Helpers for creating configuration, running node, etc.
|
||||
pub mod cli;
|
||||
|
||||
/// Utilities to set up logging.
|
||||
pub mod logging;
|
||||
|
||||
// PRIVATE MODULES
|
||||
|
||||
/// External interface for Ephemera
|
||||
mod api;
|
||||
|
||||
/// Block creation code
|
||||
mod block;
|
||||
|
||||
/// Ephemera reliable broadcast
|
||||
mod broadcast;
|
||||
|
||||
/// Ephemera configuration
|
||||
mod config;
|
||||
|
||||
/// Ephemera core. Ephemera builder and instance.
|
||||
mod core;
|
||||
|
||||
/// Ephemera networking with peers
|
||||
mod network;
|
||||
|
||||
/// Ephemera storage. Block storage and certificate storage.
|
||||
mod storage;
|
||||
|
||||
/// Ephemera utilities. Crypto, codec, etc.
|
||||
mod utilities;
|
||||
|
||||
/// Ephemera websocket. Websocket server where external clients can subscribe.
|
||||
mod websocket;
|
||||
@@ -0,0 +1,19 @@
|
||||
pub fn init() {
|
||||
if let Ok(directives) = ::std::env::var("RUST_LOG") {
|
||||
println!("Logging enabled with directives: {directives}",);
|
||||
pretty_env_logger::formatted_timed_builder()
|
||||
.parse_filters(&directives)
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
} else {
|
||||
println!("Logging disabled");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_with_directives(directives: &str) {
|
||||
println!("Logging enabled with directives: {directives}",);
|
||||
pretty_env_logger::formatted_timed_builder()
|
||||
.parse_filters(directives)
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
//! # Membership behaviour.
|
||||
//!
|
||||
//! Ephemera `reliable broadcast` needs to know the list of peers who participate in the protocol.
|
||||
//! Also it's not enough to have just the list but to make sure that they are actually online.
|
||||
//! When the list of available peers changes, `reliable broadcast` needs to be notified so that it can adjust accordingly.
|
||||
//!
|
||||
//! This behaviour is responsible for keeping membership up to date.
|
||||
//!
|
||||
//! `MembersProviderFuture`: `Future<Output = membership::Result<Vec<PeerInfo>>> + Send + 'static`.
|
||||
//!
|
||||
//! User provides a [`MembersProviderFuture`] implementation to the [Behaviour] which is responsible for fetching the list of peers.
|
||||
//!
|
||||
//! [Behaviour] accepts only peers that are actually online.
|
||||
//!
|
||||
//! When peers become available or unavailable, [Behaviour] adjusts the list of connected peers accordingly and notifies `reliable broadcast`
|
||||
//! about the membership change.
|
||||
//!
|
||||
//! It is configurable what `threshold` of peers(from the total list provided by [`MembersProviderFuture`]) should be available at any given time.
|
||||
//! Or if just to use all peers who are online. See [`MembershipKind`] for more details.
|
||||
//!
|
||||
//! Ideally [`MembersProviderFuture`] can depend on a resource that gives reliable results. Some kind of registry which itself keeps track of actually online nodes.
|
||||
//! As Ephemera uses only peers provided by [`MembersProviderFuture`], it depends on its accuracy.
|
||||
//! At the same time it tries to be flexible and robust to handle less reliable [`MembersProviderFuture`] implementations.
|
||||
|
||||
// When peer gets disconnected, we try to dial it and if that fails, we update group.
|
||||
// (it may connect us meanwhile).
|
||||
// Although we can retry to connect to disconnected peers, it's simpler if we just assume that when
|
||||
// they come online again, they will connect us.
|
||||
|
||||
//So the main goal is to remove offline peers from the group so that rb can make progress.
|
||||
|
||||
//a)when peer disconnects, we try to dial it and if that fails, we update group.
|
||||
//b)when peer connects, we will update the group.
|
||||
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::HashSet,
|
||||
fmt::Debug,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use futures_util::FutureExt;
|
||||
use libp2p::core::Endpoint;
|
||||
use libp2p::swarm::{ConnectionDenied, NotifyHandler, THandler};
|
||||
use libp2p::{
|
||||
swarm::ToSwarm,
|
||||
swarm::{
|
||||
behaviour::ConnectionEstablished,
|
||||
dial_opts::{DialOpts, PeerCondition},
|
||||
ConnectionClosed, ConnectionId, DialFailure, FromSwarm, NetworkBehaviour, PollParameters,
|
||||
THandlerInEvent, THandlerOutEvent,
|
||||
},
|
||||
Multiaddr,
|
||||
};
|
||||
use libp2p_identity::PeerId;
|
||||
use log::{debug, error, trace, warn};
|
||||
use tokio::time;
|
||||
use tokio::time::{Instant, Interval};
|
||||
|
||||
use crate::network::libp2p::behaviours::membership::handler::ToHandler;
|
||||
use crate::network::libp2p::behaviours::membership::{Membership, MEMBERSHIP_SYNC_INTERVAL_SEC};
|
||||
use crate::network::Peer;
|
||||
use crate::{
|
||||
membership,
|
||||
network::{
|
||||
libp2p::behaviours::{
|
||||
membership::connections::ConnectedPeers,
|
||||
membership::protocol::ProtocolMessage,
|
||||
membership::{handler::Handler, MAX_DIAL_ATTEMPT_ROUNDS},
|
||||
membership::{MembershipKind, Memberships},
|
||||
},
|
||||
members::PeerInfo,
|
||||
},
|
||||
};
|
||||
|
||||
/// [`MembersProviderFuture`] state when we are trying to connect to new peers.
|
||||
///
|
||||
/// We try to connect few times before giving up. Generally speaking an another peer is either online or offline
|
||||
/// at any given time. But it has been helpful for testing when whole cluster comes up around the same time.
|
||||
#[derive(Debug, Default)]
|
||||
struct PendingPeersUpdate {
|
||||
/// Peers that we are haven't tried to connect to yet.
|
||||
waiting_to_dial: HashSet<PeerId>,
|
||||
/// Number of dial attempts per round.
|
||||
dial_attempts: usize,
|
||||
/// How long we wait between dial attempts.
|
||||
interval_between_dial_attempts: Option<Interval>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct SyncPeers {
|
||||
pending_peers: Vec<PeerId>,
|
||||
}
|
||||
|
||||
impl SyncPeers {
|
||||
fn new(pending_peers: Vec<PeerId>) -> Self {
|
||||
Self { pending_peers }
|
||||
}
|
||||
}
|
||||
|
||||
/// Behaviour states.
|
||||
enum State {
|
||||
/// Waiting for new peers from the members provider trait.
|
||||
WaitingPeers,
|
||||
/// Trying to connect to new peers.
|
||||
WaitingDial(PendingPeersUpdate),
|
||||
/// We have finished trying to connect to new peers and going to report it.
|
||||
NotifyPeersUpdated,
|
||||
/// Notify other members that we have updated members.
|
||||
SyncPeers(SyncPeers),
|
||||
}
|
||||
|
||||
/// Events that can be emitted by the `Behaviour`.
|
||||
pub(crate) enum Event {
|
||||
/// We have received new peers from the members provider trait.
|
||||
/// We are going to try to connect to them.
|
||||
PeerUpdatePending,
|
||||
/// We have finished trying to connect to new peers and going to report it.
|
||||
PeersUpdated(HashSet<PeerId>),
|
||||
/// MembersProviderFuture reported us new peers and this set doesn't contain our local peer.
|
||||
LocalRemoved(HashSet<PeerId>),
|
||||
/// MembersProviderFuture reported us new peers and we failed to connect to enough of them.
|
||||
NotEnoughPeers(HashSet<PeerId>),
|
||||
}
|
||||
|
||||
pub(crate) struct Behaviour<P>
|
||||
where
|
||||
P: Future<Output = membership::Result<Vec<PeerInfo>>> + Send + 'static,
|
||||
{
|
||||
/// All peers that are part of the current group.
|
||||
memberships: Memberships,
|
||||
/// Local peer id.
|
||||
local_peer_id: PeerId,
|
||||
/// Future that provides new peers.
|
||||
members_provider: P,
|
||||
/// Interval between requesting new peers from the members provider.
|
||||
members_provider_interval: Option<Interval>,
|
||||
/// Delay between dial attempts.
|
||||
members_provider_delay: Duration,
|
||||
/// Current behaviour state.
|
||||
state: State,
|
||||
/// Current state of all incoming and outgoing connections.
|
||||
all_connections: ConnectedPeers,
|
||||
/// Membership kind.
|
||||
membership_kind: MembershipKind,
|
||||
/// Last time we broadcast SYNC
|
||||
last_sync_time: Instant,
|
||||
/// Minimum time between members provider updates.
|
||||
minimum_time_between_sync: Duration,
|
||||
}
|
||||
|
||||
impl<P> Behaviour<P>
|
||||
where
|
||||
P: Future<Output = membership::Result<Vec<PeerInfo>>> + Send + Unpin + 'static,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
members_provider: P,
|
||||
members_provider_delay: Duration,
|
||||
local_peer_id: PeerId,
|
||||
membership_kind: MembershipKind,
|
||||
) -> Self {
|
||||
let initial_delay = Instant::now() + Duration::from_secs(5);
|
||||
let delay = tokio::time::interval_at(initial_delay, members_provider_delay);
|
||||
Behaviour {
|
||||
memberships: Memberships::new(),
|
||||
local_peer_id,
|
||||
members_provider,
|
||||
members_provider_interval: Some(delay),
|
||||
members_provider_delay,
|
||||
state: State::WaitingPeers,
|
||||
all_connections: ConnectedPeers::default(),
|
||||
membership_kind,
|
||||
last_sync_time: Instant::now(),
|
||||
minimum_time_between_sync: Duration::from_secs(MEMBERSHIP_SYNC_INTERVAL_SEC),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list of peers that are part of current group.
|
||||
pub(crate) fn active_peer_ids(&mut self) -> &HashSet<PeerId> {
|
||||
self.memberships.current().connected_peers()
|
||||
}
|
||||
|
||||
pub(crate) fn active_peer_ids_with_local(&mut self) -> HashSet<PeerId> {
|
||||
self.memberships.current().connected_peer_ids_with_local()
|
||||
}
|
||||
|
||||
fn waiting_peers(&mut self, cx: &mut Context) -> Poll<ToSwarm<Event, ToHandler>> {
|
||||
if let Some(mut tick) = self.members_provider_interval.take() {
|
||||
if !tick.poll_tick(cx).is_ready() {
|
||||
self.members_provider_interval = Some(tick);
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
let peers = match self.members_provider.poll_unpin(cx) {
|
||||
Poll::Ready(peers) => {
|
||||
let wait_time = Instant::now() + self.members_provider_delay;
|
||||
self.members_provider_interval =
|
||||
time::interval_at(wait_time, self.members_provider_delay).into();
|
||||
self.last_sync_time = Instant::now();
|
||||
peers
|
||||
}
|
||||
Poll::Pending => {
|
||||
return Poll::Pending;
|
||||
}
|
||||
};
|
||||
|
||||
match peers {
|
||||
Ok(peers) => {
|
||||
if peers.is_empty() {
|
||||
//Not sure what to do here. Tempted to think that if this happens
|
||||
//we should ignore it and assume that this is probably a bug in the membership service.
|
||||
|
||||
warn!("Received empty peers from provider. To try again before preconfigured interval, please restart the node.");
|
||||
return Poll::Ready(ToSwarm::GenerateEvent(Event::NotEnoughPeers(
|
||||
HashSet::default(),
|
||||
)));
|
||||
}
|
||||
|
||||
let mut new_peers = HashMap::new();
|
||||
|
||||
for peer_info in peers {
|
||||
match <PeerInfo as TryInto<Peer>>::try_into(peer_info) {
|
||||
Ok(peer) => {
|
||||
debug!("Received peer: {:?}, {:?}", peer.peer_id.inner(), peer.name);
|
||||
new_peers.insert(*peer.peer_id.inner(), peer);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error while converting peer info to peer: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If we are not part of the new membership, notify immediately
|
||||
if !new_peers.contains_key(&self.local_peer_id) {
|
||||
debug!(
|
||||
"Local peer {:?} is not part of the new membership. Notifying immediately.",
|
||||
self.local_peer_id
|
||||
);
|
||||
let pending_membership = Membership::new(new_peers.clone());
|
||||
self.memberships.set_pending(pending_membership);
|
||||
self.state = State::NotifyPeersUpdated;
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
let mut pending_membership =
|
||||
Membership::new_with_local(new_peers.clone(), self.local_peer_id);
|
||||
let mut pending_update = PendingPeersUpdate::default();
|
||||
|
||||
for peer_id in new_peers.keys() {
|
||||
if self.all_connections.is_peer_connected(peer_id) {
|
||||
pending_membership.peer_connected(*peer_id);
|
||||
} else {
|
||||
pending_update.waiting_to_dial.insert(*peer_id);
|
||||
}
|
||||
}
|
||||
|
||||
self.memberships.set_pending(pending_membership);
|
||||
|
||||
//It seems that all peers from updated membership set are already connected
|
||||
if pending_update.waiting_to_dial.is_empty() {
|
||||
self.state = State::NotifyPeersUpdated;
|
||||
Poll::Pending
|
||||
} else {
|
||||
self.state = State::WaitingDial(pending_update);
|
||||
|
||||
//Just let the rest of the system to know that we are in the middle of updating membership
|
||||
Poll::Ready(ToSwarm::GenerateEvent(Event::PeerUpdatePending))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error while getting peers from provider: {:?}", err);
|
||||
Poll::Ready(ToSwarm::GenerateEvent(Event::NotEnoughPeers(
|
||||
HashSet::default(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn waiting_dial(
|
||||
&mut self,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<ToSwarm<Event, THandlerInEvent<Self>>> {
|
||||
if let State::WaitingDial(PendingPeersUpdate {
|
||||
waiting_to_dial,
|
||||
dial_attempts,
|
||||
interval_between_dial_attempts,
|
||||
}) = &mut self.state
|
||||
{
|
||||
//Refresh the list of connected peers
|
||||
for peer_id in self.all_connections.all_connected_peers_ref() {
|
||||
waiting_to_dial.remove(peer_id);
|
||||
}
|
||||
|
||||
//FIXME
|
||||
waiting_to_dial.remove(&self.local_peer_id);
|
||||
|
||||
let pending_membership = self
|
||||
.memberships
|
||||
.pending()
|
||||
.expect("Pending membership should be set");
|
||||
|
||||
//With each 'poll' we can tell Swarm to dial one peer
|
||||
let next_waiting = waiting_to_dial.iter().next().copied();
|
||||
|
||||
if let Some(peer_id) = next_waiting {
|
||||
waiting_to_dial.remove(&peer_id);
|
||||
|
||||
let address = pending_membership
|
||||
.peer_address(&peer_id)
|
||||
.expect("Peer should exist");
|
||||
|
||||
trace!("Dialing peer: {:?} {:?}", peer_id, address);
|
||||
|
||||
let opts = DialOpts::peer_id(peer_id)
|
||||
.condition(PeerCondition::NotDialing)
|
||||
.addresses(vec![address.clone()])
|
||||
.build();
|
||||
|
||||
Poll::Ready(ToSwarm::Dial { opts })
|
||||
} else {
|
||||
let all_peers = pending_membership.all_peer_ids();
|
||||
let connected_peers = pending_membership.connected_peers();
|
||||
|
||||
//Exclude local peer
|
||||
let all_connected = connected_peers.len() == all_peers.len() - 1;
|
||||
|
||||
if all_connected || *dial_attempts >= MAX_DIAL_ATTEMPT_ROUNDS {
|
||||
interval_between_dial_attempts.take();
|
||||
self.state = State::NotifyPeersUpdated;
|
||||
return Poll::Pending;
|
||||
}
|
||||
//Try again few times before notifying the rest of the system about membership update.
|
||||
//Dialing attempt
|
||||
if let Some(interval) = interval_between_dial_attempts {
|
||||
if interval.poll_tick(cx) == Poll::Pending {
|
||||
return Poll::Pending;
|
||||
}
|
||||
*dial_attempts += 1;
|
||||
trace!("Next attempt({dial_attempts:?}) to dial failed peers");
|
||||
} else {
|
||||
let start_at = Instant::now() + Duration::from_secs(5);
|
||||
*interval_between_dial_attempts =
|
||||
Some(time::interval_at(start_at, Duration::from_secs(10)));
|
||||
}
|
||||
if *dial_attempts > 0 {
|
||||
waiting_to_dial.extend(all_peers.difference(connected_peers).copied());
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_peers_updated(&mut self) -> Poll<ToSwarm<Event, ToHandler>> {
|
||||
if let Some(membership) = self.memberships.remove_pending() {
|
||||
self.memberships.update(membership);
|
||||
}
|
||||
|
||||
let membership = self.memberships.current();
|
||||
let membership_connected_peers = membership.connected_peer_ids();
|
||||
|
||||
let event = if membership.includes_local() {
|
||||
if self.membership_kind.accept(membership) {
|
||||
debug!("Membership accepted by kind: {:?}", self.membership_kind);
|
||||
Event::PeersUpdated(membership_connected_peers)
|
||||
} else {
|
||||
debug!("Membership rejected by kind: {:?}", self.membership_kind);
|
||||
Event::NotEnoughPeers(membership_connected_peers)
|
||||
}
|
||||
} else {
|
||||
debug!("Membership does not include local peer");
|
||||
Event::LocalRemoved(membership_connected_peers)
|
||||
};
|
||||
|
||||
//TODO: this list should also include "old" peers(peers who aren't part of new membership).
|
||||
let connected_peers = membership
|
||||
.connected_peer_ids()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
self.state = State::SyncPeers(SyncPeers::new(connected_peers));
|
||||
Poll::Ready(ToSwarm::GenerateEvent(event))
|
||||
}
|
||||
|
||||
fn sync_peers(&mut self) -> Poll<ToSwarm<Event, ToHandler>> {
|
||||
if let State::SyncPeers(SyncPeers { pending_peers }) = &mut self.state {
|
||||
match pending_peers.pop() {
|
||||
None => {
|
||||
self.state = State::WaitingPeers;
|
||||
Poll::Pending
|
||||
}
|
||||
Some(peer_id) => {
|
||||
debug!("Notifying {peer_id:?} about membership update",);
|
||||
Poll::Ready(ToSwarm::NotifyHandler {
|
||||
peer_id,
|
||||
handler: NotifyHandler::Any,
|
||||
event: ToHandler::Message(ProtocolMessage::Sync),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unreachable!("State should be SyncPeers")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> NetworkBehaviour for Behaviour<P>
|
||||
where
|
||||
P: Future<Output = membership::Result<Vec<PeerInfo>>> + Send + Unpin + 'static,
|
||||
{
|
||||
type ConnectionHandler = Handler;
|
||||
type OutEvent = Event;
|
||||
|
||||
fn handle_pending_inbound_connection(
|
||||
&mut self,
|
||||
_connection_id: ConnectionId,
|
||||
_local_addr: &Multiaddr,
|
||||
_remote_addr: &Multiaddr,
|
||||
) -> Result<(), ConnectionDenied> {
|
||||
//TODO: we can refuse connections from peers that are not part of the current membership.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_established_inbound_connection(
|
||||
&mut self,
|
||||
_connection_id: ConnectionId,
|
||||
peer: PeerId,
|
||||
_local_addr: &Multiaddr,
|
||||
_remote_addr: &Multiaddr,
|
||||
) -> Result<THandler<Self>, ConnectionDenied> {
|
||||
trace!("Established inbound connection with peer: {:?}", peer);
|
||||
Ok(Handler::new())
|
||||
}
|
||||
|
||||
fn handle_pending_outbound_connection(
|
||||
&mut self,
|
||||
_connection_id: ConnectionId,
|
||||
maybe_peer: Option<PeerId>,
|
||||
_addresses: &[Multiaddr],
|
||||
_effective_role: Endpoint,
|
||||
) -> Result<Vec<Multiaddr>, ConnectionDenied> {
|
||||
//FIXME: deprecated
|
||||
#[allow(deprecated)]
|
||||
match maybe_peer {
|
||||
Some(peer_id) => Ok(self.addresses_of_peer(&peer_id)),
|
||||
None => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_established_outbound_connection(
|
||||
&mut self,
|
||||
_connection_id: ConnectionId,
|
||||
peer: PeerId,
|
||||
addr: &Multiaddr,
|
||||
_role_override: Endpoint,
|
||||
) -> Result<THandler<Self>, ConnectionDenied> {
|
||||
trace!(
|
||||
"Established outbound connection with peer: {:?} {:?}",
|
||||
peer,
|
||||
addr
|
||||
);
|
||||
Ok(Handler::new())
|
||||
}
|
||||
|
||||
///Membership behaviour is responsible for providing addresses to another Swarm behaviours.
|
||||
fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec<Multiaddr> {
|
||||
self.memberships
|
||||
.current()
|
||||
.peer_address(peer_id)
|
||||
.cloned()
|
||||
.map_or(vec![], |addr| vec![addr])
|
||||
}
|
||||
|
||||
fn on_swarm_event(&mut self, event: FromSwarm<Self::ConnectionHandler>) {
|
||||
match event {
|
||||
FromSwarm::ConnectionEstablished(ConnectionEstablished {
|
||||
peer_id,
|
||||
connection_id: _,
|
||||
endpoint,
|
||||
failed_addresses: _,
|
||||
other_established: _,
|
||||
}) => {
|
||||
self.all_connections
|
||||
.insert(peer_id, endpoint.clone().into());
|
||||
if let Some(pending) = self.memberships.pending_mut() {
|
||||
pending.peer_connected(peer_id);
|
||||
}
|
||||
trace!("{:?}", self.all_connections);
|
||||
}
|
||||
|
||||
FromSwarm::ConnectionClosed(ConnectionClosed {
|
||||
peer_id,
|
||||
connection_id: _,
|
||||
endpoint,
|
||||
handler: _h,
|
||||
remaining_established: _,
|
||||
}) => {
|
||||
self.all_connections
|
||||
.remove(&peer_id, &endpoint.clone().into());
|
||||
if let Some(pending) = self.memberships.pending_mut() {
|
||||
pending.peer_disconnected(&peer_id);
|
||||
}
|
||||
debug!("{:?}", self.all_connections);
|
||||
}
|
||||
FromSwarm::DialFailure(DialFailure {
|
||||
peer_id: Some(peer_id),
|
||||
error,
|
||||
connection_id: _,
|
||||
}) => {
|
||||
trace!("Dial failure: {:?} {:?}", peer_id, error);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_connection_handler_event(
|
||||
&mut self,
|
||||
peer_id: PeerId,
|
||||
_connection_id: ConnectionId,
|
||||
event: THandlerOutEvent<Self>,
|
||||
) {
|
||||
trace!(
|
||||
"Received event from connection handler: {:?} from peer: {:?}",
|
||||
event,
|
||||
peer_id
|
||||
);
|
||||
|
||||
//TODO: we may need to check who sent the update: probably we should accept only updates from members who we already know
|
||||
if let State::WaitingPeers = self.state {
|
||||
if self.last_sync_time + self.minimum_time_between_sync < Instant::now() {
|
||||
self.members_provider_interval = None;
|
||||
debug!("Received sync notification from peer {peer_id:?}, requesting membership update");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll(
|
||||
&mut self,
|
||||
cx: &mut Context<'_>,
|
||||
_params: &mut impl PollParameters,
|
||||
) -> Poll<ToSwarm<Self::OutEvent, THandlerInEvent<Self>>> {
|
||||
match &mut self.state {
|
||||
State::WaitingPeers => self.waiting_peers(cx),
|
||||
State::WaitingDial(_) => self.waiting_dial(cx),
|
||||
State::NotifyPeersUpdated => self.notify_peers_updated(),
|
||||
State::SyncPeers(_) => self.sync_peers(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use libp2p::core::ConnectedPoint;
|
||||
use libp2p::Multiaddr;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Hash, Serialize)]
|
||||
pub(crate) enum Endpoint {
|
||||
Dialer {
|
||||
address: Multiaddr,
|
||||
},
|
||||
Listener {
|
||||
local_address: Multiaddr,
|
||||
remote_address: Multiaddr,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ConnectedPoint> for Endpoint {
|
||||
fn from(connected_point: ConnectedPoint) -> Self {
|
||||
match connected_point {
|
||||
ConnectedPoint::Dialer { address, .. } => Endpoint::Dialer { address },
|
||||
ConnectedPoint::Listener {
|
||||
local_addr,
|
||||
send_back_addr,
|
||||
} => Endpoint::Listener {
|
||||
local_address: local_addr,
|
||||
remote_address: send_back_addr,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub(crate) struct Connections {
|
||||
dialer: HashSet<Endpoint>,
|
||||
listener: HashSet<Endpoint>,
|
||||
}
|
||||
|
||||
impl Connections {
|
||||
fn insert(&mut self, connected_point: Endpoint) {
|
||||
match connected_point {
|
||||
Endpoint::Dialer { .. } => {
|
||||
self.dialer.insert(connected_point);
|
||||
}
|
||||
Endpoint::Listener { .. } => {
|
||||
self.listener.insert(connected_point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&mut self, connected_point: &Endpoint) {
|
||||
match connected_point {
|
||||
Endpoint::Dialer { .. } => {
|
||||
self.dialer.remove(connected_point);
|
||||
}
|
||||
Endpoint::Listener { .. } => {
|
||||
self.listener.remove(connected_point);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub(crate) struct ConnectedPeers {
|
||||
connections: HashMap<libp2p_identity::PeerId, Connections>,
|
||||
}
|
||||
|
||||
impl ConnectedPeers {
|
||||
pub(crate) fn is_peer_connected(&self, peer_id: &libp2p_identity::PeerId) -> bool {
|
||||
self.connections.contains_key(peer_id)
|
||||
}
|
||||
|
||||
pub(crate) fn all_connected_peers_ref(&self) -> Vec<&libp2p_identity::PeerId> {
|
||||
self.connections.keys().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn insert(&mut self, peer_id: libp2p_identity::PeerId, connected_point: Endpoint) {
|
||||
let connections = self.connections.entry(peer_id).or_default();
|
||||
connections.insert(connected_point);
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, peer_id: &libp2p_identity::PeerId, connected_point: &Endpoint) {
|
||||
if let Some(connections) = self.connections.get_mut(peer_id) {
|
||||
connections.remove(connected_point);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
//! Because each Ephemera instance requests peers at arbitrary time, a node needs to notify other
|
||||
//! peers when it has just requested an update. That helps to keep the whole cluster in sync and avoid
|
||||
//! nodes' membership diverging.
|
||||
//!
|
||||
//! Overall this synchronizes all nodes' view of the membership.
|
||||
//!
|
||||
//! Current approach is a bit 'burst'. It makes all nodes to request membership info at the same time.
|
||||
//!
|
||||
//! TODO
|
||||
//! Because we actually can verify peers' membership, it would be possible that one peer(or subset of peers) requests the
|
||||
//! peers from a rendezvous point and then sends the list to the other peers. Or possibly only the difference.
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use asynchronous_codec::Framed;
|
||||
use futures::Sink;
|
||||
use futures_util::StreamExt;
|
||||
use libp2p::{
|
||||
swarm::handler::{
|
||||
DialUpgradeError, FullyNegotiatedInbound, FullyNegotiatedOutbound, ListenUpgradeError,
|
||||
},
|
||||
swarm::NegotiatedSubstream,
|
||||
swarm::{
|
||||
handler::ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, KeepAlive,
|
||||
SubstreamProtocol,
|
||||
},
|
||||
};
|
||||
use log::{debug, error};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::network::libp2p::behaviours::membership::protocol::{
|
||||
MembershipCodec, Protocol, ProtocolMessage,
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum Error {
|
||||
#[error("HandlerError: {0}")]
|
||||
Handler(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
//Because we keep long lived connections, we need to restrict number of substream attempts.
|
||||
//Here we don't need more than 1 because it's a 'quiet' protocol.
|
||||
const MAX_SUBSTREAM_ATTEMPTS: usize = 1;
|
||||
|
||||
enum InboundSubstreamState {
|
||||
WaitingInput(Framed<NegotiatedSubstream, MembershipCodec>),
|
||||
Closing(Framed<NegotiatedSubstream, MembershipCodec>),
|
||||
}
|
||||
|
||||
enum OutboundSubstreamState {
|
||||
WaitingOutput(Framed<NegotiatedSubstream, MembershipCodec>),
|
||||
PendingSend(
|
||||
Framed<NegotiatedSubstream, MembershipCodec>,
|
||||
ProtocolMessage,
|
||||
),
|
||||
PendingFlush(Framed<NegotiatedSubstream, MembershipCodec>),
|
||||
}
|
||||
|
||||
pub(crate) struct Handler {
|
||||
outbound_substream: Option<OutboundSubstreamState>,
|
||||
inbound_substream: Option<InboundSubstreamState>,
|
||||
send_queue: Vec<ProtocolMessage>,
|
||||
outbound_substream_establishing: bool,
|
||||
outbound_substream_attempts: usize,
|
||||
inbound_substream_attempts: usize,
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
outbound_substream: None,
|
||||
inbound_substream: None,
|
||||
send_queue: vec![],
|
||||
outbound_substream_establishing: false,
|
||||
outbound_substream_attempts: 0,
|
||||
inbound_substream_attempts: 0,
|
||||
}
|
||||
}
|
||||
|
||||
//Process inbound stream messages
|
||||
//WAITING_INPUT
|
||||
// - if message received, send it to behaviour
|
||||
// - if receive error or None, close substream
|
||||
//
|
||||
//CLOSING
|
||||
// - Wait buffer to be flushed
|
||||
// - Close substream
|
||||
fn process_inbound_stream(
|
||||
&mut self,
|
||||
cx: &mut Context,
|
||||
) -> Option<Poll<ConnectionHandlerEvent<Protocol, (), FromHandler, Error>>> {
|
||||
loop {
|
||||
match std::mem::take(&mut self.inbound_substream) {
|
||||
// inbound idle state
|
||||
Some(InboundSubstreamState::WaitingInput(mut substream)) => {
|
||||
match substream.poll_next_unpin(cx) {
|
||||
Poll::Ready(Some(Ok(message))) => {
|
||||
self.inbound_substream =
|
||||
Some(InboundSubstreamState::WaitingInput(substream));
|
||||
|
||||
let from_handler = FromHandler::Message(message);
|
||||
|
||||
return Poll::Ready(ConnectionHandlerEvent::Custom(from_handler))
|
||||
.into();
|
||||
}
|
||||
Poll::Ready(Some(Err(err))) => {
|
||||
error!("Failed to read from substream: {err}",);
|
||||
self.inbound_substream =
|
||||
Some(InboundSubstreamState::Closing(substream));
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
debug!("Inbound stream closed by remote");
|
||||
self.inbound_substream =
|
||||
Some(InboundSubstreamState::Closing(substream));
|
||||
}
|
||||
Poll::Pending => {
|
||||
self.inbound_substream =
|
||||
Some(InboundSubstreamState::WaitingInput(substream));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(InboundSubstreamState::Closing(mut substream)) => {
|
||||
match Sink::poll_close(Pin::new(&mut substream), cx) {
|
||||
Poll::Ready(res) => {
|
||||
if let Err(e) = res {
|
||||
error!("Inbound substream error while closing: {e}");
|
||||
}
|
||||
self.inbound_substream = None;
|
||||
break;
|
||||
}
|
||||
Poll::Pending => {
|
||||
self.inbound_substream =
|
||||
Some(InboundSubstreamState::Closing(substream));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.inbound_substream = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
//Process outbound stream messages
|
||||
//WAITING_OUTPUT
|
||||
// - if send queue is not empty, go to PENDING_SEND
|
||||
//
|
||||
//PENDING_SEND
|
||||
// - send message to substream
|
||||
// - if send error, mark substream as Closing
|
||||
//
|
||||
//PENDING_FLUSH
|
||||
// - flush substream
|
||||
// - if flush error, mark substream as Closing
|
||||
fn process_outbound_stream(&mut self, cx: &mut Context) {
|
||||
loop {
|
||||
match std::mem::take(&mut self.outbound_substream) {
|
||||
// outbound idle state
|
||||
Some(OutboundSubstreamState::WaitingOutput(substream)) => {
|
||||
if let Some(message) = self.send_queue.pop() {
|
||||
self.outbound_substream =
|
||||
Some(OutboundSubstreamState::PendingSend(substream, message));
|
||||
continue;
|
||||
}
|
||||
|
||||
self.outbound_substream =
|
||||
Some(OutboundSubstreamState::WaitingOutput(substream));
|
||||
break;
|
||||
}
|
||||
Some(OutboundSubstreamState::PendingSend(mut substream, message)) => {
|
||||
match Sink::poll_ready(Pin::new(&mut substream), cx) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
match Sink::start_send(Pin::new(&mut substream), message) {
|
||||
Ok(()) => {
|
||||
self.outbound_substream =
|
||||
Some(OutboundSubstreamState::PendingFlush(substream));
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to send message on outbound stream: {e}");
|
||||
self.outbound_substream = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Poll::Ready(Err(e)) => {
|
||||
debug!("Failed to send message on outbound stream: {e}");
|
||||
self.outbound_substream = None;
|
||||
break;
|
||||
}
|
||||
Poll::Pending => {
|
||||
self.outbound_substream =
|
||||
Some(OutboundSubstreamState::PendingSend(substream, message));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(OutboundSubstreamState::PendingFlush(mut substream)) => {
|
||||
match Sink::poll_flush(Pin::new(&mut substream), cx) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
self.outbound_substream =
|
||||
Some(OutboundSubstreamState::WaitingOutput(substream));
|
||||
}
|
||||
Poll::Ready(Err(e)) => {
|
||||
debug!("Failed to flush outbound stream: {e}");
|
||||
self.outbound_substream = None;
|
||||
break;
|
||||
}
|
||||
Poll::Pending => {
|
||||
self.outbound_substream =
|
||||
Some(OutboundSubstreamState::PendingFlush(substream));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.outbound_substream = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum FromHandler {
|
||||
Message(ProtocolMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ToHandler {
|
||||
Message(ProtocolMessage),
|
||||
}
|
||||
|
||||
impl ConnectionHandler for Handler {
|
||||
type InEvent = ToHandler;
|
||||
type OutEvent = FromHandler;
|
||||
type Error = Error;
|
||||
type InboundProtocol = Protocol;
|
||||
type OutboundProtocol = Protocol;
|
||||
type InboundOpenInfo = ();
|
||||
type OutboundOpenInfo = ();
|
||||
|
||||
fn listen_protocol(&self) -> SubstreamProtocol<Protocol, ()> {
|
||||
SubstreamProtocol::new(Protocol, ())
|
||||
}
|
||||
|
||||
fn connection_keep_alive(&self) -> KeepAlive {
|
||||
//we could add idle timeout here
|
||||
KeepAlive::Yes
|
||||
}
|
||||
|
||||
fn poll(
|
||||
&mut self,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<
|
||||
ConnectionHandlerEvent<
|
||||
Self::OutboundProtocol,
|
||||
Self::OutboundOpenInfo,
|
||||
Self::OutEvent,
|
||||
Self::Error,
|
||||
>,
|
||||
> {
|
||||
// poll STATE_MACHINE
|
||||
//- Request outbound substream if neccessary
|
||||
//- poll inbound substream
|
||||
//- poll outbound substream
|
||||
|
||||
//Establish new connection when behaviour wants to send a message and we don't have an outbound substream yet
|
||||
if !self.send_queue.is_empty()
|
||||
&& self.outbound_substream.is_none()
|
||||
&& !self.outbound_substream_establishing
|
||||
{
|
||||
self.outbound_substream_establishing = true;
|
||||
return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest {
|
||||
protocol: SubstreamProtocol::new(Protocol, ()),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(res) = self.process_inbound_stream(cx) {
|
||||
return res;
|
||||
}
|
||||
|
||||
self.process_outbound_stream(cx);
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
fn on_behaviour_event(&mut self, event: Self::InEvent) {
|
||||
match event {
|
||||
ToHandler::Message(message) => {
|
||||
self.send_queue.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_connection_event(
|
||||
&mut self,
|
||||
event: ConnectionEvent<
|
||||
Self::InboundProtocol,
|
||||
Self::OutboundProtocol,
|
||||
Self::InboundOpenInfo,
|
||||
Self::OutboundOpenInfo,
|
||||
>,
|
||||
) {
|
||||
match event {
|
||||
ConnectionEvent::FullyNegotiatedInbound(FullyNegotiatedInbound {
|
||||
protocol: stream,
|
||||
info: _,
|
||||
}) => {
|
||||
if self.inbound_substream_attempts > MAX_SUBSTREAM_ATTEMPTS {
|
||||
log::warn!("Too many inbound substream attempts, refusing stream");
|
||||
return;
|
||||
}
|
||||
self.inbound_substream_attempts += 1;
|
||||
self.inbound_substream = Some(InboundSubstreamState::WaitingInput(stream));
|
||||
}
|
||||
ConnectionEvent::FullyNegotiatedOutbound(FullyNegotiatedOutbound {
|
||||
protocol,
|
||||
info: _,
|
||||
}) => {
|
||||
if self.outbound_substream_attempts > MAX_SUBSTREAM_ATTEMPTS {
|
||||
log::warn!("Too many outbound substream attempts, refusing stream");
|
||||
return;
|
||||
}
|
||||
self.outbound_substream = Some(OutboundSubstreamState::WaitingOutput(protocol));
|
||||
}
|
||||
ConnectionEvent::DialUpgradeError(DialUpgradeError { info, error }) => {
|
||||
error!("DialUpgradeError: info: {:?}, error: {:?}", info, error);
|
||||
}
|
||||
ConnectionEvent::ListenUpgradeError(ListenUpgradeError { info, error }) => {
|
||||
error!("ListenUpgradeError: info: {:?}, error: {:?}", info, error);
|
||||
}
|
||||
ConnectionEvent::AddressChange(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
//! In Ephemera, membership of reliable broadcast protocol is decided by membership provider.
|
||||
//! Only peers who are returned by [`crate::membership::MembersProviderFut`] are allowed to participate.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use libp2p_identity::PeerId;
|
||||
use lru::LruCache;
|
||||
|
||||
use crate::network::Peer;
|
||||
|
||||
pub(crate) mod behaviour;
|
||||
mod connections;
|
||||
mod handler;
|
||||
mod protocol;
|
||||
|
||||
const MAX_DIAL_ATTEMPT_ROUNDS: usize = 6;
|
||||
|
||||
/// Minimum percentage of available nodes to consider the network healthy.
|
||||
//TODO: make this configurable
|
||||
const MEMBERSHIP_MINIMUM_AVAILABLE_NODES_RATIO: f64 = 0.8;
|
||||
|
||||
/// Minimum time between syncs of membership.
|
||||
const MEMBERSHIP_SYNC_INTERVAL_SEC: u64 = 60;
|
||||
|
||||
/// Maximum percentage of nodes that can change in a single membership update.
|
||||
/// In general it should be considered a security risk if it has changed too much.
|
||||
/// //TODO: make this configurable
|
||||
const _MEMBERSHIP_MAXIMUM_ALLOWED_CHANGE_RATIO: f64 = 0.2;
|
||||
|
||||
/// Membership provider returns list of peers. But it is up to the Ephemera user to decide
|
||||
/// how reliable the list is. For example, it can contain peers who are offline.
|
||||
|
||||
/// This enum defines how the actual membership is decided.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum MembershipKind {
|
||||
/// Mandatory minimum membership size is defined by threshold of all peers returned by membership provider.
|
||||
Threshold(f64),
|
||||
/// Mandatory minimum membership size is all peers who are online.
|
||||
AnyOnline,
|
||||
/// Mandatory minimum membership size is all peers returned by membership provider.
|
||||
AllOnline,
|
||||
}
|
||||
|
||||
impl MembershipKind {
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_possible_truncation
|
||||
)]
|
||||
pub(crate) fn accept(&self, membership: &Membership) -> bool {
|
||||
let total_number_of_peers = membership.all_members.len();
|
||||
let connected_peers = membership.connected_peers_ids.len();
|
||||
match self {
|
||||
MembershipKind::Threshold(threshold) => {
|
||||
let minimum_available_nodes = (total_number_of_peers as f64 * threshold) as usize;
|
||||
connected_peers >= minimum_available_nodes
|
||||
}
|
||||
MembershipKind::AnyOnline => connected_peers > 0,
|
||||
MembershipKind::AllOnline => connected_peers == total_number_of_peers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Memberships {
|
||||
snapshots: LruCache<u64, Membership>,
|
||||
current: u64,
|
||||
/// This is set when we get new peers set from [crate::membership::MembersProviderFut]
|
||||
/// but haven't yet activated it.
|
||||
pending_membership: Option<Membership>,
|
||||
}
|
||||
|
||||
impl Memberships {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut snapshots = LruCache::new(NonZeroUsize::new(1000).unwrap());
|
||||
snapshots.put(0, Membership::new(HashMap::default()));
|
||||
Self {
|
||||
snapshots,
|
||||
current: 0,
|
||||
pending_membership: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current(&mut self) -> &Membership {
|
||||
//Unwrap is safe because we always have current membership
|
||||
self.snapshots.get(&self.current).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, membership: Membership) {
|
||||
self.current += 1;
|
||||
self.snapshots.put(self.current, membership);
|
||||
}
|
||||
|
||||
pub(crate) fn set_pending(&mut self, membership: Membership) {
|
||||
self.pending_membership = Some(membership);
|
||||
}
|
||||
|
||||
pub(crate) fn remove_pending(&mut self) -> Option<Membership> {
|
||||
self.pending_membership.take()
|
||||
}
|
||||
|
||||
pub(crate) fn pending(&self) -> Option<&Membership> {
|
||||
self.pending_membership.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn pending_mut(&mut self) -> Option<&mut Membership> {
|
||||
self.pending_membership.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Membership {
|
||||
local_peer_id: PeerId,
|
||||
all_members: HashMap<PeerId, Peer>,
|
||||
all_peers_ids: HashSet<PeerId>,
|
||||
connected_peers_ids: HashSet<PeerId>,
|
||||
}
|
||||
|
||||
impl Membership {
|
||||
pub(crate) fn new_with_local(
|
||||
all_members: HashMap<PeerId, Peer>,
|
||||
local_peer_id: PeerId,
|
||||
) -> Self {
|
||||
let all_peers_ids = all_members.keys().copied().collect();
|
||||
Self {
|
||||
local_peer_id,
|
||||
all_members,
|
||||
all_peers_ids,
|
||||
connected_peers_ids: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new(all_members: HashMap<PeerId, Peer>) -> Self {
|
||||
let all_peers_ids = all_members.keys().copied().collect();
|
||||
Self {
|
||||
local_peer_id: PeerId::random(),
|
||||
all_members,
|
||||
all_peers_ids,
|
||||
connected_peers_ids: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn includes_local(&self) -> bool {
|
||||
self.all_members.contains_key(&self.local_peer_id)
|
||||
}
|
||||
|
||||
pub(crate) fn peer_connected(&mut self, peer_id: PeerId) {
|
||||
self.connected_peers_ids.insert(peer_id);
|
||||
}
|
||||
|
||||
pub(crate) fn peer_disconnected(&mut self, peer_id: &PeerId) {
|
||||
self.connected_peers_ids.remove(peer_id);
|
||||
}
|
||||
|
||||
pub(crate) fn all_peer_ids(&self) -> &HashSet<PeerId> {
|
||||
&self.all_peers_ids
|
||||
}
|
||||
|
||||
pub(crate) fn connected_peer_ids(&self) -> HashSet<PeerId> {
|
||||
self.connected_peers_ids.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn connected_peer_ids_with_local(&self) -> HashSet<PeerId> {
|
||||
let mut active_peers = self.connected_peers_ids.clone();
|
||||
active_peers.insert(self.local_peer_id);
|
||||
active_peers
|
||||
}
|
||||
|
||||
pub(crate) fn connected_peers(&self) -> &HashSet<PeerId> {
|
||||
&self.connected_peers_ids
|
||||
}
|
||||
|
||||
pub(crate) fn peer_address(&self, peer_id: &PeerId) -> Option<&libp2p::Multiaddr> {
|
||||
self.all_members
|
||||
.get(peer_id)
|
||||
.map(|peer| peer.address.inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::config::MembershipKind> for MembershipKind {
|
||||
fn from(kind: crate::config::MembershipKind) -> Self {
|
||||
match kind {
|
||||
crate::config::MembershipKind::Threshold => {
|
||||
MembershipKind::Threshold(MEMBERSHIP_MINIMUM_AVAILABLE_NODES_RATIO)
|
||||
}
|
||||
crate::config::MembershipKind::AnyOnline => MembershipKind::AnyOnline,
|
||||
crate::config::MembershipKind::AllOnline => MembershipKind::AllOnline,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
use std::future::Future;
|
||||
use std::iter;
|
||||
use std::pin::Pin;
|
||||
|
||||
use asynchronous_codec::{Decoder, Encoder, Framed};
|
||||
use bytes::BytesMut;
|
||||
use futures::{AsyncRead, AsyncWrite};
|
||||
use futures_util::future;
|
||||
use libp2p::core::UpgradeInfo;
|
||||
use libp2p::{InboundUpgrade, OutboundUpgrade};
|
||||
use log::trace;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::utilities::codec::varint_bytes::{read_length_prefixed, write_length_prefixed};
|
||||
|
||||
pub const PROTOCOL_NAME: &[u8] = b"/ephemera/membership/1.0.0";
|
||||
|
||||
pub(crate) struct Protocol;
|
||||
|
||||
impl UpgradeInfo for Protocol {
|
||||
type Info = &'static [u8];
|
||||
type InfoIter = iter::Once<Self::Info>;
|
||||
|
||||
fn protocol_info(&self) -> Self::InfoIter {
|
||||
iter::once(PROTOCOL_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> InboundUpgrade<C> for Protocol
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
type Output = Framed<C, MembershipCodec>;
|
||||
type Error = anyhow::Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
|
||||
|
||||
fn upgrade_inbound(self, socket: C, _: Self::Info) -> Self::Future {
|
||||
trace!(
|
||||
"Inbound upgrade for protocol: {}",
|
||||
String::from_utf8_lossy(PROTOCOL_NAME)
|
||||
);
|
||||
Box::pin(future::ok(Framed::new(socket, MembershipCodec {})))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> OutboundUpgrade<C> for Protocol
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
type Output = Framed<C, MembershipCodec>;
|
||||
type Error = anyhow::Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
|
||||
|
||||
fn upgrade_outbound(self, socket: C, _: Self::Info) -> Self::Future {
|
||||
trace!(
|
||||
"Outbound upgrade for protocol: {}",
|
||||
String::from_utf8_lossy(PROTOCOL_NAME)
|
||||
);
|
||||
Box::pin(future::ok(Framed::new(socket, MembershipCodec {})))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) enum ProtocolMessage {
|
||||
Sync,
|
||||
}
|
||||
|
||||
pub(crate) struct MembershipCodec {}
|
||||
|
||||
impl Encoder for MembershipCodec {
|
||||
type Item = ProtocolMessage;
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
//FIXME: switch to binary
|
||||
let data = serde_json::to_vec(&item).unwrap();
|
||||
write_length_prefixed(dst, data);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for MembershipCodec {
|
||||
type Item = ProtocolMessage;
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
if src.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let data = read_length_prefixed(src, 1024 * 1024)?;
|
||||
match data {
|
||||
None => Ok(None),
|
||||
Some(data) => {
|
||||
//FIXME: switch to binary
|
||||
let msg = serde_json::from_slice(&data)?;
|
||||
Ok(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
use std::future::Future;
|
||||
use std::{iter, sync::Arc, time::Duration};
|
||||
|
||||
use libp2p::{
|
||||
core::{muxing::StreamMuxerBox, transport::Boxed},
|
||||
dns, gossipsub,
|
||||
gossipsub::{IdentTopic as Topic, MessageAuthenticity, ValidationMode},
|
||||
kad, noise, request_response as libp2p_request_response,
|
||||
swarm::NetworkBehaviour,
|
||||
tcp::{tokio::Transport as TokioTransport, Config as TokioConfig},
|
||||
yamux, PeerId as Libp2pPeerId, Transport,
|
||||
};
|
||||
use log::info;
|
||||
|
||||
use crate::membership::PeerInfo;
|
||||
use crate::network::libp2p::behaviours::membership::MembershipKind;
|
||||
use crate::{
|
||||
broadcast::RbMsg,
|
||||
crypto::Keypair,
|
||||
network::libp2p::behaviours::request_response::{
|
||||
RbMsgMessagesCodec, RbMsgProtocol, RbMsgResponse,
|
||||
},
|
||||
peer::{PeerId, ToPeerId},
|
||||
utilities::hash::{EphemeraHasher, Hasher},
|
||||
};
|
||||
|
||||
pub(crate) mod membership;
|
||||
pub(crate) mod request_response;
|
||||
|
||||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(out_event = "GroupBehaviourEvent")]
|
||||
pub(crate) struct GroupNetworkBehaviour<P>
|
||||
where
|
||||
P: Future<Output = crate::membership::Result<Vec<PeerInfo>>> + Send + 'static,
|
||||
{
|
||||
pub(crate) members_provider: membership::behaviour::Behaviour<P>,
|
||||
pub(crate) gossipsub: gossipsub::Behaviour,
|
||||
pub(crate) request_response: libp2p_request_response::Behaviour<RbMsgMessagesCodec>,
|
||||
pub(crate) kademlia: kad::Kademlia<kad::store::MemoryStore>,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum GroupBehaviourEvent {
|
||||
Gossipsub(gossipsub::Event),
|
||||
RequestResponse(libp2p_request_response::Event<RbMsg, RbMsgResponse>),
|
||||
Membership(membership::behaviour::Event),
|
||||
Kademlia(kad::KademliaEvent),
|
||||
}
|
||||
|
||||
impl From<gossipsub::Event> for GroupBehaviourEvent {
|
||||
fn from(event: gossipsub::Event) -> Self {
|
||||
GroupBehaviourEvent::Gossipsub(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libp2p_request_response::Event<RbMsg, RbMsgResponse>> for GroupBehaviourEvent {
|
||||
fn from(event: libp2p_request_response::Event<RbMsg, RbMsgResponse>) -> Self {
|
||||
GroupBehaviourEvent::RequestResponse(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<membership::behaviour::Event> for GroupBehaviourEvent {
|
||||
fn from(event: membership::behaviour::Event) -> Self {
|
||||
GroupBehaviourEvent::Membership(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<kad::KademliaEvent> for GroupBehaviourEvent {
|
||||
fn from(event: kad::KademliaEvent) -> Self {
|
||||
GroupBehaviourEvent::Kademlia(event)
|
||||
}
|
||||
}
|
||||
|
||||
//Create combined behaviour.
|
||||
//Gossipsub takes care of message delivery semantics
|
||||
//Membership takes care of providing peers who are part of the reliable broadcast group
|
||||
//Kademlia takes provides closest neighbours and general DHT functionality
|
||||
pub(crate) fn create_behaviour<P>(
|
||||
keypair: &Arc<Keypair>,
|
||||
ephemera_msg_topic: &Topic,
|
||||
members_provider: P,
|
||||
members_provider_delay: Duration,
|
||||
membership_kind: MembershipKind,
|
||||
) -> GroupNetworkBehaviour<P>
|
||||
where
|
||||
P: Future<Output = crate::membership::Result<Vec<PeerInfo>>> + Send + Unpin + 'static,
|
||||
{
|
||||
//TODO: review behaviours config(eg. gossipsub minimum peers, kademlia ttl, request-response timeouts etc.)
|
||||
let local_peer_id = keypair.peer_id();
|
||||
let gossipsub = create_gossipsub(keypair, ephemera_msg_topic);
|
||||
let request_response = create_request_response();
|
||||
let rendezvous_behaviour = create_membership(
|
||||
members_provider,
|
||||
members_provider_delay,
|
||||
membership_kind,
|
||||
local_peer_id,
|
||||
);
|
||||
let kademlia = create_kademlia(keypair);
|
||||
|
||||
GroupNetworkBehaviour {
|
||||
members_provider: rendezvous_behaviour,
|
||||
gossipsub,
|
||||
request_response,
|
||||
kademlia,
|
||||
}
|
||||
}
|
||||
|
||||
// Configure networking messaging stack(Gossipsub)
|
||||
pub(crate) fn create_gossipsub(local_key: &Arc<Keypair>, topic: &Topic) -> gossipsub::Behaviour {
|
||||
let gossipsub_config = gossipsub::ConfigBuilder::default()
|
||||
//TODO: settings from config
|
||||
.heartbeat_interval(Duration::from_secs(5))
|
||||
.message_id_fn(|msg: &gossipsub::Message| Hasher::digest(&msg.data).into())
|
||||
.validation_mode(ValidationMode::Strict)
|
||||
.build()
|
||||
.expect("Valid config");
|
||||
|
||||
let mut behaviour = gossipsub::Behaviour::new(
|
||||
MessageAuthenticity::Signed(local_key.inner().clone()),
|
||||
gossipsub_config,
|
||||
)
|
||||
.expect("Correct configuration");
|
||||
|
||||
info!("Subscribing to topic: {}", topic);
|
||||
behaviour.subscribe(topic).expect("Valid topic");
|
||||
behaviour
|
||||
}
|
||||
|
||||
pub(crate) fn create_request_response() -> libp2p_request_response::Behaviour<RbMsgMessagesCodec> {
|
||||
let config = libp2p_request_response::Config::default();
|
||||
libp2p_request_response::Behaviour::new(
|
||||
RbMsgMessagesCodec,
|
||||
iter::once((
|
||||
RbMsgProtocol,
|
||||
libp2p_request_response::ProtocolSupport::Full,
|
||||
)),
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn create_membership<P>(
|
||||
members_provider: P,
|
||||
members_provider_delay: Duration,
|
||||
membership_kind: MembershipKind,
|
||||
local_peer_id: PeerId,
|
||||
) -> membership::behaviour::Behaviour<P>
|
||||
where
|
||||
P: Future<Output = crate::membership::Result<Vec<PeerInfo>>> + Send + Unpin + 'static,
|
||||
{
|
||||
membership::behaviour::Behaviour::new(
|
||||
members_provider,
|
||||
members_provider_delay,
|
||||
local_peer_id.into(),
|
||||
membership_kind,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn create_kademlia(local_key: &Arc<Keypair>) -> kad::Kademlia<kad::store::MemoryStore> {
|
||||
let peer_id = local_key.peer_id();
|
||||
let mut cfg = kad::KademliaConfig::default();
|
||||
cfg.set_query_timeout(Duration::from_secs(5 * 60));
|
||||
let store = kad::store::MemoryStore::new(peer_id.0);
|
||||
kad::Kademlia::with_config(*peer_id.inner(), store, cfg)
|
||||
}
|
||||
|
||||
//Configure networking connection stack(Tcp, Noise, Yamux)
|
||||
//Tcp protocol for networking
|
||||
//Noise protocol for encryption
|
||||
//Yamux protocol for multiplexing
|
||||
pub(crate) fn create_transport(
|
||||
local_key: &Arc<Keypair>,
|
||||
) -> anyhow::Result<Boxed<(Libp2pPeerId, StreamMuxerBox)>> {
|
||||
let transport = TokioTransport::new(TokioConfig::default().nodelay(true));
|
||||
let transport = dns::TokioDnsConfig::system(transport)?;
|
||||
|
||||
let noise_config = noise::Config::new(local_key.inner())?;
|
||||
Ok(transport
|
||||
.upgrade(libp2p::core::upgrade::Version::V1)
|
||||
.authenticate(noise_config)
|
||||
.multiplex(yamux::Config::default())
|
||||
.timeout(Duration::from_secs(20))
|
||||
.boxed())
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use async_trait::async_trait;
|
||||
use futures::{AsyncRead, AsyncWrite};
|
||||
use libp2p::request_response;
|
||||
use log::trace;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::broadcast::RbMsg;
|
||||
use crate::utilities::codec::varint_async::{read_length_prefixed, write_length_prefixed};
|
||||
use crate::utilities::id::EphemeraId;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RbMsgMessagesCodec;
|
||||
|
||||
impl RbMsgMessagesCodec {}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RbMsgProtocol;
|
||||
|
||||
impl request_response::ProtocolName for RbMsgProtocol {
|
||||
fn protocol_name(&self) -> &[u8] {
|
||||
"/ephemera/reliable_broadcast/1.0.0".as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct RbMsgResponse {
|
||||
pub(crate) id: EphemeraId,
|
||||
}
|
||||
|
||||
impl RbMsgResponse {
|
||||
pub(crate) fn new(id: EphemeraId) -> Self {
|
||||
Self { id }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl request_response::Codec for RbMsgMessagesCodec {
|
||||
type Protocol = RbMsgProtocol;
|
||||
type Request = RbMsg;
|
||||
type Response = RbMsgResponse;
|
||||
|
||||
async fn read_request<T>(
|
||||
&mut self,
|
||||
_: &Self::Protocol,
|
||||
io: &mut T,
|
||||
) -> Result<Self::Request, std::io::Error>
|
||||
where
|
||||
T: AsyncRead + Unpin + Send,
|
||||
{
|
||||
//FIXME: max size
|
||||
let data = read_length_prefixed(io, 1024 * 1024).await?;
|
||||
//FIXME: switch to binary
|
||||
let msg = serde_json::from_slice(&data)?;
|
||||
trace!("Received request {:?}", msg);
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
async fn read_response<T>(
|
||||
&mut self,
|
||||
_: &Self::Protocol,
|
||||
io: &mut T,
|
||||
) -> std::io::Result<Self::Response>
|
||||
where
|
||||
T: AsyncRead + Unpin + Send,
|
||||
{
|
||||
//FIXME: max size
|
||||
let response = read_length_prefixed(io, 1024 * 1024).await?;
|
||||
//FIXME: switch to binary
|
||||
let response = serde_json::from_slice(&response)?;
|
||||
trace!("Received response {:?}", response);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn write_request<T>(
|
||||
&mut self,
|
||||
_: &Self::Protocol,
|
||||
io: &mut T,
|
||||
req: Self::Request,
|
||||
) -> Result<(), std::io::Error>
|
||||
where
|
||||
T: AsyncWrite + Unpin + Send,
|
||||
{
|
||||
let data = serde_json::to_vec(&req).unwrap();
|
||||
write_length_prefixed(io, data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_response<T>(
|
||||
&mut self,
|
||||
_: &Self::Protocol,
|
||||
io: &mut T,
|
||||
response: Self::Response,
|
||||
) -> std::io::Result<()>
|
||||
where
|
||||
T: AsyncWrite + Unpin + Send,
|
||||
{
|
||||
let response = serde_json::to_vec(&response).unwrap();
|
||||
write_length_prefixed(io, response).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user