Feature/fixes while testing (#3668)

* Commit local peer before querying contract

* Default to anyonline

* Remove string from template

* Fix avg computing

* Use updated qa env

* Fix clippy

* Add unit tests for ephemera contract

* Upload ephemera contract in CI

* Add group check for peer signup

* Peer registration unit test
This commit is contained in:
Bogdan-Ștefan Neacşu
2023-07-14 11:31:59 +03:00
committed by GitHub
parent c60345e8ae
commit 543eb8e8f5
17 changed files with 382 additions and 56 deletions
@@ -109,6 +109,7 @@ jobs:
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_service_provider_directory.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_name_service.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_ephemera.wasm $OUTPUT_DIR
- name: Deploy branch to CI www
continue-on-error: true
+29
View File
@@ -66,3 +66,32 @@ pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result<QueryResponse,
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
Ok(Default::default())
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::Addr;
#[test]
fn initialize_contract() {
let mut deps = mock_dependencies();
let env = mock_env();
let init_msg = InstantiateMsg {
group_addr: "group_addr".to_string(),
mix_denom: "uatom".to_string(),
};
let sender = mock_info("sender", &[]);
let res = instantiate(deps.as_mut(), env, sender, init_msg);
assert!(res.is_ok());
let expected_state = State {
group_addr: Cw4Contract::new(Addr::unchecked("group_addr")),
mix_denom: "uatom".to_string(),
};
let state = STATE.load(deps.as_ref().storage).unwrap();
assert_eq!(state, expected_state);
}
}
+3
View File
@@ -13,6 +13,9 @@ pub enum ContractError {
#[error("Group contract invalid address '{addr}'")]
InvalidGroup { addr: String },
#[error("This potential ephemera peer is not in the ephemera group")]
Unauthorized,
#[error("This sender is already registered")]
AlreadyRegistered,
}
+1
View File
@@ -5,3 +5,4 @@ pub mod contract;
pub mod error;
mod peers;
mod state;
mod support;
+129
View File
@@ -33,3 +33,132 @@ pub fn query_peers_paged(
Ok(PagedPeerResponse::new(peers, limit, start_next_after))
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::peers::storage::{PEERS_PAGE_DEFAULT_LIMIT, PEERS_PAGE_MAX_LIMIT};
use crate::support::tests::fixtures::peer_fixture;
use crate::support::tests::helpers::init_contract;
use cosmwasm_std::DepsMut;
fn fill_peers(deps: DepsMut<'_>, size: usize) {
for n in 0..size {
let peer = peer_fixture(&format!("peer{}", n));
PEERS
.save(deps.storage, peer.cosmos_address.clone(), &peer)
.unwrap();
}
}
fn remove_peers(deps: DepsMut<'_>, size: usize) {
for n in 0..size {
let peer = peer_fixture(&format!("peer{}", n));
PEERS.remove(deps.storage, peer.cosmos_address);
}
}
#[test]
fn peers_empty_on_init() {
let deps = init_contract();
let page1 = query_peers_paged(deps.as_ref(), None, None).unwrap();
assert_eq!(0, page1.peers.len() as u32);
}
#[test]
fn peers_paged_retrieval_obeys_limits() {
let mut deps = init_contract();
let limit = 2;
fill_peers(deps.as_mut(), 1000);
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(limit)).unwrap();
assert_eq!(limit, page1.peers.len() as u32);
remove_peers(deps.as_mut(), 1000);
}
#[test]
fn peers_paged_retrieval_has_default_limit() {
let mut deps = init_contract();
fill_peers(deps.as_mut(), 1000);
// query without explicitly setting a limit
let page1 = query_peers_paged(deps.as_ref(), None, None).unwrap();
assert_eq!(PEERS_PAGE_DEFAULT_LIMIT, page1.peers.len() as u32);
remove_peers(deps.as_mut(), 1000);
}
#[test]
fn peers_paged_retrieval_has_max_limit() {
let mut deps = init_contract();
// query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000 * PEERS_PAGE_MAX_LIMIT;
fill_peers(deps.as_mut(), 1000);
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap();
// we default to a decent sized upper bound instead
let expected_limit = PEERS_PAGE_MAX_LIMIT;
assert_eq!(expected_limit, page1.peers.len() as u32);
remove_peers(deps.as_mut(), 1000);
}
#[test]
fn peers_pagination_works() {
let mut deps = init_contract();
let per_page = 2;
fill_peers(deps.as_mut(), 1);
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
// page should have 1 result on it
assert_eq!(1, page1.peers.len());
remove_peers(deps.as_mut(), 1);
fill_peers(deps.as_mut(), 2);
// page1 should have 2 results on it
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.peers.len());
remove_peers(deps.as_mut(), 2);
fill_peers(deps.as_mut(), 3);
// page1 still has 2 results
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.peers.len());
// retrieving the next page should start after the last key on this page
let start_after = page1.start_next_after.unwrap();
let page2 = query_peers_paged(
deps.as_ref(),
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.peers.len());
remove_peers(deps.as_mut(), 3);
fill_peers(deps.as_mut(), 4);
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
let start_after = page1.start_next_after.unwrap();
let page2 = query_peers_paged(
deps.as_ref(),
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.peers.len());
remove_peers(deps.as_mut(), 4);
}
}
+46 -3
View File
@@ -3,6 +3,7 @@
use crate::error::ContractError;
use crate::peers::storage::PEERS;
use crate::state::STATE;
use cosmwasm_std::{DepsMut, MessageInfo, Response};
use nym_ephemera_common::types::JsonPeerInfo;
@@ -12,9 +13,51 @@ pub fn try_register_peer(
peer_info: JsonPeerInfo,
) -> Result<Response, ContractError> {
if PEERS.may_load(deps.storage, info.sender.clone())?.is_none() {
PEERS.save(deps.storage, info.sender, &peer_info)?;
if STATE
.load(deps.storage)?
.group_addr
.is_voting_member(&deps.querier, &info.sender, None)?
.is_some()
{
PEERS.save(deps.storage, info.sender, &peer_info)?;
Ok(Default::default())
} else {
Err(ContractError::Unauthorized {})
}
} else {
return Err(ContractError::AlreadyRegistered);
Err(ContractError::AlreadyRegistered)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::support::tests::fixtures::peer_fixture;
use crate::support::tests::helpers;
use crate::support::tests::helpers::GROUP_MEMBERS;
use cosmwasm_std::testing::mock_info;
use cw4::Member;
#[test]
fn peer_registration() {
let mut deps = helpers::init_contract();
let peer_info = peer_fixture("owner");
let info = mock_info("owner", &[]);
let ret = try_register_peer(deps.as_mut(), info.clone(), peer_info.clone()).unwrap_err();
assert_eq!(ret, ContractError::Unauthorized);
GROUP_MEMBERS.lock().unwrap().push((
Member {
addr: "owner".to_string(),
weight: 10,
},
1,
));
try_register_peer(deps.as_mut(), info.clone(), peer_info.clone()).unwrap();
let ret = try_register_peer(deps.as_mut(), info, peer_info).unwrap_err();
assert_eq!(ret, ContractError::AlreadyRegistered);
}
Ok(Default::default())
}
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(test)]
pub mod tests;
@@ -0,0 +1,15 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use nym_ephemera_common::types::JsonPeerInfo;
pub const TEST_MIX_DENOM: &str = "unym";
pub fn peer_fixture(cosmos_address: &str) -> JsonPeerInfo {
JsonPeerInfo {
cosmos_address: Addr::unchecked(cosmos_address),
ip_address: "127.0.0.1".to_string(),
public_key: "random_key".to_string(),
}
}
@@ -0,0 +1,74 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::contract::instantiate;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier};
use cosmwasm_std::{
from_binary, to_binary, ContractResult, Empty, MemoryStorage, OwnedDeps, QuerierResult,
SystemResult, WasmQuery,
};
use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse};
use lazy_static::lazy_static;
use nym_ephemera_common::msg::InstantiateMsg;
use std::sync::Mutex;
use super::fixtures::TEST_MIX_DENOM;
pub const ADMIN_ADDRESS: &str = "admin address";
pub const GROUP_CONTRACT: &str = "group contract address";
lazy_static! {
pub static ref GROUP_MEMBERS: Mutex<Vec<(Member, u64)>> = Mutex::new(vec![]);
}
fn querier_handler(query: &WasmQuery) -> QuerierResult {
let bin = match query {
WasmQuery::Smart { contract_addr, msg } => {
if contract_addr != GROUP_CONTRACT {
panic!("Not supported");
}
match from_binary(msg) {
Ok(Cw4QueryMsg::Member { addr, at_height }) => {
let weight = GROUP_MEMBERS.lock().unwrap().iter().find_map(|(m, h)| {
if m.addr == addr {
if let Some(height) = at_height {
if height != *h {
return None;
}
}
Some(m.weight)
} else {
None
}
});
to_binary(&MemberResponse { weight }).unwrap()
}
Ok(Cw4QueryMsg::ListMembers { .. }) => {
let members = GROUP_MEMBERS
.lock()
.unwrap()
.iter()
.map(|m| m.0.clone())
.collect();
to_binary(&MemberListResponse { members }).unwrap()
}
_ => panic!("Not supported"),
}
}
_ => panic!("Not supported"),
};
SystemResult::Ok(ContractResult::Ok(bin))
}
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
let mut deps = mock_dependencies();
deps.querier.update_wasm(querier_handler);
let msg = InstantiateMsg {
group_addr: GROUP_CONTRACT.to_string(),
mix_denom: TEST_MIX_DENOM.to_string(),
};
let env = mock_env();
let info = mock_info(ADMIN_ADDRESS, &[]);
instantiate(deps.as_mut(), env, info, msg).unwrap();
deps
}
@@ -0,0 +1,5 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod fixtures;
pub mod helpers;
+14 -12
View File
@@ -9,17 +9,19 @@ MIX_DENOM_DISPLAY=nym
STAKE_DENOM=unyx
STAKE_DENOM_DISPLAY=nyx
DENOMS_EXPONENT=6
MIXNET_CONTRACT_ADDRESS=n1rjzps6qrmdqmf0xz4cn4x4rcmqeqzq6hnzqg4wcvd0r2lyasdq5sepn5s8
VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav
MIXNET_CONTRACT_ADDRESS=n10qt8wg0n7z740ssvf3urmvgtjhxpyp74hxqvqt7z226gykuus7eq5u9pvq
VESTING_CONTRACT_ADDRESS=n1vguuxez2h5ekltfj9gjd62fs5k4rl2zy5hfrncasykzw08rezpfstk9xtk
BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw
GROUP_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m
MULTISIG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs
COCONUT_DKG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs
EPHEMERA_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs
REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
NYXD="https://qa-validator.nymtech.net"
NYM_API="https://qa-validator-api.nymtech.net/api"
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1x22q8lfhz7qcvtzs0dakhgx2th64l79kepjujhhxk5x804taeqlqcywn96
GROUP_CONTRACT_ADDRESS=n1g4xlpqy29m50j5y69reguae328tc9y83l4299pf2wmjn0xczq5js3704ql
MULTISIG_CONTRACT_ADDRESS=n1p54qvfde6mpnqvz3dnpa78x2qyyr5k4sgw9qr97mxjgklc5gze9sv6t964
COCONUT_DKG_CONTRACT_ADDRESS=n1xqkp8x4gqwjnhemtemc5dqhwll6w6rrgpywvhka7sh8vz8swul9sp3lv3w
EPHEMERA_CONTRACT_ADDRESS=n1fc7nakzuyfn2qzkclafcsc54asamnclg064962lwne40w2lq558qftzjza
REWARDING_VALIDATOR_ADDRESS=n1547eraje66h9mf99fm50mea04n9mrzsc8c23r5
NAME=n1uz24lsnwxvhep8m3gjec7ev86twlhlqrf5rphlgn3rda3zu048ssjqr5w9
SERVICE_PROVICER=n1nhdr07kmjns2x8dnp53tdk4qxreze8zdxj6xucyvkdj9tta73rjqa96wps
STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090"
NYXD="https://qa-validator.qa.nymte.ch/"
NYM_API="https://qa-nym-api.qa.nymte.ch/api"
DKG_TIME_CONFIGURATION="600,300,300,60,60,1209600"
DKG_TIME_CONFIGURATION="600,300,300,60,60,1209600"
+2 -2
View File
@@ -36,8 +36,8 @@ impl Default for MembershipKind {
fn default() -> Self {
MembershipKind {
threshold: None,
all: true,
any: false,
all: false,
any: true,
}
}
}
+35 -10
View File
@@ -7,15 +7,16 @@ use ephemera::{
};
use log::{debug, error, info};
use crate::ephemera::client::Client;
use crate::ephemera::epoch::Epoch;
use crate::ephemera::peers::members::MembersProvider;
use crate::ephemera::peers::NymApiEphemeraPeerInfo;
use crate::ephemera::peers::{NymApiEphemeraPeerInfo, NymPeer};
use crate::ephemera::reward::aggregator::RewardsAggregator;
use crate::ephemera::reward::MixnodeToReward;
use crate::ephemera::reward::{EphemeraAccess, RewardManager};
use crate::ephemera::Args;
use crate::support::nyxd;
use ephemera::crypto::{EphemeraKeypair, Keypair};
use ephemera::crypto::{EphemeraKeypair, EphemeraPublicKey, Keypair};
use ephemera::ephemera_api::CommandExecutor;
use ephemera::{Ephemera, EphemeraStarterInit};
use nym_task::TaskManager;
@@ -55,15 +56,39 @@ impl NymApi {
) -> anyhow::Result<Ephemera<RewardsEphemeraApplication>> {
info!("Initializing ephemera ...");
//Application for Ephemera
let rewards_ephemera_application =
RewardsEphemeraApplication::init(ephemera_config.clone(), nyxd_client.clone()).await?;
let node_info = ephemera_config.node.clone();
let keypair = bs58::decode(&node_info.private_key).into_vec().unwrap();
let keypair = Keypair::from_bytes(&keypair).unwrap();
let local_peer_id = keypair.public_key().to_base58();
//Members provider for Ephemera
let members_provider = MembersProvider::new(nyxd_client);
let members_provider = MembersProvider::new(nyxd_client.clone());
let local_peer = rewards_ephemera_application.peer_info.local_peer.clone();
members_provider.register_peer(local_peer).await?;
let cosmos_address = nyxd_client.client_address().await.to_string();
let ip_address = format!(
"/ip4/{}/tcp/{}",
ephemera_config.node.ip, ephemera_config.libp2p.port
);
if !nyxd_client
.get_ephemera_peers()
.await?
.iter()
.any(|peer_info| peer_info.cosmos_address == cosmos_address)
{
let local_peer = NymPeer::new(
cosmos_address,
ip_address,
keypair.public_key(),
local_peer_id.clone(),
);
members_provider.register_peer(local_peer).await?;
}
//Application for Ephemera
let rewards_ephemera_application =
RewardsEphemeraApplication::init(local_peer_id, nyxd_client.clone()).await?;
//EPHEMERA
let ephemera_builder = EphemeraStarterInit::new(ephemera_config)?;
@@ -107,11 +132,11 @@ pub(crate) struct RewardsEphemeraApplication {
impl RewardsEphemeraApplication {
pub(crate) async fn init(
ephemera_config: Configuration,
local_peer_id: String,
nyxd_client: nyxd::Client,
) -> anyhow::Result<Self> {
let peer_info = match NymApiEphemeraPeerInfo::from_ephemera_dev_cluster_conf(
&ephemera_config,
local_peer_id,
nyxd_client,
)
.await
+5 -12
View File
@@ -5,8 +5,7 @@ use anyhow::anyhow;
use crate::ephemera::client::Client;
use crate::support::nyxd;
use ephemera::configuration::Configuration;
use ephemera::crypto::{EphemeraKeypair, EphemeraPublicKey, Keypair, PublicKey};
use ephemera::crypto::PublicKey;
pub(crate) type PeerId = String;
@@ -38,13 +37,13 @@ impl NymPeer {
// Information about other Nym-Apis.
pub(crate) struct NymApiEphemeraPeerInfo {
pub(crate) local_peer: NymPeer,
pub(crate) _local_peer: NymPeer,
pub(crate) peers: HashMap<PeerId, NymPeer>,
}
impl NymApiEphemeraPeerInfo {
fn new(local_peer: NymPeer, peers: HashMap<PeerId, NymPeer>) -> Self {
Self { local_peer, peers }
fn new(_local_peer: NymPeer, peers: HashMap<PeerId, NymPeer>) -> Self {
Self { _local_peer, peers }
}
pub(crate) fn get_peers_count(&self) -> usize {
@@ -52,15 +51,9 @@ impl NymApiEphemeraPeerInfo {
}
pub(crate) async fn from_ephemera_dev_cluster_conf(
conf: &Configuration,
local_peer_id: String,
nyxd_client: nyxd::Client,
) -> anyhow::Result<NymApiEphemeraPeerInfo> {
let node_info = conf.node.clone();
let keypair = bs58::decode(&node_info.private_key).into_vec().unwrap();
let keypair = Keypair::from_bytes(&keypair).unwrap();
let local_peer_id = keypair.public_key().to_base58();
let mut peers = HashMap::new();
for peer_info in nyxd_client.get_ephemera_peers().await? {
let public_key = PublicKey::from_str(&peer_info.public_key)?;
+5 -4
View File
@@ -13,7 +13,7 @@ impl RewardsAggregator {
pub(crate) fn aggregate(
&self,
all_rewards: Vec<Vec<MixnodeToReward>>,
) -> Vec<MixnodeWithPerformance> {
) -> anyhow::Result<Vec<MixnodeWithPerformance>> {
let mut mix_rewards = HashMap::new();
for api_rewards in all_rewards {
for mixnode in api_rewards {
@@ -29,14 +29,15 @@ impl RewardsAggregator {
let mut mean_avg = vec![];
for (mix_id, rewards) in mix_rewards {
let sum: Decimal = rewards.iter().map(|r| r.value()).sum();
let avg = sum / Decimal::raw(rewards.len() as u128);
let avg = sum / Decimal::from_ratio(rewards.len() as u128, 1u128);
let performance = Performance::new(avg)?;
mean_avg.push(MixnodeWithPerformance {
mix_id,
performance: Performance::new(avg).expect("Decimal average done wrong"),
performance,
});
}
info!("Mean average rewards: {:?}", mean_avg);
mean_avg
Ok(mean_avg)
}
}
+1 -1
View File
@@ -259,7 +259,7 @@ where
mix_node_rewards.push(mix_node_reward);
}
let aggregated_rewards = self.aggregator().aggregate(mix_node_rewards);
let aggregated_rewards = self.aggregator().aggregate(mix_node_rewards)?;
debug!("Aggregated rewards: {:?}", aggregated_rewards);
Ok(aggregated_rewards)
+12 -12
View File
@@ -138,10 +138,10 @@ public_key_with_proof_path = '{{ coconut_signer.storage_paths.public_key_with_pr
ephemera_config = '{{ ephemera.args.ephemera_config }}'
# Interval, in seconds, for polling the Ephemera blocks
block_polling_interval_seconds = '{{ ephemera.args.block_polling_interval_seconds }}'
block_polling_interval_seconds = {{ ephemera.args.block_polling_interval_seconds }}
# Maximum number of attempts for polling the Ephemera blocks
block_polling_max_attempts = '{{ ephemera.args.block_polling_max_attempts }}'
block_polling_max_attempts = {{ ephemera.args.block_polling_max_attempts }}
[ephemera.args.cmd]
@@ -149,35 +149,35 @@ block_polling_max_attempts = '{{ ephemera.args.block_polling_max_attempts }}'
ip = '{{ ephemera.args.cmd.ip }}'
# The port which Ephemera uses for peer to peer communication
protocol_port = '{{ ephemera.args.cmd.protocol_port }}'
protocol_port = {{ ephemera.args.cmd.protocol_port }}
# The port which Ephemera listens on for websocket subscriptions
websocket_port = '{{ ephemera.args.cmd.websocket_port }}'
websocket_port = {{ ephemera.args.cmd.websocket_port }}
# The port which Ephemera listens on for http api
http_api_port = '{{ ephemera.args.cmd.http_api_port }}'
http_api_port = {{ ephemera.args.cmd.http_api_port }}
# Either this node produces blocks or not
block_producer = '{{ ephemera.args.cmd.block_producer }}'
block_producer = {{ ephemera.args.cmd.block_producer }}
# At which interval to produce blocks
block_creation_interval_sec = '{{ ephemera.args.cmd.block_creation_interval_sec }}'
block_creation_interval_sec = {{ ephemera.args.cmd.block_creation_interval_sec }}
# When next block is created before previous one is finished, should we repeat it with the same messages
repeat_last_block_messages = '{{ ephemera.args.cmd.repeat_last_block_messages }}'
repeat_last_block_messages = {{ ephemera.args.cmd.repeat_last_block_messages }}
# The interval at which Ephemera requests the list of members
members_provider_delay_sec = '{{ ephemera.args.cmd.members_provider_delay_sec }}'
members_provider_delay_sec = {{ ephemera.args.cmd.members_provider_delay_sec }}
[ephemera.args.cmd.membership_kind]
# Requires the threshold of peers returned by membership provider to be online
threshold = '{{ ephemera.args.cmd.membership_kind.threshold }}'
threshold = {{ ephemera.args.cmd.membership_kind.threshold }}
# Requires that all peers returned by membership provider peers to be online
all = '{{ ephemera.args.cmd.membership_kind.all }}'
all = {{ ephemera.args.cmd.membership_kind.all }}
# Membership is just all online peers from the set returned by membership provider
any = '{{ ephemera.args.cmd.membership_kind.any }}'
any = {{ ephemera.args.cmd.membership_kind.any }}
"#;