Compare commits

..

2 Commits

Author SHA1 Message Date
Bogdan-Ștefan Neacșu f25f76c1df Merge remote-tracking branch 'origin/incoming' into base 2023-06-29 17:02:11 +03:00
Bogdan-Ștefan Neacşu 29f95febe9 Feature/ephemera compile (#3437)
* Include ephemera node code in repo

* Upgrade deps

* Bump minor version of cosmwasm-std

* Include ephemera in nym-api dep and downgrade rusqlite

* Fix clippy and ephemera docs code

* More clippy on ephemera

---------

Co-authored-by: Andrus Salumets <andrus@nymtech.net>
2023-05-25 11:24:49 +03:00
135 changed files with 14139 additions and 814 deletions
+13 -10
View File
@@ -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};
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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",
+235 -103
View File
@@ -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
View File
@@ -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,
),
+1 -1
View File
@@ -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(),
+1 -1
View File
@@ -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);
+2 -2
View File
@@ -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,
),
+2 -2
View File
@@ -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,
),
+2 -2
View File
@@ -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(),
+2 -2
View File
@@ -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");
+3 -3
View File
@@ -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"
+5 -5
View File
@@ -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 }
+32 -367
View File
@@ -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);
}
}
+10 -4
View File
@@ -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 },
}
+17 -3
View File
@@ -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(())
}
+19
View File
@@ -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;
+16 -8
View File
@@ -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,
);
+439
View File
@@ -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);
}
+2 -2
View File
@@ -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,
),
+1 -1
View File
@@ -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>,
+57
View File
@@ -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"]
+142
View File
@@ -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.
+12
View File
@@ -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(())
}
+24
View File
@@ -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
);
+104
View File
@@ -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(())
}
}
+497
View File
@@ -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()),
}
}
}
+88
View File
@@ -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);
}
+182
View File
@@ -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")
}
}
}
+88
View File
@@ -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")
}
}
}
+309
View File
@@ -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())
})?
}
}
+659
View File
@@ -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());
}
}
+74
View File
@@ -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,
})
}
}
+688
View File
@@ -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()
}
}
+87
View File
@@ -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);
}
}
+26
View File
@@ -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;
+92
View File
@@ -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");
}
}
}
}
+327
View File
@@ -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
}
}
+99
View File
@@ -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)
}
}
+2
View File
@@ -0,0 +1,2 @@
pub(crate) mod block;
pub(crate) mod message;
+320
View File
@@ -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
}
}
+2
View File
@@ -0,0 +1,2 @@
pub(crate) mod broadcast;
pub(crate) mod quorum;
+254
View File
@@ -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
}
}
+249
View File
@@ -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
}
}
+191
View File
@@ -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),
}
+150
View File
@@ -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)
}
}
+132
View File
@@ -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
}
}
+15
View File
@@ -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());
}
}
+136
View File
@@ -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:?}",);
}
}
}
+49
View File
@@ -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(())
}
}
+68
View File
@@ -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)
}
}
+169
View File
@@ -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(())
}
}
+327
View File
@@ -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(())
}
}
+390
View File
@@ -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");
}
}
}
}
+413
View File
@@ -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,
}
}
}
+420
View File
@@ -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(())
}
}
+4
View File
@@ -0,0 +1,4 @@
pub(crate) mod api_cmd;
pub(crate) mod builder;
pub(crate) mod ephemera;
pub(crate) mod shutdown;
+84
View File
@@ -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);
}
}
+119
View File
@@ -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;
+19
View File
@@ -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