Audit fixes (#2922)
* oak-2 * oak-8 * oak-13 * oak-15 * oak-18 * Minor clippy nit * 2023-01-13-OAK-6 * 2023-01-13-OAK-3 * 2023-01-13-OAK-13 Implemented via direct dependency on cw2 and calling the appropriate code on migration * Removed few instances of password being unecessarily copied * 2023-01-13-OAK-10 * 2023-01-13-OAK-12 * 2021-09-13-JP-S-NYM-02 * 2021-09-13-JP-S-NYM-03 * Removed further instances of needlessly copying the mnemonic * 2021-09-13-JP-O-PROT-03 * 2021-09-13-JP-S-NYM-01* *: we still have one vulnerability on 'time' pulled from chrono via sqlx. However, apparently its usage is fine... Having said that, I'd still recommend removing all dependencies on chrono, but this will require some database migrations... * 2023-01-13-OAK-11 (#3009) * wip * Introducing the concept of starting epoch transition in `nym-api` * split epoch operations into multiple files * epoch operation failure recovery * sending rewarding transactions in correct order * tests and fixes due to epoch state progression * lint * missed rebasing import changes * Setting cw2 contract version during first migration run * calling 'reconcile_epoch_events' at least once * Made message to BeginEpochTransition more consistent with other variants * Merge layer assignment updates --------- Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
This commit is contained in:
Generated
+32
-174
@@ -214,7 +214,7 @@ dependencies = [
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"itoa 1.0.5",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
@@ -433,18 +433,6 @@ dependencies = [
|
||||
"sha2 0.9.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.12.0"
|
||||
@@ -587,17 +575,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"textwrap 0.11.0",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.23"
|
||||
@@ -607,7 +584,7 @@ dependencies = [
|
||||
"bitflags",
|
||||
"clap_lex 0.2.4",
|
||||
"indexmap",
|
||||
"textwrap 0.16.0",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1039,32 +1016,6 @@ dependencies = [
|
||||
"validator-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"cast",
|
||||
"clap 2.34.0",
|
||||
"criterion-plot 0.4.5",
|
||||
"csv",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_cbor",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.4.0"
|
||||
@@ -1076,7 +1027,7 @@ dependencies = [
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap 3.2.23",
|
||||
"criterion-plot 0.5.0",
|
||||
"criterion-plot",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
@@ -1091,16 +1042,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
@@ -1238,28 +1179,6 @@ dependencies = [
|
||||
"subtle 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"csv-core",
|
||||
"itoa 0.4.8",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ct-logs"
|
||||
version = "0.8.0"
|
||||
@@ -1586,7 +1505,7 @@ dependencies = [
|
||||
"bitvec",
|
||||
"bls12_381 0.6.0",
|
||||
"bs58",
|
||||
"criterion 0.3.6",
|
||||
"criterion",
|
||||
"ff 0.11.1",
|
||||
"group 0.11.0",
|
||||
"lazy_static",
|
||||
@@ -1713,31 +1632,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "0.8.1"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2953d1df47ac0eb70086ccabf0275aa8da8591a28bd358ee2b52bd9f9e3ff9e9"
|
||||
checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401"
|
||||
dependencies = [
|
||||
"enum-iterator-derive 0.8.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91a4ec26efacf4aeff80887a175a419493cb6f8b5480d26387eb0bd038976187"
|
||||
dependencies = [
|
||||
"enum-iterator-derive 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator-derive"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8958699f9359f0b04e691a13850d48b7de329138023876d07cbd024c2c820598"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"enum-iterator-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1806,7 +1705,7 @@ version = "1.1.11"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap 4.1.4",
|
||||
"dotenv",
|
||||
"dotenvy",
|
||||
"humantime-serde",
|
||||
"isocountry",
|
||||
"itertools",
|
||||
@@ -2207,9 +2106,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.16.1"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc"
|
||||
checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
@@ -2494,7 +2393,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa 1.0.5",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2566,7 +2465,7 @@ dependencies = [
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.5",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
@@ -2796,12 +2695,6 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.5"
|
||||
@@ -2917,9 +2810,9 @@ checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.14.2+1.5.1"
|
||||
version = "0.13.5+1.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
|
||||
checksum = "51e5ea06c26926f1002dd553fded6cfcdc9784c1f60feeb58368b4d9b07b6dba"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -3301,7 +3194,6 @@ dependencies = [
|
||||
"cw4",
|
||||
"dirs",
|
||||
"dkg",
|
||||
"dotenv",
|
||||
"futures",
|
||||
"gateway-client",
|
||||
"getset",
|
||||
@@ -3371,7 +3263,7 @@ dependencies = [
|
||||
"pretty_env_logger",
|
||||
"semver 0.11.0",
|
||||
"serde",
|
||||
"vergen 7.5.1",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3400,7 +3292,7 @@ dependencies = [
|
||||
"clap 4.1.4",
|
||||
"clap_complete",
|
||||
"clap_complete_fig",
|
||||
"dotenv",
|
||||
"dotenvy",
|
||||
"log",
|
||||
"nym-bin-common",
|
||||
"nym-cli-commands",
|
||||
@@ -3556,7 +3448,7 @@ dependencies = [
|
||||
"credentials",
|
||||
"dashmap 4.0.2",
|
||||
"dirs",
|
||||
"dotenv",
|
||||
"dotenvy",
|
||||
"futures",
|
||||
"gateway-requests",
|
||||
"humantime-serde",
|
||||
@@ -3629,7 +3521,6 @@ dependencies = [
|
||||
"colored",
|
||||
"cupid",
|
||||
"dirs",
|
||||
"dotenv",
|
||||
"futures",
|
||||
"humantime-serde",
|
||||
"lazy_static",
|
||||
@@ -3665,7 +3556,7 @@ name = "nym-network-defaults"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dotenv",
|
||||
"dotenvy",
|
||||
"hex-literal",
|
||||
"once_cell",
|
||||
"serde",
|
||||
@@ -3746,7 +3637,7 @@ dependencies = [
|
||||
"blake3",
|
||||
"chacha20",
|
||||
"chacha20poly1305",
|
||||
"criterion 0.4.0",
|
||||
"criterion",
|
||||
"curve25519-dalek",
|
||||
"fastrand",
|
||||
"getrandom 0.2.8",
|
||||
@@ -4026,13 +3917,15 @@ version = "1.1.4"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"nym-contracts-common",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-vesting-contract-common",
|
||||
"schemars",
|
||||
"semver 1.0.16",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"vergen 5.1.17",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4073,7 +3966,7 @@ version = "0.5.0"
|
||||
dependencies = [
|
||||
"bls12_381 0.6.0",
|
||||
"bs58",
|
||||
"criterion 0.3.6",
|
||||
"criterion",
|
||||
"digest 0.9.0",
|
||||
"dkg",
|
||||
"doc-comment",
|
||||
@@ -5451,16 +5344,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_cbor"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
|
||||
dependencies = [
|
||||
"half",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.152"
|
||||
@@ -5489,7 +5372,7 @@ version = "1.0.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
|
||||
dependencies = [
|
||||
"itoa 1.0.5",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
@@ -5512,7 +5395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa 1.0.5",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
@@ -5836,7 +5719,7 @@ dependencies = [
|
||||
"hashlink 0.7.0",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"itoa 1.0.5",
|
||||
"itoa",
|
||||
"libc",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
@@ -5883,7 +5766,7 @@ dependencies = [
|
||||
"hashlink 0.8.1",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"itoa 1.0.5",
|
||||
"itoa",
|
||||
"libc",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
@@ -6235,15 +6118,6 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.0"
|
||||
@@ -6296,7 +6170,7 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
|
||||
dependencies = [
|
||||
"itoa 1.0.5",
|
||||
"itoa",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"time-core",
|
||||
@@ -6900,6 +6774,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"ts-rs",
|
||||
"url",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6916,30 +6791,13 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vergen"
|
||||
version = "5.1.17"
|
||||
version = "7.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cf88d94e969e7956d924ba70741316796177fa0c79a2c9f4ab04998d96e966e"
|
||||
checksum = "447f9238a4553957277b3ee09d80babeae0811f1b3baefb093de1c0448437a37"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
"enum-iterator 0.8.1",
|
||||
"getset",
|
||||
"git2",
|
||||
"rustc_version 0.4.0",
|
||||
"rustversion",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vergen"
|
||||
version = "7.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f21b881cd6636ece9735721cf03c1fe1e774fe258683d084bb2812ab67435749"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"enum-iterator 1.2.0",
|
||||
"enum-iterator",
|
||||
"getset",
|
||||
"git2",
|
||||
"rustc_version 0.4.0",
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ license = "Apache-2.0"
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.63"
|
||||
cfg-if = "1.0.0"
|
||||
dotenv = "0.15.0"
|
||||
dotenvy = "0.15.6"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4"
|
||||
once_cell = "1.7.2"
|
||||
|
||||
@@ -17,7 +17,7 @@ semver = "0.11"
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "7", default-features = false, features = ["build", "git", "rustc", "cargo"] }
|
||||
vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc", "cargo"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -31,6 +31,7 @@ futures = "0.3"
|
||||
coconut-interface = { path = "../../coconut-interface" }
|
||||
nym-network-defaults = { path = "../../network-defaults" }
|
||||
nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
|
||||
nym-execute = { path = "../../execute" }
|
||||
|
||||
# required for nyxd-client
|
||||
# at some point it might be possible to make it wasm-compatible
|
||||
@@ -46,7 +47,7 @@ flate2 = { version = "1.0.20", optional = true }
|
||||
sha2 = { version = "0.9.5", optional = true }
|
||||
itertools = { version = "0.10", optional = true }
|
||||
cosmwasm-std = { version = "1.0.0", optional = true }
|
||||
nym-execute = { path = "../../execute" }
|
||||
zeroize = { version = "1.5.7", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ts-rs = "6.1.2"
|
||||
@@ -64,6 +65,7 @@ nyxd-client = [
|
||||
"sha2",
|
||||
"itertools",
|
||||
"cosmwasm-std",
|
||||
"zeroize"
|
||||
]
|
||||
generate-ts = []
|
||||
|
||||
|
||||
@@ -130,10 +130,9 @@ impl Config {
|
||||
#[cfg(feature = "nyxd-client")]
|
||||
#[derive(Clone)]
|
||||
pub struct Client<C: Clone> {
|
||||
// TODO: we really shouldn't be storing a mnemonic here, but removing it would be
|
||||
// non-trivial amount of work and it's out of scope of the current branch
|
||||
mnemonic: Option<bip39::Mnemonic>,
|
||||
|
||||
// // TODO: we really shouldn't be storing a mnemonic here, but removing it would be
|
||||
// // non-trivial amount of work and it's out of scope of the current branch
|
||||
// mnemonic: Option<bip39::Mnemonic>,
|
||||
mixnode_page_limit: Option<u32>,
|
||||
gateway_page_limit: Option<u32>,
|
||||
mixnode_delegations_page_limit: Option<u32>,
|
||||
@@ -157,12 +156,11 @@ impl Client<SigningNyxdClient> {
|
||||
let nyxd_client = NyxdClient::connect_with_mnemonic(
|
||||
config.nyxd_config.clone(),
|
||||
config.nyxd_url.as_str(),
|
||||
mnemonic.clone(),
|
||||
mnemonic,
|
||||
None,
|
||||
)?;
|
||||
|
||||
Ok(Client {
|
||||
mnemonic: Some(mnemonic),
|
||||
mixnode_page_limit: config.mixnode_page_limit,
|
||||
gateway_page_limit: config.gateway_page_limit,
|
||||
mixnode_delegations_page_limit: config.mixnode_delegations_page_limit,
|
||||
@@ -176,12 +174,7 @@ impl Client<SigningNyxdClient> {
|
||||
}
|
||||
|
||||
pub fn change_nyxd(&mut self, new_endpoint: Url) -> Result<(), ValidatorClientError> {
|
||||
self.nyxd = NyxdClient::connect_with_mnemonic(
|
||||
self.nyxd.current_config().clone(),
|
||||
new_endpoint.as_ref(),
|
||||
self.mnemonic.clone().unwrap(),
|
||||
None,
|
||||
)?;
|
||||
self.nyxd.change_endpoint(new_endpoint.as_ref())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -198,7 +191,6 @@ impl Client<QueryNyxdClient> {
|
||||
NyxdClient::connect(config.nyxd_config.clone(), config.nyxd_url.as_str())?;
|
||||
|
||||
Ok(Client {
|
||||
mnemonic: None,
|
||||
mixnode_page_limit: config.mixnode_page_limit,
|
||||
gateway_page_limit: config.gateway_page_limit,
|
||||
mixnode_delegations_page_limit: config.mixnode_delegations_page_limit,
|
||||
|
||||
@@ -756,6 +756,19 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn change_endpoint<U>(&mut self, new_endpoint: U) -> Result<(), NyxdError>
|
||||
where
|
||||
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
|
||||
{
|
||||
let new_rpc_client = HttpClient::new(new_endpoint)?;
|
||||
self.rpc_client = new_rpc_client;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn into_signer(self) -> DirectSecp256k1HdWallet {
|
||||
self.signer
|
||||
}
|
||||
|
||||
pub fn set_broadcast_polling_rate(&mut self, broadcast_polling_rate: Duration) {
|
||||
self.broadcast_polling_rate = broadcast_polling_rate
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ impl NyxdClient<SigningNyxdClient> {
|
||||
{
|
||||
let prefix = &config.chain_details.bech32_account_prefix;
|
||||
let denom = &config.chain_details.mix_denom.base;
|
||||
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic)?;
|
||||
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic);
|
||||
let client_address = wallet
|
||||
.try_derive_accounts()?
|
||||
.into_iter()
|
||||
@@ -212,6 +212,17 @@ impl NyxdClient<SigningNyxdClient> {
|
||||
simulated_gas_multiplier: DEFAULT_SIMULATED_GAS_MULTIPLIER,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn change_endpoint<U>(&mut self, new_endpoint: U) -> Result<(), NyxdError>
|
||||
where
|
||||
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
|
||||
{
|
||||
self.client.change_endpoint(new_endpoint)
|
||||
}
|
||||
|
||||
pub fn into_signer(self) -> DirectSecp256k1HdWallet {
|
||||
self.client.into_signer()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> NyxdClient<C>
|
||||
|
||||
@@ -18,12 +18,13 @@ use nym_mixnet_contract_common::rewarding::{
|
||||
};
|
||||
use nym_mixnet_contract_common::{
|
||||
delegation, ContractBuildInformation, ContractState, ContractStateParams,
|
||||
CurrentIntervalResponse, EpochEventId, GatewayBondResponse, GatewayOwnershipResponse,
|
||||
IdentityKey, IntervalEventId, LayerDistribution, MixId, MixOwnershipResponse,
|
||||
MixnodeDetailsResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse,
|
||||
PagedFamiliesResponse, PagedGatewayResponse, PagedMembersResponse,
|
||||
PagedMixNodeDelegationsResponse, PagedMixnodeBondsResponse, PagedRewardedSetResponse,
|
||||
PendingEpochEventsResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg,
|
||||
CurrentIntervalResponse, EpochEventId, EpochStatus, GatewayBondResponse,
|
||||
GatewayOwnershipResponse, IdentityKey, IntervalEventId, LayerDistribution, MixId,
|
||||
MixOwnershipResponse, MixnodeDetailsResponse, NumberOfPendingEventsResponse,
|
||||
PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedFamiliesResponse,
|
||||
PagedGatewayResponse, PagedMembersResponse, PagedMixNodeDelegationsResponse,
|
||||
PagedMixnodeBondsResponse, PagedRewardedSetResponse, PendingEpochEventsResponse,
|
||||
PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -60,6 +61,11 @@ pub trait MixnetQueryClient {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_current_epoch_status(&self) -> Result<EpochStatus, NyxdError> {
|
||||
self.query_mixnet_contract(MixnetQueryMsg::GetEpochStatus {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_current_interval_details(&self) -> Result<CurrentIntervalResponse, NyxdError> {
|
||||
self.query_mixnet_contract(MixnetQueryMsg::GetCurrentIntervalDetails {})
|
||||
.await
|
||||
@@ -377,6 +383,13 @@ pub trait MixnetQueryClient {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_number_of_pending_events(
|
||||
&self,
|
||||
) -> Result<NumberOfPendingEventsResponse, NyxdError> {
|
||||
self.query_mixnet_contract(MixnetQueryMsg::GetNumberOfPendingEvents {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_node_family_by_label(&self, label: &str) -> Result<Option<Family>, NyxdError> {
|
||||
self.query_mixnet_contract(MixnetQueryMsg::GetFamilyByLabel {
|
||||
label: label.to_string(),
|
||||
|
||||
@@ -106,6 +106,11 @@ pub trait MixnetSigningClient {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn begin_epoch_transition(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_mixnet_contract(fee, MixnetExecuteMsg::BeginEpochTransition {}, vec![])
|
||||
.await
|
||||
}
|
||||
|
||||
async fn advance_current_epoch(
|
||||
&self,
|
||||
new_rewarded_set: Vec<LayerAssignment>,
|
||||
@@ -193,7 +198,8 @@ pub trait MixnetSigningClient {
|
||||
async fn join_family_on_behalf(
|
||||
&self,
|
||||
member_address: String,
|
||||
signature: String,
|
||||
node_identity_signature: String,
|
||||
family_signature: String,
|
||||
family_head: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
@@ -201,7 +207,8 @@ pub trait MixnetSigningClient {
|
||||
fee,
|
||||
MixnetExecuteMsg::JoinFamilyOnBehalf {
|
||||
member_address,
|
||||
signature,
|
||||
node_identity_signature,
|
||||
family_signature,
|
||||
family_head,
|
||||
},
|
||||
vec![],
|
||||
@@ -229,7 +236,7 @@ pub trait MixnetSigningClient {
|
||||
async fn leave_family_on_behalf(
|
||||
&self,
|
||||
member_address: String,
|
||||
signature: String,
|
||||
node_identity_signature: String,
|
||||
family_head: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
@@ -237,7 +244,7 @@ pub trait MixnetSigningClient {
|
||||
fee,
|
||||
MixnetExecuteMsg::LeaveFamilyOnBehalf {
|
||||
member_address,
|
||||
signature,
|
||||
node_identity_signature,
|
||||
family_head,
|
||||
},
|
||||
vec![],
|
||||
|
||||
@@ -8,6 +8,7 @@ use cosmrs::crypto::PublicKey;
|
||||
use cosmrs::tx::SignDoc;
|
||||
use cosmrs::{tx, AccountId};
|
||||
use nym_config::defaults;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// Derivation information required to derive a keypair and an address from a mnemonic.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -56,19 +57,40 @@ pub struct DirectSecp256k1HdWallet {
|
||||
accounts: Vec<Secp256k1Derivation>,
|
||||
}
|
||||
|
||||
impl Zeroize for DirectSecp256k1HdWallet {
|
||||
fn zeroize(&mut self) {
|
||||
// in ideal world, Mnemonic would have had zeroize defined on it (there's an almost year old PR that introduces it)
|
||||
// and the memory would have been filled with zeroes.
|
||||
//
|
||||
// we really don't want to keep our real mnemonic in memory, so let's do the semi-nasty thing
|
||||
// of overwriting it with a fresh mnemonic that was never used before
|
||||
//
|
||||
// note: this function can only fail on an invalid word count, which clearly is not the case here
|
||||
self.secret = bip39::Mnemonic::generate(self.secret.word_count()).unwrap();
|
||||
self.seed.zeroize();
|
||||
// there's nothing secret about derivation paths
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DirectSecp256k1HdWallet {
|
||||
fn drop(&mut self) {
|
||||
self.zeroize()
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectSecp256k1HdWallet {
|
||||
pub fn builder(prefix: &str) -> DirectSecp256k1HdWalletBuilder {
|
||||
DirectSecp256k1HdWalletBuilder::new(prefix)
|
||||
}
|
||||
|
||||
/// Restores a wallet from the given BIP39 mnemonic using default options.
|
||||
pub fn from_mnemonic(prefix: &str, mnemonic: bip39::Mnemonic) -> Result<Self, NyxdError> {
|
||||
pub fn from_mnemonic(prefix: &str, mnemonic: bip39::Mnemonic) -> Self {
|
||||
DirectSecp256k1HdWalletBuilder::new(prefix).build(mnemonic)
|
||||
}
|
||||
|
||||
pub fn generate(prefix: &str, word_count: usize) -> Result<Self, NyxdError> {
|
||||
let mneomonic = bip39::Mnemonic::generate(word_count)?;
|
||||
Self::from_mnemonic(prefix, mneomonic)
|
||||
Ok(Self::from_mnemonic(prefix, mneomonic))
|
||||
}
|
||||
|
||||
fn derive_keypair(&self, hd_path: &DerivationPath) -> Result<Secp256k1Keypair, NyxdError> {
|
||||
@@ -145,11 +167,14 @@ impl DirectSecp256k1HdWallet {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[derive(Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct DirectSecp256k1HdWalletBuilder {
|
||||
/// The password to use when deriving a BIP39 seed from a mnemonic.
|
||||
bip39_password: String,
|
||||
|
||||
/// The BIP-32/SLIP-10 derivation paths
|
||||
#[zeroize(skip)]
|
||||
hd_paths: Vec<DerivationPath>,
|
||||
|
||||
/// The bech32 address prefix (human readable part)
|
||||
@@ -185,23 +210,23 @@ impl DirectSecp256k1HdWalletBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self, mnemonic: bip39::Mnemonic) -> Result<DirectSecp256k1HdWallet, NyxdError> {
|
||||
pub fn build(self, mnemonic: bip39::Mnemonic) -> DirectSecp256k1HdWallet {
|
||||
let seed = mnemonic.to_seed(&self.bip39_password);
|
||||
let prefix = self.prefix;
|
||||
let prefix = self.prefix.clone();
|
||||
let accounts = self
|
||||
.hd_paths
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|hd_path| Secp256k1Derivation {
|
||||
hd_path,
|
||||
hd_path: hd_path.clone(),
|
||||
prefix: prefix.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(DirectSecp256k1HdWallet {
|
||||
DirectSecp256k1HdWallet {
|
||||
accounts,
|
||||
seed,
|
||||
secret: mnemonic,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,8 +254,7 @@ mod tests {
|
||||
"n17n9flp6jflljg6fp05dsy07wcprf2uuu8g40rf",
|
||||
];
|
||||
for (idx, mnemonic) in mnemonics.iter().enumerate() {
|
||||
let wallet =
|
||||
DirectSecp256k1HdWallet::from_mnemonic(&prefix, mnemonic.parse().unwrap()).unwrap();
|
||||
let wallet = DirectSecp256k1HdWallet::from_mnemonic(&prefix, mnemonic.parse().unwrap());
|
||||
assert_eq!(
|
||||
wallet.try_derive_accounts().unwrap()[0].address,
|
||||
addrs[idx].parse().unwrap()
|
||||
|
||||
@@ -15,8 +15,7 @@ pub fn create_account(args: Args, prefix: &str) {
|
||||
let word_count = args.word_count.unwrap_or(24);
|
||||
let mnemonic = bip39::Mnemonic::generate(word_count).expect("failed to generate mnemonic!");
|
||||
|
||||
let wallet =
|
||||
DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic).expect("failed to build wallet!");
|
||||
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic);
|
||||
|
||||
// Output address and mnemonics into separate lines for easier parsing
|
||||
println!("{}", wallet.mnemonic());
|
||||
|
||||
@@ -51,21 +51,19 @@ pub async fn get_pubkey(
|
||||
}
|
||||
|
||||
pub fn get_pubkey_from_mnemonic(address: AccountId, prefix: &str, mnemonic: bip39::Mnemonic) {
|
||||
match DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic) {
|
||||
Ok(wallet) => match wallet.try_derive_accounts() {
|
||||
Ok(accounts) => match accounts.iter().find(|a| *a.address() == address) {
|
||||
Some(account) => {
|
||||
println!("{}", account.public_key().to_string());
|
||||
}
|
||||
None => {
|
||||
error!("Could not derive key that matches {}", address)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to derive accounts. {}", e);
|
||||
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic);
|
||||
match wallet.try_derive_accounts() {
|
||||
Ok(accounts) => match accounts.iter().find(|a| *a.address() == address) {
|
||||
Some(account) => {
|
||||
println!("{}", account.public_key().to_string());
|
||||
}
|
||||
None => {
|
||||
error!("Could not derive key that matches {}", address)
|
||||
}
|
||||
},
|
||||
Err(e) => show_error(e),
|
||||
Err(e) => {
|
||||
error!("Failed to derive accounts. {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::utils::show_error;
|
||||
use clap::Parser;
|
||||
use cosmrs::crypto::PublicKey;
|
||||
use log::error;
|
||||
@@ -36,33 +35,32 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option<bip39::Mnemonic>) {
|
||||
return;
|
||||
}
|
||||
|
||||
match DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.expect("mnemonic not set")) {
|
||||
Ok(wallet) => match wallet.try_derive_accounts() {
|
||||
Ok(accounts) => match accounts.first() {
|
||||
Some(account) => {
|
||||
let msg = args.message.into_bytes();
|
||||
match wallet.sign_raw_with_account(account, &msg) {
|
||||
Ok(signature) => {
|
||||
let output = SignatureOutputJson {
|
||||
account_id: account.address().to_string(),
|
||||
public_key: account.public_key(),
|
||||
signature_as_hex: signature.to_string(),
|
||||
};
|
||||
println!("{}", json!(output));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to sign message. {}", e);
|
||||
}
|
||||
let wallet =
|
||||
DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.expect("mnemonic not set"));
|
||||
match wallet.try_derive_accounts() {
|
||||
Ok(accounts) => match accounts.first() {
|
||||
Some(account) => {
|
||||
let msg = args.message.into_bytes();
|
||||
match wallet.sign_raw_with_account(account, &msg) {
|
||||
Ok(signature) => {
|
||||
let output = SignatureOutputJson {
|
||||
account_id: account.address().to_string(),
|
||||
public_key: account.public_key(),
|
||||
signature_as_hex: signature.to_string(),
|
||||
};
|
||||
println!("{}", json!(output));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to sign message. {}", e);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!("Could not derive an account key from the mnemonic",)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to derive accounts. {}", e);
|
||||
}
|
||||
None => {
|
||||
error!("Could not derive an account key from the mnemonic",)
|
||||
}
|
||||
},
|
||||
Err(e) => show_error(e),
|
||||
Err(e) => {
|
||||
error!("Failed to derive accounts. {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,19 @@ use serde::{Deserialize, Serialize};
|
||||
pub type OwnerProxySubKey = String;
|
||||
pub type StorageKey = (MixId, OwnerProxySubKey);
|
||||
|
||||
pub fn generate_owner_storage_subkey(address: &Addr, proxy: Option<&Addr>) -> String {
|
||||
if let Some(proxy) = &proxy {
|
||||
// throughout the contract we ensure that our proxy can ONLY ever be the vesting contract
|
||||
// thus this method is equivalent to either using the existing address (for when there's no proxy)
|
||||
// or to XORing with a constant (vesting contract address) since the vesting contract address never changes.
|
||||
// Thus there should be no collisions in here.
|
||||
pub fn generate_owner_storage_subkey(
|
||||
address: &Addr,
|
||||
maybe_vesting_contract: Option<&Addr>,
|
||||
) -> String {
|
||||
if let Some(vesting_contract) = &maybe_vesting_contract {
|
||||
let key_bytes = address
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.zip(proxy.as_bytes())
|
||||
.zip(vesting_contract.as_bytes())
|
||||
.map(|(x, y)| x ^ y)
|
||||
.collect::<Vec<_>>();
|
||||
bs58::encode(key_bytes).into_string()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{IdentityKey, MixId};
|
||||
use crate::{EpochState, IdentityKey, MixId};
|
||||
use cosmwasm_std::{Addr, Coin, Decimal};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -64,6 +64,19 @@ pub enum MixnetContractError {
|
||||
#[error("Proxy address mismatch, expected {existing}, got {incoming}")]
|
||||
ProxyMismatch { existing: String, incoming: String },
|
||||
|
||||
#[error("Proxy address ({received}) is not set to the vesting contract ({vesting_contract})")]
|
||||
ProxyIsNotVestingContract {
|
||||
received: Addr,
|
||||
vesting_contract: Addr,
|
||||
},
|
||||
#[error(
|
||||
"Sender of this message ({received}) is not the vesting contract ({vesting_contract})"
|
||||
)]
|
||||
SenderIsNotVestingContract {
|
||||
received: Addr,
|
||||
vesting_contract: Addr,
|
||||
},
|
||||
|
||||
#[error("Failed to recover ed25519 public key from its base58 representation - {0}")]
|
||||
MalformedEd25519IdentityKey(String),
|
||||
|
||||
@@ -169,4 +182,40 @@ pub enum MixnetContractError {
|
||||
|
||||
#[error("Feature is not yet implemented")]
|
||||
NotImplemented,
|
||||
|
||||
#[error("epochs_in_interval must be > 0")]
|
||||
EpochsInIntervalZero,
|
||||
|
||||
#[error("epoch duration must be > 0")]
|
||||
EpochDurationZero,
|
||||
|
||||
#[error("this validator ({current_validator}) is not the one responsible for advancing this epoch. It's responsibility of {chosen_validator}.")]
|
||||
RewardingValidatorMismatch {
|
||||
current_validator: Addr,
|
||||
chosen_validator: Addr,
|
||||
},
|
||||
|
||||
#[error("the epoch is currently in the process of being advanced. (the state is {current_state}) Please try sending your transaction again once this has finished")]
|
||||
EpochAdvancementInProgress { current_state: EpochState },
|
||||
|
||||
#[error("the epoch is in an unexpected state. expected 'mix rewarding' state, but we're in {current_state} instead.")]
|
||||
UnexpectedNonRewardingEpochState { current_state: EpochState },
|
||||
|
||||
#[error("attempted to reward mixnode out of order. Attempted to reward {attempted_to_reward} while last rewarded was {last_rewarded}.")]
|
||||
RewardingOutOfOrder {
|
||||
last_rewarded: MixId,
|
||||
attempted_to_reward: MixId,
|
||||
},
|
||||
|
||||
#[error("the epoch is currently not in the 'event reconciliation' state. (the state is {current_state})")]
|
||||
EpochNotInEventReconciliationState { current_state: EpochState },
|
||||
|
||||
#[error("the epoch is currently not in the 'epoch advancement' state. (the state is {current_state})")]
|
||||
EpochNotInAdvancementState { current_state: EpochState },
|
||||
|
||||
#[error("failed to parse {value} into a valid SemVer version: {error_message}")]
|
||||
SemVerFailure {
|
||||
value: String,
|
||||
error_message: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ pub enum MixnetEventType {
|
||||
Undelegation,
|
||||
ContractSettingsUpdate,
|
||||
RewardingValidatorUpdate,
|
||||
BeginEpochTransition,
|
||||
AdvanceEpoch,
|
||||
ExecutePendingEpochEvents,
|
||||
ExecutePendingIntervalEvents,
|
||||
@@ -77,6 +78,7 @@ impl ToString for MixnetEventType {
|
||||
MixnetEventType::Undelegation => "undelegation",
|
||||
MixnetEventType::ContractSettingsUpdate => "settings_update",
|
||||
MixnetEventType::RewardingValidatorUpdate => "rewarding_validator_address_update",
|
||||
MixnetEventType::BeginEpochTransition => "beginning_epoch_transition",
|
||||
MixnetEventType::AdvanceEpoch => "advance_epoch",
|
||||
MixnetEventType::ExecutePendingEpochEvents => "execute_pending_epoch_events",
|
||||
MixnetEventType::ExecutePendingIntervalEvents => "execute_pending_interval_events",
|
||||
@@ -139,6 +141,7 @@ pub const ZERO_PERFORMANCE_VALUE: &str = "zero_performance";
|
||||
// rewarded set update
|
||||
pub const ACTIVE_SET_SIZE_KEY: &str = "active_set_size";
|
||||
|
||||
pub const CURRENT_EPOCH_KEY: &str = "current_epoch";
|
||||
pub const NEW_CURRENT_EPOCH_KEY: &str = "new_current_epoch";
|
||||
|
||||
// interval
|
||||
@@ -500,6 +503,13 @@ pub fn new_mix_rewarding_event(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_epoch_transition_start_event(current_interval: Interval) -> Event {
|
||||
Event::new(MixnetEventType::BeginEpochTransition).add_attribute(
|
||||
CURRENT_EPOCH_KEY,
|
||||
current_interval.current_epoch_absolute_id().to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_advance_epoch_event(interval: Interval, rewarded_nodes: u32) -> Event {
|
||||
Event::new(MixnetEventType::AdvanceEpoch)
|
||||
.add_attribute(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::pending_events::{PendingEpochEvent, PendingIntervalEvent};
|
||||
use crate::{EpochId, IntervalId};
|
||||
use cosmwasm_std::Env;
|
||||
use crate::{EpochId, IntervalId, MixId};
|
||||
use cosmwasm_std::{Addr, Env};
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::{InstanceType, Schema, SchemaObject};
|
||||
use schemars::JsonSchema;
|
||||
@@ -60,6 +61,133 @@ pub(crate) mod string_rfc3339_offset_date_time {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct EpochStatus {
|
||||
// TODO: introduce mechanism to allow another validator to take over if no progress has been made in X blocks / Y seconds
|
||||
/// Specifies either, which validator is currently performing progression into the following epoch (if the epoch is currently being progressed),
|
||||
/// or which validator was responsible for progressing into the current epoch (if the epoch is currently in progress)
|
||||
pub being_advanced_by: Addr,
|
||||
pub state: EpochState,
|
||||
}
|
||||
|
||||
impl EpochStatus {
|
||||
pub fn new(being_advanced_by: Addr) -> Self {
|
||||
EpochStatus {
|
||||
being_advanced_by,
|
||||
state: EpochState::InProgress,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_last_rewarded(
|
||||
&mut self,
|
||||
new_last_rewarded: MixId,
|
||||
) -> Result<bool, MixnetContractError> {
|
||||
match &mut self.state {
|
||||
EpochState::Rewarding {
|
||||
ref mut last_rewarded,
|
||||
final_node_id,
|
||||
} => {
|
||||
if new_last_rewarded <= *last_rewarded {
|
||||
return Err(MixnetContractError::RewardingOutOfOrder {
|
||||
last_rewarded: *last_rewarded,
|
||||
attempted_to_reward: new_last_rewarded,
|
||||
});
|
||||
}
|
||||
|
||||
*last_rewarded = new_last_rewarded;
|
||||
Ok(new_last_rewarded == *final_node_id)
|
||||
}
|
||||
state => Err(MixnetContractError::UnexpectedNonRewardingEpochState {
|
||||
current_state: *state,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn last_rewarded(&self) -> Result<MixId, MixnetContractError> {
|
||||
match self.state {
|
||||
EpochState::Rewarding { last_rewarded, .. } => Ok(last_rewarded),
|
||||
state => Err(MixnetContractError::UnexpectedNonRewardingEpochState {
|
||||
current_state: state,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_is_in_event_reconciliation_state(&self) -> Result<(), MixnetContractError> {
|
||||
if !matches!(self.state, EpochState::ReconcilingEvents) {
|
||||
return Err(MixnetContractError::EpochNotInEventReconciliationState {
|
||||
current_state: self.state,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_is_in_advancement_state(&self) -> Result<(), MixnetContractError> {
|
||||
if !matches!(self.state, EpochState::AdvancingEpoch) {
|
||||
return Err(MixnetContractError::EpochNotInAdvancementState {
|
||||
current_state: self.state,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_in_progress(&self) -> bool {
|
||||
matches!(self.state, EpochState::InProgress)
|
||||
}
|
||||
|
||||
pub fn is_rewarding(&self) -> bool {
|
||||
matches!(self.state, EpochState::Rewarding { .. })
|
||||
}
|
||||
|
||||
pub fn is_reconciling(&self) -> bool {
|
||||
matches!(self.state, EpochState::ReconcilingEvents)
|
||||
}
|
||||
|
||||
pub fn is_advancing(&self) -> bool {
|
||||
matches!(self.state, EpochState::AdvancingEpoch)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub enum EpochState {
|
||||
/// Represents the state of an epoch that's in progress (well, duh.)
|
||||
/// All actions are allowed to be issued.
|
||||
InProgress,
|
||||
|
||||
/// Represents the state of an epoch when the rewarding entity has been decided on,
|
||||
/// and the mixnodes are in the process of being rewarded for their work in this epoch.
|
||||
Rewarding {
|
||||
last_rewarded: MixId,
|
||||
|
||||
final_node_id: MixId,
|
||||
// total_rewarded: u32,
|
||||
},
|
||||
|
||||
/// Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch
|
||||
/// and all issued actions should now get resolved before being allowed to advance into the next epoch.
|
||||
ReconcilingEvents,
|
||||
|
||||
/// Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch,
|
||||
/// all issued actions got resolved and the epoch should now be advanced whilst assigning new rewarded set.
|
||||
AdvancingEpoch,
|
||||
}
|
||||
|
||||
impl Display for EpochState {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
EpochState::InProgress => write!(f, "in progress"),
|
||||
EpochState::Rewarding {
|
||||
last_rewarded,
|
||||
final_node_id,
|
||||
} => write!(
|
||||
f,
|
||||
"mix rewarding (last rewarded: {last_rewarded}, final node: {final_node_id})"
|
||||
),
|
||||
EpochState::ReconcilingEvents => write!(f, "event reconciliation"),
|
||||
EpochState::AdvancingEpoch => write!(f, "advancing epoch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "generate-ts",
|
||||
@@ -400,6 +528,21 @@ impl PendingIntervalEventsResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||
pub struct NumberOfPendingEventsResponse {
|
||||
pub epoch_events: u32,
|
||||
pub interval_events: u32,
|
||||
}
|
||||
|
||||
impl NumberOfPendingEventsResponse {
|
||||
pub fn new(epoch_events: u32, interval_events: u32) -> Self {
|
||||
Self {
|
||||
epoch_events,
|
||||
interval_events,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
@@ -29,7 +29,8 @@ pub use gateway::{
|
||||
Gateway, GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, PagedGatewayResponse,
|
||||
};
|
||||
pub use interval::{
|
||||
CurrentIntervalResponse, Interval, PendingEpochEventsResponse, PendingIntervalEventsResponse,
|
||||
CurrentIntervalResponse, EpochState, EpochStatus, Interval, NumberOfPendingEventsResponse,
|
||||
PendingEpochEventsResponse, PendingIntervalEventsResponse,
|
||||
};
|
||||
pub use mixnode::{
|
||||
Layer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeCostParams, MixNodeDetails,
|
||||
|
||||
@@ -101,15 +101,16 @@ pub enum ExecuteMsg {
|
||||
owner_signature: String,
|
||||
label: String,
|
||||
},
|
||||
/// Family head needs to sign the joining node IdentityKey
|
||||
/// Family head needs to sign the joining node IdentityKey, MixNode needs to provide its signature proving that it wants to join the family
|
||||
JoinFamilyOnBehalf {
|
||||
member_address: String,
|
||||
signature: String,
|
||||
node_identity_signature: String,
|
||||
family_signature: String,
|
||||
family_head: IdentityKey,
|
||||
},
|
||||
LeaveFamilyOnBehalf {
|
||||
member_address: String,
|
||||
signature: String,
|
||||
node_identity_signature: String,
|
||||
family_head: IdentityKey,
|
||||
},
|
||||
KickFamilyMemberOnBehalf {
|
||||
@@ -138,6 +139,7 @@ pub enum ExecuteMsg {
|
||||
epoch_duration_secs: u64,
|
||||
force_immediately: bool,
|
||||
},
|
||||
BeginEpochTransition {},
|
||||
AdvanceCurrentEpoch {
|
||||
new_rewarded_set: Vec<LayerAssignment>,
|
||||
// families_in_layer: HashMap<String, Layer>,
|
||||
@@ -281,6 +283,7 @@ impl ExecuteMsg {
|
||||
ExecuteMsg::UpdateIntervalConfig {
|
||||
force_immediately, ..
|
||||
} => format!("updating mixnet interval configuration. forced: {force_immediately}"),
|
||||
ExecuteMsg::BeginEpochTransition {} => "beginning epoch transition".into(),
|
||||
ExecuteMsg::AdvanceCurrentEpoch { .. } => "advancing current epoch".into(),
|
||||
ExecuteMsg::ReconcileEpochEvents { .. } => "reconciling epoch events".into(),
|
||||
ExecuteMsg::BondMixnode { mix_node, .. } => {
|
||||
@@ -367,10 +370,12 @@ pub enum QueryMsg {
|
||||
},
|
||||
// state/sys-params-related
|
||||
GetContractVersion {},
|
||||
GetCW2ContractVersion {},
|
||||
GetRewardingValidatorAddress {},
|
||||
GetStateParams {},
|
||||
GetState {},
|
||||
GetRewardingParams {},
|
||||
GetEpochStatus {},
|
||||
GetCurrentIntervalDetails {},
|
||||
GetRewardedSet {
|
||||
limit: Option<u32>,
|
||||
@@ -493,6 +498,7 @@ pub enum QueryMsg {
|
||||
limit: Option<u32>,
|
||||
start_after: Option<u32>,
|
||||
},
|
||||
GetNumberOfPendingEvents {},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
|
||||
@@ -61,13 +61,14 @@ pub enum ExecuteMsg {
|
||||
owner_signature: String,
|
||||
label: String,
|
||||
},
|
||||
/// Family head needs to sign the joining node IdentityKey
|
||||
/// Family head needs to sign the joining node IdentityKey, the Node provides its signature signaling consent to join the family
|
||||
JoinFamily {
|
||||
signature: String,
|
||||
node_identity_signature: String,
|
||||
family_signature: String,
|
||||
family_head: IdentityKey,
|
||||
},
|
||||
LeaveFamily {
|
||||
signature: String,
|
||||
node_identity_signature: String,
|
||||
family_head: IdentityKey,
|
||||
},
|
||||
KickFamilyMember {
|
||||
@@ -188,6 +189,7 @@ impl ExecuteMsg {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
GetContractVersion {},
|
||||
GetCW2ContractVersion {},
|
||||
GetAccountsPaged {
|
||||
start_next_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
|
||||
@@ -37,7 +37,7 @@ version = "0.11"
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.3"
|
||||
criterion = "0.4"
|
||||
|
||||
[[bench]]
|
||||
name = "benchmarks"
|
||||
|
||||
@@ -9,7 +9,7 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cfg-if = { workspace = true }
|
||||
dotenv = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
hex-literal = "0.3.3"
|
||||
once_cell = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"]}
|
||||
|
||||
@@ -320,7 +320,7 @@ pub fn setup_env(config_env_file: Option<&PathBuf>) {
|
||||
// if the configuration is not already set in the env vars
|
||||
Err(std::env::VarError::NotPresent) => {
|
||||
if let Some(config_env_file) = config_env_file {
|
||||
dotenv::from_path(config_env_file)
|
||||
dotenvy::from_path(config_env_file)
|
||||
.expect("Invalid path to environment configuration file");
|
||||
fix_deprecated_environmental_variables();
|
||||
} else {
|
||||
|
||||
@@ -29,7 +29,7 @@ version = "0.11"
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version="0.3", features=["html_reports"] }
|
||||
criterion = { version="0.4", features=["html_reports"] }
|
||||
doc-comment = "0.3"
|
||||
rand_chacha = "0.3"
|
||||
|
||||
|
||||
@@ -66,13 +66,15 @@ pub(crate) type FragmentSet = Vec<Fragment>;
|
||||
/// `Fragment`s thus allowing for some additional optimizations by letting it skip
|
||||
/// certain procedures when reconstructing.
|
||||
pub(crate) fn generate_set_id<R: Rng>(rng: &mut R) -> i32 {
|
||||
let potential_id = rng.gen::<i32>().abs();
|
||||
let potential_id = rng.gen::<i32>();
|
||||
// make sure id is always non-zero, as we do not want to accidentally have weird
|
||||
// reconstruction cases where unfragmented payload overwrites some part of set with id0
|
||||
if potential_id == 0 {
|
||||
// furthermore, make sure it's not i32::MIN (-2147483648) as due to 2-complement encoding,
|
||||
// attempting to calculate the absolutely value is going to panic
|
||||
if potential_id == 0 || potential_id == i32::MIN {
|
||||
generate_set_id(rng)
|
||||
} else {
|
||||
potential_id
|
||||
potential_id.abs()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,3 +137,16 @@ impl PacketSize {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::AckEncryptionAlgorithm;
|
||||
use nym_crypto::symmetric::stream_cipher::IvSizeUser;
|
||||
|
||||
#[test]
|
||||
fn ack_iv_size_assertion() {
|
||||
let iv_size = AckEncryptionAlgorithm::iv_size();
|
||||
assert_eq!(iv_size, ACK_IV_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@ impl From<PlainMessage> for ReconstructedMessage {
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MessageRecoveryError {
|
||||
#[error("The received message did not contain enough bytes to recover the ephemeral public key. Got {provided}. required: {required}")]
|
||||
NotEnoughBytesForEphemeralKey { provided: usize, required: usize },
|
||||
|
||||
#[error("Recovered remote x25519 public key is invalid - {0}")]
|
||||
InvalidRemoteEphemeralKey(#[from] encryption::KeyRecoveryError),
|
||||
|
||||
@@ -123,6 +126,13 @@ impl MessageReceiver {
|
||||
local_key: &encryption::PrivateKey,
|
||||
raw_enc_frag: &'a mut [u8],
|
||||
) -> Result<&'a mut [u8], MessageRecoveryError> {
|
||||
if raw_enc_frag.len() < encryption::PUBLIC_KEY_SIZE {
|
||||
return Err(MessageRecoveryError::NotEnoughBytesForEphemeralKey {
|
||||
provided: raw_enc_frag.len(),
|
||||
required: encryption::PUBLIC_KEY_SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
// 1. recover remote encryption key
|
||||
let remote_key_bytes = &raw_enc_frag[..encryption::PUBLIC_KEY_SIZE];
|
||||
let remote_ephemeral_key = encryption::PublicKey::from_bytes(remote_key_bytes)?;
|
||||
|
||||
@@ -7,7 +7,7 @@ use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use nym_mixnet_contract_common::GatewayBond;
|
||||
use nym_sphinx_addressing::nodes::NodeIdentity;
|
||||
use nym_sphinx_types::Node as SphinxNode;
|
||||
use rand::Rng;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
@@ -181,8 +181,7 @@ impl NymTopology {
|
||||
gateway_identity: &NodeIdentity,
|
||||
) -> Result<Vec<SphinxNode>, NymTopologyError>
|
||||
where
|
||||
// I don't think there's a need for this RNG to be crypto-secure
|
||||
R: Rng + ?Sized,
|
||||
R: Rng + CryptoRng + ?Sized,
|
||||
{
|
||||
let gateway = self.get_gateway(gateway_identity).ok_or(
|
||||
NymTopologyError::NonExistentGatewayError {
|
||||
|
||||
@@ -28,6 +28,7 @@ vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vestin
|
||||
|
||||
cosmwasm-std = "1.0.0"
|
||||
cosmwasm-storage = "1.0.0"
|
||||
cw2 = { version = "0.13.4" }
|
||||
cw-storage-plus = "0.13.4"
|
||||
|
||||
bs58 = "0.4.0"
|
||||
@@ -35,6 +36,7 @@ schemars = "0.8"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
thiserror = { version = "1.0.23" }
|
||||
time = { version = "0.3", features = ["macros"] }
|
||||
semver = { version = "1.0.16", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
cosmwasm-schema = "1.0.0"
|
||||
@@ -43,8 +45,11 @@ rand_chacha = "0.2"
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] }
|
||||
vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
contract-testing = ["mixnet-contract-common/contract-testing"]
|
||||
|
||||
[profile.release]
|
||||
overflow-checks = true
|
||||
|
||||
@@ -47,6 +47,7 @@ pub(crate) const GATEWAYS_PK_NAMESPACE: &str = "gt";
|
||||
pub(crate) const GATEWAYS_OWNER_IDX_NAMESPACE: &str = "gto";
|
||||
|
||||
pub(crate) const REWARDED_SET_KEY: &str = "rs";
|
||||
pub(crate) const CURRENT_EPOCH_STATUS_KEY: &str = "ces";
|
||||
pub(crate) const CURRENT_INTERVAL_KEY: &str = "ci";
|
||||
pub(crate) const EPOCH_EVENT_ID_COUNTER_KEY: &str = "eic";
|
||||
pub(crate) const INTERVAL_EVENT_ID_COUNTER_KEY: &str = "iic";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT};
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
@@ -7,11 +8,17 @@ use crate::mixnodes::storage as mixnode_storage;
|
||||
use crate::rewards::storage as rewards_storage;
|
||||
use cosmwasm_std::{
|
||||
entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response,
|
||||
StdError,
|
||||
};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::{
|
||||
ContractState, ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, QueryMsg,
|
||||
};
|
||||
use semver::Version;
|
||||
|
||||
// version info for migration info
|
||||
const CONTRACT_NAME: &str = "crate:nym-mixnet-contract";
|
||||
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn default_initial_state(
|
||||
owner: Addr,
|
||||
@@ -50,11 +57,19 @@ pub fn instantiate(
|
||||
info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
if msg.epochs_in_interval == 0 {
|
||||
return Err(MixnetContractError::EpochsInIntervalZero);
|
||||
}
|
||||
|
||||
if msg.epoch_duration.as_secs() == 0 {
|
||||
return Err(MixnetContractError::EpochDurationZero);
|
||||
}
|
||||
|
||||
let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?;
|
||||
let vesting_contract_address = deps.api.addr_validate(&msg.vesting_contract_address)?;
|
||||
let state = default_initial_state(
|
||||
info.sender,
|
||||
rewarding_validator_address,
|
||||
rewarding_validator_address.clone(),
|
||||
msg.rewarding_denom,
|
||||
vesting_contract_address,
|
||||
);
|
||||
@@ -64,10 +79,15 @@ pub fn instantiate(
|
||||
.initial_rewarding_params
|
||||
.into_rewarding_params(msg.epochs_in_interval)?;
|
||||
|
||||
interval_storage::initialise_storage(deps.storage, starting_interval)?;
|
||||
interval_storage::initialise_storage(
|
||||
deps.storage,
|
||||
starting_interval,
|
||||
rewarding_validator_address,
|
||||
)?;
|
||||
mixnet_params_storage::initialise_storage(deps.storage, state)?;
|
||||
mixnode_storage::initialise_storage(deps.storage)?;
|
||||
rewards_storage::initialise_storage(deps.storage, reward_params)?;
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
@@ -92,7 +112,9 @@ pub fn execute(
|
||||
ExecuteMsg::JoinFamily {
|
||||
signature,
|
||||
family_head,
|
||||
} => crate::families::transactions::try_join_family(deps, info, signature, family_head),
|
||||
} => {
|
||||
crate::families::transactions::try_join_family(deps, info, None, signature, family_head)
|
||||
}
|
||||
ExecuteMsg::LeaveFamily {
|
||||
signature,
|
||||
family_head,
|
||||
@@ -113,24 +135,26 @@ pub fn execute(
|
||||
),
|
||||
ExecuteMsg::JoinFamilyOnBehalf {
|
||||
member_address,
|
||||
signature,
|
||||
node_identity_signature,
|
||||
family_signature,
|
||||
family_head,
|
||||
} => crate::families::transactions::try_join_family_on_behalf(
|
||||
deps,
|
||||
info,
|
||||
member_address,
|
||||
signature,
|
||||
Some(node_identity_signature),
|
||||
family_signature,
|
||||
family_head,
|
||||
),
|
||||
ExecuteMsg::LeaveFamilyOnBehalf {
|
||||
member_address,
|
||||
signature,
|
||||
node_identity_signature,
|
||||
family_head,
|
||||
} => crate::families::transactions::try_leave_family_on_behalf(
|
||||
deps,
|
||||
info,
|
||||
member_address,
|
||||
signature,
|
||||
node_identity_signature,
|
||||
family_head,
|
||||
),
|
||||
ExecuteMsg::KickFamilyMemberOnBehalf {
|
||||
@@ -189,6 +213,9 @@ pub fn execute(
|
||||
epoch_duration_secs,
|
||||
force_immediately,
|
||||
),
|
||||
ExecuteMsg::BeginEpochTransition {} => {
|
||||
crate::interval::transactions::try_begin_epoch_transition(deps, env, info)
|
||||
}
|
||||
ExecuteMsg::AdvanceCurrentEpoch {
|
||||
new_rewarded_set,
|
||||
// families_in_layer,
|
||||
@@ -369,6 +396,7 @@ pub fn query(
|
||||
QueryMsg::GetContractVersion {} => {
|
||||
to_binary(&crate::mixnet_contract_settings::queries::query_contract_version())
|
||||
}
|
||||
QueryMsg::GetCW2ContractVersion {} => to_binary(&cw2::get_contract_version(deps.storage)?),
|
||||
QueryMsg::GetStateParams {} => to_binary(
|
||||
&crate::mixnet_contract_settings::queries::query_contract_settings_params(deps)?,
|
||||
),
|
||||
@@ -381,6 +409,9 @@ pub fn query(
|
||||
QueryMsg::GetRewardingParams {} => {
|
||||
to_binary(&crate::rewards::queries::query_rewarding_params(deps)?)
|
||||
}
|
||||
QueryMsg::GetEpochStatus {} => {
|
||||
to_binary(&crate::interval::queries::query_epoch_status(deps)?)
|
||||
}
|
||||
QueryMsg::GetCurrentIntervalDetails {} => to_binary(
|
||||
&crate::interval::queries::query_current_interval_details(deps, env)?,
|
||||
),
|
||||
@@ -547,6 +578,9 @@ pub fn query(
|
||||
limit,
|
||||
)?,
|
||||
),
|
||||
QueryMsg::GetNumberOfPendingEvents {} => to_binary(
|
||||
&crate::interval::queries::query_number_of_pending_events(deps)?,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(query_res?)
|
||||
@@ -558,6 +592,42 @@ pub fn migrate(
|
||||
_env: Env,
|
||||
msg: MigrateMsg,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
// this is the first migration that uses cw2 standard and thus the value in the storage doesn't yet exist
|
||||
// set it instead.
|
||||
if matches!(
|
||||
cw2::get_contract_version(deps.storage),
|
||||
Err(StdError::NotFound { .. })
|
||||
) {
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
crate::queued_migrations::create_epoch_status(deps.storage)?;
|
||||
return Ok(Response::new());
|
||||
}
|
||||
|
||||
// note: don't remove this particular bit of code as we have to ALWAYS check whether we have to update the stored version
|
||||
let version: Version = CONTRACT_VERSION.parse().map_err(|error: semver::Error| {
|
||||
MixnetContractError::SemVerFailure {
|
||||
value: CONTRACT_VERSION.to_string(),
|
||||
error_message: error.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
let storage_version_raw = cw2::get_contract_version(deps.storage)?.version;
|
||||
let storage_version: Version =
|
||||
storage_version_raw
|
||||
.parse()
|
||||
.map_err(|error: semver::Error| MixnetContractError::SemVerFailure {
|
||||
value: storage_version_raw,
|
||||
error_message: error.to_string(),
|
||||
})?;
|
||||
|
||||
if storage_version < version {
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
// If state structure changed in any contract version in the way migration is needed, it
|
||||
// should occur here
|
||||
crate::queued_migrations::create_epoch_status(deps.storage)?;
|
||||
}
|
||||
|
||||
// due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address
|
||||
// and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh
|
||||
// environment, one of the contracts will HAVE TO go through a migration
|
||||
|
||||
@@ -38,10 +38,10 @@ mod tests {
|
||||
test.add_immediate_delegation(delegator, og_amount, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
let dist1 = test.reward_with_distribution(mix_id, performance(100.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0));
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist2 = test.reward_with_distribution(mix_id, performance(100.0));
|
||||
let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0));
|
||||
|
||||
let mix_rewarding = test.mix_rewarding(mix_id);
|
||||
let delegation = test.delegation(mix_id, delegator, &None);
|
||||
|
||||
@@ -5,7 +5,9 @@ use super::storage;
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::support::helpers::validate_delegation_stake;
|
||||
use crate::support::helpers::{
|
||||
ensure_epoch_in_progress_state, ensure_sent_by_vesting_contract, validate_delegation_stake,
|
||||
};
|
||||
use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::events::{
|
||||
@@ -30,6 +32,8 @@ pub(crate) fn try_delegate_to_mixnode_on_behalf(
|
||||
mix_id: MixId,
|
||||
delegate: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let delegate = deps.api.addr_validate(&delegate)?;
|
||||
_try_delegate_to_mixnode(deps, env, mix_id, delegate, info.funds, Some(info.sender))
|
||||
}
|
||||
@@ -42,6 +46,9 @@ pub(crate) fn _try_delegate_to_mixnode(
|
||||
amount: Vec<Coin>,
|
||||
proxy: Option<Addr>,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
// delegation is only allowed if the epoch is currently not in the process of being advanced
|
||||
ensure_epoch_in_progress_state(deps.storage)?;
|
||||
|
||||
// check if the delegation contains any funds of the appropriate denomination
|
||||
let contract_state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?;
|
||||
let delegation = validate_delegation_stake(
|
||||
@@ -89,6 +96,8 @@ pub(crate) fn try_remove_delegation_from_mixnode_on_behalf(
|
||||
mix_id: MixId,
|
||||
delegate: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let delegate = deps.api.addr_validate(&delegate)?;
|
||||
_try_remove_delegation_from_mixnode(deps, env, mix_id, delegate, Some(info.sender))
|
||||
}
|
||||
@@ -100,6 +109,9 @@ pub(crate) fn _try_remove_delegation_from_mixnode(
|
||||
delegate: Addr,
|
||||
proxy: Option<Addr>,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
// undelegation is only allowed if the epoch is currently not in the process of being advanced
|
||||
ensure_epoch_in_progress_state(deps.storage)?;
|
||||
|
||||
// see if the delegation even exists
|
||||
let storage_key = Delegation::generate_storage_key(mix_id, &delegate, proxy.as_ref());
|
||||
|
||||
@@ -140,6 +152,40 @@ mod tests {
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::{coin, Decimal};
|
||||
use mixnet_contract_common::{EpochState, EpochStatus};
|
||||
|
||||
#[test]
|
||||
fn cant_be_performed_if_epoch_transition_is_in_progress() {
|
||||
let bad_states = vec![
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 0,
|
||||
},
|
||||
EpochState::ReconcilingEvents,
|
||||
EpochState::AdvancingEpoch,
|
||||
];
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
interval_storage::save_current_epoch_status(test.deps_mut().storage, &status)
|
||||
.unwrap();
|
||||
|
||||
let env = test.env();
|
||||
|
||||
let owner = "delegator";
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
let sender = mock_info(owner, &[coin(50_000_000, TEST_COIN_DENOM)]);
|
||||
|
||||
let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender, mix_id);
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochAdvancementInProgress { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_only_be_done_towards_an_existing_mixnode() {
|
||||
@@ -353,6 +399,35 @@ mod tests {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_for_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let owner = "delegator";
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
|
||||
let res = try_delegate_to_mixnode_on_behalf(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]),
|
||||
mix_id,
|
||||
owner.into(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -363,6 +438,42 @@ mod tests {
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use cosmwasm_std::coin;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use mixnet_contract_common::{EpochState, EpochStatus};
|
||||
|
||||
#[test]
|
||||
fn cant_be_performed_if_epoch_transition_is_in_progress() {
|
||||
let bad_states = vec![
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 0,
|
||||
},
|
||||
EpochState::ReconcilingEvents,
|
||||
EpochState::AdvancingEpoch,
|
||||
];
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
let mix_id = test.add_dummy_mixnode("owner", None);
|
||||
test.add_immediate_delegation("foomp", 1000u32, mix_id);
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
interval_storage::save_current_epoch_status(test.deps_mut().storage, &status)
|
||||
.unwrap();
|
||||
|
||||
let env = test.env();
|
||||
let res = try_remove_delegation_from_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
mock_info("sender", &[]),
|
||||
mix_id,
|
||||
);
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochAdvancementInProgress { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_be_performed_if_delegation_never_existed() {
|
||||
@@ -462,5 +573,40 @@ mod tests {
|
||||
);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_for_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let owner = "delegator";
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
test.add_immediate_delegation_with_illegal_proxy(
|
||||
owner,
|
||||
10000u32,
|
||||
mix_id,
|
||||
illegal_proxy.clone(),
|
||||
);
|
||||
|
||||
let res = try_remove_delegation_from_mixnode_on_behalf(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]),
|
||||
mix_id,
|
||||
owner.into(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,14 +81,13 @@ pub fn remove_family_member(store: &mut dyn Storage, member: IdentityKeyRef<'_>)
|
||||
MEMBERS.remove(store, member.to_string())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_family_member(
|
||||
store: &dyn Storage,
|
||||
f: &Family,
|
||||
member: IdentityKeyRef<'_>,
|
||||
) -> Result<bool, MixnetContractError> {
|
||||
let m = get_members(f, store)?;
|
||||
Ok(m.contains(member))
|
||||
let existing_head = MEMBERS.may_load(store, member.to_owned())?;
|
||||
Ok(existing_head.as_ref() == Some(f.head()))
|
||||
}
|
||||
|
||||
pub fn is_any_member(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::support::helpers::{
|
||||
ensure_bonded, validate_family_signature, validate_node_identity_signature,
|
||||
ensure_bonded, ensure_sent_by_vesting_contract, validate_family_signature,
|
||||
validate_node_identity_signature,
|
||||
};
|
||||
|
||||
use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response};
|
||||
@@ -28,6 +29,8 @@ pub fn try_create_family_on_behalf(
|
||||
owner_signature: String,
|
||||
label: &str,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let owner_address = deps.api.addr_validate(&owner_address)?;
|
||||
_try_create_family(
|
||||
deps,
|
||||
@@ -77,29 +80,47 @@ fn _try_create_family(
|
||||
pub fn try_join_family(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
signature: String,
|
||||
// Required for proxy joining
|
||||
node_identity_signature: Option<String>,
|
||||
family_signature: String,
|
||||
family_head: IdentityKey,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
let family_head = FamilyHead::new(&family_head);
|
||||
_try_join_family(deps, &info.sender, signature, family_head)
|
||||
_try_join_family(
|
||||
deps,
|
||||
&info.sender,
|
||||
node_identity_signature,
|
||||
family_signature,
|
||||
family_head,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn try_join_family_on_behalf(
|
||||
deps: DepsMut,
|
||||
_info: MessageInfo,
|
||||
info: MessageInfo,
|
||||
member_address: String,
|
||||
signature: String,
|
||||
node_identity_signature: Option<String>,
|
||||
family_signature: String,
|
||||
family_head: IdentityKey,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let member_address = deps.api.addr_validate(&member_address)?;
|
||||
let family_head = FamilyHead::new(&family_head);
|
||||
_try_join_family(deps, &member_address, signature, family_head)
|
||||
_try_join_family(
|
||||
deps,
|
||||
&member_address,
|
||||
node_identity_signature,
|
||||
family_signature,
|
||||
family_head,
|
||||
)
|
||||
}
|
||||
|
||||
fn _try_join_family(
|
||||
deps: DepsMut,
|
||||
owner: &Addr,
|
||||
signature: String,
|
||||
node_identity_signature: Option<String>,
|
||||
family_signature: String,
|
||||
family_head: FamilyHead,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
let existing_bond = crate::mixnodes::storage::mixnode_bonds()
|
||||
@@ -126,10 +147,19 @@ fn _try_join_family(
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(node_identity_signature) = node_identity_signature {
|
||||
validate_node_identity_signature(
|
||||
deps.as_ref(),
|
||||
owner,
|
||||
&node_identity_signature,
|
||||
existing_bond.identity(),
|
||||
)?;
|
||||
}
|
||||
|
||||
validate_family_signature(
|
||||
deps.as_ref(),
|
||||
existing_bond.identity(),
|
||||
&signature,
|
||||
&family_signature,
|
||||
family_head.identity(),
|
||||
)?;
|
||||
|
||||
@@ -152,20 +182,22 @@ pub fn try_leave_family(
|
||||
|
||||
pub fn try_leave_family_on_behalf(
|
||||
deps: DepsMut,
|
||||
_info: MessageInfo,
|
||||
info: MessageInfo,
|
||||
member_address: String,
|
||||
signature: String,
|
||||
node_family_signature: String,
|
||||
family_head: IdentityKey,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let family_head = FamilyHead::new(&family_head);
|
||||
let member_address = deps.api.addr_validate(&member_address)?;
|
||||
_try_leave_family(deps, &member_address, signature, family_head)
|
||||
_try_leave_family(deps, &member_address, node_family_signature, family_head)
|
||||
}
|
||||
|
||||
fn _try_leave_family(
|
||||
deps: DepsMut,
|
||||
owner: &Addr,
|
||||
signature: String,
|
||||
node_family_signature: String,
|
||||
family_head: FamilyHead,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
let existing_bond = crate::mixnodes::storage::mixnode_bonds()
|
||||
@@ -194,11 +226,11 @@ fn _try_leave_family(
|
||||
});
|
||||
}
|
||||
|
||||
validate_family_signature(
|
||||
validate_node_identity_signature(
|
||||
deps.as_ref(),
|
||||
owner,
|
||||
&node_family_signature,
|
||||
existing_bond.identity(),
|
||||
&signature,
|
||||
family_head.identity(),
|
||||
)?;
|
||||
|
||||
remove_family_member(deps.storage, existing_bond.identity());
|
||||
@@ -217,11 +249,13 @@ pub fn try_head_kick_member(
|
||||
|
||||
pub fn try_head_kick_member_on_behalf(
|
||||
deps: DepsMut,
|
||||
_info: MessageInfo,
|
||||
info: MessageInfo,
|
||||
head_address: String,
|
||||
owner_signature: String,
|
||||
member: IdentityKeyRef,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let head_address = deps.api.addr_validate(&head_address)?;
|
||||
_try_head_kick_member(deps, &head_address, owner_signature, member)
|
||||
}
|
||||
@@ -312,7 +346,7 @@ mod test {
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
mock_info(malicious_head, &[minimum_pledge.clone()]),
|
||||
malicious_mixnode.clone(),
|
||||
malicious_mixnode,
|
||||
cost_params.clone(),
|
||||
malicious_sig.clone(),
|
||||
)
|
||||
@@ -320,28 +354,22 @@ mod test {
|
||||
|
||||
crate::mixnodes::transactions::try_add_mixnode(
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
mock_info(member, &[minimum_pledge.clone()]),
|
||||
env,
|
||||
mock_info(member, &[minimum_pledge]),
|
||||
member_mixnode.clone(),
|
||||
cost_params.clone(),
|
||||
cost_params,
|
||||
member_sig.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
try_create_family(
|
||||
deps.as_mut(),
|
||||
mock_info(head.clone(), &[]),
|
||||
head_sig.clone(),
|
||||
"test",
|
||||
)
|
||||
.unwrap();
|
||||
try_create_family(deps.as_mut(), mock_info(head, &[]), head_sig, "test").unwrap();
|
||||
let family_head = FamilyHead::new(&head_mixnode.identity_key);
|
||||
assert!(get_family(&family_head, &deps.storage).is_ok());
|
||||
|
||||
let nope = try_create_family(
|
||||
deps.as_mut(),
|
||||
mock_info(malicious_head.clone(), &[]),
|
||||
malicious_sig.clone(),
|
||||
mock_info(malicious_head, &[]),
|
||||
malicious_sig,
|
||||
"test",
|
||||
);
|
||||
|
||||
@@ -368,6 +396,7 @@ mod test {
|
||||
try_join_family(
|
||||
deps.as_mut(),
|
||||
mock_info(member, &[]),
|
||||
Some(member_sig.clone()),
|
||||
join_signature.clone(),
|
||||
head_mixnode.identity_key.clone(),
|
||||
)
|
||||
@@ -380,7 +409,7 @@ mod test {
|
||||
try_leave_family(
|
||||
deps.as_mut(),
|
||||
mock_info(member, &[]),
|
||||
join_signature.clone(),
|
||||
member_sig.clone(),
|
||||
head_mixnode.identity_key.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -391,6 +420,7 @@ mod test {
|
||||
try_join_family(
|
||||
deps.as_mut(),
|
||||
mock_info(member, &[]),
|
||||
Some(member_sig.clone()),
|
||||
join_signature.clone(),
|
||||
head_mixnode.identity_key.clone(),
|
||||
)
|
||||
@@ -411,4 +441,201 @@ mod test {
|
||||
// let family = get_family(&family_head, &deps.storage).unwrap();
|
||||
// assert!(!is_family_member(&deps.storage, &family, &member_mixnode.identity_key).unwrap());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod creating_family {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
|
||||
#[test]
|
||||
fn fails_for_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let head = "alice";
|
||||
|
||||
let (_, keypair) = test.add_dummy_mixnode_with_keypair(head, None);
|
||||
let sig = keypair.private_key().sign_text(head);
|
||||
|
||||
let res = try_create_family_on_behalf(
|
||||
test.deps_mut(),
|
||||
mock_info(illegal_proxy.as_ref(), &[]),
|
||||
head.to_string(),
|
||||
sig,
|
||||
"label",
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod joining_family {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
|
||||
#[test]
|
||||
fn fails_for_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let head = "alice";
|
||||
let label = "family";
|
||||
let new_member = "vin-diesel";
|
||||
|
||||
let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label);
|
||||
let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None);
|
||||
|
||||
// TODO: those signatures are WRONG and have to be c hanged
|
||||
let join_signature = head_keys
|
||||
.private_key()
|
||||
.sign_text(&member_keys.public_key().to_base58_string());
|
||||
|
||||
let member_sig = member_keys.private_key().sign_text(new_member);
|
||||
|
||||
let head_identity = head_keys.public_key().to_base58_string();
|
||||
let res = try_join_family_on_behalf(
|
||||
test.deps_mut(),
|
||||
mock_info(illegal_proxy.as_ref(), &[]),
|
||||
new_member.to_string(),
|
||||
Some(member_sig),
|
||||
join_signature,
|
||||
head_identity,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod leaving_family {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
|
||||
#[test]
|
||||
fn fails_for_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let head = "alice";
|
||||
let label = "family";
|
||||
let new_member = "vin-diesel";
|
||||
|
||||
let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label);
|
||||
let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None);
|
||||
|
||||
// TODO: those signatures are WRONG and have to be changed
|
||||
let join_signature = head_keys
|
||||
.private_key()
|
||||
.sign_text(&member_keys.public_key().to_base58_string());
|
||||
|
||||
let member_sig = member_keys.private_key().sign_text(new_member);
|
||||
|
||||
let head_identity = head_keys.public_key().to_base58_string();
|
||||
try_join_family_on_behalf(
|
||||
test.deps_mut(),
|
||||
mock_info(vesting_contract.as_ref(), &[]),
|
||||
new_member.to_string(),
|
||||
Some(member_sig.clone()),
|
||||
join_signature,
|
||||
head_identity,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let leave_signature = member_sig;
|
||||
let res = try_leave_family_on_behalf(
|
||||
test.deps_mut(),
|
||||
mock_info(illegal_proxy.as_ref(), &[]),
|
||||
new_member.to_string(),
|
||||
leave_signature,
|
||||
head_keys.public_key().to_base58_string(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod kicking_family_member {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
|
||||
#[test]
|
||||
fn fails_for_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let head = "alice";
|
||||
let label = "family";
|
||||
let new_member = "vin-diesel";
|
||||
|
||||
let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label);
|
||||
let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None);
|
||||
|
||||
// TODO: those signatures are WRONG and have to be c hanged
|
||||
let join_signature = head_keys
|
||||
.private_key()
|
||||
.sign_text(&member_keys.public_key().to_base58_string());
|
||||
|
||||
let member_sig = member_keys.private_key().sign_text(new_member);
|
||||
|
||||
let head_identity = head_keys.public_key().to_base58_string();
|
||||
try_join_family_on_behalf(
|
||||
test.deps_mut(),
|
||||
mock_info(vesting_contract.as_ref(), &[]),
|
||||
new_member.to_string(),
|
||||
Some(member_sig.clone()),
|
||||
join_signature,
|
||||
head_identity,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// TODO: a completely wrong signature is being used
|
||||
let res = try_head_kick_member_on_behalf(
|
||||
test.deps_mut(),
|
||||
mock_info(illegal_proxy.as_ref(), &[]),
|
||||
head.to_string(),
|
||||
head_keys.private_key().sign_text(head),
|
||||
&member_keys.public_key().to_base58_string(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
use super::storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::support::helpers::{
|
||||
ensure_no_existing_bond, validate_node_identity_signature, validate_pledge,
|
||||
ensure_no_existing_bond, ensure_sent_by_vesting_contract, validate_node_identity_signature,
|
||||
validate_pledge,
|
||||
};
|
||||
use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
@@ -38,6 +39,8 @@ pub fn try_add_gateway_on_behalf(
|
||||
owner: String,
|
||||
owner_signature: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let proxy = info.sender;
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
_try_add_gateway(
|
||||
@@ -65,7 +68,7 @@ pub(crate) fn _try_add_gateway(
|
||||
let pledge = validate_pledge(pledge, minimum_pledge)?;
|
||||
|
||||
// if the client has an active bonded mixnode or gateway, don't allow bonding
|
||||
ensure_no_existing_bond(deps.storage, &owner)?;
|
||||
ensure_no_existing_bond(&owner, deps.storage)?;
|
||||
|
||||
// check if somebody else has already bonded a gateway with this identity
|
||||
if let Some(existing_bond) =
|
||||
@@ -110,6 +113,8 @@ pub fn try_remove_gateway_on_behalf(
|
||||
info: MessageInfo,
|
||||
owner: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let proxy = info.sender;
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
_try_remove_gateway(deps, owner, Some(proxy))
|
||||
@@ -178,11 +183,14 @@ pub(crate) fn _try_remove_gateway(
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::contract::execute;
|
||||
use crate::gateways::transactions::try_add_gateway;
|
||||
use crate::gateways::transactions::{
|
||||
try_add_gateway, try_add_gateway_on_behalf, try_remove_gateway_on_behalf,
|
||||
};
|
||||
use crate::interval::pending_events;
|
||||
use crate::mixnet_contract_settings::storage::minimum_gateway_pledge;
|
||||
use crate::support::tests;
|
||||
use crate::support::tests::fixtures::TEST_COIN_DENOM;
|
||||
use crate::support::tests::fixtures::{good_gateway_pledge, TEST_COIN_DENOM};
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use crate::support::tests::{fixtures, test_helpers};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::{coin, Addr, BankMsg, Response, Uint128};
|
||||
@@ -280,6 +288,36 @@ pub mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_add_with_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let owner = "alice";
|
||||
let (gateway, sig) = test_helpers::gateway_with_signature(&mut test.rng, owner);
|
||||
|
||||
let res = try_add_gateway_on_behalf(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
mock_info(illegal_proxy.as_ref(), &good_gateway_pledge()),
|
||||
gateway,
|
||||
owner.to_string(),
|
||||
sig,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_remove() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
@@ -367,4 +405,31 @@ pub mod tests {
|
||||
assert_eq!(1, gateway_bonds.len());
|
||||
assert_eq!(&Addr::unchecked("bob"), gateway_bonds[0].owner());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_remove_with_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let owner = "alice";
|
||||
|
||||
test.add_dummy_gateway_with_illegal_proxy(owner, None, illegal_proxy.clone());
|
||||
|
||||
let res = try_remove_gateway_on_behalf(
|
||||
test.deps_mut(),
|
||||
mock_info(illegal_proxy.as_ref(), &good_gateway_pledge()),
|
||||
owner.to_string(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,11 @@ use crate::delegations;
|
||||
use crate::delegations::storage as delegations_storage;
|
||||
use crate::interval::helpers::change_interval_config;
|
||||
use crate::interval::storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_details_by_id};
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::rewards::storage as rewards_storage;
|
||||
use crate::support::helpers::send_to_proxy_or_owner;
|
||||
use cosmwasm_std::{wasm_execute, Addr, Coin, DepsMut, Env, Response};
|
||||
use crate::support::helpers::{send_to_proxy_or_owner, VestingTracking};
|
||||
use cosmwasm_std::{Addr, Coin, DepsMut, Env, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::events::{
|
||||
new_active_set_update_event, new_delegation_event, new_delegation_on_unbonded_node_event,
|
||||
@@ -24,7 +23,6 @@ use mixnet_contract_common::pending_events::{
|
||||
};
|
||||
use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate;
|
||||
use mixnet_contract_common::{BlockHeight, Delegation, MixId};
|
||||
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
|
||||
|
||||
pub(crate) trait ContractExecutableEvent {
|
||||
// note: the error only means a HARD error like we failed to read from storage.
|
||||
@@ -56,27 +54,20 @@ pub(crate) fn delegate(
|
||||
_ => {
|
||||
// if mixnode is no longer bonded or in the process of unbonding, return the tokens back to the
|
||||
// delegator;
|
||||
// (read the notes regarding possible epoch progressiong halting behaviour in `maybe_add_track_undelegation_message`)
|
||||
let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![amount.clone()]);
|
||||
let mut response = Response::new().add_message(return_tokens).add_event(
|
||||
new_delegation_on_unbonded_node_event(&owner, &proxy, mix_id),
|
||||
);
|
||||
|
||||
if let Some(proxy) = &proxy {
|
||||
// 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 {
|
||||
let msg = VestingContractExecuteMsg::TrackUndelegation {
|
||||
owner: owner.into_string(),
|
||||
mix_id,
|
||||
amount,
|
||||
};
|
||||
|
||||
let track_undelegate_message = wasm_execute(proxy, &msg, vec![])?;
|
||||
response = response.add_message(track_undelegate_message);
|
||||
}
|
||||
}
|
||||
let response = Response::new()
|
||||
.add_message(return_tokens)
|
||||
.add_event(new_delegation_on_unbonded_node_event(
|
||||
&owner, &proxy, mix_id,
|
||||
))
|
||||
.maybe_add_track_vesting_undelegation_message(
|
||||
deps.storage,
|
||||
proxy,
|
||||
owner.to_string(),
|
||||
mix_id,
|
||||
amount,
|
||||
)?;
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
@@ -163,26 +154,18 @@ pub(crate) fn undelegate(
|
||||
let tokens_to_return =
|
||||
delegations::helpers::undelegate(deps.storage, delegation, mix_rewarding)?;
|
||||
|
||||
// (read the notes regarding possible epoch progressiong halting behaviour in `maybe_add_track_undelegation_message`)
|
||||
let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![tokens_to_return.clone()]);
|
||||
let mut response = Response::new()
|
||||
let response = Response::new()
|
||||
.add_message(return_tokens)
|
||||
.add_event(new_undelegation_event(created_at, &owner, &proxy, mix_id));
|
||||
|
||||
if let Some(proxy) = &proxy {
|
||||
// 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 {
|
||||
let msg = VestingContractExecuteMsg::TrackUndelegation {
|
||||
owner: owner.into_string(),
|
||||
mix_id,
|
||||
amount: tokens_to_return,
|
||||
};
|
||||
|
||||
let track_undelegate_message = wasm_execute(proxy, &msg, vec![])?;
|
||||
response = response.add_message(track_undelegate_message);
|
||||
}
|
||||
}
|
||||
.add_event(new_undelegation_event(created_at, &owner, &proxy, mix_id))
|
||||
.maybe_add_track_vesting_undelegation_message(
|
||||
deps.storage,
|
||||
proxy,
|
||||
owner.to_string(),
|
||||
mix_id,
|
||||
tokens_to_return,
|
||||
)?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
@@ -219,24 +202,15 @@ pub(crate) fn unbond_mixnode(
|
||||
// decrement the associated layer count
|
||||
cleanup_post_unbond_mixnode_storage(deps.storage, env, &node_details)?;
|
||||
|
||||
let mut response = Response::new()
|
||||
let response = Response::new()
|
||||
.add_message(return_tokens)
|
||||
.add_event(new_mixnode_unbonding_event(created_at, mix_id));
|
||||
|
||||
if let Some(proxy) = &proxy {
|
||||
// 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 {
|
||||
let msg = VestingContractExecuteMsg::TrackUnbondMixnode {
|
||||
owner: owner.clone().into_string(),
|
||||
amount: tokens,
|
||||
};
|
||||
|
||||
let track_unbond_message = wasm_execute(proxy, &msg, vec![])?;
|
||||
response = response.add_message(track_unbond_message);
|
||||
}
|
||||
}
|
||||
.add_event(new_mixnode_unbonding_event(created_at, mix_id))
|
||||
.maybe_add_track_vesting_unbond_mixnode_message(
|
||||
deps.storage,
|
||||
proxy.clone(),
|
||||
owner.clone().into_string(),
|
||||
tokens,
|
||||
)?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
@@ -429,6 +403,7 @@ mod tests {
|
||||
use cosmwasm_std::Decimal;
|
||||
use mixnet_contract_common::Percent;
|
||||
use std::time::Duration;
|
||||
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
|
||||
|
||||
// note that authorization and basic validation has already been performed for all of those
|
||||
// before being pushed onto the event queues
|
||||
@@ -628,19 +603,31 @@ mod tests {
|
||||
let delegation_coin_new = coin(delegation_new, TEST_COIN_DENOM);
|
||||
|
||||
// perform some rewarding here to advance the unit delegation beyond the initial value
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let owner = "delegator";
|
||||
test.add_immediate_delegation(owner, delegation_og, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist1 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist1 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist2 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist2 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let storage_key =
|
||||
Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None);
|
||||
@@ -699,11 +686,17 @@ mod tests {
|
||||
let delegation_coin = coin(120_000_000u128, TEST_COIN_DENOM);
|
||||
|
||||
// perform some rewarding here to advance the unit delegation beyond the initial value
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let storage_key =
|
||||
Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None);
|
||||
@@ -749,21 +742,19 @@ mod tests {
|
||||
|
||||
let delegation = 120_000_000u128;
|
||||
let delegation_coin = coin(delegation, TEST_COIN_DENOM);
|
||||
let owner1 = "delegator1";
|
||||
let owner2 = "delegator2";
|
||||
let owner = "delegator";
|
||||
|
||||
let env = test.env();
|
||||
unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap();
|
||||
|
||||
let vesting_contract = test.vesting_contract();
|
||||
let dummy_proxy = Addr::unchecked("not-vesting-contract");
|
||||
|
||||
// for a fresh delegation, nothing was added to the storage either
|
||||
let res_vesting = delegate(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
Addr::unchecked(owner1),
|
||||
Addr::unchecked(owner),
|
||||
mix_id,
|
||||
delegation_coin.clone(),
|
||||
Some(vesting_contract.clone()),
|
||||
@@ -771,7 +762,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(owner1),
|
||||
&Addr::unchecked(owner),
|
||||
Some(vesting_contract.clone()).as_ref(),
|
||||
);
|
||||
assert!(delegations_storage::delegations()
|
||||
@@ -795,7 +786,7 @@ mod tests {
|
||||
found_track = true;
|
||||
assert_eq!(contract_addr, vesting_contract.as_str());
|
||||
let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUndelegation {
|
||||
owner: owner1.to_string(),
|
||||
owner: owner.to_string(),
|
||||
mix_id,
|
||||
amount: delegation_coin.clone(),
|
||||
})
|
||||
@@ -805,37 +796,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
assert!(found_track);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_error_for_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
|
||||
let delegation = 120_000_000u128;
|
||||
let delegation_coin = coin(delegation, TEST_COIN_DENOM);
|
||||
let owner = "delegator";
|
||||
let dummy_proxy = Addr::unchecked("not-vesting-contract");
|
||||
|
||||
let env = test.env();
|
||||
unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap();
|
||||
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
// try to add illegal delegation (with invalid proxy)
|
||||
let res_other_proxy = delegate(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
Addr::unchecked(owner1),
|
||||
Addr::unchecked(owner),
|
||||
mix_id,
|
||||
delegation_coin.clone(),
|
||||
delegation_coin,
|
||||
Some(dummy_proxy.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(owner2),
|
||||
Some(dummy_proxy.clone()).as_ref(),
|
||||
);
|
||||
assert!(delegations_storage::delegations()
|
||||
.may_load(test.deps().storage, storage_key)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
// and all tokens are returned back to the proxy
|
||||
let (receiver, sent_amount) = get_bank_send_msg(&res_other_proxy).unwrap();
|
||||
assert_eq!(receiver, dummy_proxy.as_str());
|
||||
assert_eq!(sent_amount[0], delegation_coin);
|
||||
.unwrap_err();
|
||||
|
||||
// no track messages here!
|
||||
for msg in &res_other_proxy.messages {
|
||||
if let CosmosMsg::Wasm(WasmMsg::Execute { .. }) = &msg.msg {
|
||||
panic!("we shouldn't have attempted to call any other contract!")
|
||||
assert_eq!(
|
||||
res_other_proxy,
|
||||
MixnetContractError::ProxyIsNotVestingContract {
|
||||
received: dummy_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,18 +881,30 @@ mod tests {
|
||||
let delegation = 120_000_000u128;
|
||||
|
||||
// perform some rewarding here to advance the unit delegation beyond the initial value
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
test.add_immediate_delegation(owner, delegation, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist1 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist1 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist2 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist2 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let expected_reward = dist1.delegates + dist2.delegates;
|
||||
let truncated_reward = truncate_reward_amount(expected_reward);
|
||||
@@ -930,37 +938,23 @@ mod tests {
|
||||
|
||||
let delegation = 120_000_000u128;
|
||||
let delegation_coin = coin(delegation, TEST_COIN_DENOM);
|
||||
let owner1 = "delegator1";
|
||||
let owner2 = "delegator2";
|
||||
let owner = "delegator";
|
||||
|
||||
let vesting_contract = test.vesting_contract();
|
||||
let dummy_proxy = Addr::unchecked("not-vesting-contract");
|
||||
|
||||
test.add_immediate_delegation_with_proxy(
|
||||
owner1,
|
||||
delegation,
|
||||
mix_id,
|
||||
vesting_contract.clone(),
|
||||
);
|
||||
test.add_immediate_delegation_with_proxy(
|
||||
owner2,
|
||||
delegation,
|
||||
mix_id,
|
||||
dummy_proxy.clone(),
|
||||
);
|
||||
test.add_immediate_delegation_with_legal_proxy(owner, delegation, mix_id);
|
||||
|
||||
// for a fresh delegation, nothing was added to the storage either
|
||||
let res_vesting = undelegate(
|
||||
test.deps_mut(),
|
||||
123,
|
||||
Addr::unchecked(owner1),
|
||||
Addr::unchecked(owner),
|
||||
mix_id,
|
||||
Some(vesting_contract.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(owner1),
|
||||
&Addr::unchecked(owner),
|
||||
Some(vesting_contract.clone()).as_ref(),
|
||||
);
|
||||
assert!(delegations_storage::delegations()
|
||||
@@ -985,7 +979,7 @@ mod tests {
|
||||
found_track = true;
|
||||
assert_eq!(contract_addr, vesting_contract.as_str());
|
||||
let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUndelegation {
|
||||
owner: owner1.to_string(),
|
||||
owner: owner.to_string(),
|
||||
mix_id,
|
||||
amount: delegation_coin.clone(),
|
||||
})
|
||||
@@ -995,35 +989,41 @@ mod tests {
|
||||
}
|
||||
}
|
||||
assert!(found_track);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_error_for_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
|
||||
let delegation = 120_000_000u128;
|
||||
let owner = "delegator1";
|
||||
|
||||
let vesting_contract = test.vesting_contract();
|
||||
let dummy_proxy = Addr::unchecked("not-vesting-contract");
|
||||
|
||||
test.add_immediate_delegation_with_illegal_proxy(
|
||||
owner,
|
||||
delegation,
|
||||
mix_id,
|
||||
dummy_proxy.clone(),
|
||||
);
|
||||
|
||||
let res_other_proxy = undelegate(
|
||||
test.deps_mut(),
|
||||
123,
|
||||
Addr::unchecked(owner2),
|
||||
Addr::unchecked(owner),
|
||||
mix_id,
|
||||
Some(dummy_proxy.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(owner2),
|
||||
Some(dummy_proxy.clone()).as_ref(),
|
||||
);
|
||||
assert!(delegations_storage::delegations()
|
||||
.may_load(test.deps().storage, storage_key)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
// and all tokens are returned back to the proxy
|
||||
let (receiver, sent_amount) = get_bank_send_msg(&res_other_proxy).unwrap();
|
||||
assert_eq!(receiver, dummy_proxy.as_str());
|
||||
assert_eq!(sent_amount[0], delegation_coin);
|
||||
|
||||
// no track messages here!
|
||||
for msg in &res_other_proxy.messages {
|
||||
if let CosmosMsg::Wasm(WasmMsg::Execute { .. }) = &msg.msg {
|
||||
panic!("we shouldn't have attempted to call any other contract!")
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
res_other_proxy,
|
||||
MixnetContractError::ProxyIsNotVestingContract {
|
||||
received: dummy_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1062,11 +1062,17 @@ mod tests {
|
||||
.unwrap();
|
||||
let layer = mix_details.layer;
|
||||
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist1 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist1 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist2 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist2 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let expected_reward = dist1.operator + dist2.operator;
|
||||
let truncated_reward = truncate_reward_amount(expected_reward);
|
||||
@@ -1109,19 +1115,14 @@ mod tests {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let vesting_contract = test.vesting_contract();
|
||||
let dummy_proxy = Addr::unchecked("not-vesting-contract");
|
||||
|
||||
let pledge = Uint128::new(250_000_000);
|
||||
let pledge_coin = coin(250_000_000, TEST_COIN_DENOM);
|
||||
let owner1 = "mix-owner1";
|
||||
let owner2 = "mix-owner2";
|
||||
let mix_id_vesting =
|
||||
test.add_dummy_mixnode_with_proxy(owner1, Some(pledge), vesting_contract.clone());
|
||||
let mix_id_other_proxy =
|
||||
test.add_dummy_mixnode_with_proxy(owner2, Some(pledge), dummy_proxy.clone());
|
||||
let owner = "mix-owner1";
|
||||
let mix_id_vesting = test.add_dummy_mixnode_with_legal_proxy(owner, Some(pledge));
|
||||
|
||||
let env = test.env();
|
||||
let res_vesting = unbond_mixnode(test.deps_mut(), &env, 123, mix_id_vesting).unwrap();
|
||||
let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id_vesting).unwrap();
|
||||
|
||||
assert!(mixnodes_storage::mixnode_bonds()
|
||||
.may_load(test.deps().storage, mix_id_vesting)
|
||||
@@ -1129,13 +1130,13 @@ mod tests {
|
||||
.is_none());
|
||||
|
||||
// and all tokens are returned back to the proxy
|
||||
let (receiver, sent_amount) = get_bank_send_msg(&res_vesting).unwrap();
|
||||
let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap();
|
||||
assert_eq!(receiver, vesting_contract.as_str());
|
||||
assert_eq!(sent_amount[0], pledge_coin);
|
||||
|
||||
// and we get appropriate track message
|
||||
let mut found_track = true;
|
||||
for msg in &res_vesting.messages {
|
||||
for msg in &res.messages {
|
||||
if let CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr,
|
||||
msg,
|
||||
@@ -1145,7 +1146,7 @@ mod tests {
|
||||
found_track = true;
|
||||
assert_eq!(contract_addr, vesting_contract.as_str());
|
||||
let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUnbondMixnode {
|
||||
owner: owner1.to_string(),
|
||||
owner: owner.to_string(),
|
||||
amount: pledge_coin.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
@@ -1154,24 +1155,32 @@ mod tests {
|
||||
}
|
||||
}
|
||||
assert!(found_track);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_error_for_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let dummy_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let env = test.env();
|
||||
|
||||
let vesting_contract = test.vesting_contract();
|
||||
let owner = "mix-owner";
|
||||
let pledge = Uint128::new(250_000_000);
|
||||
|
||||
let mix_id_illegal_proxy =
|
||||
test.add_dummy_mixnode_with_illegal_proxy(owner, Some(pledge), dummy_proxy.clone());
|
||||
|
||||
// this is the halting issue that should have never occurred
|
||||
let res_other_proxy =
|
||||
unbond_mixnode(test.deps_mut(), &env, 123, mix_id_other_proxy).unwrap();
|
||||
assert!(mixnodes_storage::mixnode_bonds()
|
||||
.may_load(test.deps().storage, mix_id_other_proxy)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
// and all tokens are returned back to the proxy
|
||||
let (receiver, sent_amount) = get_bank_send_msg(&res_other_proxy).unwrap();
|
||||
assert_eq!(receiver, dummy_proxy.as_str());
|
||||
assert_eq!(sent_amount[0], pledge_coin);
|
||||
|
||||
// no track messages here!
|
||||
for msg in &res_other_proxy.messages {
|
||||
if let CosmosMsg::Wasm(WasmMsg::Execute { .. }) = &msg.msg {
|
||||
panic!("we shouldn't have attempted to call any other contract!")
|
||||
unbond_mixnode(test.deps_mut(), &env, 123, mix_id_illegal_proxy).unwrap_err();
|
||||
assert_eq!(
|
||||
res_other_proxy,
|
||||
MixnetContractError::ProxyIsNotVestingContract {
|
||||
received: dummy_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1244,12 +1253,16 @@ mod tests {
|
||||
test.add_immediate_delegation("carol", 111_111_111u128, mix_id_full_pledge);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]);
|
||||
test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]);
|
||||
|
||||
let dist1 =
|
||||
test.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0));
|
||||
let dist2 =
|
||||
test.reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0));
|
||||
let dist1 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id_repledge,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
let dist2 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id_full_pledge,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
assert_eq!(dist1, dist2)
|
||||
}
|
||||
@@ -1267,10 +1280,12 @@ mod tests {
|
||||
test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id_repledge]);
|
||||
test.force_change_rewarded_set(vec![mix_id_repledge]);
|
||||
|
||||
let dist =
|
||||
test.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id_repledge,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let increase = test.coin(pledge2.u128());
|
||||
increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap();
|
||||
@@ -1306,15 +1321,19 @@ mod tests {
|
||||
);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]);
|
||||
test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]);
|
||||
|
||||
// go through few epochs of rewarding
|
||||
for _ in 0..500 {
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist1 = test
|
||||
.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0));
|
||||
let dist2 = test
|
||||
.reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0));
|
||||
let dist1 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id_repledge,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
let dist2 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id_full_pledge,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
assert_eq!(dist1, dist2)
|
||||
}
|
||||
@@ -1333,7 +1352,7 @@ mod tests {
|
||||
test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id_repledge]);
|
||||
test.force_change_rewarded_set(vec![mix_id_repledge]);
|
||||
|
||||
let mut cumulative_op_reward = Decimal::zero();
|
||||
let mut cumulative_del_reward = Decimal::zero();
|
||||
@@ -1341,8 +1360,10 @@ mod tests {
|
||||
// go few epochs of rewarding before adding more pledge
|
||||
for _ in 0..500 {
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist = test
|
||||
.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id_repledge,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
cumulative_op_reward += dist.operator;
|
||||
cumulative_del_reward += dist.delegates;
|
||||
}
|
||||
@@ -1382,15 +1403,19 @@ mod tests {
|
||||
);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]);
|
||||
test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]);
|
||||
|
||||
// go through few more epochs of rewarding
|
||||
for _ in 0..500 {
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist1 = test
|
||||
.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0));
|
||||
let dist2 = test
|
||||
.reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0));
|
||||
let dist1 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id_repledge,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
let dist2 = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id_full_pledge,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
assert_eq!(dist1, dist2)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::constants::{
|
||||
@@ -9,12 +9,18 @@ use crate::constants::{
|
||||
use crate::interval::storage;
|
||||
use cosmwasm_std::{Deps, Env, Order, StdResult};
|
||||
use cw_storage_plus::Bound;
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::pending_events::{PendingEpochEvent, PendingIntervalEvent};
|
||||
use mixnet_contract_common::{
|
||||
CurrentIntervalResponse, EpochEventId, IntervalEventId, MixId, PagedRewardedSetResponse,
|
||||
PendingEpochEventsResponse, PendingIntervalEventsResponse,
|
||||
CurrentIntervalResponse, EpochEventId, EpochStatus, IntervalEventId, MixId,
|
||||
NumberOfPendingEventsResponse, PagedRewardedSetResponse, PendingEpochEventsResponse,
|
||||
PendingIntervalEventsResponse,
|
||||
};
|
||||
|
||||
pub fn query_epoch_status(deps: Deps<'_>) -> StdResult<EpochStatus> {
|
||||
storage::current_epoch_status(deps.storage)
|
||||
}
|
||||
|
||||
pub fn query_current_interval_details(
|
||||
deps: Deps<'_>,
|
||||
env: Env,
|
||||
@@ -106,10 +112,60 @@ pub fn query_pending_interval_events_paged(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_number_of_pending_events(
|
||||
deps: Deps<'_>,
|
||||
) -> Result<NumberOfPendingEventsResponse, MixnetContractError> {
|
||||
let last_executed_epoch_id = storage::LAST_PROCESSED_EPOCH_EVENT.load(deps.storage)?;
|
||||
let last_inserted_epoch_id = storage::EPOCH_EVENT_ID_COUNTER.load(deps.storage)?;
|
||||
|
||||
let last_executed_interval_id = storage::LAST_PROCESSED_INTERVAL_EVENT.load(deps.storage)?;
|
||||
let last_inserted_interval_id = storage::INTERVAL_EVENT_ID_COUNTER.load(deps.storage)?;
|
||||
|
||||
Ok(NumberOfPendingEventsResponse {
|
||||
epoch_events: last_inserted_epoch_id - last_executed_epoch_id,
|
||||
interval_events: last_inserted_interval_id - last_executed_interval_id,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::fixtures;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use cosmwasm_std::Addr;
|
||||
use mixnet_contract_common::{PendingEpochEventKind, PendingIntervalEventKind};
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
|
||||
fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) {
|
||||
for _ in 0..n {
|
||||
push_dummy_epoch_action(test)
|
||||
}
|
||||
}
|
||||
|
||||
fn push_dummy_epoch_action(test: &mut TestSetup) {
|
||||
let dummy_action = PendingEpochEventKind::Undelegate {
|
||||
owner: Addr::unchecked("foomp"),
|
||||
mix_id: test.rng.next_u32(),
|
||||
proxy: None,
|
||||
};
|
||||
let env = test.env();
|
||||
storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap();
|
||||
}
|
||||
|
||||
fn push_n_dummy_interval_actions(test: &mut TestSetup, n: usize) {
|
||||
for _ in 0..n {
|
||||
push_dummy_interval_action(test)
|
||||
}
|
||||
}
|
||||
|
||||
fn push_dummy_interval_action(test: &mut TestSetup) {
|
||||
let dummy_action = PendingIntervalEventKind::ChangeMixCostParams {
|
||||
mix_id: test.rng.next_u32(),
|
||||
new_costs: fixtures::mix_node_cost_params_fixture(),
|
||||
};
|
||||
let env = test.env();
|
||||
storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn querying_for_current_interval_details() {
|
||||
@@ -151,7 +207,7 @@ mod tests {
|
||||
|
||||
fn set_rewarded_set_to_n_nodes(test: &mut TestSetup, n: usize) {
|
||||
let set = (1u32..).take(n).collect::<Vec<_>>();
|
||||
test.update_rewarded_set(set)
|
||||
test.force_change_rewarded_set(set)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -237,25 +293,6 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod pending_epoch_events {
|
||||
use super::*;
|
||||
use cosmwasm_std::Addr;
|
||||
use mixnet_contract_common::pending_events::PendingEpochEventKind;
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
|
||||
fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) {
|
||||
for _ in 0..n {
|
||||
push_dummy_epoch_action(test)
|
||||
}
|
||||
}
|
||||
|
||||
fn push_dummy_epoch_action(test: &mut TestSetup) {
|
||||
let dummy_action = PendingEpochEventKind::Undelegate {
|
||||
owner: Addr::unchecked("foomp"),
|
||||
mix_id: test.rng.next_u32(),
|
||||
proxy: None,
|
||||
};
|
||||
let env = test.env();
|
||||
storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn obeys_limits() {
|
||||
@@ -379,24 +416,6 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod pending_interval_events {
|
||||
use super::*;
|
||||
use crate::support::tests::fixtures;
|
||||
use mixnet_contract_common::pending_events::PendingIntervalEventKind;
|
||||
use rand_chacha::rand_core::RngCore;
|
||||
|
||||
fn push_n_dummy_interval_actions(test: &mut TestSetup, n: usize) {
|
||||
for _ in 0..n {
|
||||
push_dummy_interval_action(test)
|
||||
}
|
||||
}
|
||||
|
||||
fn push_dummy_interval_action(test: &mut TestSetup) {
|
||||
let dummy_action = PendingIntervalEventKind::ChangeMixCostParams {
|
||||
mix_id: test.rng.next_u32(),
|
||||
new_costs: fixtures::mix_node_cost_params_fixture(),
|
||||
};
|
||||
let env = test.env();
|
||||
storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn obeys_limits() {
|
||||
@@ -520,4 +539,59 @@ mod tests {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn querying_for_number_of_pending_events() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
// no events
|
||||
assert_eq!(
|
||||
Ok(NumberOfPendingEventsResponse {
|
||||
epoch_events: 0,
|
||||
interval_events: 0
|
||||
}),
|
||||
query_number_of_pending_events(test.deps())
|
||||
);
|
||||
|
||||
// add epoch event
|
||||
push_dummy_epoch_action(&mut test);
|
||||
assert_eq!(
|
||||
Ok(NumberOfPendingEventsResponse {
|
||||
epoch_events: 1,
|
||||
interval_events: 0
|
||||
}),
|
||||
query_number_of_pending_events(test.deps())
|
||||
);
|
||||
|
||||
// add more epoch events
|
||||
push_n_dummy_epoch_actions(&mut test, 42);
|
||||
assert_eq!(
|
||||
Ok(NumberOfPendingEventsResponse {
|
||||
epoch_events: 43,
|
||||
interval_events: 0
|
||||
}),
|
||||
query_number_of_pending_events(test.deps())
|
||||
);
|
||||
|
||||
// and now for interval...
|
||||
// add interval event
|
||||
push_dummy_interval_action(&mut test);
|
||||
assert_eq!(
|
||||
Ok(NumberOfPendingEventsResponse {
|
||||
epoch_events: 43,
|
||||
interval_events: 1
|
||||
}),
|
||||
query_number_of_pending_events(test.deps())
|
||||
);
|
||||
|
||||
// add more epoch events
|
||||
push_n_dummy_interval_actions(&mut test, 42);
|
||||
assert_eq!(
|
||||
Ok(NumberOfPendingEventsResponse {
|
||||
epoch_events: 43,
|
||||
interval_events: 43
|
||||
}),
|
||||
query_number_of_pending_events(test.deps())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::constants::{
|
||||
CURRENT_INTERVAL_KEY, EPOCH_EVENT_ID_COUNTER_KEY, INTERVAL_EVENT_ID_COUNTER_KEY,
|
||||
LAST_EPOCH_EVENT_ID_KEY, LAST_INTERVAL_EVENT_ID_KEY, PENDING_EPOCH_EVENTS_NAMESPACE,
|
||||
PENDING_INTERVAL_EVENTS_NAMESPACE, REWARDED_SET_KEY,
|
||||
CURRENT_EPOCH_STATUS_KEY, CURRENT_INTERVAL_KEY, EPOCH_EVENT_ID_COUNTER_KEY,
|
||||
INTERVAL_EVENT_ID_COUNTER_KEY, LAST_EPOCH_EVENT_ID_KEY, LAST_INTERVAL_EVENT_ID_KEY,
|
||||
PENDING_EPOCH_EVENTS_NAMESPACE, PENDING_INTERVAL_EVENTS_NAMESPACE, REWARDED_SET_KEY,
|
||||
};
|
||||
use cosmwasm_std::{Env, Order, StdResult, Storage};
|
||||
use cosmwasm_std::{Addr, Env, Order, StdResult, Storage};
|
||||
use cw_storage_plus::{Item, Map};
|
||||
use mixnet_contract_common::pending_events::{
|
||||
PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData,
|
||||
};
|
||||
use mixnet_contract_common::{
|
||||
EpochEventId, Interval, IntervalEventId, MixId, PendingIntervalEventKind, RewardedSetNodeStatus,
|
||||
EpochEventId, EpochStatus, Interval, IntervalEventId, MixId, PendingIntervalEventKind,
|
||||
RewardedSetNodeStatus,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) const CURRENT_EPOCH_STATUS: Item<'_, EpochStatus> = Item::new(CURRENT_EPOCH_STATUS_KEY);
|
||||
pub(crate) const CURRENT_INTERVAL: Item<'_, Interval> = Item::new(CURRENT_INTERVAL_KEY);
|
||||
pub(crate) const REWARDED_SET: Map<MixId, RewardedSetNodeStatus> = Map::new(REWARDED_SET_KEY);
|
||||
|
||||
@@ -39,6 +41,17 @@ pub(crate) const PENDING_EPOCH_EVENTS: Map<EpochEventId, PendingEpochEventData>
|
||||
pub(crate) const PENDING_INTERVAL_EVENTS: Map<IntervalEventId, PendingIntervalEventData> =
|
||||
Map::new(PENDING_INTERVAL_EVENTS_NAMESPACE);
|
||||
|
||||
pub(crate) fn current_epoch_status(storage: &dyn Storage) -> StdResult<EpochStatus> {
|
||||
CURRENT_EPOCH_STATUS.load(storage)
|
||||
}
|
||||
|
||||
pub(crate) fn save_current_epoch_status(
|
||||
storage: &mut dyn Storage,
|
||||
status: &EpochStatus,
|
||||
) -> StdResult<()> {
|
||||
CURRENT_EPOCH_STATUS.save(storage, status)
|
||||
}
|
||||
|
||||
pub(crate) fn current_interval(storage: &dyn Storage) -> StdResult<Interval> {
|
||||
CURRENT_INTERVAL.load(storage)
|
||||
}
|
||||
@@ -69,6 +82,12 @@ pub(crate) fn push_new_epoch_event(
|
||||
env: &Env,
|
||||
event: PendingEpochEventKind,
|
||||
) -> StdResult<()> {
|
||||
// not included in non-test code as it messes with our return types as we expected `StdResult`
|
||||
// from all storage-related operations.
|
||||
// However, the callers MUST HAVE ensured the below invariant
|
||||
#[cfg(test)]
|
||||
crate::support::helpers::ensure_epoch_in_progress_state(storage).unwrap();
|
||||
|
||||
let event_id = next_epoch_event_id_counter(storage)?;
|
||||
let event_data = event.attach_source_height(env.block.height);
|
||||
PENDING_EPOCH_EVENTS.save(storage, event_id, &event_data)
|
||||
@@ -79,6 +98,12 @@ pub(crate) fn push_new_interval_event(
|
||||
env: &Env,
|
||||
event: PendingIntervalEventKind,
|
||||
) -> StdResult<()> {
|
||||
// not included in non-test code as it messes with our return types as we expected `StdResult`
|
||||
// from all storage-related operations.
|
||||
// However, the callers MUST HAVE ensured the below invariant
|
||||
#[cfg(test)]
|
||||
crate::support::helpers::ensure_epoch_in_progress_state(storage).unwrap();
|
||||
|
||||
let event_id = next_interval_event_id_counter(storage)?;
|
||||
let event_data = event.attach_source_height(env.block.height);
|
||||
PENDING_INTERVAL_EVENTS.save(storage, event_id, &event_data)
|
||||
@@ -130,12 +155,14 @@ pub(crate) fn update_rewarded_set(
|
||||
pub(crate) fn initialise_storage(
|
||||
storage: &mut dyn Storage,
|
||||
starting_interval: Interval,
|
||||
rewarding_validator: Addr,
|
||||
) -> StdResult<()> {
|
||||
save_interval(storage, &starting_interval)?;
|
||||
LAST_PROCESSED_EPOCH_EVENT.save(storage, &0)?;
|
||||
LAST_PROCESSED_INTERVAL_EVENT.save(storage, &0)?;
|
||||
EPOCH_EVENT_ID_COUNTER.save(storage, &0)?;
|
||||
INTERVAL_EVENT_ID_COUNTER.save(storage, &0)
|
||||
INTERVAL_EVENT_ID_COUNTER.save(storage, &0)?;
|
||||
CURRENT_EPOCH_STATUS.save(storage, &EpochStatus::new(rewarding_validator))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::storage;
|
||||
@@ -8,16 +8,18 @@ use crate::interval::storage::push_new_interval_event;
|
||||
use crate::mixnodes::transactions::update_mixnode_layer;
|
||||
use crate::rewards;
|
||||
use crate::rewards::storage as rewards_storage;
|
||||
use crate::support::helpers::{ensure_is_authorized, ensure_is_owner};
|
||||
use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Storage};
|
||||
use crate::support::helpers::{
|
||||
ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_authorized, ensure_is_owner,
|
||||
};
|
||||
use cosmwasm_std::{DepsMut, Env, MessageInfo, Order, Response, Storage};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::events::{
|
||||
new_advance_epoch_event, new_pending_epoch_events_execution_event,
|
||||
new_pending_interval_config_update_event, new_pending_interval_events_execution_event,
|
||||
new_reconcile_pending_events,
|
||||
new_advance_epoch_event, new_epoch_transition_start_event,
|
||||
new_pending_epoch_events_execution_event, new_pending_interval_config_update_event,
|
||||
new_pending_interval_events_execution_event, new_reconcile_pending_events,
|
||||
};
|
||||
use mixnet_contract_common::pending_events::PendingIntervalEventKind;
|
||||
use mixnet_contract_common::{LayerAssignment, MixId};
|
||||
use mixnet_contract_common::{EpochState, EpochStatus, LayerAssignment, MixId};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
// those two should be called in separate tx (from advancing epoch),
|
||||
@@ -121,7 +123,8 @@ pub fn try_reconcile_epoch_events(
|
||||
// - somebody sneaks in some extra delegations
|
||||
// - the same person decides to pay the transaction fees and reconcile epoch events themselves
|
||||
// - the validator API distributes the rewards -> this new sneaky delegation is now included in reward calculation!
|
||||
ensure_is_authorized(info.sender, deps.storage)?;
|
||||
let mut current_epoch_status = ensure_can_advance_epoch(&info.sender, deps.storage)?;
|
||||
current_epoch_status.ensure_is_in_event_reconciliation_state()?;
|
||||
|
||||
let mut response = Response::new().add_event(new_reconcile_pending_events());
|
||||
|
||||
@@ -158,6 +161,24 @@ pub fn try_reconcile_epoch_events(
|
||||
.push(new_pending_interval_events_execution_event(executed));
|
||||
}
|
||||
|
||||
// if there are no more events to clear, go into the next state
|
||||
let pending_events = super::queries::query_number_of_pending_events(deps.as_ref())?;
|
||||
// we can only progress if there are no epoch events AND if the interval has finished, that there are no interval events
|
||||
let progress = if pending_events.epoch_events == 0 {
|
||||
if interval.is_current_interval_over(&env) {
|
||||
pending_events.interval_events == 0
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if progress {
|
||||
current_epoch_status.state = EpochState::AdvancingEpoch;
|
||||
storage::save_current_epoch_status(deps.storage, ¤t_epoch_status)?;
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -199,17 +220,66 @@ fn update_rewarded_set(
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn try_begin_epoch_transition(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
// Only the rewarding validator(s) can attempt to advance epoch
|
||||
ensure_is_authorized(&info.sender, deps.storage)?;
|
||||
|
||||
// can't do pre-mature epoch transition...
|
||||
let current_interval = storage::current_interval(deps.storage)?;
|
||||
if !current_interval.is_current_epoch_over(&env) {
|
||||
return Err(MixnetContractError::EpochInProgress {
|
||||
current_block_time: env.block.time.seconds(),
|
||||
epoch_start: current_interval.current_epoch_start_unix_timestamp(),
|
||||
epoch_end: current_interval.current_epoch_end_unix_timestamp(),
|
||||
});
|
||||
}
|
||||
|
||||
// ensure some other validator (currently not a problem), hasn't already committed to epoch progression
|
||||
ensure_epoch_in_progress_state(deps.storage)?;
|
||||
|
||||
// Note: if at any point we decide to change our rewarded set to be few thousand nodes
|
||||
// and the below call fails, we'll have to pass `last_node_in_rewarded_set` as an argument to this function
|
||||
// and then verify whether the provided value is valid (by using range iterator on `REWARDED_SET`
|
||||
// and checking if there are any other entries following the provided value)
|
||||
let rewarded_set = storage::REWARDED_SET
|
||||
.range(deps.storage, None, None, Order::Ascending)
|
||||
.map(|kv| kv.map(|kv| kv.0))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// if there are no nodes to reward (i.e. empty rewarded set), we go straight into event reconciliation
|
||||
let new_epoch_state = if let Some(last) = rewarded_set.last() {
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: *last,
|
||||
}
|
||||
} else {
|
||||
EpochState::ReconcilingEvents
|
||||
};
|
||||
|
||||
// progress into the first stage of epoch progression
|
||||
let new_epoch_status = EpochStatus {
|
||||
being_advanced_by: info.sender,
|
||||
state: new_epoch_state,
|
||||
};
|
||||
|
||||
storage::save_current_epoch_status(deps.storage, &new_epoch_status)?;
|
||||
Ok(Response::new().add_event(new_epoch_transition_start_event(current_interval)))
|
||||
}
|
||||
|
||||
pub fn try_advance_epoch(
|
||||
mut deps: DepsMut<'_>,
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
layer_assignments: Vec<LayerAssignment>,
|
||||
expected_active_set_size: u32,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
// Only rewarding validator can attempt to advance epoch
|
||||
ensure_is_authorized(info.sender, deps.storage)?;
|
||||
|
||||
let mut response = Response::new();
|
||||
let mut current_epoch_status = ensure_can_advance_epoch(&info.sender, deps.storage)?;
|
||||
current_epoch_status.ensure_is_in_advancement_state()?;
|
||||
|
||||
// we must make sure that we roll into new epoch / interval with up to date state
|
||||
// with no pending actions (like somebody wanting to update their profit margin)
|
||||
@@ -220,29 +290,10 @@ pub fn try_advance_epoch(
|
||||
epoch_start: current_interval.current_epoch_start_unix_timestamp(),
|
||||
epoch_end: current_interval.current_epoch_end_unix_timestamp(),
|
||||
});
|
||||
} else {
|
||||
let (mut sub_response, executed) =
|
||||
perform_pending_epoch_actions(deps.branch(), &env, None)?;
|
||||
response.messages.append(&mut sub_response.messages);
|
||||
response.attributes.append(&mut sub_response.attributes);
|
||||
response.events.append(&mut sub_response.events);
|
||||
response
|
||||
.events
|
||||
.push(new_pending_epoch_events_execution_event(executed));
|
||||
}
|
||||
|
||||
// first clear epoch events queue and then touch the interval actions
|
||||
// if the current interval is over, apply reward pool changes
|
||||
if current_interval.is_current_interval_over(&env) {
|
||||
// the interval has finished -> we can change things such as the profit margin
|
||||
let (mut sub_response, executed) =
|
||||
perform_pending_interval_actions(deps.branch(), &env, None)?;
|
||||
response.messages.append(&mut sub_response.messages);
|
||||
response.attributes.append(&mut sub_response.attributes);
|
||||
response.events.append(&mut sub_response.events);
|
||||
response
|
||||
.events
|
||||
.push(new_pending_interval_events_execution_event(executed));
|
||||
|
||||
// this one is a very important one!
|
||||
rewards::helpers::apply_reward_pool_changes(deps.storage)?;
|
||||
}
|
||||
@@ -260,7 +311,10 @@ pub fn try_advance_epoch(
|
||||
update_mixnode_layer(a.mix_id(), a.layer(), deps.storage)?;
|
||||
}
|
||||
|
||||
Ok(response.add_event(new_advance_epoch_event(updated_interval, num_nodes as u32)))
|
||||
current_epoch_status.state = EpochState::InProgress;
|
||||
storage::save_current_epoch_status(deps.storage, ¤t_epoch_status)?;
|
||||
|
||||
Ok(Response::new().add_event(new_advance_epoch_event(updated_interval, num_nodes as u32)))
|
||||
}
|
||||
|
||||
pub(crate) fn try_update_interval_config(
|
||||
@@ -283,6 +337,10 @@ pub(crate) fn try_update_interval_config(
|
||||
epoch_duration_secs,
|
||||
)
|
||||
} else {
|
||||
// changing interval config is only allowed if the epoch is currently not in the process of being advanced
|
||||
// (unless the force flag was used)
|
||||
ensure_epoch_in_progress_state(deps.storage)?;
|
||||
|
||||
// push the interval event
|
||||
let interval_event = PendingIntervalEventKind::UpdateIntervalConfig {
|
||||
epochs_in_interval,
|
||||
@@ -804,6 +862,136 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod beginning_epoch_transition {
|
||||
use super::*;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
|
||||
#[test]
|
||||
fn returns_error_if_epoch_is_in_progress() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
let res = try_begin_epoch_transition(test.deps_mut(), env, rewarding_validator);
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochInProgress { .. })
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_if_in_progress_state() {
|
||||
let bad_states = vec![
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 0,
|
||||
},
|
||||
EpochState::ReconcilingEvents,
|
||||
EpochState::AdvancingEpoch,
|
||||
];
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap();
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
let env = test.env();
|
||||
|
||||
let res = try_begin_epoch_transition(test.deps_mut(), env, rewarding_validator);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochAdvancementInProgress {
|
||||
current_state: bad_state
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_error_if_not_performed_by_the_rewarding_validator() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
|
||||
let random = mock_info("alice", &[]);
|
||||
let owner = test.owner();
|
||||
|
||||
let res = try_begin_epoch_transition(test.deps_mut(), env.clone(), random);
|
||||
assert!(matches!(res, Err(MixnetContractError::Unauthorized)));
|
||||
|
||||
let res = try_begin_epoch_transition(test.deps_mut(), env, owner);
|
||||
assert!(matches!(res, Err(MixnetContractError::Unauthorized)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_error_if_epoch_is_already_being_advanced() {
|
||||
let mut test = TestSetup::new();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
let env = test.env();
|
||||
|
||||
try_begin_epoch_transition(test.deps_mut(), env.clone(), rewarding_validator.clone())
|
||||
.unwrap();
|
||||
|
||||
let res = try_begin_epoch_transition(test.deps_mut(), env, rewarding_validator);
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochAdvancementInProgress { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn epoch_state_is_correctly_updated_for_empty_rewarded_set() {
|
||||
let mut test = TestSetup::new();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
let env = test.env();
|
||||
|
||||
try_begin_epoch_transition(test.deps_mut(), env, rewarding_validator).unwrap();
|
||||
|
||||
let expected = EpochStatus {
|
||||
being_advanced_by: test.rewarding_validator().sender,
|
||||
state: EpochState::ReconcilingEvents,
|
||||
};
|
||||
assert_eq!(
|
||||
expected,
|
||||
storage::current_epoch_status(test.deps().storage).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn epoch_state_is_correctly_updated_for_nonempty_rewarded_set() {
|
||||
let mut test = TestSetup::new();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
test.force_change_rewarded_set(vec![1, 2, 3, 4, 5]);
|
||||
test.skip_to_current_epoch_end();
|
||||
let env = test.env();
|
||||
|
||||
try_begin_epoch_transition(test.deps_mut(), env, rewarding_validator).unwrap();
|
||||
|
||||
let expected = EpochStatus {
|
||||
being_advanced_by: test.rewarding_validator().sender,
|
||||
state: EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 5,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
expected,
|
||||
storage::current_epoch_status(test.deps().storage).unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod reconciling_epoch_events {
|
||||
use super::*;
|
||||
@@ -816,12 +1004,140 @@ mod tests {
|
||||
use mixnet_contract_common::pending_events::PendingEpochEventKind;
|
||||
use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate;
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_if_in_reconciling_state() {
|
||||
let bad_states = vec![
|
||||
EpochState::InProgress,
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 0,
|
||||
},
|
||||
EpochState::AdvancingEpoch,
|
||||
];
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap();
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
let env = test.env();
|
||||
|
||||
let res =
|
||||
try_reconcile_epoch_events(test.deps_mut(), env, rewarding_validator, None);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochNotInEventReconciliationState {
|
||||
current_state: bad_state
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn epoch_state_is_correctly_updated_if_there_are_no_events() {
|
||||
let mut test = TestSetup::new();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
test.set_epoch_reconciliation_state();
|
||||
let env = test.env();
|
||||
|
||||
try_reconcile_epoch_events(test.deps_mut(), env, rewarding_validator, None).unwrap();
|
||||
|
||||
let expected = EpochStatus {
|
||||
being_advanced_by: test.rewarding_validator().sender,
|
||||
state: EpochState::AdvancingEpoch,
|
||||
};
|
||||
assert_eq!(
|
||||
expected,
|
||||
storage::current_epoch_status(test.deps().storage).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn epoch_state_is_not_updated_if_some_events_are_cleared() {
|
||||
let mut test = TestSetup::new();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
let env = test.env();
|
||||
|
||||
push_n_dummy_epoch_actions(&mut test, 10);
|
||||
push_n_dummy_interval_actions(&mut test, 10);
|
||||
test.set_epoch_reconciliation_state();
|
||||
|
||||
try_reconcile_epoch_events(test.deps_mut(), env, rewarding_validator, Some(5)).unwrap();
|
||||
|
||||
let expected = EpochStatus {
|
||||
being_advanced_by: test.rewarding_validator().sender,
|
||||
state: EpochState::ReconcilingEvents,
|
||||
};
|
||||
assert_eq!(
|
||||
expected,
|
||||
storage::current_epoch_status(test.deps().storage).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn epoch_state_is_correctly_updated_if_even_with_leftover_interval_events_if_interval_is_not_over(
|
||||
) {
|
||||
let mut test = TestSetup::new();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
let env = test.env();
|
||||
|
||||
push_n_dummy_epoch_actions(&mut test, 10);
|
||||
push_n_dummy_interval_actions(&mut test, 10);
|
||||
test.set_epoch_reconciliation_state();
|
||||
|
||||
try_reconcile_epoch_events(test.deps_mut(), env, rewarding_validator, None).unwrap();
|
||||
|
||||
let expected = EpochStatus {
|
||||
being_advanced_by: test.rewarding_validator().sender,
|
||||
state: EpochState::AdvancingEpoch,
|
||||
};
|
||||
assert_eq!(
|
||||
expected,
|
||||
storage::current_epoch_status(test.deps().storage).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn epoch_state_is_correctly_updated_if_all_events_are_cleared() {
|
||||
let mut test = TestSetup::new();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
test.skip_to_current_interval_end();
|
||||
let env = test.env();
|
||||
|
||||
push_n_dummy_epoch_actions(&mut test, 10);
|
||||
push_n_dummy_interval_actions(&mut test, 10);
|
||||
test.set_epoch_reconciliation_state();
|
||||
|
||||
try_reconcile_epoch_events(test.deps_mut(), env, rewarding_validator, None).unwrap();
|
||||
|
||||
let expected = EpochStatus {
|
||||
being_advanced_by: test.rewarding_validator().sender,
|
||||
state: EpochState::AdvancingEpoch,
|
||||
};
|
||||
assert_eq!(
|
||||
expected,
|
||||
storage::current_epoch_status(test.deps().storage).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_error_if_epoch_is_in_progress() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
test.set_epoch_reconciliation_state();
|
||||
let res = try_reconcile_epoch_events(test.deps_mut(), env, rewarding_validator, None);
|
||||
assert!(matches!(
|
||||
res,
|
||||
@@ -835,6 +1151,7 @@ mod tests {
|
||||
let env = test.env();
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
test.set_epoch_reconciliation_state();
|
||||
|
||||
let random = mock_info("alice", &[]);
|
||||
let owner = test.owner();
|
||||
@@ -853,6 +1170,7 @@ mod tests {
|
||||
push_n_dummy_epoch_actions(&mut test, 10);
|
||||
push_n_dummy_interval_actions(&mut test, 10);
|
||||
test.skip_to_current_epoch_end();
|
||||
test.set_epoch_reconciliation_state();
|
||||
|
||||
let env = test.env();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
@@ -872,6 +1190,7 @@ mod tests {
|
||||
push_n_dummy_epoch_actions(&mut test, 10);
|
||||
push_n_dummy_interval_actions(&mut test, 10);
|
||||
test.skip_to_current_interval_end();
|
||||
test.set_epoch_reconciliation_state();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
let env = test.env();
|
||||
@@ -900,6 +1219,7 @@ mod tests {
|
||||
// all test cases are using the same one
|
||||
let rewarding_validator = test1.rewarding_validator();
|
||||
|
||||
test1.set_epoch_reconciliation_state();
|
||||
try_reconcile_epoch_events(
|
||||
test1.deps_mut(),
|
||||
env.clone(),
|
||||
@@ -948,6 +1268,7 @@ mod tests {
|
||||
assert!(epoch_events.is_empty());
|
||||
assert!(interval_events.is_empty());
|
||||
|
||||
test2.set_epoch_reconciliation_state();
|
||||
try_reconcile_epoch_events(
|
||||
test2.deps_mut(),
|
||||
env.clone(),
|
||||
@@ -972,6 +1293,7 @@ mod tests {
|
||||
assert!(epoch_events.is_empty());
|
||||
assert!(interval_events.is_empty());
|
||||
|
||||
test3.set_epoch_reconciliation_state();
|
||||
try_reconcile_epoch_events(
|
||||
test3.deps_mut(),
|
||||
env.clone(),
|
||||
@@ -984,6 +1306,7 @@ mod tests {
|
||||
assert!(epoch_events.is_empty());
|
||||
assert!(interval_events.is_empty());
|
||||
|
||||
test4.set_epoch_reconciliation_state();
|
||||
try_reconcile_epoch_events(test4.deps_mut(), env, rewarding_validator, Some(100))
|
||||
.unwrap();
|
||||
let epoch_events = test4.pending_epoch_events();
|
||||
@@ -1040,6 +1363,7 @@ mod tests {
|
||||
expected_events.push(new_pending_interval_events_execution_event(1));
|
||||
|
||||
test.skip_to_current_interval_end();
|
||||
test.set_epoch_reconciliation_state();
|
||||
let env = test.env();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
@@ -1142,15 +1466,97 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::mixnodes::queries::query_mixnode_details;
|
||||
use crate::rewards::models::RewardPoolChange;
|
||||
use crate::support::tests::fixtures::TEST_COIN_DENOM;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::{coin, coins, BankMsg, Decimal, Empty, SubMsg, Uint128};
|
||||
use mixnet_contract_common::events::{
|
||||
new_delegation_on_unbonded_node_event, new_rewarding_params_update_event,
|
||||
};
|
||||
use cosmwasm_std::{Decimal, Uint128};
|
||||
use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate;
|
||||
use mixnet_contract_common::{Layer, RewardedSetNodeStatus};
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_if_in_advancing_epoch_state() {
|
||||
let bad_states = vec![
|
||||
EpochState::InProgress,
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 0,
|
||||
},
|
||||
EpochState::ReconcilingEvents,
|
||||
];
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
test.add_dummy_mixnode("1", Some(Uint128::new(100000000)));
|
||||
test.add_dummy_mixnode("2", Some(Uint128::new(100000000)));
|
||||
test.add_dummy_mixnode("3", Some(Uint128::new(100000000)));
|
||||
let current_active_set = test.rewarding_params().active_set_size;
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap();
|
||||
|
||||
let layer_assignments = vec![
|
||||
LayerAssignment::new(1, Layer::One),
|
||||
LayerAssignment::new(2, Layer::Two),
|
||||
LayerAssignment::new(3, Layer::Three),
|
||||
];
|
||||
|
||||
let env = test.env();
|
||||
let sender = test.rewarding_validator();
|
||||
let res = try_advance_epoch(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
sender,
|
||||
layer_assignments,
|
||||
current_active_set,
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochNotInAdvancementState {
|
||||
current_state: bad_state
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn epoch_state_is_correctly_updated() {
|
||||
let mut test = TestSetup::new();
|
||||
test.add_dummy_mixnode("1", Some(Uint128::new(100000000)));
|
||||
test.add_dummy_mixnode("2", Some(Uint128::new(100000000)));
|
||||
test.add_dummy_mixnode("3", Some(Uint128::new(100000000)));
|
||||
let current_active_set = test.rewarding_params().active_set_size;
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
test.set_epoch_advancement_state();
|
||||
|
||||
let layer_assignments = vec![
|
||||
LayerAssignment::new(1, Layer::One),
|
||||
LayerAssignment::new(2, Layer::Two),
|
||||
LayerAssignment::new(3, Layer::Three),
|
||||
];
|
||||
|
||||
let env = test.env();
|
||||
let sender = test.rewarding_validator();
|
||||
try_advance_epoch(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
sender,
|
||||
layer_assignments,
|
||||
current_active_set,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let expected = EpochStatus {
|
||||
being_advanced_by: test.rewarding_validator().sender,
|
||||
state: EpochState::InProgress,
|
||||
};
|
||||
assert_eq!(
|
||||
expected,
|
||||
storage::current_epoch_status(test.deps().storage).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_by_specified_rewarding_validator() {
|
||||
let mut test = TestSetup::new();
|
||||
@@ -1161,6 +1567,7 @@ mod tests {
|
||||
let some_sender = mock_info("foomper", &[]);
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
test.set_epoch_advancement_state();
|
||||
|
||||
let layer_assignments = vec![
|
||||
LayerAssignment::new(1, Layer::One),
|
||||
@@ -1188,13 +1595,14 @@ mod tests {
|
||||
layer_assignments,
|
||||
current_active_set,
|
||||
);
|
||||
println!("{:?}", res);
|
||||
assert!(res.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_if_epoch_is_over() {
|
||||
let mut test = TestSetup::new();
|
||||
test.set_epoch_advancement_state();
|
||||
|
||||
let current_active_set = test.rewarding_params().active_set_size;
|
||||
|
||||
test.add_dummy_mixnode("1", Some(Uint128::new(100000000)));
|
||||
@@ -1241,6 +1649,7 @@ mod tests {
|
||||
|
||||
// sanity check
|
||||
test.skip_to_current_epoch_end();
|
||||
|
||||
let env = test.env();
|
||||
let res = try_advance_epoch(
|
||||
test.deps_mut(),
|
||||
@@ -1252,171 +1661,11 @@ mod tests {
|
||||
assert!(res.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_clears_epoch_events_if_interval_is_in_progress() {
|
||||
let mut test = TestSetup::new();
|
||||
let current_active_set = test.rewarding_params().active_set_size;
|
||||
|
||||
test.add_dummy_mixnode("1", Some(Uint128::new(100000000)));
|
||||
test.add_dummy_mixnode("2", Some(Uint128::new(100000000)));
|
||||
test.add_dummy_mixnode("3", Some(Uint128::new(100000000)));
|
||||
|
||||
push_n_dummy_epoch_actions(&mut test, 10);
|
||||
push_n_dummy_interval_actions(&mut test, 10);
|
||||
test.skip_to_current_epoch_end();
|
||||
|
||||
let layer_assignments = vec![
|
||||
LayerAssignment::new(1, Layer::One),
|
||||
LayerAssignment::new(2, Layer::Two),
|
||||
LayerAssignment::new(3, Layer::Three),
|
||||
];
|
||||
|
||||
let env = test.env();
|
||||
let sender = test.rewarding_validator();
|
||||
try_advance_epoch(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
sender,
|
||||
layer_assignments,
|
||||
current_active_set,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let epoch_events = test.pending_epoch_events();
|
||||
let interval_events = test.pending_interval_events();
|
||||
assert!(epoch_events.is_empty());
|
||||
assert_eq!(interval_events.len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clears_both_epoch_and_interval_events_if_interval_has_finished() {
|
||||
let mut test = TestSetup::new();
|
||||
let current_active_set = test.rewarding_params().active_set_size;
|
||||
|
||||
test.add_dummy_mixnode("1", Some(Uint128::new(100000000)));
|
||||
test.add_dummy_mixnode("2", Some(Uint128::new(100000000)));
|
||||
test.add_dummy_mixnode("3", Some(Uint128::new(100000000)));
|
||||
|
||||
push_n_dummy_epoch_actions(&mut test, 10);
|
||||
push_n_dummy_interval_actions(&mut test, 10);
|
||||
test.skip_to_current_interval_end();
|
||||
|
||||
let layer_assignments = vec![
|
||||
LayerAssignment::new(1, Layer::One),
|
||||
LayerAssignment::new(2, Layer::Two),
|
||||
LayerAssignment::new(3, Layer::Three),
|
||||
];
|
||||
|
||||
let env = test.env();
|
||||
let sender = test.rewarding_validator();
|
||||
try_advance_epoch(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
sender,
|
||||
layer_assignments,
|
||||
current_active_set,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let epoch_events = test.pending_epoch_events();
|
||||
let interval_events = test.pending_interval_events();
|
||||
assert!(epoch_events.is_empty());
|
||||
assert!(interval_events.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_executes_any_events_it_propagates_responses() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
let current_active_set = test.rewarding_params().active_set_size;
|
||||
|
||||
test.add_dummy_mixnode("1", Some(Uint128::new(100000000)));
|
||||
test.add_dummy_mixnode("2", Some(Uint128::new(100000000)));
|
||||
test.add_dummy_mixnode("3", Some(Uint128::new(100000000)));
|
||||
|
||||
let mut expected_events = Vec::new();
|
||||
let mut expected_messages: Vec<SubMsg<Empty>> = Vec::new();
|
||||
|
||||
let non_existent_delegation = PendingEpochEventKind::Delegate {
|
||||
owner: Addr::unchecked("foomp"),
|
||||
mix_id: 123,
|
||||
amount: coin(123, TEST_COIN_DENOM),
|
||||
proxy: None,
|
||||
};
|
||||
storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation)
|
||||
.unwrap();
|
||||
expected_events.push(new_delegation_on_unbonded_node_event(
|
||||
&Addr::unchecked("foomp"),
|
||||
&None,
|
||||
123,
|
||||
));
|
||||
expected_messages.push(SubMsg::new(BankMsg::Send {
|
||||
to_address: "foomp".to_string(),
|
||||
amount: coins(123, TEST_COIN_DENOM),
|
||||
}));
|
||||
expected_events.push(new_pending_epoch_events_execution_event(1));
|
||||
|
||||
// interval event
|
||||
let update = IntervalRewardingParamsUpdate {
|
||||
rewarded_set_size: Some(500),
|
||||
..Default::default()
|
||||
};
|
||||
let change_params = PendingIntervalEventKind::UpdateRewardingParams { update };
|
||||
storage::push_new_interval_event(test.deps_mut().storage, &env, change_params).unwrap();
|
||||
let interval = test.current_interval();
|
||||
let mut expected_updated = test.rewarding_params();
|
||||
expected_updated
|
||||
.try_apply_updates(update, interval.epochs_in_interval())
|
||||
.unwrap();
|
||||
expected_events.push(new_rewarding_params_update_event(
|
||||
env.block.height,
|
||||
update,
|
||||
expected_updated.interval,
|
||||
));
|
||||
expected_events.push(new_pending_interval_events_execution_event(1));
|
||||
let current_interval = test.current_interval();
|
||||
let expected = current_interval.advance_epoch();
|
||||
expected_events.push(new_advance_epoch_event(expected, 3));
|
||||
|
||||
test.skip_to_current_interval_end();
|
||||
|
||||
let layer_assignments = vec![
|
||||
LayerAssignment::new(1, Layer::One),
|
||||
LayerAssignment::new(2, Layer::Two),
|
||||
LayerAssignment::new(3, Layer::Three),
|
||||
];
|
||||
|
||||
let env = test.env();
|
||||
let sender = test.rewarding_validator();
|
||||
let res = try_advance_epoch(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
sender,
|
||||
layer_assignments,
|
||||
current_active_set,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut expected = Response::new().add_events(expected_events);
|
||||
expected.messages = expected_messages;
|
||||
assert_eq!(res, expected);
|
||||
assert_eq!(
|
||||
1,
|
||||
storage::LAST_PROCESSED_EPOCH_EVENT
|
||||
.load(test.deps().storage)
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
1,
|
||||
storage::LAST_PROCESSED_INTERVAL_EVENT
|
||||
.load(test.deps().storage)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_interval_is_over_applies_reward_pool_changes() {
|
||||
let mut test = TestSetup::new();
|
||||
test.set_epoch_advancement_state();
|
||||
|
||||
let current_active_set = test.rewarding_params().active_set_size;
|
||||
|
||||
test.add_dummy_mixnode("1", Some(Uint128::new(100000000)));
|
||||
@@ -1446,6 +1695,7 @@ mod tests {
|
||||
// end of epoch - nothing has happened
|
||||
let sender = test.rewarding_validator();
|
||||
test.skip_to_current_epoch_end();
|
||||
|
||||
let env = test.env();
|
||||
try_advance_epoch(
|
||||
test.deps_mut(),
|
||||
@@ -1465,6 +1715,8 @@ mod tests {
|
||||
|
||||
let sender = test.rewarding_validator();
|
||||
test.skip_to_current_interval_end();
|
||||
test.set_epoch_advancement_state();
|
||||
|
||||
let env = test.env();
|
||||
try_advance_epoch(
|
||||
test.deps_mut(),
|
||||
@@ -1497,6 +1749,8 @@ mod tests {
|
||||
#[test]
|
||||
fn updates_rewarded_set_and_interval_data() {
|
||||
let mut test = TestSetup::new();
|
||||
test.set_epoch_advancement_state();
|
||||
|
||||
let current_active_set = test.rewarding_params().active_set_size;
|
||||
|
||||
test.add_dummy_mixnode("1", Some(Uint128::new(100000000)));
|
||||
@@ -1548,6 +1802,52 @@ mod tests {
|
||||
use cosmwasm_std::Decimal;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn cant_be_performed_if_epoch_transition_is_in_progress_unless_forced() {
|
||||
let bad_states = vec![
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 0,
|
||||
},
|
||||
EpochState::ReconcilingEvents,
|
||||
EpochState::AdvancingEpoch,
|
||||
];
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
let owner = test.owner();
|
||||
let env = test.env();
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
|
||||
storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap();
|
||||
|
||||
let res = try_update_interval_config(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
owner.clone(),
|
||||
100,
|
||||
1000,
|
||||
false,
|
||||
);
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochAdvancementInProgress { .. })
|
||||
));
|
||||
|
||||
let res_forced = try_update_interval_config(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
owner,
|
||||
100,
|
||||
1000,
|
||||
true,
|
||||
);
|
||||
assert!(res_forced.is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_only_be_done_by_contract_owner() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
@@ -12,6 +12,7 @@ mod gateways;
|
||||
mod interval;
|
||||
mod mixnet_contract_settings;
|
||||
mod mixnodes;
|
||||
mod queued_migrations;
|
||||
mod rewards;
|
||||
mod support;
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@ use crate::mixnodes::helpers::{
|
||||
get_mixnode_details_by_owner, must_get_mixnode_bond_by_owner, save_new_mixnode,
|
||||
};
|
||||
use crate::support::helpers::{
|
||||
ensure_bonded, ensure_is_authorized, ensure_no_existing_bond, ensure_proxy_match,
|
||||
validate_node_identity_signature, validate_pledge,
|
||||
ensure_bonded, ensure_epoch_in_progress_state, ensure_is_authorized, ensure_no_existing_bond,
|
||||
ensure_proxy_match, ensure_sent_by_vesting_contract, validate_node_identity_signature,
|
||||
validate_pledge,
|
||||
};
|
||||
use cosmwasm_std::{coin, Addr, Coin, DepsMut, Env, MessageInfo, Response, Storage};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
@@ -47,7 +48,7 @@ pub fn assign_mixnode_layer(
|
||||
mix_id: MixId,
|
||||
layer: Layer,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_is_authorized(info.sender, deps.storage)?;
|
||||
ensure_is_authorized(&info.sender, deps.storage)?;
|
||||
|
||||
update_mixnode_layer(mix_id, layer, deps.storage)?;
|
||||
|
||||
@@ -83,6 +84,8 @@ pub fn try_add_mixnode_on_behalf(
|
||||
owner: String,
|
||||
owner_signature: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let proxy = info.sender;
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
_try_add_mixnode(
|
||||
@@ -116,7 +119,7 @@ fn _try_add_mixnode(
|
||||
// if the client has an active bonded mixnode or gateway, don't allow bonding
|
||||
// note that this has to be done explicitly as `UniqueIndex` constraint would not protect us
|
||||
// against attempting to use different node types (i.e. gateways and mixnodes)
|
||||
ensure_no_existing_bond(deps.storage, &owner)?;
|
||||
ensure_no_existing_bond(&owner, deps.storage)?;
|
||||
|
||||
// there's no need to explicitly check whether there already exists mixnode with the same
|
||||
// identity or sphinx keys as this is going to be done implicitly when attempting to save
|
||||
@@ -165,6 +168,8 @@ pub fn try_increase_pledge_on_behalf(
|
||||
info: MessageInfo,
|
||||
owner: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let proxy = info.sender;
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
_try_increase_pledge(deps, env, info.funds, owner, Some(proxy))
|
||||
@@ -181,6 +186,9 @@ pub fn _try_increase_pledge(
|
||||
.ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })?;
|
||||
let mix_id = mix_details.mix_id();
|
||||
|
||||
// increasing pledge is only allowed if the epoch is currently not in the process of being advanced
|
||||
ensure_epoch_in_progress_state(deps.storage)?;
|
||||
|
||||
ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?;
|
||||
ensure_bonded(&mix_details.bond_information)?;
|
||||
|
||||
@@ -205,6 +213,8 @@ pub fn try_remove_mixnode_on_behalf(
|
||||
info: MessageInfo,
|
||||
owner: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let proxy = info.sender;
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
_try_remove_mixnode(deps, env, owner, Some(proxy))
|
||||
@@ -231,6 +241,9 @@ pub(crate) fn _try_remove_mixnode(
|
||||
.ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })?
|
||||
.1;
|
||||
|
||||
// unbonding is only allowed if the epoch is currently not in the process of being advanced
|
||||
ensure_epoch_in_progress_state(deps.storage)?;
|
||||
|
||||
// see if the proxy matches
|
||||
ensure_proxy_match(&proxy, &existing_bond.proxy)?;
|
||||
ensure_bonded(&existing_bond)?;
|
||||
@@ -276,6 +289,8 @@ pub(crate) fn try_update_mixnode_config_on_behalf(
|
||||
new_config: MixNodeConfigUpdate,
|
||||
owner: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
let proxy = info.sender;
|
||||
_try_update_mixnode_config(deps, new_config, owner, Some(proxy))
|
||||
@@ -329,6 +344,8 @@ pub(crate) fn try_update_mixnode_cost_params_on_behalf(
|
||||
new_costs: MixNodeCostParams,
|
||||
owner: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
let proxy = info.sender;
|
||||
_try_update_mixnode_cost_params(deps, env, new_costs, owner, Some(proxy))
|
||||
@@ -344,6 +361,9 @@ pub(crate) fn _try_update_mixnode_cost_params(
|
||||
// see if the node still exists
|
||||
let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?;
|
||||
|
||||
// changing cost params is only allowed if the epoch is currently not in the process of being advanced
|
||||
ensure_epoch_in_progress_state(deps.storage)?;
|
||||
|
||||
ensure_proxy_match(&proxy, &existing_bond.proxy)?;
|
||||
ensure_bonded(&existing_bond)?;
|
||||
|
||||
@@ -371,10 +391,13 @@ pub mod tests {
|
||||
use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge;
|
||||
use crate::mixnodes::helpers::get_mixnode_details_by_id;
|
||||
use crate::support::tests::fixtures::{good_mixnode_pledge, TEST_COIN_DENOM};
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use crate::support::tests::{fixtures, test_helpers};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::{Order, StdResult, Uint128};
|
||||
use mixnet_contract_common::{ExecuteMsg, Layer, LayerDistribution, Percent};
|
||||
use mixnet_contract_common::{
|
||||
EpochState, EpochStatus, ExecuteMsg, Layer, LayerDistribution, Percent,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn mixnode_add() {
|
||||
@@ -497,51 +520,110 @@ pub mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnode_remove() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let env = mock_env();
|
||||
let mut rng = test_helpers::test_rng();
|
||||
fn mixnode_add_with_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let sender = "alice";
|
||||
let info = mock_info(sender, &[]);
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let owner = "alice";
|
||||
let (mixnode, sig, _) = test_helpers::mixnode_with_signature(&mut test.rng, owner);
|
||||
let cost_params = fixtures::mix_node_cost_params_fixture();
|
||||
|
||||
// we are informed that we didn't send enough funds
|
||||
let res = try_add_mixnode_on_behalf(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
mock_info(illegal_proxy.as_ref(), &good_mixnode_pledge()),
|
||||
mixnode,
|
||||
cost_params,
|
||||
owner.to_string(),
|
||||
sig,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_mixnode_cant_be_performed_if_epoch_transition_is_in_progress() {
|
||||
let bad_states = vec![
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 0,
|
||||
},
|
||||
EpochState::ReconcilingEvents,
|
||||
EpochState::AdvancingEpoch,
|
||||
];
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
let owner = "alice";
|
||||
let info = mock_info(owner, &[]);
|
||||
|
||||
test.add_dummy_mixnode(owner, None);
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
interval_storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap();
|
||||
|
||||
let res = try_remove_mixnode(test.deps_mut(), env.clone(), info);
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochAdvancementInProgress { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnode_remove() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let owner = "alice";
|
||||
let info = mock_info(owner, &[]);
|
||||
|
||||
// trying to remove your mixnode fails if you never had one in the first place
|
||||
let res = try_remove_mixnode(deps.as_mut(), env.clone(), info.clone());
|
||||
let res = try_remove_mixnode(test.deps_mut(), env.clone(), info.clone());
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::NoAssociatedMixNodeBond {
|
||||
owner: Addr::unchecked(sender)
|
||||
owner: Addr::unchecked(owner)
|
||||
})
|
||||
);
|
||||
|
||||
let mix_id = test_helpers::add_mixnode(
|
||||
&mut rng,
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
sender,
|
||||
good_mixnode_pledge(),
|
||||
);
|
||||
let mix_id = test.add_dummy_mixnode(owner, None);
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
// attempted to remove on behalf with invalid proxy (current is `None`)
|
||||
let res = try_remove_mixnode_on_behalf(
|
||||
deps.as_mut(),
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
mock_info("proxy", &[]),
|
||||
sender.to_string(),
|
||||
mock_info(vesting_contract.as_ref(), &[]),
|
||||
owner.to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::ProxyMismatch {
|
||||
existing: "None".to_string(),
|
||||
incoming: "proxy".to_string()
|
||||
incoming: vesting_contract.into_string()
|
||||
})
|
||||
);
|
||||
|
||||
// "normal" unbonding succeeds and unbonding event is pushed to the pending epoch events
|
||||
let res = try_remove_mixnode(deps.as_mut(), env.clone(), info.clone());
|
||||
let res = try_remove_mixnode(test.deps_mut(), env.clone(), info.clone());
|
||||
assert!(res.is_ok());
|
||||
let mut pending_events = interval_storage::PENDING_EPOCH_EVENTS
|
||||
.range(deps.as_ref().storage, None, None, Order::Ascending)
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.collect::<StdResult<Vec<_>>>()
|
||||
.unwrap();
|
||||
assert_eq!(pending_events.len(), 1);
|
||||
@@ -553,18 +635,46 @@ pub mod tests {
|
||||
);
|
||||
|
||||
// but fails if repeated (since the node is already in the "unbonding" state)(
|
||||
let res = try_remove_mixnode(deps.as_mut(), env, info);
|
||||
let res = try_remove_mixnode(test.deps_mut(), env, info);
|
||||
assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id }))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updating_mixnode_config() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let env = mock_env();
|
||||
let mut rng = test_helpers::test_rng();
|
||||
fn mixnode_remove_with_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let sender = "alice";
|
||||
let info = mock_info(sender, &[]);
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let owner = "alice";
|
||||
|
||||
test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone());
|
||||
|
||||
let res = try_remove_mixnode_on_behalf(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
mock_info(illegal_proxy.as_ref(), &[]),
|
||||
owner.to_string(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updating_mixnode_config() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let owner = "alice";
|
||||
let info = mock_info(owner, &[]);
|
||||
let update = MixNodeConfigUpdate {
|
||||
host: "1.1.1.1:1234".to_string(),
|
||||
mix_port: 1234,
|
||||
@@ -574,43 +684,38 @@ pub mod tests {
|
||||
};
|
||||
|
||||
// try updating a non existing mixnode bond
|
||||
let res = try_update_mixnode_config(deps.as_mut(), info.clone(), update.clone());
|
||||
let res = try_update_mixnode_config(test.deps_mut(), info.clone(), update.clone());
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::NoAssociatedMixNodeBond {
|
||||
owner: Addr::unchecked(sender)
|
||||
owner: Addr::unchecked(owner)
|
||||
})
|
||||
);
|
||||
|
||||
let mix_id = test_helpers::add_mixnode(
|
||||
&mut rng,
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
sender,
|
||||
tests::fixtures::good_mixnode_pledge(),
|
||||
);
|
||||
let mix_id = test.add_dummy_mixnode(owner, None);
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
// attempted to remove on behalf with invalid proxy (current is `None`)
|
||||
let res = try_update_mixnode_config_on_behalf(
|
||||
deps.as_mut(),
|
||||
mock_info("proxy", &[]),
|
||||
test.deps_mut(),
|
||||
mock_info(vesting_contract.as_ref(), &[]),
|
||||
update.clone(),
|
||||
sender.to_string(),
|
||||
owner.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::ProxyMismatch {
|
||||
existing: "None".to_string(),
|
||||
incoming: "proxy".to_string()
|
||||
incoming: vesting_contract.into_string()
|
||||
})
|
||||
);
|
||||
// "normal" update succeeds
|
||||
let res = try_update_mixnode_config(deps.as_mut(), info.clone(), update.clone());
|
||||
let res = try_update_mixnode_config(test.deps_mut(), info.clone(), update.clone());
|
||||
assert!(res.is_ok());
|
||||
|
||||
// and the config has actually been updated
|
||||
let mix = must_get_mixnode_bond_by_owner(deps.as_ref().storage, &Addr::unchecked(sender))
|
||||
.unwrap();
|
||||
let mix =
|
||||
must_get_mixnode_bond_by_owner(test.deps().storage, &Addr::unchecked(owner)).unwrap();
|
||||
assert_eq!(mix.mix_node.host, update.host);
|
||||
assert_eq!(mix.mix_node.mix_port, update.mix_port);
|
||||
assert_eq!(mix.mix_node.verloc_port, update.verloc_port);
|
||||
@@ -618,19 +723,90 @@ pub mod tests {
|
||||
assert_eq!(mix.mix_node.version, update.version);
|
||||
|
||||
// but we cannot perform any updates whilst the mixnode is already unbonding
|
||||
try_remove_mixnode(deps.as_mut(), env, info.clone()).unwrap();
|
||||
let res = try_update_mixnode_config(deps.as_mut(), info, update);
|
||||
try_remove_mixnode(test.deps_mut(), env, info.clone()).unwrap();
|
||||
let res = try_update_mixnode_config(test.deps_mut(), info, update);
|
||||
assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id }))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updating_mixnode_cost_params() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let env = mock_env();
|
||||
let mut rng = test_helpers::test_rng();
|
||||
fn updating_mixnode_config_with_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let sender = "alice";
|
||||
let info = mock_info(sender, &[]);
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let owner = "alice";
|
||||
|
||||
test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone());
|
||||
let update = MixNodeConfigUpdate {
|
||||
host: "1.1.1.1:1234".to_string(),
|
||||
mix_port: 1234,
|
||||
verloc_port: 1235,
|
||||
http_api_port: 1236,
|
||||
version: "v1.2.3".to_string(),
|
||||
};
|
||||
|
||||
let res = try_update_mixnode_config_on_behalf(
|
||||
test.deps_mut(),
|
||||
mock_info(illegal_proxy.as_ref(), &[]),
|
||||
update,
|
||||
owner.to_string(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnode_cost_params_cant_be_updated_when_epoch_transition_is_in_progress() {
|
||||
let bad_states = vec![
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 0,
|
||||
},
|
||||
EpochState::ReconcilingEvents,
|
||||
EpochState::AdvancingEpoch,
|
||||
];
|
||||
|
||||
let update = MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(42).unwrap(),
|
||||
interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM),
|
||||
};
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
let owner = "alice";
|
||||
let info = mock_info(owner, &[]);
|
||||
|
||||
test.add_dummy_mixnode(owner, None);
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
interval_storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap();
|
||||
|
||||
let res =
|
||||
try_update_mixnode_cost_params(test.deps_mut(), env.clone(), info, update.clone());
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochAdvancementInProgress { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updating_mixnode_cost_params() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let owner = "alice";
|
||||
let info = mock_info(owner, &[]);
|
||||
let update = MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(42).unwrap(),
|
||||
interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM),
|
||||
@@ -638,7 +814,7 @@ pub mod tests {
|
||||
|
||||
// try updating a non existing mixnode bond
|
||||
let res = try_update_mixnode_cost_params(
|
||||
deps.as_mut(),
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
info.clone(),
|
||||
update.clone(),
|
||||
@@ -646,36 +822,31 @@ pub mod tests {
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::NoAssociatedMixNodeBond {
|
||||
owner: Addr::unchecked(sender)
|
||||
owner: Addr::unchecked(owner)
|
||||
})
|
||||
);
|
||||
|
||||
let mix_id = test_helpers::add_mixnode(
|
||||
&mut rng,
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
sender,
|
||||
tests::fixtures::good_mixnode_pledge(),
|
||||
);
|
||||
let mix_id = test.add_dummy_mixnode(owner, None);
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
// attempted to remove on behalf with invalid proxy (current is `None`)
|
||||
let res = try_update_mixnode_cost_params_on_behalf(
|
||||
deps.as_mut(),
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
mock_info("proxy", &[]),
|
||||
mock_info(vesting_contract.as_ref(), &[]),
|
||||
update.clone(),
|
||||
sender.to_string(),
|
||||
owner.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::ProxyMismatch {
|
||||
existing: "None".to_string(),
|
||||
incoming: "proxy".to_string()
|
||||
incoming: vesting_contract.into_string()
|
||||
})
|
||||
);
|
||||
// "normal" update succeeds
|
||||
let res = try_update_mixnode_cost_params(
|
||||
deps.as_mut(),
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
info.clone(),
|
||||
update.clone(),
|
||||
@@ -684,7 +855,7 @@ pub mod tests {
|
||||
|
||||
// see if the event has been pushed onto the queue
|
||||
let mut pending_events = interval_storage::PENDING_INTERVAL_EVENTS
|
||||
.range(deps.as_ref().storage, None, None, Order::Ascending)
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.collect::<StdResult<Vec<_>>>()
|
||||
.unwrap();
|
||||
assert_eq!(pending_events.len(), 1);
|
||||
@@ -699,20 +870,54 @@ pub mod tests {
|
||||
);
|
||||
|
||||
// execute the event
|
||||
test_helpers::execute_all_pending_events(deps.as_mut(), env.clone());
|
||||
test_helpers::execute_all_pending_events(test.deps_mut(), env.clone());
|
||||
|
||||
// and see if the config has actually been updated
|
||||
let mix = get_mixnode_details_by_id(deps.as_ref().storage, mix_id)
|
||||
let mix = get_mixnode_details_by_id(test.deps().storage, mix_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(mix.rewarding_details.cost_params, update);
|
||||
|
||||
// but we cannot perform any updates whilst the mixnode is already unbonding
|
||||
try_remove_mixnode(deps.as_mut(), env.clone(), info.clone()).unwrap();
|
||||
let res = try_update_mixnode_cost_params(deps.as_mut(), env, info, update);
|
||||
try_remove_mixnode(test.deps_mut(), env.clone(), info.clone()).unwrap();
|
||||
let res = try_update_mixnode_cost_params(test.deps_mut(), env, info, update);
|
||||
assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id }))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updating_mixnode_cost_params_with_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let owner = "alice";
|
||||
|
||||
test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone());
|
||||
let update = MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(42).unwrap(),
|
||||
interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM),
|
||||
};
|
||||
|
||||
let res = try_update_mixnode_cost_params_on_behalf(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
mock_info(illegal_proxy.as_ref(), &[]),
|
||||
update,
|
||||
owner.to_string(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_mixnode_with_duplicate_sphinx_key_errors_out() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
@@ -770,6 +975,40 @@ pub mod tests {
|
||||
setup_mix_combinations, OWNER_UNBONDED, OWNER_UNBONDED_LEFTOVER, OWNER_UNBONDING,
|
||||
};
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use mixnet_contract_common::{EpochState, EpochStatus};
|
||||
|
||||
#[test]
|
||||
fn cant_be_performed_if_epoch_transition_is_in_progress() {
|
||||
let bad_states = vec![
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 0,
|
||||
},
|
||||
EpochState::ReconcilingEvents,
|
||||
EpochState::AdvancingEpoch,
|
||||
];
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
let owner = "mix-owner";
|
||||
|
||||
test.add_dummy_mixnode(owner, None);
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
interval_storage::save_current_epoch_status(test.deps_mut().storage, &status)
|
||||
.unwrap();
|
||||
|
||||
let sender = mock_info(owner, &[test.coin(1000)]);
|
||||
let res = try_increase_pledge(test.deps_mut(), env, sender);
|
||||
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochAdvancementInProgress { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_not_allowed_if_account_doesnt_own_mixnode() {
|
||||
@@ -797,7 +1036,11 @@ pub mod tests {
|
||||
let wrong_proxy = Addr::unchecked("unrelated-proxy");
|
||||
|
||||
test.add_dummy_mixnode(owner_without_proxy.as_str(), None);
|
||||
test.add_dummy_mixnode_with_proxy(owner_with_proxy.as_str(), None, proxy.clone());
|
||||
test.add_dummy_mixnode_with_illegal_proxy(
|
||||
owner_with_proxy.as_str(),
|
||||
None,
|
||||
proxy.clone(),
|
||||
);
|
||||
|
||||
let res = _try_increase_pledge(
|
||||
test.deps_mut(),
|
||||
@@ -945,4 +1188,33 @@ pub mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_for_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
let env = test.env();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let owner = "alice";
|
||||
|
||||
test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone());
|
||||
|
||||
let res = try_increase_pledge_on_behalf(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]),
|
||||
owner.to_string(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,18 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::interval::storage as interval_storage;
|
||||
use cosmwasm_std::Storage;
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::EpochStatus;
|
||||
|
||||
pub(crate) fn create_epoch_status(storage: &mut dyn Storage) -> Result<(), MixnetContractError> {
|
||||
let current_rewarding_validator =
|
||||
crate::mixnet_contract_settings::storage::rewarding_validator_address(storage)?;
|
||||
interval_storage::save_current_epoch_status(
|
||||
storage,
|
||||
&EpochStatus::new(current_rewarding_validator),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::storage;
|
||||
@@ -8,7 +8,20 @@ use cosmwasm_std::{Coin, Storage};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::helpers::IntoBaseDecimal;
|
||||
use mixnet_contract_common::mixnode::{MixNodeDetails, MixNodeRewarding};
|
||||
use mixnet_contract_common::Delegation;
|
||||
use mixnet_contract_common::{Delegation, EpochState, EpochStatus, MixId};
|
||||
|
||||
pub(crate) fn update_and_save_last_rewarded(
|
||||
storage: &mut dyn Storage,
|
||||
mut current_epoch_status: EpochStatus,
|
||||
new_last_rewarded: MixId,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
let is_done = current_epoch_status.update_last_rewarded(new_last_rewarded)?;
|
||||
if is_done {
|
||||
current_epoch_status.state = EpochState::ReconcilingEvents
|
||||
}
|
||||
interval_storage::save_current_epoch_status(storage, ¤t_epoch_status)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recomputes rewarding parameters (such as staking supply, saturation point, etc) based on
|
||||
/// pending changes currently stored in `PENDING_REWARD_POOL_CHANGE`.
|
||||
@@ -215,14 +228,14 @@ mod tests {
|
||||
assert_eq!(res.amount, Uint128::zero());
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
let dist1 = test.reward_with_distribution(mix_id, performance(100.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist2 = test.reward_with_distribution(mix_id, performance(100.0));
|
||||
let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist3 = test.reward_with_distribution(mix_id, performance(100.0));
|
||||
let dist3 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0));
|
||||
|
||||
let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id)
|
||||
.unwrap()
|
||||
@@ -254,14 +267,14 @@ mod tests {
|
||||
assert_eq!(res.amount, Uint128::zero());
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
let dist1 = test.reward_with_distribution(mix_id, performance(100.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist2 = test.reward_with_distribution(mix_id, performance(100.0));
|
||||
let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist3 = test.reward_with_distribution(mix_id, performance(100.0));
|
||||
let dist3 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0));
|
||||
|
||||
let delegation_pre = test.delegation(mix_id, delegator, &None);
|
||||
let mix_rewarding = test.mix_rewarding(mix_id);
|
||||
|
||||
@@ -297,10 +297,13 @@ mod tests {
|
||||
let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
|
||||
let mut total_earned = Decimal::zero();
|
||||
let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
total_earned += dist.operator;
|
||||
|
||||
let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap();
|
||||
@@ -317,7 +320,10 @@ mod tests {
|
||||
// reward it few more times for good measure
|
||||
for _ in 0..10 {
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
total_earned += dist.operator;
|
||||
|
||||
let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap();
|
||||
@@ -341,10 +347,13 @@ mod tests {
|
||||
let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
|
||||
let mut total_earned = Decimal::zero();
|
||||
let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
total_earned += dist.operator;
|
||||
|
||||
let sender = mock_info(owner, &[]);
|
||||
@@ -370,9 +379,12 @@ mod tests {
|
||||
let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let sender = mock_info(owner, &[]);
|
||||
let env = test.env();
|
||||
@@ -447,10 +459,13 @@ mod tests {
|
||||
test.add_immediate_delegation(owner, initial_stake, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
|
||||
let mut total_earned = Decimal::zero();
|
||||
let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
total_earned += dist.delegates;
|
||||
|
||||
let res =
|
||||
@@ -466,7 +481,10 @@ mod tests {
|
||||
// reward it few more times for good measure
|
||||
for _ in 0..10 {
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
total_earned += dist.delegates;
|
||||
|
||||
let res = query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None)
|
||||
@@ -491,10 +509,13 @@ mod tests {
|
||||
test.add_immediate_delegation(owner, initial_stake, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
|
||||
let mut total_earned = Decimal::zero();
|
||||
let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
total_earned += dist.delegates;
|
||||
|
||||
let sender = mock_info("mix-owner", &[]);
|
||||
@@ -521,10 +542,13 @@ mod tests {
|
||||
test.add_immediate_delegation(owner, initial_stake, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
|
||||
let mut total_earned = Decimal::zero();
|
||||
let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
total_earned += dist.delegates;
|
||||
|
||||
let sender = mock_info("mix-owner", &[]);
|
||||
@@ -557,34 +581,58 @@ mod tests {
|
||||
test.add_immediate_delegation(del2, 150_000_000u32, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
test.add_immediate_delegation(del3, 500_000_000u32, mix_id);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(85.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(85.0),
|
||||
);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(5.0));
|
||||
test.reward_with_distribution_with_state_bypass(mix_id, test_helpers::performance(5.0));
|
||||
|
||||
test.add_immediate_delegation(del4, 5_000_000u32, mix_id);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
test.add_immediate_delegation(del2, 250_000_000u32, mix_id);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(98.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(98.0),
|
||||
);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
test.remove_immediate_delegation(del3, mix_id);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(98.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(98.0),
|
||||
);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let pending1 =
|
||||
query_pending_delegator_reward(test.deps(), del1.into(), mix_id, None).unwrap();
|
||||
@@ -660,8 +708,11 @@ mod tests {
|
||||
let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let sender = mock_info(owner, &[]);
|
||||
let env = test.env();
|
||||
@@ -686,8 +737,11 @@ mod tests {
|
||||
let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let sender = mock_info(owner, &[]);
|
||||
let env = test.env();
|
||||
@@ -710,9 +764,12 @@ mod tests {
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.update_rewarded_set(vec![]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
test.force_change_rewarded_set(vec![]);
|
||||
|
||||
let res = query_estimated_current_epoch_operator_reward(
|
||||
test.deps(),
|
||||
@@ -732,8 +789,11 @@ mod tests {
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let res = query_estimated_current_epoch_operator_reward(
|
||||
test.deps(),
|
||||
@@ -753,8 +813,11 @@ mod tests {
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let mix_rewarding = test.mix_rewarding(mix_id);
|
||||
let res = query_estimated_current_epoch_operator_reward(
|
||||
@@ -765,7 +828,10 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist = test.reward_with_distribution(mix_id, test_helpers::performance(95.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(95.0),
|
||||
);
|
||||
|
||||
let expected = EstimatedCurrentEpochRewardResponse {
|
||||
original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)),
|
||||
@@ -816,8 +882,11 @@ mod tests {
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let res = query_estimated_current_epoch_delegator_reward(
|
||||
test.deps(),
|
||||
@@ -841,8 +910,11 @@ mod tests {
|
||||
test.add_immediate_delegation(owner, initial_stake, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let sender = mock_info("mix-owner", &[]);
|
||||
let env = test.env();
|
||||
@@ -871,8 +943,11 @@ mod tests {
|
||||
test.add_immediate_delegation(owner, initial_stake, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let sender = mock_info("mix-owner", &[]);
|
||||
let env = test.env();
|
||||
@@ -902,9 +977,12 @@ mod tests {
|
||||
test.add_immediate_delegation(owner, initial_stake, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.update_rewarded_set(vec![]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
test.force_change_rewarded_set(vec![]);
|
||||
|
||||
let res = query_estimated_current_epoch_delegator_reward(
|
||||
test.deps(),
|
||||
@@ -929,8 +1007,11 @@ mod tests {
|
||||
test.add_immediate_delegation(owner, initial_stake, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(100.0),
|
||||
);
|
||||
|
||||
let res = query_estimated_current_epoch_delegator_reward(
|
||||
test.deps(),
|
||||
@@ -955,7 +1036,7 @@ mod tests {
|
||||
test.add_immediate_delegation(owner, initial_stake, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
|
||||
let mix_rewarding = test.mix_rewarding(mix_id);
|
||||
let res = query_estimated_current_epoch_delegator_reward(
|
||||
@@ -968,7 +1049,10 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist = test.reward_with_distribution(mix_id, test_helpers::performance(95.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(95.0),
|
||||
);
|
||||
|
||||
let expected = EstimatedCurrentEpochRewardResponse {
|
||||
original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)),
|
||||
@@ -1003,12 +1087,18 @@ mod tests {
|
||||
test.add_immediate_delegation(del2, initial_stake2, mix_id);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(95.0));
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(95.0),
|
||||
);
|
||||
|
||||
test.add_immediate_delegation(del3, initial_stake3, mix_id);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(85.0));
|
||||
test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(85.0),
|
||||
);
|
||||
|
||||
let mix_rewarding = test.mix_rewarding(mix_id);
|
||||
|
||||
@@ -1045,7 +1135,10 @@ mod tests {
|
||||
let cur3 = initial_stake3_dec + est3;
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
let dist = test.reward_with_distribution(mix_id, test_helpers::performance(95.0));
|
||||
let dist = test.reward_with_distribution_with_state_bypass(
|
||||
mix_id,
|
||||
test_helpers::performance(95.0),
|
||||
);
|
||||
|
||||
let share1 = cur1 / mix_rewarding.delegates * dist.delegates;
|
||||
let share2 = cur2 / mix_rewarding.delegates * dist.delegates;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::storage;
|
||||
@@ -9,9 +9,10 @@ use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::helpers::get_mixnode_details_by_owner;
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::rewards::helpers;
|
||||
use crate::rewards::helpers::update_and_save_last_rewarded;
|
||||
use crate::support::helpers::{
|
||||
ensure_bonded, ensure_is_authorized, ensure_is_owner, ensure_proxy_match,
|
||||
send_to_proxy_or_owner,
|
||||
ensure_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_owner,
|
||||
ensure_proxy_match, ensure_sent_by_vesting_contract, send_to_proxy_or_owner,
|
||||
};
|
||||
use cosmwasm_std::{wasm_execute, Addr, DepsMut, Env, MessageInfo, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
@@ -26,7 +27,7 @@ use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingInter
|
||||
use mixnet_contract_common::reward_params::{
|
||||
IntervalRewardingParamsUpdate, NodeRewardParams, Performance,
|
||||
};
|
||||
use mixnet_contract_common::{Delegation, MixId};
|
||||
use mixnet_contract_common::{Delegation, EpochState, MixId};
|
||||
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
|
||||
|
||||
pub(crate) fn try_reward_mixnode(
|
||||
@@ -36,7 +37,9 @@ pub(crate) fn try_reward_mixnode(
|
||||
mix_id: MixId,
|
||||
node_performance: Performance,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_is_authorized(info.sender, deps.storage)?;
|
||||
// check whether this `info.sender` is the same one as set in `epoch_status.being_advanced_by`
|
||||
// if so, return `epoch_status` so we could avoid having to perform extra read from the storage
|
||||
let current_epoch_status = ensure_can_advance_epoch(&info.sender, deps.storage)?;
|
||||
|
||||
// see if the epoch has finished
|
||||
let interval = interval_storage::current_interval(deps.storage)?;
|
||||
@@ -47,6 +50,19 @@ pub(crate) fn try_reward_mixnode(
|
||||
epoch_end: interval.current_epoch_end_unix_timestamp(),
|
||||
});
|
||||
}
|
||||
let absolute_epoch_id = interval.current_epoch_absolute_id();
|
||||
|
||||
if matches!(current_epoch_status.state, EpochState::Rewarding {last_rewarded, ..} if last_rewarded == mix_id)
|
||||
{
|
||||
return Err(MixnetContractError::MixnodeAlreadyRewarded {
|
||||
mix_id,
|
||||
absolute_epoch_id,
|
||||
});
|
||||
}
|
||||
|
||||
// update the epoch state with this node as being rewarded most recently
|
||||
// (if the transaction fails down the line, it will be reverted)
|
||||
update_and_save_last_rewarded(deps.storage, current_epoch_status, mix_id)?;
|
||||
|
||||
// there's a chance of this failing to load the details if the mixnode unbonded before rewards
|
||||
// were distributed and all of its delegators are also gone
|
||||
@@ -64,8 +80,7 @@ pub(crate) fn try_reward_mixnode(
|
||||
|
||||
// check if this node has already been rewarded for the current epoch.
|
||||
// unlike the previous check, this one should be a hard error since this cannot be
|
||||
// influenced by users actions
|
||||
let absolute_epoch_id = interval.current_epoch_absolute_id();
|
||||
// influenced by users actions (note that previous epoch state checks should actually already guard us against it)
|
||||
if absolute_epoch_id == mix_rewarding.last_rewarded_epoch {
|
||||
return Err(MixnetContractError::MixnodeAlreadyRewarded {
|
||||
mix_id,
|
||||
@@ -130,6 +145,8 @@ pub(crate) fn try_withdraw_operator_reward_on_behalf(
|
||||
info: MessageInfo,
|
||||
owner: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let proxy = info.sender;
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
_try_withdraw_operator_reward(deps, owner, Some(proxy))
|
||||
@@ -195,6 +212,8 @@ pub(crate) fn try_withdraw_delegator_reward_on_behalf(
|
||||
mix_id: MixId,
|
||||
owner: String,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
ensure_sent_by_vesting_contract(&info, deps.storage)?;
|
||||
|
||||
let proxy = info.sender;
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
_try_withdraw_delegator_reward(deps, mix_id, owner, Some(proxy))
|
||||
@@ -293,6 +312,10 @@ pub(crate) fn try_update_active_set_size(
|
||||
active_set_size,
|
||||
)))
|
||||
} else {
|
||||
// updating active sety size is only allowed if the epoch is currently not in the process of being advanced
|
||||
// (unless the force flag was used)
|
||||
ensure_epoch_in_progress_state(deps.storage)?;
|
||||
|
||||
// push the epoch event
|
||||
let epoch_event = PendingEpochEventKind::UpdateActiveSetSize {
|
||||
new_size: active_set_size,
|
||||
@@ -332,6 +355,10 @@ pub(crate) fn try_update_rewarding_params(
|
||||
rewarding_params.interval,
|
||||
)))
|
||||
} else {
|
||||
// changing rewarding parameters is only allowed if the epoch is currently not in the process of being advanced
|
||||
// (unless the force flag was used)
|
||||
ensure_epoch_in_progress_state(deps.storage)?;
|
||||
|
||||
// push the interval event
|
||||
let interval_event = PendingIntervalEventKind::UpdateRewardingParams {
|
||||
update: updated_params,
|
||||
@@ -366,7 +393,236 @@ pub mod tests {
|
||||
ZERO_PERFORMANCE_VALUE,
|
||||
};
|
||||
use mixnet_contract_common::helpers::compare_decimals;
|
||||
use mixnet_contract_common::RewardedSetNodeStatus;
|
||||
use mixnet_contract_common::{EpochStatus, RewardedSetNodeStatus};
|
||||
|
||||
#[cfg(test)]
|
||||
mod epoch_state_is_correctly_updated {
|
||||
use super::*;
|
||||
use mixnet_contract_common::EpochState;
|
||||
|
||||
#[test]
|
||||
fn when_target_mixnode_unbonded() {
|
||||
let mut test = TestSetup::new();
|
||||
let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None);
|
||||
let mix_id_unbonded_leftover =
|
||||
test.add_dummy_mixnode("mix-owner-unbonded-leftover", None);
|
||||
let mix_id_never_existed = 42;
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(vec![
|
||||
mix_id_unbonded,
|
||||
mix_id_unbonded_leftover,
|
||||
mix_id_never_existed,
|
||||
]);
|
||||
test.start_epoch_transition();
|
||||
|
||||
let env = test.env();
|
||||
|
||||
// note: we don't have to test for cases where `is_unbonding` is set to true on a mixnode
|
||||
// since before performing the nym-api should clear out the event queue
|
||||
|
||||
// manually adjust delegation info as to indicate the rewarding information shouldnt get removed
|
||||
let mut rewarding_details = storage::MIXNODE_REWARDING
|
||||
.load(test.deps().storage, mix_id_unbonded_leftover)
|
||||
.unwrap();
|
||||
rewarding_details.delegates = Decimal::raw(12345);
|
||||
rewarding_details.unique_delegations = 1;
|
||||
storage::MIXNODE_REWARDING
|
||||
.save(
|
||||
test.deps_mut().storage,
|
||||
mix_id_unbonded_leftover,
|
||||
&rewarding_details,
|
||||
)
|
||||
.unwrap();
|
||||
pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded)
|
||||
.unwrap();
|
||||
|
||||
pending_events::unbond_mixnode(
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
123,
|
||||
mix_id_unbonded_leftover,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let env = test.env();
|
||||
let sender = test.rewarding_validator();
|
||||
let performance = test_helpers::performance(100.0);
|
||||
|
||||
try_reward_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
sender.clone(),
|
||||
mix_id_unbonded,
|
||||
performance,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: mix_id_unbonded,
|
||||
final_node_id: mix_id_never_existed
|
||||
},
|
||||
interval_storage::current_epoch_status(test.deps().storage)
|
||||
.unwrap()
|
||||
.state
|
||||
);
|
||||
|
||||
try_reward_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
sender.clone(),
|
||||
mix_id_unbonded_leftover,
|
||||
performance,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: mix_id_unbonded_leftover,
|
||||
final_node_id: mix_id_never_existed
|
||||
},
|
||||
interval_storage::current_epoch_status(test.deps().storage)
|
||||
.unwrap()
|
||||
.state
|
||||
);
|
||||
|
||||
try_reward_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
sender.clone(),
|
||||
mix_id_never_existed,
|
||||
performance,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
EpochState::ReconcilingEvents,
|
||||
interval_storage::current_epoch_status(test.deps().storage)
|
||||
.unwrap()
|
||||
.state
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_target_mixnode_has_zero_performance() {
|
||||
let mut test = TestSetup::new();
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.start_epoch_transition();
|
||||
let zero_performance = test_helpers::performance(0.);
|
||||
let env = test.env();
|
||||
let sender = test.rewarding_validator();
|
||||
|
||||
try_reward_mixnode(test.deps_mut(), env, sender, mix_id, zero_performance).unwrap();
|
||||
assert_eq!(
|
||||
EpochState::ReconcilingEvents,
|
||||
interval_storage::current_epoch_status(test.deps().storage)
|
||||
.unwrap()
|
||||
.state
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_theres_only_one_node_to_reward() {
|
||||
let mut test = TestSetup::new();
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.start_epoch_transition();
|
||||
let performance = test_helpers::performance(100.0);
|
||||
let env = test.env();
|
||||
let sender = test.rewarding_validator();
|
||||
|
||||
try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance).unwrap();
|
||||
assert_eq!(
|
||||
EpochState::ReconcilingEvents,
|
||||
interval_storage::current_epoch_status(test.deps().storage)
|
||||
.unwrap()
|
||||
.state
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_theres_multiple_nodes_to_reward() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let mut ids = Vec::new();
|
||||
for i in 0..100 {
|
||||
let mix_id = test.add_dummy_mixnode(&format!("mix-owner{i}"), None);
|
||||
ids.push(mix_id);
|
||||
}
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(ids.clone());
|
||||
test.start_epoch_transition();
|
||||
let performance = test_helpers::performance(100.0);
|
||||
let env = test.env();
|
||||
let sender = test.rewarding_validator();
|
||||
|
||||
for mix_id in ids {
|
||||
try_reward_mixnode(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
sender.clone(),
|
||||
mix_id,
|
||||
performance,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let current_state = interval_storage::current_epoch_status(test.deps().storage)
|
||||
.unwrap()
|
||||
.state;
|
||||
if mix_id == 100 {
|
||||
assert_eq!(EpochState::ReconcilingEvents, current_state)
|
||||
} else {
|
||||
assert_eq!(
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: mix_id,
|
||||
final_node_id: 100
|
||||
},
|
||||
current_state
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_if_in_rewarding_state() {
|
||||
let bad_states = vec![
|
||||
EpochState::InProgress,
|
||||
EpochState::ReconcilingEvents,
|
||||
EpochState::AdvancingEpoch,
|
||||
];
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
let rewarding_validator = test.rewarding_validator();
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
interval_storage::save_current_epoch_status(test.deps_mut().storage, &status)
|
||||
.unwrap();
|
||||
|
||||
test.skip_to_current_epoch_end();
|
||||
test.force_change_rewarded_set(vec![1, 2, 3]);
|
||||
let env = test.env();
|
||||
|
||||
let res = try_reward_mixnode(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
rewarding_validator,
|
||||
1,
|
||||
test_helpers::performance(100.),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(MixnetContractError::UnexpectedNonRewardingEpochState {
|
||||
current_state: bad_state
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_only_be_performed_by_specified_rewarding_validator() {
|
||||
@@ -377,7 +633,8 @@ pub mod tests {
|
||||
// skip time to when the following epoch is over (since mixnodes are not eligible for rewarding
|
||||
// in the same epoch they're bonded and we need the rewarding epoch to be over)
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.start_epoch_transition();
|
||||
let performance = test_helpers::performance(100.);
|
||||
|
||||
let env = test.env();
|
||||
@@ -399,11 +656,12 @@ pub mod tests {
|
||||
let mix_id_unbonded_leftover =
|
||||
test.add_dummy_mixnode("mix-owner-unbonded-leftover", None);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![
|
||||
mix_id_never_existed,
|
||||
test.force_change_rewarded_set(vec![
|
||||
mix_id_unbonded,
|
||||
mix_id_unbonded_leftover,
|
||||
mix_id_never_existed,
|
||||
]);
|
||||
test.start_epoch_transition();
|
||||
|
||||
let env = test.env();
|
||||
|
||||
@@ -433,9 +691,9 @@ pub mod tests {
|
||||
let performance = test_helpers::performance(100.0);
|
||||
|
||||
for &mix_id in &[
|
||||
mix_id_never_existed,
|
||||
mix_id_unbonded,
|
||||
mix_id_unbonded_leftover,
|
||||
mix_id_never_existed,
|
||||
] {
|
||||
let res = try_reward_mixnode(
|
||||
test.deps_mut(),
|
||||
@@ -464,7 +722,7 @@ pub mod tests {
|
||||
|
||||
// node is in the active set BUT the current epoch has just begun
|
||||
test.skip_to_next_epoch();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
let performance = test_helpers::performance(100.);
|
||||
|
||||
let env = test.env();
|
||||
@@ -476,6 +734,7 @@ pub mod tests {
|
||||
|
||||
// epoch is over (sanity check)
|
||||
test.skip_to_current_epoch_end();
|
||||
test.start_epoch_transition();
|
||||
let env = test.env();
|
||||
let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance);
|
||||
assert!(res.is_ok());
|
||||
@@ -508,6 +767,16 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// actually add one more dummy node with high id so we wouldn't go into the next state
|
||||
interval_storage::REWARDED_SET
|
||||
.save(
|
||||
test.deps_mut().storage,
|
||||
9001,
|
||||
&RewardedSetNodeStatus::Standby,
|
||||
)
|
||||
.unwrap();
|
||||
test.start_epoch_transition();
|
||||
|
||||
let performance = test_helpers::performance(100.);
|
||||
let env = test.env();
|
||||
let res_active = try_reward_mixnode(
|
||||
@@ -541,7 +810,8 @@ pub mod tests {
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id, 42]);
|
||||
test.start_epoch_transition();
|
||||
let performance = test_helpers::performance(100.);
|
||||
let env = test.env();
|
||||
let sender = test.rewarding_validator();
|
||||
@@ -565,6 +835,8 @@ pub mod tests {
|
||||
|
||||
// in the following epoch we're good again
|
||||
test.skip_to_next_epoch_end();
|
||||
test.start_epoch_transition();
|
||||
|
||||
let env = test.env();
|
||||
let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance);
|
||||
assert!(res.is_ok());
|
||||
@@ -576,7 +848,8 @@ pub mod tests {
|
||||
let mix_id = test.add_dummy_mixnode("mix-owner", None);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id, 42]);
|
||||
test.start_epoch_transition();
|
||||
let zero_performance = test_helpers::performance(0.);
|
||||
let performance = test_helpers::performance(100.0);
|
||||
let env = test.env();
|
||||
@@ -614,6 +887,8 @@ pub mod tests {
|
||||
|
||||
// but in the next epoch, as always, we're good again
|
||||
test.skip_to_next_epoch_end();
|
||||
test.start_epoch_transition();
|
||||
|
||||
let env = test.env();
|
||||
let res =
|
||||
try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance).unwrap();
|
||||
@@ -642,7 +917,8 @@ pub mod tests {
|
||||
let mix_id3 = test.add_dummy_mixnode("mix-owner3", None);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id1, mix_id2, mix_id3]);
|
||||
test.force_change_rewarded_set(vec![mix_id1, mix_id2, mix_id3]);
|
||||
test.start_epoch_transition();
|
||||
let performance = test_helpers::performance(98.0);
|
||||
let env = test.env();
|
||||
let sender = test.rewarding_validator();
|
||||
@@ -726,7 +1002,8 @@ pub mod tests {
|
||||
let mix_id3 = test.add_dummy_mixnode("mix-owner3", Some(operator3));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id1, mix_id2, mix_id3]);
|
||||
test.start_epoch_transition();
|
||||
test.force_change_rewarded_set(vec![mix_id1, mix_id2, mix_id3]);
|
||||
let performance = test_helpers::performance(98.0);
|
||||
|
||||
test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2);
|
||||
@@ -735,8 +1012,12 @@ pub mod tests {
|
||||
test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id3);
|
||||
test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3);
|
||||
|
||||
// bypass proper epoch progression and force change the state
|
||||
test.set_epoch_in_progress_state();
|
||||
|
||||
// repeat the rewarding the same set of delegates for few epochs
|
||||
for _ in 0..10 {
|
||||
test.start_epoch_transition();
|
||||
for &mix_id in &[mix_id1, mix_id2, mix_id3] {
|
||||
let mut sim = test.instantiate_simulator(mix_id);
|
||||
let dist = test.reward_with_distribution(mix_id, performance);
|
||||
@@ -747,6 +1028,8 @@ pub mod tests {
|
||||
let sim_res = sim.simulate_epoch_single_node(node_params).unwrap();
|
||||
assert_eq!(sim_res, dist);
|
||||
}
|
||||
// bypass proper epoch progression and force change the state
|
||||
test.set_epoch_in_progress_state();
|
||||
test.skip_to_next_epoch_end();
|
||||
}
|
||||
|
||||
@@ -759,8 +1042,12 @@ pub mod tests {
|
||||
test.add_immediate_delegation("delegator5", Uint128::new(123_000_000), mix_id3);
|
||||
test.add_immediate_delegation("delegator6", Uint128::new(456_000_000), mix_id3);
|
||||
|
||||
// bypass proper epoch progression and force change the state
|
||||
test.set_epoch_in_progress_state();
|
||||
|
||||
let performance = test_helpers::performance(12.3);
|
||||
for _ in 0..10 {
|
||||
test.start_epoch_transition();
|
||||
for &mix_id in &[mix_id1, mix_id2, mix_id3] {
|
||||
let mut sim = test.instantiate_simulator(mix_id);
|
||||
let dist = test.reward_with_distribution(mix_id, performance);
|
||||
@@ -771,6 +1058,8 @@ pub mod tests {
|
||||
let sim_res = sim.simulate_epoch_single_node(node_params).unwrap();
|
||||
assert_eq!(sim_res, dist);
|
||||
}
|
||||
// bypass proper epoch progression and force change the state
|
||||
test.set_epoch_in_progress_state();
|
||||
test.skip_to_next_epoch_end();
|
||||
}
|
||||
}
|
||||
@@ -787,7 +1076,7 @@ pub mod tests {
|
||||
let mix_id2 = test.add_dummy_mixnode("mix-owner2", Some(operator2));
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id1, mix_id2]);
|
||||
test.force_change_rewarded_set(vec![mix_id1, mix_id2]);
|
||||
let performance = test_helpers::performance(98.0);
|
||||
|
||||
test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id1);
|
||||
@@ -800,6 +1089,8 @@ pub mod tests {
|
||||
let del21 = test.delegation(mix_id2, "delegator1", &None);
|
||||
|
||||
for _ in 0..10 {
|
||||
test.start_epoch_transition();
|
||||
|
||||
// we know from the previous tests that actual rewarding distribution matches the simulator
|
||||
let mut sim1 = test.instantiate_simulator(mix_id1);
|
||||
let mut sim2 = test.instantiate_simulator(mix_id2);
|
||||
@@ -933,6 +1224,8 @@ pub mod tests {
|
||||
assert_eq!(dist2.delegates, computed_del21_reward);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
// bypass proper epoch progression and force change the state
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
// add more delegations and check few more epochs (so that the delegations would start from non-default unit delegation value)
|
||||
@@ -943,6 +1236,8 @@ pub mod tests {
|
||||
let del23 = test.delegation(mix_id2, "delegator3", &None);
|
||||
|
||||
for _ in 0..10 {
|
||||
test.start_epoch_transition();
|
||||
|
||||
// we know from the previous tests that actual rewarding distribution matches the simulator
|
||||
let mut sim1 = test.instantiate_simulator(mix_id1);
|
||||
let mut sim2 = test.instantiate_simulator(mix_id2);
|
||||
@@ -1096,6 +1391,8 @@ pub mod tests {
|
||||
);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
// bypass proper epoch progression and force change the state
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1104,8 +1401,9 @@ pub mod tests {
|
||||
mod withdrawing_delegator_reward {
|
||||
use super::*;
|
||||
use crate::interval::pending_events;
|
||||
use crate::support::tests::fixtures::TEST_COIN_DENOM;
|
||||
use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup};
|
||||
use cosmwasm_std::{BankMsg, CosmosMsg, Decimal, Uint128};
|
||||
use cosmwasm_std::{coin, BankMsg, CosmosMsg, Decimal, Uint128};
|
||||
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
|
||||
|
||||
#[test]
|
||||
@@ -1131,7 +1429,8 @@ pub mod tests {
|
||||
|
||||
// perform some rewarding so that we'd have non-zero rewards
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id1, mix_id2]);
|
||||
test.force_change_rewarded_set(vec![mix_id1, mix_id2]);
|
||||
test.start_epoch_transition();
|
||||
test.reward_with_distribution(mix_id1, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution(mix_id2, test_helpers::performance(100.0));
|
||||
|
||||
@@ -1178,7 +1477,8 @@ pub mod tests {
|
||||
|
||||
// reward mix1, but don't reward mix2
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id1, low_stake_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id1, low_stake_id]);
|
||||
test.start_epoch_transition();
|
||||
test.reward_with_distribution(mix_id1, test_helpers::performance(100.0));
|
||||
test.reward_with_distribution(low_stake_id, test_helpers::performance(100.0));
|
||||
|
||||
@@ -1216,14 +1516,18 @@ pub mod tests {
|
||||
|
||||
let performance = test_helpers::performance(100.0);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]);
|
||||
test.force_change_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]);
|
||||
|
||||
// go through few rewarding cycles before unbonding nodes (partially or fully)
|
||||
for _ in 0..10 {
|
||||
test.start_epoch_transition();
|
||||
|
||||
test.reward_with_distribution(mix_id_unbonding, performance);
|
||||
test.reward_with_distribution(mix_id_unbonded_leftover, performance);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
// bypass proper epoch progression and force change the state
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
// start unbonding the first node and fully unbond the other
|
||||
@@ -1291,12 +1595,13 @@ pub mod tests {
|
||||
|
||||
let performance = test_helpers::performance(100.0);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id_single, mix_id_quad]);
|
||||
test.force_change_rewarded_set(vec![mix_id_single, mix_id_quad]);
|
||||
|
||||
// accumulate some rewards
|
||||
let mut accumulated_single = Decimal::zero();
|
||||
let mut accumulated_quad = Decimal::zero();
|
||||
for _ in 0..10 {
|
||||
test.start_epoch_transition();
|
||||
let dist = test.reward_with_distribution(mix_id_single, performance);
|
||||
// sanity check to make sure test is actually doing what it's supposed to be doing
|
||||
assert!(!dist.delegates.is_zero());
|
||||
@@ -1306,6 +1611,8 @@ pub mod tests {
|
||||
accumulated_quad += dist.delegates;
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
// bypass proper epoch progression and force change the state
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
let before = test.read_delegation(mix_id_single, delegator1, None);
|
||||
@@ -1361,9 +1668,13 @@ pub mod tests {
|
||||
|
||||
// accumulate some more
|
||||
for _ in 0..10 {
|
||||
test.start_epoch_transition();
|
||||
|
||||
let dist = test.reward_with_distribution(mix_id_quad, performance);
|
||||
accumulated_quad += dist.delegates;
|
||||
test.skip_to_next_epoch_end();
|
||||
// bypass proper epoch progression and force change the state
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
let before1_new = test.read_delegation(mix_id_quad, delegator1, None);
|
||||
@@ -1422,14 +1733,58 @@ pub mod tests {
|
||||
let accumulated_actual = truncate_reward_amount(accumulated_quad);
|
||||
assert_eq_with_leeway(total_claimed, accumulated_actual, Uint128::new(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_for_illegal_proxy() {
|
||||
let test = TestSetup::new();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let mut test = TestSetup::new();
|
||||
let mix_id =
|
||||
test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000)));
|
||||
|
||||
let delegator = "delegator";
|
||||
|
||||
test.add_immediate_delegation_with_illegal_proxy(
|
||||
delegator,
|
||||
100_000_000u128,
|
||||
mix_id,
|
||||
illegal_proxy.clone(),
|
||||
);
|
||||
|
||||
// reward the node
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.start_epoch_transition();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
|
||||
let res = try_withdraw_delegator_reward_on_behalf(
|
||||
test.deps_mut(),
|
||||
mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]),
|
||||
mix_id,
|
||||
delegator.to_string(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod withdrawing_operator_reward {
|
||||
use super::*;
|
||||
use crate::interval::pending_events;
|
||||
use crate::support::tests::fixtures::TEST_COIN_DENOM;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use cosmwasm_std::{BankMsg, CosmosMsg, Uint128};
|
||||
use cosmwasm_std::{coin, BankMsg, CosmosMsg, Uint128};
|
||||
|
||||
#[test]
|
||||
fn can_only_be_done_if_bond_exists() {
|
||||
@@ -1440,7 +1795,8 @@ pub mod tests {
|
||||
let sender = mock_info("random-guy", &[]);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id]);
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.start_epoch_transition();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
|
||||
let res = try_withdraw_operator_reward(test.deps_mut(), sender.clone());
|
||||
@@ -1466,7 +1822,8 @@ pub mod tests {
|
||||
|
||||
// reward mix1, but don't reward mix2
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id1]);
|
||||
test.force_change_rewarded_set(vec![mix_id1]);
|
||||
test.start_epoch_transition();
|
||||
test.reward_with_distribution(mix_id1, test_helpers::performance(100.0));
|
||||
|
||||
let res1 = try_withdraw_operator_reward(test.deps_mut(), sender1).unwrap();
|
||||
@@ -1498,14 +1855,17 @@ pub mod tests {
|
||||
|
||||
let performance = test_helpers::performance(100.0);
|
||||
test.skip_to_next_epoch_end();
|
||||
test.update_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]);
|
||||
test.force_change_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]);
|
||||
|
||||
// go through few rewarding cycles before unbonding nodes (partially or fully)
|
||||
for _ in 0..10 {
|
||||
test.start_epoch_transition();
|
||||
test.reward_with_distribution(mix_id_unbonding, performance);
|
||||
test.reward_with_distribution(mix_id_unbonded_leftover, performance);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
// bypass proper epoch progression and force change the state
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
// start unbonding the first node and fully unbond the other
|
||||
@@ -1537,12 +1897,89 @@ pub mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_for_illegal_proxy() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let illegal_proxy = Addr::unchecked("not-vesting-contract");
|
||||
let vesting_contract = test.vesting_contract();
|
||||
|
||||
let owner = "mix-owner1";
|
||||
let mix_id = test.add_dummy_mixnode_with_illegal_proxy(
|
||||
owner,
|
||||
Some(Uint128::new(1_000_000_000_000)),
|
||||
illegal_proxy.clone(),
|
||||
);
|
||||
|
||||
// reward the node
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(vec![mix_id]);
|
||||
test.start_epoch_transition();
|
||||
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
|
||||
|
||||
let res = try_withdraw_operator_reward_on_behalf(
|
||||
test.deps_mut(),
|
||||
mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]),
|
||||
owner.to_string(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::SenderIsNotVestingContract {
|
||||
received: illegal_proxy,
|
||||
vesting_contract
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod updating_active_set {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use mixnet_contract_common::{EpochState, EpochStatus};
|
||||
|
||||
#[test]
|
||||
fn cant_be_performed_if_epoch_transition_is_in_progress_unless_forced() {
|
||||
let bad_states = vec![
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 0,
|
||||
},
|
||||
EpochState::ReconcilingEvents,
|
||||
EpochState::AdvancingEpoch,
|
||||
];
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
let owner = test.owner();
|
||||
let env = test.env();
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
|
||||
interval_storage::save_current_epoch_status(test.deps_mut().storage, &status)
|
||||
.unwrap();
|
||||
|
||||
let res = try_update_active_set_size(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
owner.clone(),
|
||||
100,
|
||||
false,
|
||||
);
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochAdvancementInProgress { .. })
|
||||
));
|
||||
|
||||
let res_forced =
|
||||
try_update_active_set_size(test.deps_mut(), env.clone(), owner, 100, true);
|
||||
assert!(res_forced.is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_only_be_done_by_contract_owner() {
|
||||
@@ -1706,6 +2143,62 @@ pub mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers::{assert_decimals, TestSetup};
|
||||
use cosmwasm_std::Decimal;
|
||||
use mixnet_contract_common::{EpochState, EpochStatus};
|
||||
|
||||
#[test]
|
||||
fn cant_be_performed_if_epoch_transition_is_in_progress_unless_forced() {
|
||||
let bad_states = vec![
|
||||
EpochState::Rewarding {
|
||||
last_rewarded: 0,
|
||||
final_node_id: 0,
|
||||
},
|
||||
EpochState::ReconcilingEvents,
|
||||
EpochState::AdvancingEpoch,
|
||||
];
|
||||
|
||||
let update = IntervalRewardingParamsUpdate {
|
||||
reward_pool: None,
|
||||
staking_supply: None,
|
||||
staking_supply_scale_factor: None,
|
||||
sybil_resistance_percent: None,
|
||||
active_set_work_factor: None,
|
||||
interval_pool_emission: None,
|
||||
rewarded_set_size: Some(123),
|
||||
};
|
||||
|
||||
for bad_state in bad_states {
|
||||
let mut test = TestSetup::new();
|
||||
let owner = test.owner();
|
||||
let env = test.env();
|
||||
|
||||
let mut status = EpochStatus::new(test.rewarding_validator().sender);
|
||||
status.state = bad_state;
|
||||
|
||||
interval_storage::save_current_epoch_status(test.deps_mut().storage, &status)
|
||||
.unwrap();
|
||||
|
||||
let res = try_update_rewarding_params(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
owner.clone(),
|
||||
update.clone(),
|
||||
false,
|
||||
);
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(MixnetContractError::EpochAdvancementInProgress { .. })
|
||||
));
|
||||
|
||||
let res_forced = try_update_rewarding_params(
|
||||
test.deps_mut(),
|
||||
env.clone(),
|
||||
owner,
|
||||
update.clone(),
|
||||
true,
|
||||
);
|
||||
assert!(res_forced.is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_only_be_done_by_contract_owner() {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::gateways::storage as gateways_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Deps, Response, Storage};
|
||||
use cosmwasm_std::{
|
||||
wasm_execute, Addr, BankMsg, Coin, CosmosMsg, Deps, MessageInfo, Response, Storage,
|
||||
};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::{IdentityKeyRef, MixNodeBond};
|
||||
use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixId, MixNodeBond};
|
||||
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
|
||||
|
||||
// helper trait to attach `Msg` to a response if it's provided
|
||||
pub(crate) trait AttachOptionalMessage<T> {
|
||||
@@ -22,6 +26,99 @@ impl<T> AttachOptionalMessage<T> for Response<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// another helper trait to remove some duplicate code and consolidate comments regarding
|
||||
// possible epoch progression halting behaviour
|
||||
pub(crate) trait VestingTracking
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn maybe_add_track_vesting_undelegation_message(
|
||||
self,
|
||||
storage: &dyn Storage,
|
||||
proxy: Option<Addr>,
|
||||
owner: String,
|
||||
mix_id: MixId,
|
||||
amount: Coin,
|
||||
) -> Result<Self, MixnetContractError>;
|
||||
|
||||
fn maybe_add_track_vesting_unbond_mixnode_message(
|
||||
self,
|
||||
storage: &dyn Storage,
|
||||
proxy: Option<Addr>,
|
||||
owner: String,
|
||||
amount: Coin,
|
||||
) -> Result<Self, MixnetContractError>;
|
||||
}
|
||||
|
||||
impl VestingTracking for Response {
|
||||
fn maybe_add_track_vesting_undelegation_message(
|
||||
self,
|
||||
storage: &dyn Storage,
|
||||
proxy: Option<Addr>,
|
||||
owner: String,
|
||||
mix_id: MixId,
|
||||
amount: Coin,
|
||||
) -> Result<Self, MixnetContractError> {
|
||||
// if there's a proxy set (i.e. the vesting contract), send the track message
|
||||
if let Some(proxy) = proxy {
|
||||
let vesting_contract = mixnet_params_storage::vesting_contract_address(storage)?;
|
||||
|
||||
// Note: this can INTENTIONALLY cause epoch progression halt if the proxy is not the vesting contract
|
||||
// But this is fine, since this situation should have NEVER occurred in the first place
|
||||
// (as all 'on_behalf' methods, including 'DelegateToMixnodeOnBehalf' that got us here,
|
||||
// explicitly require the proxy to be the vesting contract)
|
||||
// 'fixing' it would require manually inspecting the problematic event, investigating
|
||||
// it's cause and manually (presumably via migration) clearing it.
|
||||
if proxy != vesting_contract {
|
||||
return Err(MixnetContractError::ProxyIsNotVestingContract {
|
||||
received: proxy,
|
||||
vesting_contract,
|
||||
});
|
||||
}
|
||||
|
||||
let msg = VestingContractExecuteMsg::TrackUndelegation {
|
||||
owner,
|
||||
mix_id,
|
||||
amount,
|
||||
};
|
||||
|
||||
let track_undelegate_message = wasm_execute(proxy, &msg, vec![])?;
|
||||
Ok(self.add_message(track_undelegate_message))
|
||||
} else {
|
||||
// there's no proxy so nothing to do
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_add_track_vesting_unbond_mixnode_message(
|
||||
self,
|
||||
storage: &dyn Storage,
|
||||
proxy: Option<Addr>,
|
||||
owner: String,
|
||||
amount: Coin,
|
||||
) -> Result<Self, MixnetContractError> {
|
||||
// if there's a proxy set (i.e. the vesting contract), send the track message
|
||||
if let Some(proxy) = proxy {
|
||||
let vesting_contract = mixnet_params_storage::vesting_contract_address(storage)?;
|
||||
|
||||
// exactly the same possible halting behaviour as in `maybe_add_track_vesting_undelegation_message`.
|
||||
if proxy != vesting_contract {
|
||||
return Err(MixnetContractError::ProxyIsNotVestingContract {
|
||||
received: proxy,
|
||||
vesting_contract,
|
||||
});
|
||||
}
|
||||
|
||||
let msg = VestingContractExecuteMsg::TrackUnbondMixnode { owner, amount };
|
||||
let track_unbond_message = wasm_execute(proxy, &msg, vec![])?;
|
||||
Ok(self.add_message(track_unbond_message))
|
||||
} else {
|
||||
// there's no proxy so nothing to do
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn debug_with_visibility<S: Into<String>>(api: &dyn Api, msg: S) {
|
||||
// api.debug(&*format!("\n\n\n=========================================\n{}\n=========================================\n\n\n", msg.into()));
|
||||
// }
|
||||
@@ -117,16 +214,80 @@ pub(crate) fn validate_delegation_stake(
|
||||
Ok(delegation.pop().unwrap())
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_is_authorized(
|
||||
sender: Addr,
|
||||
pub(crate) fn ensure_epoch_in_progress_state(
|
||||
storage: &dyn Storage,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
if sender != crate::mixnet_contract_settings::storage::rewarding_validator_address(storage)? {
|
||||
let epoch_status = crate::interval::storage::current_epoch_status(storage)?;
|
||||
if !matches!(epoch_status.state, EpochState::InProgress) {
|
||||
return Err(MixnetContractError::EpochAdvancementInProgress {
|
||||
current_state: epoch_status.state,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// pub(crate) fn ensure_mix_rewarding_state(storage: &dyn Storage) -> Result<(), MixnetContractError> {
|
||||
// let epoch_status = crate::interval::storage::current_epoch_status(storage)?;
|
||||
// if !matches!(epoch_status.state, EpochState::Rewarding { .. }) {
|
||||
// return Err(MixnetContractError::EpochNotInMixRewardingState {
|
||||
// current_state: epoch_status.state,
|
||||
// });
|
||||
// }
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// pub(crate) fn ensure_event_reconciliation_state(
|
||||
// storage: &dyn Storage,
|
||||
// ) -> Result<(), MixnetContractError> {
|
||||
// let epoch_status = crate::interval::storage::current_epoch_status(storage)?;
|
||||
// if !matches!(epoch_status.state, EpochState::ReconcilingEvents) {
|
||||
// return Err(MixnetContractError::EpochNotInEventReconciliationState {
|
||||
// current_state: epoch_status.state,
|
||||
// });
|
||||
// }
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// pub(crate) fn ensure_epoch_advancement_state(
|
||||
// storage: &dyn Storage,
|
||||
// ) -> Result<(), MixnetContractError> {
|
||||
// let epoch_status = crate::interval::storage::current_epoch_status(storage)?;
|
||||
// if !matches!(epoch_status.state, EpochState::AdvancingEpoch) {
|
||||
// return Err(MixnetContractError::EpochNotInAdvancementState {
|
||||
// current_state: epoch_status.state,
|
||||
// });
|
||||
// }
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
pub(crate) fn ensure_is_authorized(
|
||||
sender: &Addr,
|
||||
storage: &dyn Storage,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
if sender != &crate::mixnet_contract_settings::storage::rewarding_validator_address(storage)? {
|
||||
return Err(MixnetContractError::Unauthorized);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_can_advance_epoch(
|
||||
sender: &Addr,
|
||||
storage: &dyn Storage,
|
||||
) -> Result<EpochStatus, MixnetContractError> {
|
||||
let epoch_status = crate::interval::storage::current_epoch_status(storage)?;
|
||||
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
|
||||
ensure_is_authorized(sender, storage)?;
|
||||
return Err(MixnetContractError::RewardingValidatorMismatch {
|
||||
current_validator: sender.clone(),
|
||||
chosen_validator: epoch_status.being_advanced_by,
|
||||
});
|
||||
}
|
||||
Ok(epoch_status)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_is_owner(
|
||||
sender: Addr,
|
||||
storage: &dyn Storage,
|
||||
@@ -158,6 +319,22 @@ pub(crate) fn ensure_proxy_match(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_sent_by_vesting_contract(
|
||||
info: &MessageInfo,
|
||||
storage: &dyn Storage,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
let vesting_contract_address =
|
||||
crate::mixnet_contract_settings::storage::vesting_contract_address(storage)?;
|
||||
if info.sender != vesting_contract_address {
|
||||
Err(MixnetContractError::SenderIsNotVestingContract {
|
||||
received: info.sender.clone(),
|
||||
vesting_contract: vesting_contract_address,
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_bonded(bond: &MixNodeBond) -> Result<(), MixnetContractError> {
|
||||
if bond.is_unbonding {
|
||||
return Err(MixnetContractError::MixnodeIsUnbonding {
|
||||
@@ -170,8 +347,8 @@ pub(crate) fn ensure_bonded(bond: &MixNodeBond) -> Result<(), MixnetContractErro
|
||||
// check if the target address has already bonded a mixnode or gateway,
|
||||
// in either case, return an appropriate error
|
||||
pub(crate) fn ensure_no_existing_bond(
|
||||
storage: &dyn Storage,
|
||||
sender: &Addr,
|
||||
storage: &dyn Storage,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
if mixnodes_storage::mixnode_bonds()
|
||||
.idx
|
||||
|
||||
@@ -15,9 +15,11 @@ pub mod test_helpers {
|
||||
use crate::delegations::queries::query_mixnode_delegations_paged;
|
||||
use crate::delegations::storage as delegations_storage;
|
||||
use crate::delegations::transactions::try_delegate_to_mixnode;
|
||||
use crate::gateways::transactions::try_add_gateway;
|
||||
use crate::families::transactions::{try_create_family, try_join_family};
|
||||
use crate::gateways::storage as gateways_storage;
|
||||
use crate::gateways::transactions::{try_add_gateway, try_add_gateway_on_behalf};
|
||||
use crate::interval::transactions::{
|
||||
perform_pending_epoch_actions, perform_pending_interval_actions,
|
||||
perform_pending_epoch_actions, perform_pending_interval_actions, try_begin_epoch_transition,
|
||||
};
|
||||
use crate::interval::{pending_events, storage as interval_storage};
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
@@ -25,6 +27,7 @@ pub mod test_helpers {
|
||||
minimum_mixnode_pledge, rewarding_denom, rewarding_validator_address,
|
||||
};
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::mixnodes::storage::mixnode_bonds;
|
||||
use crate::mixnodes::transactions::{
|
||||
try_add_mixnode, try_add_mixnode_on_behalf, try_remove_mixnode,
|
||||
};
|
||||
@@ -40,7 +43,7 @@ pub mod test_helpers {
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::testing::MockApi;
|
||||
use cosmwasm_std::testing::MockQuerier;
|
||||
use cosmwasm_std::{coin, Addr, BankMsg, CosmosMsg, Storage};
|
||||
use cosmwasm_std::{coin, Addr, Api, BankMsg, CosmosMsg, Storage};
|
||||
use cosmwasm_std::{Coin, Order};
|
||||
use cosmwasm_std::{Decimal, Empty, MemoryStorage};
|
||||
use cosmwasm_std::{Deps, OwnedDeps};
|
||||
@@ -56,9 +59,10 @@ pub mod test_helpers {
|
||||
use mixnet_contract_common::rewarding::simulator::Simulator;
|
||||
use mixnet_contract_common::rewarding::RewardDistribution;
|
||||
use mixnet_contract_common::{
|
||||
Delegation, Gateway, InitialRewardingParams, InstantiateMsg, Interval, MixId, MixNode,
|
||||
Percent, RewardedSetNodeStatus,
|
||||
Delegation, EpochState, EpochStatus, Gateway, IdentityKey, InitialRewardingParams,
|
||||
InstantiateMsg, Interval, MixId, MixNode, MixNodeBond, Percent, RewardedSetNodeStatus,
|
||||
};
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_crypto::asymmetric::identity::KeyPair;
|
||||
use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
@@ -157,6 +161,42 @@ pub mod test_helpers {
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn join_family(
|
||||
&mut self,
|
||||
member: &str,
|
||||
member_keys: &identity::KeyPair,
|
||||
head_keys: &identity::KeyPair,
|
||||
) {
|
||||
let identity_signature = member_keys.private_key().sign_text(member);
|
||||
let join_signature = head_keys
|
||||
.private_key()
|
||||
.sign(&member_keys.public_key().to_bytes())
|
||||
.to_base58_string();
|
||||
let head_identity = head_keys.public_key().to_base58_string();
|
||||
|
||||
try_join_family(
|
||||
self.deps_mut(),
|
||||
mock_info(member, &[]),
|
||||
Some(identity_signature),
|
||||
join_signature,
|
||||
head_identity,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn create_dummy_mixnode_with_new_family(
|
||||
&mut self,
|
||||
head: &str,
|
||||
label: &str,
|
||||
) -> (MixId, identity::KeyPair) {
|
||||
let (mix_id, keys) = self.add_dummy_mixnode_with_keypair(head, None);
|
||||
let sig = keys.private_key().sign_text(head);
|
||||
|
||||
try_create_family(self.deps_mut(), mock_info(head, &[]), sig, label).unwrap();
|
||||
(mix_id, keys)
|
||||
}
|
||||
|
||||
pub fn add_dummy_mixnode(&mut self, owner: &str, stake: Option<Uint128>) -> MixId {
|
||||
let stake = match stake {
|
||||
Some(amount) => {
|
||||
@@ -170,12 +210,11 @@ pub mod test_helpers {
|
||||
add_mixnode(&mut self.rng, self.deps.as_mut(), env, owner, vec![stake])
|
||||
}
|
||||
|
||||
pub fn add_dummy_mixnode_with_proxy(
|
||||
pub fn add_dummy_mixnode_with_keypair(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
stake: Option<Uint128>,
|
||||
proxy: Addr,
|
||||
) -> MixId {
|
||||
) -> (MixId, identity::KeyPair) {
|
||||
let stake = match stake {
|
||||
Some(amount) => {
|
||||
let denom = rewarding_denom(self.deps().storage).unwrap();
|
||||
@@ -184,7 +223,9 @@ pub mod test_helpers {
|
||||
None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(),
|
||||
};
|
||||
|
||||
let keypair = nym_crypto::asymmetric::identity::KeyPair::new(&mut self.rng);
|
||||
let proxy = self.vesting_contract();
|
||||
|
||||
let keypair = identity::KeyPair::new(&mut self.rng);
|
||||
let owner_signature = keypair
|
||||
.private_key()
|
||||
.sign(owner.as_bytes())
|
||||
@@ -216,7 +257,98 @@ pub mod test_helpers {
|
||||
.unwrap();
|
||||
|
||||
// newly added mixnode gets assigned the current counter + 1
|
||||
current_id_counter + 1
|
||||
(current_id_counter + 1, keypair)
|
||||
}
|
||||
|
||||
pub fn add_dummy_mixnode_with_legal_proxy(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
stake: Option<Uint128>,
|
||||
) -> MixId {
|
||||
self.add_dummy_mixnode_with_keypair(owner, stake).0
|
||||
}
|
||||
|
||||
pub fn set_illegal_mixnode_proxy(&mut self, mix_id: MixId, proxy: Addr) {
|
||||
let mut bond_details = mixnodes_storage::mixnode_bonds()
|
||||
.load(self.deps().storage, mix_id)
|
||||
.unwrap();
|
||||
bond_details.proxy = Some(proxy);
|
||||
mixnodes_storage::mixnode_bonds()
|
||||
.save(self.deps_mut().storage, mix_id, &bond_details)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn add_dummy_gateway_with_illegal_proxy(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
stake: Option<Uint128>,
|
||||
proxy: Addr,
|
||||
) -> IdentityKey {
|
||||
let gateway_identity = self.add_dummy_gateway_with_legal_proxy(owner, stake);
|
||||
self.set_illegal_gateway_proxy(&gateway_identity, proxy);
|
||||
gateway_identity
|
||||
}
|
||||
|
||||
pub fn set_illegal_gateway_proxy(&mut self, gateway_id: &str, proxy: Addr) {
|
||||
let mut gateway = gateways_storage::gateways()
|
||||
.load(self.deps().storage, gateway_id)
|
||||
.unwrap();
|
||||
gateway.proxy = Some(proxy);
|
||||
gateways_storage::gateways()
|
||||
.save(self.deps_mut().storage, gateway_id, &gateway)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn add_dummy_gateway_with_legal_proxy(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
stake: Option<Uint128>,
|
||||
) -> IdentityKey {
|
||||
let stake = match stake {
|
||||
Some(amount) => {
|
||||
let denom = rewarding_denom(self.deps().storage).unwrap();
|
||||
Coin { denom, amount }
|
||||
}
|
||||
None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(),
|
||||
};
|
||||
|
||||
let proxy = self.vesting_contract();
|
||||
|
||||
let legit_sphinx_key = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng);
|
||||
let keypair = nym_crypto::asymmetric::identity::KeyPair::new(&mut self.rng);
|
||||
let owner_signature = keypair
|
||||
.private_key()
|
||||
.sign(owner.as_bytes())
|
||||
.to_base58_string();
|
||||
|
||||
let env = self.env();
|
||||
let info = mock_info(proxy.as_ref(), &[stake]);
|
||||
|
||||
try_add_gateway_on_behalf(
|
||||
self.deps_mut(),
|
||||
env,
|
||||
info,
|
||||
Gateway {
|
||||
identity_key: keypair.public_key().to_base58_string(),
|
||||
sphinx_key: legit_sphinx_key.public_key().to_base58_string(),
|
||||
..tests::fixtures::gateway_fixture()
|
||||
},
|
||||
owner.to_string(),
|
||||
owner_signature,
|
||||
)
|
||||
.unwrap();
|
||||
keypair.public_key().to_base58_string()
|
||||
}
|
||||
|
||||
pub fn add_dummy_mixnode_with_illegal_proxy(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
stake: Option<Uint128>,
|
||||
proxy: Addr,
|
||||
) -> MixId {
|
||||
let mix_id = self.add_dummy_mixnode_with_legal_proxy(owner, stake);
|
||||
self.set_illegal_mixnode_proxy(mix_id, proxy);
|
||||
mix_id
|
||||
}
|
||||
|
||||
pub fn start_unbonding_mixnode(&mut self, mix_id: MixId) {
|
||||
@@ -263,7 +395,34 @@ pub mod test_helpers {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn add_immediate_delegation_with_proxy(
|
||||
pub fn add_immediate_delegation_with_legal_proxy(
|
||||
&mut self,
|
||||
delegator: &str,
|
||||
amount: impl Into<Uint128>,
|
||||
target: MixId,
|
||||
) {
|
||||
let denom = rewarding_denom(self.deps().storage).unwrap();
|
||||
let amount = Coin {
|
||||
denom,
|
||||
amount: amount.into(),
|
||||
};
|
||||
let env = self.env();
|
||||
let proxy = self.vesting_contract();
|
||||
pending_events::delegate(
|
||||
self.deps_mut(),
|
||||
&env,
|
||||
env.block.height,
|
||||
Addr::unchecked(delegator),
|
||||
target,
|
||||
amount,
|
||||
Some(proxy),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// to set illegal proxy we have to bypass "normal" flow and put the value
|
||||
// directly into the storage
|
||||
pub fn add_immediate_delegation_with_illegal_proxy(
|
||||
&mut self,
|
||||
delegator: &str,
|
||||
amount: impl Into<Uint128>,
|
||||
@@ -275,17 +434,41 @@ pub mod test_helpers {
|
||||
denom,
|
||||
amount: amount.into(),
|
||||
};
|
||||
let env = self.env();
|
||||
pending_events::delegate(
|
||||
self.deps_mut(),
|
||||
&env,
|
||||
env.block.height,
|
||||
Addr::unchecked(delegator),
|
||||
|
||||
let owner = self.deps.api.addr_validate(delegator).unwrap();
|
||||
let storage_key = Delegation::generate_storage_key(target, &owner, Some(&proxy));
|
||||
|
||||
let mut mix_rewarding = self.mix_rewarding(target);
|
||||
|
||||
let mut stored_delegation_amount = amount;
|
||||
|
||||
if let Some(existing_delegation) = delegations_storage::delegations()
|
||||
.may_load(&self.deps.storage, storage_key.clone())
|
||||
.unwrap()
|
||||
{
|
||||
let og_with_reward = mix_rewarding.undelegate(&existing_delegation).unwrap();
|
||||
stored_delegation_amount.amount += og_with_reward.amount;
|
||||
}
|
||||
|
||||
mix_rewarding
|
||||
.add_base_delegation(stored_delegation_amount.amount)
|
||||
.unwrap();
|
||||
|
||||
let delegation = Delegation::new(
|
||||
owner,
|
||||
target,
|
||||
amount,
|
||||
mix_rewarding.total_unit_reward,
|
||||
stored_delegation_amount,
|
||||
self.env.block.height,
|
||||
Some(proxy),
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
delegations_storage::delegations()
|
||||
.save(&mut self.deps.storage, storage_key, &delegation)
|
||||
.unwrap();
|
||||
rewards_storage::MIXNODE_REWARDING
|
||||
.save(&mut self.deps.storage, target, &mix_rewarding)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
@@ -316,6 +499,48 @@ pub mod test_helpers {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn start_epoch_transition(&mut self) {
|
||||
let env = self.env.clone();
|
||||
let sender = self.rewarding_validator.clone();
|
||||
try_begin_epoch_transition(self.deps_mut(), env, sender).unwrap();
|
||||
}
|
||||
|
||||
pub fn set_epoch_in_progress_state(&mut self) {
|
||||
let being_advanced_by = self.rewarding_validator.sender.clone();
|
||||
interval_storage::save_current_epoch_status(
|
||||
self.deps_mut().storage,
|
||||
&EpochStatus {
|
||||
being_advanced_by,
|
||||
state: EpochState::InProgress,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set_epoch_reconciliation_state(&mut self) {
|
||||
let being_advanced_by = self.rewarding_validator.sender.clone();
|
||||
interval_storage::save_current_epoch_status(
|
||||
self.deps_mut().storage,
|
||||
&EpochStatus {
|
||||
being_advanced_by,
|
||||
state: EpochState::ReconcilingEvents,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set_epoch_advancement_state(&mut self) {
|
||||
let being_advanced_by = self.rewarding_validator.sender.clone();
|
||||
interval_storage::save_current_epoch_status(
|
||||
self.deps_mut().storage,
|
||||
&EpochStatus {
|
||||
being_advanced_by,
|
||||
state: EpochState::AdvancingEpoch,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn pending_operator_reward(&mut self, mix: MixId) -> Decimal {
|
||||
query_pending_mixnode_operator_reward(self.deps(), mix)
|
||||
@@ -376,10 +601,12 @@ pub mod test_helpers {
|
||||
)
|
||||
}
|
||||
|
||||
interval_storage::save_interval(self.deps_mut().storage, &advanced).unwrap()
|
||||
interval_storage::save_interval(self.deps_mut().storage, &advanced).unwrap();
|
||||
// if we're going into next epoch, we're back into in progress
|
||||
self.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
pub fn update_rewarded_set(&mut self, nodes: Vec<MixId>) {
|
||||
pub fn force_change_rewarded_set(&mut self, nodes: Vec<MixId>) {
|
||||
let active_set_size = rewards_storage::REWARDING_PARAMS
|
||||
.load(self.deps().storage)
|
||||
.unwrap()
|
||||
@@ -411,6 +638,20 @@ pub mod test_helpers {
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn reward_with_distribution_with_state_bypass(
|
||||
&mut self,
|
||||
mix_id: MixId,
|
||||
performance: Performance,
|
||||
) -> RewardDistribution {
|
||||
let initial_status =
|
||||
interval_storage::current_epoch_status(self.deps().storage).unwrap();
|
||||
self.start_epoch_transition();
|
||||
let res = self.reward_with_distribution(mix_id, performance);
|
||||
interval_storage::save_current_epoch_status(self.deps_mut().storage, &initial_status)
|
||||
.unwrap();
|
||||
res
|
||||
}
|
||||
|
||||
pub fn reward_with_distribution(
|
||||
&mut self,
|
||||
mix_id: MixId,
|
||||
@@ -463,6 +704,11 @@ pub mod test_helpers {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn mix_bond(&self, mix_id: MixId) -> MixNodeBond {
|
||||
mixnode_bonds().load(self.deps().storage, mix_id).unwrap()
|
||||
}
|
||||
|
||||
pub fn delegation(&self, mix: MixId, owner: &str, proxy: &Option<Addr>) -> Delegation {
|
||||
read_delegation(self.deps().storage, mix, &Addr::unchecked(owner), proxy).unwrap()
|
||||
}
|
||||
|
||||
@@ -25,11 +25,13 @@ contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-com
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.1.0" }
|
||||
|
||||
cosmwasm-std = { version = "1.0.0 "}
|
||||
cw2 = { version = "0.13.4" }
|
||||
cw-storage-plus = { version = "0.13.4", features = ["iterator"] }
|
||||
|
||||
schemars = "0.8"
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
thiserror = { version = "1.0" }
|
||||
semver = { version = "1.0.16", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.3.1"
|
||||
@@ -38,4 +40,7 @@ hex = "0.4.3"
|
||||
serde_json = "1.0.66"
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] }
|
||||
vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] }
|
||||
|
||||
[profile.release]
|
||||
overflow-checks = true
|
||||
@@ -10,11 +10,12 @@ use crate::vesting::{populate_vesting_periods, Account};
|
||||
use contracts_common::ContractBuildInformation;
|
||||
use cosmwasm_std::{
|
||||
coin, entry_point, to_binary, Addr, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Order,
|
||||
QueryResponse, Response, StdResult, Timestamp, Uint128,
|
||||
QueryResponse, Response, StdError, StdResult, Timestamp, Uint128,
|
||||
};
|
||||
use cw_storage_plus::Bound;
|
||||
use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
|
||||
use mixnet_contract_common::{Gateway, MixId, MixNode};
|
||||
use semver::Version;
|
||||
use vesting_contract_common::events::{
|
||||
new_ownership_transfer_event, new_periodic_vesting_account_event,
|
||||
new_staking_address_update_event, new_track_gateway_unbond_event,
|
||||
@@ -30,8 +31,25 @@ use vesting_contract_common::{
|
||||
VestingCoinsResponse, VestingDelegation,
|
||||
};
|
||||
|
||||
// version info for migration info
|
||||
const CONTRACT_NAME: &str = "crate:nym-vesting-contract";
|
||||
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub const INITIAL_LOCKED_PLEDGE_CAP: Uint128 = Uint128::new(100_000_000_000);
|
||||
|
||||
// as of 01.02.23 the maximum number of delegations anyone has made towards particular mixnode is 12.
|
||||
// thus restricting it to 25, which is more than double of that, doesn't seem too unreasonable.
|
||||
// and is going to alleviate the issue of unbounded iteration in `remove_delegations_for_mix`
|
||||
// that happens upon advancing the current epoch.
|
||||
//
|
||||
// However, do note it doesn't necessarily mean that upon reaching this limit it's impossible to perform
|
||||
// further delegations (towards the same node)
|
||||
// while this might not be the best workaround, you could remove the existing delegation
|
||||
// (thus removing all separate entries from the storage, i.e. the `DELEGATIONS` map)
|
||||
// and re-delegate it with the reclaimed amount (which will include all rewards)
|
||||
// which will only result in a single key-value being stored.
|
||||
pub const MAX_PER_MIX_DELEGATIONS: u32 = 25;
|
||||
|
||||
/// Instantiate the contract
|
||||
#[entry_point]
|
||||
pub fn instantiate(
|
||||
@@ -47,11 +65,48 @@ pub fn instantiate(
|
||||
ADMIN.save(deps.storage, &info.sender)?;
|
||||
MIXNET_CONTRACT_ADDRESS.save(deps.storage, &mixnet_contract_address)?;
|
||||
MIX_DENOM.save(deps.storage, &msg.mix_denom)?;
|
||||
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
|
||||
pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
|
||||
// this is the first migration that uses cw2 standard and thus the value in the storage doesn't yet exist
|
||||
// set it instead.
|
||||
if matches!(
|
||||
cw2::get_contract_version(deps.storage),
|
||||
Err(StdError::NotFound { .. })
|
||||
) {
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
}
|
||||
|
||||
// note: don't remove this particular bit of code as we have to ALWAYS check whether we have to update the stored version
|
||||
let version: Version =
|
||||
CONTRACT_VERSION
|
||||
.parse()
|
||||
.map_err(|error: semver::Error| ContractError::SemVerFailure {
|
||||
value: CONTRACT_VERSION.to_string(),
|
||||
error_message: error.to_string(),
|
||||
})?;
|
||||
|
||||
let storage_version_raw = cw2::get_contract_version(deps.storage)?.version;
|
||||
let storage_version: Version =
|
||||
storage_version_raw
|
||||
.parse()
|
||||
.map_err(|error: semver::Error| ContractError::SemVerFailure {
|
||||
value: storage_version_raw,
|
||||
error_message: error.to_string(),
|
||||
})?;
|
||||
|
||||
if storage_version < version {
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
// If state structure changed in any contract version in the way migration is needed, it
|
||||
// should occur here
|
||||
}
|
||||
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
@@ -68,13 +123,20 @@ pub fn execute(
|
||||
label,
|
||||
} => try_create_family(info, deps, owner_signature, label),
|
||||
ExecuteMsg::JoinFamily {
|
||||
signature,
|
||||
node_identity_signature,
|
||||
family_signature,
|
||||
family_head,
|
||||
} => try_join_family(info, deps, signature, family_head),
|
||||
} => try_join_family(
|
||||
info,
|
||||
deps,
|
||||
node_identity_signature,
|
||||
family_signature,
|
||||
family_head,
|
||||
),
|
||||
ExecuteMsg::LeaveFamily {
|
||||
signature,
|
||||
node_identity_signature,
|
||||
family_head,
|
||||
} => try_leave_family(info, deps, signature, family_head),
|
||||
} => try_leave_family(info, deps, node_identity_signature, family_head),
|
||||
ExecuteMsg::KickFamilyMember { signature, member } => {
|
||||
try_kick_family_member(info, deps, signature, member)
|
||||
}
|
||||
@@ -177,20 +239,26 @@ pub fn try_create_family(
|
||||
pub fn try_join_family(
|
||||
info: MessageInfo,
|
||||
deps: DepsMut,
|
||||
signature: String,
|
||||
node_identity_signature: String,
|
||||
family_signature: String,
|
||||
family_head: String,
|
||||
) -> Result<Response, ContractError> {
|
||||
let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?;
|
||||
account.try_join_family(deps.storage, signature, &family_head)
|
||||
account.try_join_family(
|
||||
deps.storage,
|
||||
node_identity_signature,
|
||||
family_signature,
|
||||
&family_head,
|
||||
)
|
||||
}
|
||||
pub fn try_leave_family(
|
||||
info: MessageInfo,
|
||||
deps: DepsMut,
|
||||
signature: String,
|
||||
node_identity_signature: String,
|
||||
family_head: String,
|
||||
) -> Result<Response, ContractError> {
|
||||
let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?;
|
||||
account.try_leave_family(deps.storage, signature, &family_head)
|
||||
account.try_leave_family(deps.storage, node_identity_signature, &family_head)
|
||||
}
|
||||
pub fn try_kick_family_member(
|
||||
info: MessageInfo,
|
||||
@@ -474,6 +542,14 @@ fn try_delegate_to_mixnode(
|
||||
env: Env,
|
||||
deps: DepsMut<'_>,
|
||||
) -> Result<Response, ContractError> {
|
||||
// TODO
|
||||
// as of 01.02.23
|
||||
// thus restricting it to 25, which is more than double of that, doesn't seem too unreasonable.
|
||||
|
||||
// while this might not be the best workaround, if user wishes to delegate more tokens towards the same node
|
||||
// they could remove the existing delegation (thus removing all separate entries from the storage)
|
||||
// and re-delegate it with the reclaimed amount (which will include all rewards).
|
||||
|
||||
let mix_denom = MIX_DENOM.load(deps.storage)?;
|
||||
let amount = validate_funds(&[amount], mix_denom)?;
|
||||
|
||||
@@ -602,6 +678,7 @@ pub(crate) fn try_create_periodic_vesting_account(
|
||||
pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
|
||||
let query_res = match msg {
|
||||
QueryMsg::GetContractVersion {} => to_binary(&get_contract_version()),
|
||||
QueryMsg::GetCW2ContractVersion {} => to_binary(&cw2::get_contract_version(deps.storage)?),
|
||||
QueryMsg::GetAccountsPaged {
|
||||
start_next_after,
|
||||
limit,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::storage::AccountStorageKey;
|
||||
use cosmwasm_std::{Addr, OverflowError, StdError, Uint128};
|
||||
use mixnet_contract_common::MixId;
|
||||
use thiserror::Error;
|
||||
@@ -84,6 +85,21 @@ pub enum ContractError {
|
||||
#[error("VESTING: {address} is not permitted to perform staking on behalf of {for_account}")]
|
||||
InvalidStakingAccount { address: Addr, for_account: Addr },
|
||||
|
||||
#[error("VESTING: {address} ({acc_id} has already performed {num} individual delegations towards {mix_id}. No further delegations are allowed. Please consider consolidating those delegations instead. The current cap is {cap}.")]
|
||||
TooManyDelegations {
|
||||
address: Addr,
|
||||
acc_id: AccountStorageKey,
|
||||
mix_id: MixId,
|
||||
num: u32,
|
||||
cap: u32,
|
||||
},
|
||||
|
||||
#[error("VESTING: Failed to parse {value} into a valid SemVer version: {error_message}")]
|
||||
SemVerFailure {
|
||||
value: String,
|
||||
error_message: String,
|
||||
},
|
||||
|
||||
#[error("VESTING: {message}")]
|
||||
Other { message: String },
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::errors::ContractError;
|
||||
use crate::vesting::Account;
|
||||
use cosmwasm_std::Order;
|
||||
use cosmwasm_std::{Addr, Api, Storage, Uint128};
|
||||
use cw_storage_plus::{Item, Map};
|
||||
use mixnet_contract_common::{IdentityKey, MixId};
|
||||
@@ -55,7 +56,13 @@ pub fn save_delegation(
|
||||
amount: Uint128,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError> {
|
||||
DELEGATIONS.save(storage, key, &amount)?;
|
||||
let existing_delegation_amount = if let Some(delegation) = DELEGATIONS.may_load(storage, key)? {
|
||||
delegation
|
||||
} else {
|
||||
Uint128::zero()
|
||||
};
|
||||
let new_delegations_amount = existing_delegation_amount + amount;
|
||||
DELEGATIONS.save(storage, key, &new_delegations_amount)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -67,6 +74,27 @@ pub fn remove_delegation(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_delegation_timestamps(
|
||||
prefix: (AccountStorageKey, MixId),
|
||||
storage: &dyn Storage,
|
||||
) -> Result<Vec<BlockTimestampSecs>, ContractError> {
|
||||
let block_timestamps = DELEGATIONS
|
||||
.prefix(prefix)
|
||||
.keys(storage, None, None, Order::Ascending)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(block_timestamps)
|
||||
}
|
||||
|
||||
pub fn count_subdelegations_for_mix(
|
||||
prefix: (AccountStorageKey, MixId),
|
||||
storage: &dyn Storage,
|
||||
) -> u32 {
|
||||
DELEGATIONS
|
||||
.prefix(prefix)
|
||||
.keys(storage, None, None, Order::Ascending)
|
||||
.count() as u32
|
||||
}
|
||||
|
||||
pub fn load_withdrawn(
|
||||
key: AccountStorageKey,
|
||||
storage: &dyn Storage,
|
||||
|
||||
@@ -13,7 +13,8 @@ pub trait NodeFamilies {
|
||||
fn try_join_family(
|
||||
&self,
|
||||
storage: &dyn Storage,
|
||||
signature: String,
|
||||
node_identity_signature: String,
|
||||
family_signature: String,
|
||||
family_head: IdentityKeyRef,
|
||||
) -> Result<Response, ContractError>;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::contract::MAX_PER_MIX_DELEGATIONS;
|
||||
use crate::errors::ContractError;
|
||||
use crate::storage::save_delegation;
|
||||
use crate::storage::MIXNET_CONTRACT_ADDRESS;
|
||||
@@ -36,6 +37,17 @@ impl DelegatingAccount for Account {
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Response, ContractError> {
|
||||
let current_balance = self.ensure_valid_additional_stake(&coin, storage)?;
|
||||
let num_subdelegations = self.num_subdelegations_for_mix(mix_id, storage);
|
||||
|
||||
if num_subdelegations >= MAX_PER_MIX_DELEGATIONS {
|
||||
return Err(ContractError::TooManyDelegations {
|
||||
address: self.owner_address.clone(),
|
||||
acc_id: self.storage_key(),
|
||||
mix_id,
|
||||
num: num_subdelegations,
|
||||
cap: MAX_PER_MIX_DELEGATIONS,
|
||||
});
|
||||
}
|
||||
|
||||
let msg = MixnetExecuteMsg::DelegateToMixnodeOnBehalf {
|
||||
mix_id,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use super::VestingPeriod;
|
||||
use crate::errors::ContractError;
|
||||
use crate::storage::{
|
||||
load_balance, load_bond_pledge, load_gateway_pledge, load_withdrawn, remove_bond_pledge,
|
||||
remove_delegation, remove_gateway_pledge, save_account, save_balance, save_bond_pledge,
|
||||
save_gateway_pledge, save_withdrawn, AccountStorageKey, BlockTimestampSecs, DELEGATIONS, KEY,
|
||||
count_subdelegations_for_mix, load_balance, load_bond_pledge, load_delegation_timestamps,
|
||||
load_gateway_pledge, load_withdrawn, remove_bond_pledge, remove_delegation,
|
||||
remove_gateway_pledge, save_account, save_balance, save_bond_pledge, save_gateway_pledge,
|
||||
save_withdrawn, AccountStorageKey, BlockTimestampSecs, DELEGATIONS, KEY,
|
||||
};
|
||||
use crate::traits::VestingAccount;
|
||||
use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128};
|
||||
use cw_storage_plus::Bound;
|
||||
use mixnet_contract_common::MixId;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -274,39 +274,21 @@ impl Account {
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn num_subdelegations_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> u32 {
|
||||
count_subdelegations_for_mix((self.storage_key(), mix_id), storage)
|
||||
}
|
||||
|
||||
pub fn remove_delegations_for_mix(
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError> {
|
||||
let limit = 50;
|
||||
let mut start_after = None;
|
||||
let mut block_heights = Vec::new();
|
||||
let mut prev_len = 0;
|
||||
// TODO: Test this
|
||||
loop {
|
||||
block_heights.extend(
|
||||
DELEGATIONS
|
||||
.prefix((self.storage_key(), mix_id))
|
||||
.keys(storage, start_after, None, Order::Ascending)
|
||||
.take(limit)
|
||||
.filter_map(|key| key.ok()),
|
||||
);
|
||||
// note that the limit is implicitly set to `MAX_PER_MIX_DELEGATIONS`
|
||||
// as it should be impossible to create more delegations than that.
|
||||
let block_timestamps = load_delegation_timestamps((self.storage_key(), mix_id), storage)?;
|
||||
|
||||
if prev_len == block_heights.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
prev_len = block_heights.len();
|
||||
|
||||
start_after = block_heights.last().map(|last| Bound::exclusive(*last));
|
||||
if start_after.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for block_height in block_heights {
|
||||
remove_delegation((self.storage_key(), mix_id, block_height), storage)?;
|
||||
for block_timestamp in block_timestamps {
|
||||
remove_delegation((self.storage_key(), mix_id, block_timestamp), storage)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -24,12 +24,14 @@ impl NodeFamilies for Account {
|
||||
fn try_join_family(
|
||||
&self,
|
||||
storage: &dyn Storage,
|
||||
signature: String,
|
||||
node_identity_signature: String,
|
||||
family_signature: String,
|
||||
family_head: IdentityKeyRef,
|
||||
) -> Result<Response, ContractError> {
|
||||
let msg = MixnetExecuteMsg::JoinFamilyOnBehalf {
|
||||
member_address: self.owner_address().to_string(),
|
||||
signature,
|
||||
node_identity_signature,
|
||||
family_signature,
|
||||
family_head: family_head.to_string(),
|
||||
};
|
||||
|
||||
@@ -41,12 +43,12 @@ impl NodeFamilies for Account {
|
||||
fn try_leave_family(
|
||||
&self,
|
||||
storage: &dyn Storage,
|
||||
signature: String,
|
||||
node_identity_signature: String,
|
||||
family_head: IdentityKeyRef,
|
||||
) -> Result<Response, ContractError> {
|
||||
let msg = MixnetExecuteMsg::LeaveFamilyOnBehalf {
|
||||
member_address: self.owner_address().to_string(),
|
||||
signature,
|
||||
node_identity_signature,
|
||||
family_head: family_head.to_string(),
|
||||
};
|
||||
|
||||
|
||||
@@ -46,11 +46,12 @@ mod tests {
|
||||
use crate::traits::DelegatingAccount;
|
||||
use crate::traits::VestingAccount;
|
||||
use crate::traits::{GatewayBondingAccount, MixnodeBondingAccount};
|
||||
use crate::vesting::{populate_vesting_periods, Account};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::{coin, coins, Addr, Coin, Timestamp, Uint128};
|
||||
use mixnet_contract_common::mixnode::MixNodeCostParams;
|
||||
use mixnet_contract_common::{Gateway, MixNode, Percent};
|
||||
use vesting_contract_common::messages::ExecuteMsg;
|
||||
use vesting_contract_common::messages::{ExecuteMsg, VestingSpecification};
|
||||
use vesting_contract_common::Period;
|
||||
use vesting_contract_common::PledgeCap;
|
||||
|
||||
@@ -778,4 +779,75 @@ mod tests {
|
||||
let pledge = account.load_gateway_pledge(&deps.storage).unwrap().unwrap();
|
||||
assert_eq!(Uint128::new(90_000_000_000), pledge.amount().amount);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delegations_cap() {
|
||||
let mut deps = init_contract();
|
||||
let mut env = mock_env();
|
||||
|
||||
let vesting_period_length_secs = 3600;
|
||||
|
||||
let account_creation_timestamp = 1650000000;
|
||||
let account_creation_blockheight = 12345;
|
||||
|
||||
env.block.height = account_creation_blockheight;
|
||||
env.block.time = Timestamp::from_seconds(account_creation_timestamp);
|
||||
|
||||
// lets define some helper timestamps
|
||||
|
||||
// lets create our vesting account
|
||||
let periods = populate_vesting_periods(
|
||||
account_creation_timestamp,
|
||||
VestingSpecification::new(None, Some(vesting_period_length_secs), None),
|
||||
);
|
||||
|
||||
let vesting_account = Account::new(
|
||||
Addr::unchecked("owner"),
|
||||
Some(Addr::unchecked("staking")),
|
||||
Coin {
|
||||
amount: Uint128::new(1_000_000_000_000),
|
||||
denom: TEST_COIN_DENOM.to_string(),
|
||||
},
|
||||
Timestamp::from_seconds(account_creation_timestamp),
|
||||
periods,
|
||||
Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))),
|
||||
deps.as_mut().storage,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// time for some delegations
|
||||
|
||||
let mix_id = 42;
|
||||
|
||||
let delegation = Coin {
|
||||
amount: Uint128::new(42),
|
||||
denom: TEST_COIN_DENOM.to_string(),
|
||||
};
|
||||
|
||||
// you can have at most `MAX_PER_MIX_DELEGATIONS` delegations so those should be fine
|
||||
for _ in 0..MAX_PER_MIX_DELEGATIONS {
|
||||
vesting_account
|
||||
.try_delegate_to_mixnode(mix_id, delegation.clone(), &env, &mut deps.storage)
|
||||
.unwrap();
|
||||
|
||||
env.block.height += 1;
|
||||
env.block.time = env.block.time.plus_seconds(42);
|
||||
}
|
||||
|
||||
// but the additional one is going to fail
|
||||
let res = vesting_account
|
||||
.try_delegate_to_mixnode(mix_id, delegation.clone(), &env, &mut deps.storage)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
ContractError::TooManyDelegations {
|
||||
address: vesting_account.owner_address(),
|
||||
acc_id: vesting_account.storage_key(),
|
||||
mix_id,
|
||||
num: MAX_PER_MIX_DELEGATIONS,
|
||||
cap: MAX_PER_MIX_DELEGATIONS
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ serde_json = "1.0.66"
|
||||
thiserror = "1.0.29"
|
||||
tokio = {version = "1.21.2", features = ["full"] }
|
||||
maxminddb = "0.23.0"
|
||||
dotenv = "0.15.0"
|
||||
dotenvy = "0.15.6"
|
||||
rand = "0.8.5"
|
||||
rand_seeder = "0.2.3"
|
||||
rand_pcg = "0.3.1"
|
||||
|
||||
@@ -4,7 +4,7 @@ extern crate rocket;
|
||||
extern crate rocket_okapi;
|
||||
|
||||
use clap::Parser;
|
||||
use dotenv::dotenv;
|
||||
use dotenvy::dotenv;
|
||||
use log::info;
|
||||
use nym_bin_common::logging::setup_logging;
|
||||
use nym_network_defaults::setup_env;
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ clap = { version = "4.0", features = ["cargo", "derive"] }
|
||||
colored = "2.0"
|
||||
dashmap = "4.0"
|
||||
dirs = "4.0"
|
||||
dotenv = "0.15.0"
|
||||
dotenvy = { workspace = true }
|
||||
futures = "0.3"
|
||||
humantime-serde = "1.0.1"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
@@ -12,9 +12,9 @@ mod tests {
|
||||
|
||||
fn get_order(prefix: &str) -> anyhow::Result<(OrderSignature, String)> {
|
||||
let mnemonic = "crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove";
|
||||
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse().unwrap())?;
|
||||
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse()?);
|
||||
|
||||
let accounts = wallet.try_derive_accounts().unwrap();
|
||||
let accounts = wallet.try_derive_accounts()?;
|
||||
|
||||
let message = "This is the order message from Bity";
|
||||
let signature = sign::sign_order(&wallet, &accounts[0], message.to_string())?;
|
||||
|
||||
@@ -22,7 +22,6 @@ clap = { version = "4.0", features = ["cargo", "derive"] }
|
||||
colored = "2.0"
|
||||
cupid = "0.6.1"
|
||||
dirs = "4.0"
|
||||
dotenv = "0.15.0"
|
||||
futures = "0.3.0"
|
||||
humantime-serde = "1.0"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
+1
-2
@@ -22,7 +22,6 @@ cfg-if = "1.0"
|
||||
clap = { version = "4.0", features = ["cargo", "derive"] }
|
||||
console-subscriber = { version = "0.1.1", optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
|
||||
dirs = "4.0"
|
||||
dotenv = "0.15.0"
|
||||
futures = "0.3.24"
|
||||
humantime-serde = "1.0"
|
||||
lazy_static = "1.4.0"
|
||||
@@ -53,7 +52,7 @@ ts-rs = {version = "6.1", optional = true}
|
||||
anyhow = "1.0"
|
||||
getset = "0.1.1"
|
||||
|
||||
sqlx = { version = "0.6.1", features = [
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"macros",
|
||||
|
||||
@@ -18,7 +18,7 @@ use coconut_dkg_common::types::EpochState;
|
||||
use dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_task::{TaskClient, TaskManager};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::time::interval;
|
||||
@@ -46,7 +46,7 @@ pub(crate) struct DkgController<R> {
|
||||
polling_rate: Duration,
|
||||
}
|
||||
|
||||
impl<R: RngCore + Clone> DkgController<R> {
|
||||
impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
|
||||
pub(crate) async fn new(
|
||||
config: &Config,
|
||||
nyxd_client: nyxd::Client,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::node_status_api::models::NymApiStorageError;
|
||||
use nym_mixnet_contract_common::EpochState;
|
||||
use thiserror::Error;
|
||||
use validator_client::nyxd::error::NyxdError;
|
||||
use validator_client::nyxd::AccountId;
|
||||
@@ -15,6 +16,12 @@ pub enum RewardingError {
|
||||
allowed_address: AccountId,
|
||||
},
|
||||
|
||||
#[error("the current epoch is in the wrong state ({current_state}) to perform the requested operation: {operation}")]
|
||||
InvalidEpochState {
|
||||
current_state: EpochState,
|
||||
operation: String,
|
||||
},
|
||||
|
||||
// #[error("There were no mixnodes to reward (network is dead)")]
|
||||
// NoMixnodesToReward,
|
||||
#[error("Failed to execute the smart contract - {0}")]
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::epoch_operations::error::RewardingError;
|
||||
use crate::RewardedSetUpdater;
|
||||
use nym_mixnet_contract_common::EpochState;
|
||||
use std::cmp::max;
|
||||
|
||||
impl RewardedSetUpdater {
|
||||
pub(super) async fn reconcile_epoch_events(&self) -> Result<(), RewardingError> {
|
||||
let epoch_status = self.nyxd_client.get_current_epoch_status().await?;
|
||||
match epoch_status.state {
|
||||
state @ EpochState::InProgress | state @ EpochState::Rewarding { .. } => {
|
||||
// hard error, this shouldn't have happened!
|
||||
error!("tried to perform node rewarding while in {state} state!");
|
||||
Err(RewardingError::InvalidEpochState {
|
||||
current_state: state,
|
||||
operation: "reconciling epoch events".to_string(),
|
||||
})
|
||||
}
|
||||
EpochState::AdvancingEpoch => {
|
||||
warn!("we seem to have crashed mid epoch operations... no need to reconcile events as we've already done that!");
|
||||
Ok(())
|
||||
}
|
||||
EpochState::ReconcilingEvents => {
|
||||
log::info!("Reconciling all pending epoch events...");
|
||||
if let Err(err) = self._reconcile_epoch_events().await {
|
||||
log::error!("FAILED to reconcile epoch events... - {err}");
|
||||
Err(err)
|
||||
} else {
|
||||
log::info!("Reconciled all pending epoch events... SUCCESS");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn _reconcile_epoch_events(&self) -> Result<(), RewardingError> {
|
||||
let pending_events = self.nyxd_client.get_pending_events_count().await?;
|
||||
|
||||
// be conservative about number of events we resolve at once.
|
||||
// contract should be able to handle few hundred at once
|
||||
// but keep it on the lower side.
|
||||
let limit = 100;
|
||||
|
||||
let mut required_calls = pending_events / limit;
|
||||
if pending_events % limit != 0 {
|
||||
required_calls += 1;
|
||||
}
|
||||
|
||||
// make sure to at least call it once so that if there are no pending events,
|
||||
// the epoch would get advanced into the next phase
|
||||
for _ in 0..max(required_calls, 1) {
|
||||
self.nyxd_client.reconcile_epoch_events(Some(limit)).await?;
|
||||
}
|
||||
|
||||
// in the incredibly unlikely/borderline impossible scenario a HUGE number of events got pushed
|
||||
// while we were reconciling the events, do it one more time
|
||||
//
|
||||
// note: it's perfectly fine if we don't clear EXACTLY everything,
|
||||
// since when we execute transaction to actually advance the epoch,
|
||||
// it will resolve all remaining events.
|
||||
let pending_events = self.nyxd_client.get_pending_events_count().await?;
|
||||
if pending_events > 20 {
|
||||
self.nyxd_client.reconcile_epoch_events(Some(limit)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -12,44 +12,24 @@
|
||||
// 3. Eventually this whole procedure is going to get expanded to allow for distribution of rewarded set generation
|
||||
// and hence this might be a good place for it.
|
||||
|
||||
use crate::epoch_operations::helpers::stake_to_f64;
|
||||
use crate::node_status_api::ONE_DAY;
|
||||
use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::support::nyxd::Client;
|
||||
use crate::support::storage::models::RewardingReport;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use error::RewardingError;
|
||||
use nym_mixnet_contract_common::families::FamilyHead;
|
||||
use nym_mixnet_contract_common::{
|
||||
reward_params::Performance, CurrentIntervalResponse, ExecuteMsg, Interval, MixId,
|
||||
};
|
||||
use nym_mixnet_contract_common::{IdentityKey, Layer, LayerAssignment, MixNodeDetails};
|
||||
use nym_mixnet_contract_common::{CurrentIntervalResponse, Interval};
|
||||
use nym_task::{TaskClient, TaskManager};
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::rngs::OsRng;
|
||||
use std::collections::HashMap;
|
||||
pub(crate) use rewarding::MixnodeToReward;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub(crate) mod error;
|
||||
mod event_reconciliation;
|
||||
mod helpers;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct MixnodeToReward {
|
||||
pub(crate) mix_id: MixId,
|
||||
|
||||
pub(crate) performance: Performance,
|
||||
}
|
||||
|
||||
impl From<MixnodeToReward> for ExecuteMsg {
|
||||
fn from(mix_reward: MixnodeToReward) -> Self {
|
||||
ExecuteMsg::RewardMixnode {
|
||||
mix_id: mix_reward.mix_id,
|
||||
performance: mix_reward.performance,
|
||||
}
|
||||
}
|
||||
}
|
||||
mod rewarded_set_assignment;
|
||||
mod rewarding;
|
||||
mod transition_beginning;
|
||||
|
||||
pub struct RewardedSetUpdater {
|
||||
nyxd_client: Client,
|
||||
@@ -57,16 +37,6 @@ pub struct RewardedSetUpdater {
|
||||
storage: NymApiStorage,
|
||||
}
|
||||
|
||||
// Weight of a layer being chose is reciprocal to current count in layer
|
||||
fn layer_weight(l: &Layer, layer_assignments: &HashMap<Layer, f32>) -> f32 {
|
||||
let total = layer_assignments.values().fold(0., |acc, i| acc + i);
|
||||
if total == 0. {
|
||||
1.
|
||||
} else {
|
||||
1. - (layer_assignments.get(l).unwrap_or(&0.) / total)
|
||||
}
|
||||
}
|
||||
|
||||
impl RewardedSetUpdater {
|
||||
pub(crate) async fn current_interval_details(
|
||||
&self,
|
||||
@@ -86,176 +56,34 @@ impl RewardedSetUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
async fn determine_layers(
|
||||
&self,
|
||||
rewarded_set: &[MixNodeDetails],
|
||||
) -> Result<(Vec<LayerAssignment>, HashMap<String, Layer>), RewardingError> {
|
||||
let mut families_in_layer: HashMap<String, Layer> = HashMap::new();
|
||||
let mut assignments = vec![];
|
||||
let mut layer_assignments: HashMap<Layer, f32> = HashMap::new();
|
||||
let mut rng = OsRng;
|
||||
let layers = vec![Layer::One, Layer::Two, Layer::Three];
|
||||
|
||||
let mix_to_family = self.nym_contract_cache.mix_to_family().await.to_vec();
|
||||
|
||||
let mix_to_family = mix_to_family
|
||||
.into_iter()
|
||||
.collect::<HashMap<IdentityKey, FamilyHead>>();
|
||||
|
||||
for mix in rewarded_set {
|
||||
let family = mix_to_family.get(&mix.bond_information.identity().to_owned());
|
||||
// Get layer already assigned to nodes family, if any
|
||||
let family_layer = family.and_then(|h| families_in_layer.get(h.identity()));
|
||||
|
||||
// Same node families are always assigned to the same layer, otherwise layer selected by a random weighted choice
|
||||
let layer = if let Some(layer) = family_layer {
|
||||
layer.to_owned()
|
||||
} else {
|
||||
layers
|
||||
.choose_weighted(&mut rng, |l| layer_weight(l, &layer_assignments))?
|
||||
.to_owned()
|
||||
};
|
||||
|
||||
assignments.push(LayerAssignment::new(mix.mix_id(), layer));
|
||||
|
||||
// layer accounting
|
||||
let layer_entry = layer_assignments.entry(layer).or_insert(0.);
|
||||
*layer_entry += 1.;
|
||||
if let Some(family) = family {
|
||||
families_in_layer.insert(family.identity().to_string(), layer);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((assignments, families_in_layer))
|
||||
}
|
||||
|
||||
fn determine_rewarded_set(
|
||||
&self,
|
||||
mixnodes: &[MixNodeDetails],
|
||||
nodes_to_select: u32,
|
||||
) -> Result<Vec<MixNodeDetails>, RewardingError> {
|
||||
if mixnodes.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut rng = OsRng;
|
||||
|
||||
// generate list of mixnodes and their relatively weight (by total stake)
|
||||
let choices = mixnodes
|
||||
.iter()
|
||||
.map(|mix| {
|
||||
let total_stake = stake_to_f64(mix.total_stake());
|
||||
(mix.to_owned(), total_stake)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// the unwrap here is fine as an error can only be thrown under one of the following conditions:
|
||||
// - our mixnode list is empty - we have already checked for that
|
||||
// - we have invalid weights, i.e. less than zero or NaNs - it shouldn't happen in our case as we safely cast down from u128
|
||||
// - all weights are zero - it's impossible in our case as the list of nodes is not empty and weight is proportional to stake. You must have non-zero stake in order to bond
|
||||
// - we have more than u32::MAX values (which is incredibly unrealistic to have 4B mixnodes bonded... literally every other person on the planet would need one)
|
||||
Ok(choices
|
||||
.choose_multiple_weighted(&mut rng, nodes_to_select as usize, |item| item.1)?
|
||||
.map(|(mix, _weight)| mix.to_owned())
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn reward_current_rewarded_set(
|
||||
&self,
|
||||
current_interval: Interval,
|
||||
) -> Result<(), RewardingError> {
|
||||
let to_reward = self.nodes_to_reward(current_interval).await;
|
||||
|
||||
if let Some(existing_report) = self
|
||||
.storage
|
||||
.get_rewarding_report(current_interval.current_epoch_absolute_id())
|
||||
.await?
|
||||
{
|
||||
warn!("We have already rewarded mixnodes for this rewarding epoch ({}). {} nodes should have gotten rewards", existing_report.absolute_epoch_id, existing_report.eligible_mixnodes);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if to_reward.is_empty() {
|
||||
info!("There are no nodes to reward in this epoch");
|
||||
} else if let Err(err) = self.nyxd_client.send_rewarding_messages(&to_reward).await {
|
||||
error!(
|
||||
"failed to perform mixnode rewarding for epoch {}! Error encountered: {err}",
|
||||
current_interval.current_epoch_absolute_id(),
|
||||
);
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
log::info!("rewarded {} mixnodes...", to_reward.len());
|
||||
|
||||
let rewarding_report = RewardingReport {
|
||||
absolute_epoch_id: current_interval.current_epoch_absolute_id(),
|
||||
eligible_mixnodes: to_reward.len() as u32,
|
||||
};
|
||||
|
||||
self.storage
|
||||
.insert_rewarding_report(rewarding_report)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn nodes_to_reward(&self, interval: Interval) -> Vec<MixnodeToReward> {
|
||||
// try to get current up to date view of the network bypassing the cache
|
||||
// in case the epochs were significantly shortened for the purposes of testing
|
||||
let rewarded_set: Vec<MixId> = match self.nyxd_client.get_rewarded_set_mixnodes().await {
|
||||
Ok(nodes) => nodes.into_iter().map(|(id, _)| id).collect::<Vec<_>>(),
|
||||
Err(err) => {
|
||||
warn!("failed to obtain the current rewarded set - {err}. falling back to the cached version");
|
||||
self.nym_contract_cache
|
||||
.rewarded_set()
|
||||
.await
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(|node| node.mix_id())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
};
|
||||
|
||||
let mut eligible_nodes = Vec::with_capacity(rewarded_set.len());
|
||||
for mix_id in rewarded_set {
|
||||
let uptime = self
|
||||
.storage
|
||||
.get_average_mixnode_uptime_in_the_last_24hrs(
|
||||
mix_id,
|
||||
interval.current_epoch_end_unix_timestamp(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
eligible_nodes.push(MixnodeToReward {
|
||||
mix_id,
|
||||
performance: uptime.into(),
|
||||
})
|
||||
}
|
||||
|
||||
eligible_nodes
|
||||
}
|
||||
|
||||
async fn update_rewarded_set_and_advance_epoch(
|
||||
&self,
|
||||
all_mixnodes: &[MixNodeDetails],
|
||||
) -> Result<(), RewardingError> {
|
||||
// we grab rewarding parameters here as they might have gotten updated when performing epoch actions
|
||||
let rewarding_parameters = self.nyxd_client.get_current_rewarding_parameters().await?;
|
||||
|
||||
let new_rewarded_set =
|
||||
self.determine_rewarded_set(all_mixnodes, rewarding_parameters.rewarded_set_size)?;
|
||||
|
||||
let (layer_assignments, _families_in_layer) =
|
||||
self.determine_layers(&new_rewarded_set).await?;
|
||||
|
||||
self.nyxd_client
|
||||
.advance_current_epoch(layer_assignments, rewarding_parameters.active_set_size)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This is where the epoch gets advanced, and all epoch related transactions originate
|
||||
/// Upon each epoch having finished the following actions are executed by this nym-api:
|
||||
/// 1. it queries the mixnet contract to check the current `EpochState` in order to figure out whether
|
||||
/// a different nym-api has already started epoch transition (not yet applicable)
|
||||
/// 2. it sends a `BeginEpochTransition` message to the mixnet contract causing the following to happen:
|
||||
/// - if successful, the address of the this validator is going to be saved as being responsible for progressing this epoch.
|
||||
/// What it means in practice is that once we have multiple instances of nym-api running,
|
||||
/// only this one will try to perform the rest of the actions. It will also allow it to
|
||||
/// more easily recover in case of crashes.
|
||||
/// - the `EpochState` changes to `Rewarding`, meaning the nym-api will now be allowed to send
|
||||
/// `RewardMixnode` transactions. However, it's not going to be able anything else like `ReconcileEpochEvents`
|
||||
/// until that is done.
|
||||
/// - ability to send transactions (by other users) that get resolved once given epoch/interval rolls over,
|
||||
/// such as `BondMixnode` or `DelegateToMixnode` will temporarily be frozen until the entire procedure is finished.
|
||||
/// 3. it obtains the current rewarded set and for each node in there (**SORTED BY MIX_ID!!**),
|
||||
/// it sends (in a single batch) `RewardMixnode` message with the measured performance.
|
||||
/// Once the final message gets executed, the mixnet contract automatically transitions
|
||||
/// the state to `ReconcilingEvents`.
|
||||
/// 4. it obtains the number of pending epoch and interval events and repeatedly sends
|
||||
/// `ReconcileEpochEvents` transaction until all of them are resolved.
|
||||
/// At this point the mixnet contract automatically transitions the state to `AdvancingEpoch`.
|
||||
/// 5. it obtains the list of all nodes on the network and pseudorandomly (but weighted by total stake)
|
||||
/// determines the new rewarded set. It then assigns layers to the provided nodes taking
|
||||
/// family information into consideration. Finally it sends `AdvanceCurrentEpoch` message
|
||||
/// containing the set and layer information thus rolling over the epoch and changing the state
|
||||
/// to `InProgress`.
|
||||
/// 6. it purges old (older than 48h) measurement data
|
||||
/// 7. the whole process repeats once the new epoch finishes
|
||||
async fn perform_epoch_operations(&self, interval: Interval) -> Result<(), RewardingError> {
|
||||
log::info!("The current epoch has finished.");
|
||||
log::info!(
|
||||
@@ -276,46 +104,37 @@ impl RewardedSetUpdater {
|
||||
|
||||
let all_mixnodes = self.nym_contract_cache.mixnodes().await;
|
||||
if all_mixnodes.is_empty() {
|
||||
// that's a bit weird, but
|
||||
log::warn!("there don't seem to be any mixnodes on the network!")
|
||||
}
|
||||
|
||||
let epoch_status = self.nyxd_client.get_current_epoch_status().await?;
|
||||
if !epoch_status.is_in_progress() {
|
||||
if epoch_status.being_advanced_by.as_str()
|
||||
!= self.nyxd_client.client_address().await.as_ref()
|
||||
{
|
||||
// another nym-api is already handling
|
||||
error!("another nym-api ({}) is already advancing the epoch... but we shouldn't have other nym-apis yet!", epoch_status.being_advanced_by);
|
||||
return Ok(());
|
||||
} else {
|
||||
warn!("we seem to have crashed mid-epoch advancement...");
|
||||
}
|
||||
} else {
|
||||
let should_continue = self.begin_epoch_transition().await?;
|
||||
if !should_continue {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Reward all the nodes in the still current, soon to be previous rewarded set
|
||||
log::info!("Rewarding the current rewarded set...");
|
||||
if let Err(err) = self.reward_current_rewarded_set(interval).await {
|
||||
log::error!("FAILED to reward rewarded set - {err}");
|
||||
// since we haven't advanced the epoch yet, we will attempt to reward those nodes again
|
||||
// next time we enter this function (i.e. within 2min or so)
|
||||
//
|
||||
// TODO: deal with the following edge case:
|
||||
// - the nym api REWARDS all mixnodes
|
||||
// - then crashes before advancing epoch
|
||||
// - upon restart it will attempt (and fail) to re-reward the mixnodes
|
||||
return Err(err);
|
||||
} else {
|
||||
log::info!("Rewarded current rewarded set... SUCCESS");
|
||||
}
|
||||
self.reward_current_rewarded_set(interval).await?;
|
||||
|
||||
// note: those operations don't really have to be atomic, so it's fine to send them
|
||||
// as separate transactions
|
||||
|
||||
log::info!("Reconciling all pending epoch events...");
|
||||
if let Err(err) = self.nyxd_client.reconcile_epoch_events().await {
|
||||
log::error!("FAILED to reconcile epoch events... - {err}");
|
||||
return Err(err.into());
|
||||
} else {
|
||||
log::info!("Reconciled all pending epoch events... SUCCESS");
|
||||
}
|
||||
|
||||
log::info!("Advancing epoch and updating the rewarded set...");
|
||||
if let Err(err) = self
|
||||
.update_rewarded_set_and_advance_epoch(&all_mixnodes)
|
||||
.await
|
||||
{
|
||||
log::error!("FAILED to advance the current epoch... - {err}");
|
||||
return Err(err);
|
||||
} else {
|
||||
log::info!("Advanced the epoch and updated the rewarded set... SUCCESS");
|
||||
}
|
||||
self.reconcile_epoch_events().await?;
|
||||
self.update_rewarded_set_and_advance_epoch(&all_mixnodes)
|
||||
.await?;
|
||||
|
||||
log::info!("Purging old node statuses from the storage...");
|
||||
let cutoff = (epoch_end - 2 * ONE_DAY).unix_timestamp();
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::epoch_operations::error::RewardingError;
|
||||
use crate::epoch_operations::helpers::stake_to_f64;
|
||||
use crate::RewardedSetUpdater;
|
||||
use nym_mixnet_contract_common::families::FamilyHead;
|
||||
use nym_mixnet_contract_common::{EpochState, IdentityKey, Layer, LayerAssignment, MixNodeDetails};
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::rngs::OsRng;
|
||||
use std::collections::HashMap;
|
||||
|
||||
impl RewardedSetUpdater {
|
||||
// Needs to run for active and reserve sets separatley, as it does not preserve order
|
||||
async fn determine_layers(
|
||||
&self,
|
||||
set: &[MixNodeDetails],
|
||||
) -> Result<Vec<LayerAssignment>, RewardingError> {
|
||||
let mut assignments = Vec::with_capacity(set.len());
|
||||
let target_layer_count = set.len() / 3;
|
||||
|
||||
let mix_to_family = self.nym_contract_cache.mix_to_family().await.to_vec();
|
||||
|
||||
let mix_to_family = mix_to_family
|
||||
.into_iter()
|
||||
.collect::<HashMap<IdentityKey, FamilyHead>>();
|
||||
|
||||
let mut regular_nodes = Vec::with_capacity(set.len());
|
||||
|
||||
let mut families = HashMap::new();
|
||||
|
||||
for node in set.iter() {
|
||||
if let Some(fh) = mix_to_family.get(node.bond_information.identity()) {
|
||||
let family: &mut Vec<u32> = families.entry(fh.identity()).or_default();
|
||||
family.push(node.mix_id())
|
||||
} else {
|
||||
regular_nodes.push(node.mix_id())
|
||||
}
|
||||
}
|
||||
|
||||
let mut layers = HashMap::new();
|
||||
layers.insert(Layer::One, Vec::with_capacity(target_layer_count));
|
||||
layers.insert(Layer::Two, Vec::with_capacity(target_layer_count));
|
||||
layers.insert(Layer::Three, Vec::with_capacity(target_layer_count));
|
||||
|
||||
// Assign all members of a family to same layer
|
||||
for (_head, members) in families.iter_mut() {
|
||||
let smallest_layer = layers
|
||||
.iter()
|
||||
.min_by_key(|(_layer, members)| members.len())
|
||||
.map(|(layer, _members)| *layer)
|
||||
.unwrap_or(Layer::One);
|
||||
|
||||
let entry = layers.entry(smallest_layer).or_default();
|
||||
if entry.len() + members.len() <= target_layer_count {
|
||||
entry.extend_from_slice(members)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign nodes with no families into layers
|
||||
for mix_id in regular_nodes.drain(..) {
|
||||
let smallest_layer = layers
|
||||
.iter()
|
||||
.min_by_key(|(_layer, members)| members.len())
|
||||
.map(|(layer, _members)| *layer)
|
||||
.unwrap_or(Layer::One);
|
||||
|
||||
let entry = layers.entry(smallest_layer).or_default();
|
||||
if entry.len() < target_layer_count {
|
||||
entry.push(mix_id)
|
||||
}
|
||||
}
|
||||
|
||||
for (layer, members) in layers {
|
||||
let layer_assignments = members
|
||||
.into_iter()
|
||||
.map(|mix_id| LayerAssignment::new(mix_id, layer));
|
||||
assignments.extend(layer_assignments);
|
||||
}
|
||||
Ok(assignments)
|
||||
}
|
||||
|
||||
fn determine_rewarded_set(
|
||||
&self,
|
||||
mixnodes: &[MixNodeDetails],
|
||||
nodes_to_select: u32,
|
||||
) -> Result<Vec<MixNodeDetails>, RewardingError> {
|
||||
if mixnodes.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut rng = OsRng;
|
||||
|
||||
// generate list of mixnodes and their relatively weight (by total stake)
|
||||
let choices = mixnodes
|
||||
.iter()
|
||||
.map(|mix| {
|
||||
let total_stake = stake_to_f64(mix.total_stake());
|
||||
(mix.to_owned(), total_stake)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// the unwrap here is fine as an error can only be thrown under one of the following conditions:
|
||||
// - our mixnode list is empty - we have already checked for that
|
||||
// - we have invalid weights, i.e. less than zero or NaNs - it shouldn't happen in our case as we safely cast down from u128
|
||||
// - all weights are zero - it's impossible in our case as the list of nodes is not empty and weight is proportional to stake. You must have non-zero stake in order to bond
|
||||
// - we have more than u32::MAX values (which is incredibly unrealistic to have 4B mixnodes bonded... literally every other person on the planet would need one)
|
||||
Ok(choices
|
||||
.choose_multiple_weighted(&mut rng, nodes_to_select as usize, |item| item.1)?
|
||||
.map(|(mix, _weight)| mix.to_owned())
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(super) async fn update_rewarded_set_and_advance_epoch(
|
||||
&self,
|
||||
all_mixnodes: &[MixNodeDetails],
|
||||
) -> Result<(), RewardingError> {
|
||||
let epoch_status = self.nyxd_client.get_current_epoch_status().await?;
|
||||
match epoch_status.state {
|
||||
EpochState::AdvancingEpoch => {
|
||||
log::info!("Advancing epoch and updating the rewarded set...");
|
||||
if let Err(err) = self
|
||||
._update_rewarded_set_and_advance_epoch(all_mixnodes)
|
||||
.await
|
||||
{
|
||||
log::error!("FAILED to advance the current epoch... - {err}");
|
||||
Err(err)
|
||||
} else {
|
||||
log::info!("Advanced the epoch and updated the rewarded set... SUCCESS");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
state => {
|
||||
// hard error, this shouldn't have happened!
|
||||
error!("tried to perform node rewarded set assignment while in {state} state!");
|
||||
Err(RewardingError::InvalidEpochState {
|
||||
current_state: state,
|
||||
operation: "assigning rewarded set".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn _update_rewarded_set_and_advance_epoch(
|
||||
&self,
|
||||
all_mixnodes: &[MixNodeDetails],
|
||||
) -> Result<(), RewardingError> {
|
||||
// we grab rewarding parameters here as they might have gotten updated when performing epoch actions
|
||||
let rewarding_parameters = self.nyxd_client.get_current_rewarding_parameters().await?;
|
||||
|
||||
debug!("Rewarding paremeters: {:?}", rewarding_parameters);
|
||||
|
||||
let new_rewarded_set =
|
||||
self.determine_rewarded_set(all_mixnodes, rewarding_parameters.rewarded_set_size)?;
|
||||
|
||||
debug!("New rewarded set: {:?}", new_rewarded_set);
|
||||
|
||||
let empty = vec![];
|
||||
|
||||
let (active_set, reserve_set) = if new_rewarded_set.len()
|
||||
<= rewarding_parameters.active_set_size as usize
|
||||
{
|
||||
warn!("Active set size ({}) is greater then rewarded set len ({}), there will be no reserve set", rewarding_parameters.active_set_size, new_rewarded_set.len());
|
||||
(new_rewarded_set.as_slice(), empty.as_slice())
|
||||
} else {
|
||||
new_rewarded_set.split_at(rewarding_parameters.active_set_size as usize)
|
||||
};
|
||||
|
||||
let mut active_set_layer_assignments = self.determine_layers(active_set).await?;
|
||||
debug!(
|
||||
"Active set layer assignments: {:?}",
|
||||
active_set_layer_assignments
|
||||
);
|
||||
let reserve_set_layer_assignments = self.determine_layers(reserve_set).await?;
|
||||
debug!(
|
||||
"Reserve set layer assignments: {:?}",
|
||||
reserve_set_layer_assignments
|
||||
);
|
||||
|
||||
active_set_layer_assignments.extend(reserve_set_layer_assignments);
|
||||
|
||||
debug!(
|
||||
"Rewarded set layer assignments: {:?}",
|
||||
active_set_layer_assignments
|
||||
);
|
||||
|
||||
self.nyxd_client
|
||||
.advance_current_epoch(
|
||||
active_set_layer_assignments,
|
||||
rewarding_parameters.active_set_size,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::epoch_operations::error::RewardingError;
|
||||
use crate::support::storage::models::RewardingReport;
|
||||
use crate::RewardedSetUpdater;
|
||||
use nym_mixnet_contract_common::reward_params::Performance;
|
||||
use nym_mixnet_contract_common::{EpochState, ExecuteMsg, Interval, MixId};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct MixnodeToReward {
|
||||
pub(crate) mix_id: MixId,
|
||||
|
||||
pub(crate) performance: Performance,
|
||||
}
|
||||
|
||||
impl From<MixnodeToReward> for ExecuteMsg {
|
||||
fn from(mix_reward: MixnodeToReward) -> Self {
|
||||
ExecuteMsg::RewardMixnode {
|
||||
mix_id: mix_reward.mix_id,
|
||||
performance: mix_reward.performance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RewardedSetUpdater {
|
||||
pub(super) async fn reward_current_rewarded_set(
|
||||
&self,
|
||||
current_interval: Interval,
|
||||
) -> Result<(), RewardingError> {
|
||||
let epoch_status = self.nyxd_client.get_current_epoch_status().await?;
|
||||
match epoch_status.state {
|
||||
EpochState::InProgress => {
|
||||
// hard error, this shouldn't have happened!
|
||||
error!("tried to perform node rewarding while the epoch is still in progress!");
|
||||
Err(RewardingError::InvalidEpochState {
|
||||
current_state: EpochState::InProgress,
|
||||
operation: "mix rewarding".to_string(),
|
||||
})
|
||||
}
|
||||
EpochState::ReconcilingEvents | EpochState::AdvancingEpoch => {
|
||||
warn!("we seem to have crashed mid epoch operations... no need to reward mixnodes as we've already done that! (or this could be a false positive if there were no nodes to reward - to fix this warning later)");
|
||||
Ok(())
|
||||
}
|
||||
EpochState::Rewarding { .. } => {
|
||||
log::info!("Rewarding the current rewarded set...");
|
||||
if let Err(err) = self._reward_current_rewarded_set(current_interval).await {
|
||||
log::error!("FAILED to reward rewarded set - {err}");
|
||||
Err(err)
|
||||
} else {
|
||||
log::info!("Rewarded current rewarded set... SUCCESS");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn _reward_current_rewarded_set(
|
||||
&self,
|
||||
current_interval: Interval,
|
||||
) -> Result<(), RewardingError> {
|
||||
let mut to_reward = self.nodes_to_reward(current_interval).await;
|
||||
to_reward.sort_by_key(|a| a.mix_id);
|
||||
|
||||
if let Some(existing_report) = self
|
||||
.storage
|
||||
.get_rewarding_report(current_interval.current_epoch_absolute_id())
|
||||
.await?
|
||||
{
|
||||
warn!("We have already rewarded mixnodes for this rewarding epoch ({}). {} nodes should have gotten rewards", existing_report.absolute_epoch_id, existing_report.eligible_mixnodes);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if to_reward.is_empty() {
|
||||
info!("There are no nodes to reward in this epoch");
|
||||
} else if let Err(err) = self.nyxd_client.send_rewarding_messages(&to_reward).await {
|
||||
error!(
|
||||
"failed to perform mixnode rewarding for epoch {}! Error encountered: {err}",
|
||||
current_interval.current_epoch_absolute_id(),
|
||||
);
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
log::info!("rewarded {} mixnodes...", to_reward.len());
|
||||
|
||||
let rewarding_report = RewardingReport {
|
||||
absolute_epoch_id: current_interval.current_epoch_absolute_id(),
|
||||
eligible_mixnodes: to_reward.len() as u32,
|
||||
};
|
||||
|
||||
self.storage
|
||||
.insert_rewarding_report(rewarding_report)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn nodes_to_reward(&self, interval: Interval) -> Vec<MixnodeToReward> {
|
||||
// try to get current up to date view of the network bypassing the cache
|
||||
// in case the epochs were significantly shortened for the purposes of testing
|
||||
let rewarded_set: Vec<MixId> = match self.nyxd_client.get_rewarded_set_mixnodes().await {
|
||||
Ok(nodes) => nodes.into_iter().map(|(id, _)| id).collect::<Vec<_>>(),
|
||||
Err(err) => {
|
||||
warn!("failed to obtain the current rewarded set - {err}. falling back to the cached version");
|
||||
self.nym_contract_cache
|
||||
.rewarded_set()
|
||||
.await
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(|node| node.mix_id())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
};
|
||||
|
||||
let mut eligible_nodes = Vec::with_capacity(rewarded_set.len());
|
||||
for mix_id in rewarded_set {
|
||||
let uptime = self
|
||||
.storage
|
||||
.get_average_mixnode_uptime_in_the_last_24hrs(
|
||||
mix_id,
|
||||
interval.current_epoch_end_unix_timestamp(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
eligible_nodes.push(MixnodeToReward {
|
||||
mix_id,
|
||||
performance: uptime.into(),
|
||||
})
|
||||
}
|
||||
|
||||
eligible_nodes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::epoch_operations::error::RewardingError;
|
||||
use crate::epoch_operations::RewardedSetUpdater;
|
||||
|
||||
impl RewardedSetUpdater {
|
||||
// returns boolean indicating whether we should bother continuing
|
||||
pub(super) async fn begin_epoch_transition(&self) -> Result<bool, RewardingError> {
|
||||
info!("starting the epoch transition...");
|
||||
if let Err(err) = self._begin_epoch_transition().await {
|
||||
// perform the state query again to make sure it's not because other nym-api (not yet applicable)
|
||||
// wasn't faster than us
|
||||
let epoch_status = self.nyxd_client.get_current_epoch_status().await?;
|
||||
if !epoch_status.is_in_progress() {
|
||||
log::error!("FAILED to begin epoch progression: {err}");
|
||||
Err(err)
|
||||
} else {
|
||||
error!("another nym-api ({}) is already advancing the epoch... but we shouldn't have other nym-apis yet!", epoch_status.being_advanced_by);
|
||||
Ok(false)
|
||||
}
|
||||
} else {
|
||||
log::info!("Begun epoch transition... SUCCESS");
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
async fn _begin_epoch_transition(&self) -> Result<(), RewardingError> {
|
||||
Ok(self.nyxd_client.begin_epoch_transition().await?)
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ use nym_mixnet_contract_common::families::{Family, FamilyHead};
|
||||
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use nym_mixnet_contract_common::reward_params::RewardingParams;
|
||||
use nym_mixnet_contract_common::{
|
||||
CurrentIntervalResponse, ExecuteMsg, GatewayBond, IdentityKey, LayerAssignment, MixId,
|
||||
RewardedSetNodeStatus,
|
||||
CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, LayerAssignment,
|
||||
MixId, RewardedSetNodeStatus,
|
||||
};
|
||||
use nym_vesting_contract_common::AccountVestingCoins;
|
||||
use std::sync::Arc;
|
||||
@@ -157,6 +157,12 @@ impl Client {
|
||||
Ok(self.0.read().await.get_current_interval_details().await?)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_current_epoch_status(
|
||||
&self,
|
||||
) -> Result<EpochStatus, ValidatorClientError> {
|
||||
Ok(self.0.read().await.nyxd.get_current_epoch_status().await?)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_current_rewarding_parameters(
|
||||
&self,
|
||||
) -> Result<RewardingParams, ValidatorClientError> {
|
||||
@@ -213,6 +219,21 @@ impl Client {
|
||||
self.0.read().await.get_all_family_members().await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_pending_events_count(&self) -> Result<u32, ValidatorClientError> {
|
||||
let pending = self.0.read().await.get_number_of_pending_events().await?;
|
||||
Ok(pending.epoch_events + pending.interval_events)
|
||||
}
|
||||
|
||||
pub(crate) async fn begin_epoch_transition(&self) -> Result<(), ValidatorClientError> {
|
||||
self.0
|
||||
.write()
|
||||
.await
|
||||
.nyxd
|
||||
.begin_epoch_transition(None)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn send_rewarding_messages(
|
||||
&self,
|
||||
nodes: &[MixnodeToReward],
|
||||
@@ -268,12 +289,15 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn reconcile_epoch_events(&self) -> Result<(), ValidatorClientError> {
|
||||
pub(crate) async fn reconcile_epoch_events(
|
||||
&self,
|
||||
limit: Option<u32>,
|
||||
) -> Result<(), ValidatorClientError> {
|
||||
self.0
|
||||
.write()
|
||||
.await
|
||||
.nyxd
|
||||
.reconcile_epoch_events(None, None)
|
||||
.reconcile_epoch_events(limit, None)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ tauri-macros = "^1.2.1"
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
bip39 = "1.0"
|
||||
chrono = "0.4"
|
||||
dirs = "4.0"
|
||||
eyre = "0.6.5"
|
||||
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", branch = "release"}
|
||||
@@ -40,10 +39,12 @@ tap = "1.0.1"
|
||||
tauri = { version = "^1.2.2", features = ["clipboard-write-text", "macos-private-api", "notification-all", "shell-open", "system-tray", "updater", "window-close", "window-minimize", "window-start-dragging"] }
|
||||
tendermint-rpc = "0.23.0"
|
||||
thiserror = "1.0"
|
||||
time = { version = "0.3.17", features = ["local-offset"] }
|
||||
tokio = { version = "1.24.1", features = ["sync", "time"] }
|
||||
url = "2.2"
|
||||
yaml-rust = "0.4"
|
||||
|
||||
|
||||
client-core = { path = "../../../clients/client-core" }
|
||||
nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
|
||||
nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common"}
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use serde::Serialize;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use std::str::FromStr;
|
||||
use tauri::Manager;
|
||||
use time::{format_description, OffsetDateTime};
|
||||
|
||||
fn formatted_time() -> String {
|
||||
// if we fail to obtain local time according to local offset, fallback to utc
|
||||
let _now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
|
||||
|
||||
// if we're running it in the unit test, use the hardcoded value
|
||||
#[cfg(test)]
|
||||
let _now = OffsetDateTime::from_unix_timestamp(1666666666).unwrap();
|
||||
|
||||
// the unwraps are fine as we know this description is correct
|
||||
// note: the reason for this very particular format is a very simple one
|
||||
// it's what we've always been using since we copied it from the example,
|
||||
// so feel free to update it to whatever
|
||||
let format =
|
||||
format_description::parse("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]").unwrap();
|
||||
_now.format(&format).unwrap()
|
||||
}
|
||||
|
||||
pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerError> {
|
||||
let colors = ColoredLevelConfig::new()
|
||||
@@ -21,7 +38,7 @@ pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerE
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}][{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
formatted_time(),
|
||||
record.target(),
|
||||
colors.color(record.level()),
|
||||
message,
|
||||
@@ -33,7 +50,7 @@ pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerE
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
formatted_time(),
|
||||
record.target(),
|
||||
message,
|
||||
))
|
||||
@@ -116,3 +133,15 @@ impl From<log::Level> for LogLevel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn log_formatting() {
|
||||
let expected_chrono_formated = "[2022-10-25][02:57:46]".to_string();
|
||||
let new_time_based = formatted_time();
|
||||
assert_eq!(new_time_based, expected_chrono_formated)
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+20
-52
@@ -612,7 +612,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "client-core"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dashmap",
|
||||
@@ -639,6 +639,8 @@ dependencies = [
|
||||
"time 0.3.19",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite",
|
||||
"tungstenite",
|
||||
"url",
|
||||
"validator-client",
|
||||
"wasm-bindgen",
|
||||
@@ -1557,31 +1559,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "0.8.1"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2953d1df47ac0eb70086ccabf0275aa8da8591a28bd358ee2b52bd9f9e3ff9e9"
|
||||
checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401"
|
||||
dependencies = [
|
||||
"enum-iterator-derive 0.8.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ea166b3f7dc1032f7866d13f8d8e02c8d87507b61750176b86554964dc6a7bf"
|
||||
dependencies = [
|
||||
"enum-iterator-derive 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator-derive"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8958699f9359f0b04e691a13850d48b7de329138023876d07cbd024c2c820598"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"enum-iterator-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2143,9 +2125,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.16.1"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc"
|
||||
checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
@@ -3017,9 +2999,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.14.2+1.5.1"
|
||||
version = "0.13.5+1.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
|
||||
checksum = "51e5ea06c26926f1002dd553fded6cfcdc9784c1f60feeb58368b4d9b07b6dba"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -3473,7 +3455,7 @@ dependencies = [
|
||||
"pretty_env_logger",
|
||||
"semver 0.11.0",
|
||||
"serde",
|
||||
"vergen 7.5.1",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3593,7 +3575,7 @@ name = "nym-network-defaults"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dotenv",
|
||||
"dotenvy",
|
||||
"hex-literal",
|
||||
"once_cell",
|
||||
"serde",
|
||||
@@ -3622,7 +3604,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"client-core",
|
||||
@@ -3817,13 +3799,15 @@ version = "1.1.4"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"nym-contracts-common",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-vesting-contract-common",
|
||||
"schemars",
|
||||
"semver 1.0.16",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"vergen 5.1.17",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6795,6 +6779,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"url",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6811,30 +6796,13 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vergen"
|
||||
version = "5.1.17"
|
||||
version = "7.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cf88d94e969e7956d924ba70741316796177fa0c79a2c9f4ab04998d96e966e"
|
||||
checksum = "447f9238a4553957277b3ee09d80babeae0811f1b3baefb093de1c0448437a37"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
"enum-iterator 0.8.1",
|
||||
"getset",
|
||||
"git2",
|
||||
"rustc_version 0.4.0",
|
||||
"rustversion",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vergen"
|
||||
version = "7.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f21b881cd6636ece9735721cf03c1fe1e774fe258683d084bb2812ab67435749"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"enum-iterator 1.3.0",
|
||||
"enum-iterator",
|
||||
"getset",
|
||||
"git2",
|
||||
"rustc_version 0.4.0",
|
||||
|
||||
Generated
+42
-205
@@ -84,15 +84,6 @@ dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.69"
|
||||
@@ -478,21 +469,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time 0.1.45",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.3.0"
|
||||
@@ -519,9 +495,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.1.2"
|
||||
version = "4.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd125be87bf4c255ebc50de0b7f4d2a6201e8ac3dc86e39c0ad081dc5e7236fe"
|
||||
checksum = "501ff0a401473ea1d4c3b125ff95506b62c5bc5768d818634195fbb7c4ad5ff4"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
@@ -631,16 +607,6 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
@@ -1027,50 +993,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90d59d9acd2a682b4e40605a242f6670eaa58c5957471cbf85e8aa6a0b97a5e8"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxxbridge-flags",
|
||||
"cxxbridge-macro",
|
||||
"link-cplusplus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebfa40bda659dd5c864e65f4c9a2b0aff19bea56b017b9b77c73d3766a453a38"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"scratch",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "457ce6757c5c70dc6ecdbda6925b958aae7f959bda7d8fb9bde889e34a09dc03"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebf883b7aacd7b2aeb2a7b338648ee19f57c140d4ee8e52c68979c6b2f7f2263"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.13.4"
|
||||
@@ -1217,10 +1139,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.15.0"
|
||||
name = "dotenvy"
|
||||
version = "0.15.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
@@ -1338,31 +1260,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "0.8.1"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2953d1df47ac0eb70086ccabf0275aa8da8591a28bd358ee2b52bd9f9e3ff9e9"
|
||||
checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401"
|
||||
dependencies = [
|
||||
"enum-iterator-derive 0.8.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ea166b3f7dc1032f7866d13f8d8e02c8d87507b61750176b86554964dc6a7bf"
|
||||
dependencies = [
|
||||
"enum-iterator-derive 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator-derive"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8958699f9359f0b04e691a13850d48b7de329138023876d07cbd024c2c820598"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"enum-iterator-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1820,9 +1722,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.16.1"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc"
|
||||
checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
@@ -2263,30 +2165,6 @@ dependencies = [
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"cxx-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ico"
|
||||
version = "0.2.0"
|
||||
@@ -2549,9 +2427,9 @@ checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.14.2+1.5.1"
|
||||
version = "0.13.5+1.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
|
||||
checksum = "51e5ea06c26926f1002dd553fded6cfcdc9784c1f60feeb58368b4d9b07b6dba"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -2580,15 +2458,6 @@ dependencies = [
|
||||
"safemem",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.1.4"
|
||||
@@ -2891,6 +2760,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-api-requests"
|
||||
version = "0.1.0"
|
||||
@@ -2915,7 +2793,7 @@ dependencies = [
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
"semver 0.11.0",
|
||||
"vergen 7.5.1",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2964,7 +2842,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"thiserror",
|
||||
"time 0.3.17",
|
||||
"time",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
@@ -2973,7 +2851,7 @@ name = "nym-network-defaults"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dotenv",
|
||||
"dotenvy",
|
||||
"hex-literal",
|
||||
"once_cell",
|
||||
"serde",
|
||||
@@ -3019,13 +2897,15 @@ version = "1.1.4"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"nym-contracts-common",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-vesting-contract-common",
|
||||
"schemars",
|
||||
"semver 1.0.16",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"vergen 5.1.17",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3084,13 +2964,12 @@ dependencies = [
|
||||
"base64 0.13.1",
|
||||
"bip39",
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
"coconut-interface",
|
||||
"colored 2.0.0",
|
||||
"cosmrs",
|
||||
"cosmwasm-std",
|
||||
"dirs",
|
||||
"dotenv",
|
||||
"dotenvy",
|
||||
"eyre",
|
||||
"fern",
|
||||
"futures",
|
||||
@@ -3118,6 +2997,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tendermint-rpc",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"toml",
|
||||
"ts-rs",
|
||||
@@ -3610,7 +3490,7 @@ dependencies = [
|
||||
"line-wrap",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"time 0.3.17",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4265,12 +4145,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "scratch"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.6.1"
|
||||
@@ -4905,7 +4779,7 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"time 0.3.17",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid 1.3.0",
|
||||
@@ -4952,7 +4826,7 @@ dependencies = [
|
||||
"sha2 0.10.6",
|
||||
"tauri-utils",
|
||||
"thiserror",
|
||||
"time 0.3.17",
|
||||
"time",
|
||||
"uuid 1.3.0",
|
||||
"walkdir",
|
||||
]
|
||||
@@ -5080,7 +4954,7 @@ dependencies = [
|
||||
"subtle",
|
||||
"subtle-encoding",
|
||||
"tendermint-proto",
|
||||
"time 0.3.17",
|
||||
"time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -5113,7 +4987,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"subtle-encoding",
|
||||
"time 0.3.17",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5141,7 +5015,7 @@ dependencies = [
|
||||
"tendermint-config",
|
||||
"tendermint-proto",
|
||||
"thiserror",
|
||||
"time 0.3.17",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -5205,17 +5079,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.17"
|
||||
@@ -5223,6 +5086,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
|
||||
dependencies = [
|
||||
"itoa 1.0.5",
|
||||
"libc",
|
||||
"num_threads",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
@@ -5492,12 +5357,6 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.4"
|
||||
@@ -5590,6 +5449,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"url",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5606,36 +5466,19 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vergen"
|
||||
version = "5.1.17"
|
||||
version = "7.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cf88d94e969e7956d924ba70741316796177fa0c79a2c9f4ab04998d96e966e"
|
||||
checksum = "447f9238a4553957277b3ee09d80babeae0811f1b3baefb093de1c0448437a37"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
"enum-iterator 0.8.1",
|
||||
"enum-iterator",
|
||||
"getset",
|
||||
"git2",
|
||||
"rustc_version 0.4.0",
|
||||
"rustversion",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vergen"
|
||||
version = "7.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f21b881cd6636ece9735721cf03c1fe1e774fe258683d084bb2812ab67435749"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"enum-iterator 1.3.0",
|
||||
"getset",
|
||||
"git2",
|
||||
"rustc_version 0.4.0",
|
||||
"rustversion",
|
||||
"thiserror",
|
||||
"time 0.3.17",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5683,12 +5526,6 @@ version = "0.9.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
||||
@@ -21,10 +21,9 @@ tauri-macros = "=1.2.1"
|
||||
[dependencies]
|
||||
bip39 = "1.0"
|
||||
cfg-if = "1.0.0"
|
||||
chrono = "0.4"
|
||||
colored = "2.0"
|
||||
dirs = "4.0"
|
||||
dotenv = "0.15.0"
|
||||
dotenvy = "0.15.6"
|
||||
eyre = "0.6.5"
|
||||
fern = { version = "0.6.1", features = ["colored"] }
|
||||
futures = "0.3.15"
|
||||
@@ -40,6 +39,7 @@ serde_repr = "0.1"
|
||||
strum = { version = "0.23", features = ["derive"] }
|
||||
tauri = { version = "=1.2.3", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] }
|
||||
tendermint-rpc = "0.23.0"
|
||||
time = { version = "0.3.17", features = ["local-offset"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.10", features = ["full"] }
|
||||
toml = "0.5.8"
|
||||
|
||||
@@ -4,6 +4,24 @@ use fern::colors::{Color, ColoredLevelConfig};
|
||||
use serde::Serialize;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use tauri::Manager;
|
||||
use time::{format_description, OffsetDateTime};
|
||||
|
||||
fn formatted_time() -> String {
|
||||
// if we fail to obtain local time according to local offset, fallback to utc
|
||||
let _now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
|
||||
|
||||
// if we're running it in the unit test, use the hardcoded value
|
||||
#[cfg(test)]
|
||||
let _now = OffsetDateTime::from_unix_timestamp(1666666666).unwrap();
|
||||
|
||||
// the unwraps are fine as we know this description is correct
|
||||
// note: the reason for this very particular format is a very simple one
|
||||
// it's what we've always been using since we copied it from the example,
|
||||
// so feel free to update it to whatever
|
||||
let format =
|
||||
format_description::parse("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]").unwrap();
|
||||
_now.format(&format).unwrap()
|
||||
}
|
||||
|
||||
pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerError> {
|
||||
let colors = ColoredLevelConfig::new()
|
||||
@@ -21,7 +39,7 @@ pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerE
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}][{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
formatted_time(),
|
||||
record.target(),
|
||||
colors.color(record.level()),
|
||||
message,
|
||||
@@ -33,7 +51,7 @@ pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerE
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
formatted_time(),
|
||||
record.target(),
|
||||
message,
|
||||
))
|
||||
@@ -116,3 +134,15 @@ impl From<log::Level> for LogLevel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn log_formatting() {
|
||||
let expected_chrono_formated = "[2022-10-25][02:57:46]".to_string();
|
||||
let new_time_based = formatted_time();
|
||||
assert_eq!(new_time_based, expected_chrono_formated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ mod wallet_storage;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn main() {
|
||||
dotenv::dotenv().ok();
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
let context = tauri::generate_context!();
|
||||
tauri::Builder::default()
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::config::{Config, CUSTOM_SIMULATED_GAS_MULTIPLIER};
|
||||
use crate::error::BackendError;
|
||||
use crate::network_config;
|
||||
use crate::state::{WalletAccountIds, WalletState};
|
||||
use crate::wallet_storage::{self, DEFAULT_LOGIN_ID};
|
||||
use crate::utils::{SensitiveStringWrapper, ZeroizeMnemonicWrapper};
|
||||
use crate::wallet_storage::{self, UserPassword, DEFAULT_LOGIN_ID};
|
||||
use bip39::{Language, Mnemonic};
|
||||
use cosmrs::bip32::DerivationPath;
|
||||
use itertools::Itertools;
|
||||
@@ -11,7 +12,6 @@ use nym_types::account::{Account, AccountEntry, Balance};
|
||||
use nym_wallet_types::network::Network as WalletNetwork;
|
||||
use rand::seq::SliceRandom;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use strum::IntoEnumIterator;
|
||||
use url::Url;
|
||||
use validator_client::nyxd::wallet::{AccountData, DirectSecp256k1HdWallet};
|
||||
@@ -19,10 +19,10 @@ use validator_client::{nyxd::SigningNyxdClient, Client};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn connect_with_mnemonic(
|
||||
mnemonic: String,
|
||||
mnemonic: SensitiveStringWrapper,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<Account, BackendError> {
|
||||
let mnemonic = Mnemonic::from_str(&mnemonic)?;
|
||||
let mnemonic = ZeroizeMnemonicWrapper::try_from_string(mnemonic)?;
|
||||
_connect_with_mnemonic(mnemonic, state).await
|
||||
}
|
||||
|
||||
@@ -48,13 +48,13 @@ pub async fn get_balance(state: tauri::State<'_, WalletState>) -> Result<Balance
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_new_mnemonic() -> String {
|
||||
random_mnemonic().to_string()
|
||||
pub fn create_new_mnemonic() -> SensitiveStringWrapper {
|
||||
random_mnemonic().into_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn validate_mnemonic(mnemonic: &str) -> bool {
|
||||
Mnemonic::from_str(mnemonic).is_ok()
|
||||
pub fn validate_mnemonic(mnemonic: SensitiveStringWrapper) -> bool {
|
||||
ZeroizeMnemonicWrapper::try_from_string(mnemonic).is_ok()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -82,13 +82,15 @@ pub async fn logout(state: tauri::State<'_, WalletState>) -> Result<(), BackendE
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn random_mnemonic() -> Mnemonic {
|
||||
fn random_mnemonic() -> ZeroizeMnemonicWrapper {
|
||||
let mut rng = rand::thread_rng();
|
||||
Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap()
|
||||
Mnemonic::generate_in_with(&mut rng, Language::English, 24)
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
|
||||
async fn _connect_with_mnemonic(
|
||||
mnemonic: Mnemonic,
|
||||
mnemonic: ZeroizeMnemonicWrapper,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<Account, BackendError> {
|
||||
{
|
||||
@@ -139,7 +141,7 @@ async fn _connect_with_mnemonic(
|
||||
&default_nyxd_urls,
|
||||
&default_api_urls,
|
||||
&config,
|
||||
&mnemonic,
|
||||
mnemonic.as_ref(),
|
||||
)?;
|
||||
|
||||
// Set the default account
|
||||
@@ -296,30 +298,31 @@ pub fn does_password_file_exist() -> Result<bool, BackendError> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_password(mnemonic: &str, password: String) -> Result<(), BackendError> {
|
||||
pub fn create_password(
|
||||
mnemonic: SensitiveStringWrapper,
|
||||
password: UserPassword,
|
||||
) -> Result<(), BackendError> {
|
||||
if does_password_file_exist()? {
|
||||
return Err(BackendError::WalletFileAlreadyExists);
|
||||
}
|
||||
log::info!("Creating password");
|
||||
|
||||
let mnemonic = Mnemonic::from_str(mnemonic)?;
|
||||
let mnemonic = ZeroizeMnemonicWrapper::try_from_string(mnemonic)?;
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
// Currently we only support a single, default, login id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
wallet_storage::store_login_with_multiple_accounts(mnemonic, hd_path, login_id, &password)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sign_in_with_password(
|
||||
password: String,
|
||||
password: UserPassword,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<Account, BackendError> {
|
||||
log::info!("Signing in with password");
|
||||
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
|
||||
let mnemonic = extract_first_mnemonic(&stored_login)?;
|
||||
@@ -327,7 +330,7 @@ pub async fn sign_in_with_password(
|
||||
set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone())
|
||||
.await?;
|
||||
|
||||
_connect_with_mnemonic(mnemonic, state).await
|
||||
_connect_with_mnemonic(mnemonic.into(), state).await
|
||||
}
|
||||
|
||||
fn extract_first_mnemonic(
|
||||
@@ -352,7 +355,7 @@ fn extract_first_mnemonic(
|
||||
#[tauri::command]
|
||||
pub async fn sign_in_with_password_and_account_id(
|
||||
account_id: &str,
|
||||
password: &str,
|
||||
password: UserPassword,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<Account, BackendError> {
|
||||
log::info!("Signing in with password");
|
||||
@@ -360,7 +363,6 @@ pub async fn sign_in_with_password_and_account_id(
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
|
||||
let mnemonic = extract_mnemonic(&stored_login, &account_id)?;
|
||||
@@ -368,7 +370,7 @@ pub async fn sign_in_with_password_and_account_id(
|
||||
set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone())
|
||||
.await?;
|
||||
|
||||
_connect_with_mnemonic(mnemonic, state).await
|
||||
_connect_with_mnemonic(mnemonic.into(), state).await
|
||||
}
|
||||
|
||||
fn extract_mnemonic(
|
||||
@@ -402,18 +404,17 @@ pub fn archive_wallet_file() -> Result<(), BackendError> {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_account_for_password(
|
||||
mnemonic: &str,
|
||||
password: &str,
|
||||
mnemonic: SensitiveStringWrapper,
|
||||
password: UserPassword,
|
||||
account_id: &str,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<AccountEntry, BackendError> {
|
||||
log::info!("Adding account for the current password: {account_id}");
|
||||
let mnemonic = Mnemonic::from_str(mnemonic)?;
|
||||
let mnemonic = ZeroizeMnemonicWrapper::try_from_string(mnemonic)?;
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
// Currently we only support a single, default, login id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
|
||||
wallet_storage::append_account_to_login(
|
||||
mnemonic.clone(),
|
||||
@@ -426,7 +427,13 @@ pub async fn add_account_for_password(
|
||||
let address = {
|
||||
let state = state.read().await;
|
||||
let network: NymNetworkDetails = state.current_network().into();
|
||||
derive_address(mnemonic, &network.chain_details.bech32_account_prefix)?.to_string()
|
||||
// safety: the call to `clone_inner` is fine here as the raw mnemonic will get immediately
|
||||
// passed to `DirectSecp256k1HdWallet` which will zeroize it on drop
|
||||
derive_address(
|
||||
mnemonic.into_cloned_inner(),
|
||||
&network.chain_details.bech32_account_prefix,
|
||||
)?
|
||||
.to_string()
|
||||
};
|
||||
|
||||
// Re-read all the acccounts from the wallet to reset the state, rather than updating it
|
||||
@@ -450,12 +457,10 @@ async fn set_state_with_all_accounts(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<(), BackendError> {
|
||||
log::trace!("Set state with accounts:");
|
||||
let all_accounts: Vec<_> = stored_login
|
||||
.unwrap_into_multiple_accounts(first_id_when_converting)
|
||||
.into_accounts()
|
||||
.collect();
|
||||
let stored = stored_login.unwrap_into_multiple_accounts(first_id_when_converting);
|
||||
let all_accounts = stored.inner();
|
||||
|
||||
for account in &all_accounts {
|
||||
for account in all_accounts {
|
||||
log::trace!("account: {:?}", account.id());
|
||||
}
|
||||
|
||||
@@ -490,7 +495,7 @@ async fn set_state_with_all_accounts(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_account_for_password(
|
||||
password: &str,
|
||||
password: UserPassword,
|
||||
account_id: &str,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<(), BackendError> {
|
||||
@@ -498,7 +503,6 @@ pub async fn remove_account_for_password(
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
wallet_storage::remove_account_from_login(&login_id, &account_id, &password)?;
|
||||
|
||||
// Load to reset the internal state
|
||||
@@ -513,7 +517,8 @@ fn derive_address(
|
||||
mnemonic: bip39::Mnemonic,
|
||||
prefix: &str,
|
||||
) -> Result<cosmrs::AccountId, BackendError> {
|
||||
DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic)?
|
||||
// note: the ephemeral wallet will zeroize the mnemonic on drop
|
||||
DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic)
|
||||
.try_derive_accounts()?
|
||||
.first()
|
||||
.map(AccountData::address)
|
||||
@@ -547,21 +552,20 @@ pub async fn list_accounts(
|
||||
#[tauri::command]
|
||||
pub fn show_mnemonic_for_account_in_password(
|
||||
account_id: String,
|
||||
password: String,
|
||||
) -> Result<String, BackendError> {
|
||||
password: UserPassword,
|
||||
) -> Result<SensitiveStringWrapper, BackendError> {
|
||||
log::info!("Getting mnemonic for: {account_id}");
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id);
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
let mnemonic = _show_mnemonic_for_account_in_password(&login_id, &account_id, &password)?;
|
||||
Ok(mnemonic.to_string())
|
||||
Ok(mnemonic.into_string())
|
||||
}
|
||||
|
||||
fn _show_mnemonic_for_account_in_password(
|
||||
login_id: &wallet_storage::LoginId,
|
||||
account_id: &wallet_storage::AccountId,
|
||||
password: &wallet_storage::UserPassword,
|
||||
) -> Result<bip39::Mnemonic, BackendError> {
|
||||
) -> Result<ZeroizeMnemonicWrapper, BackendError> {
|
||||
let stored_account = wallet_storage::load_existing_login(login_id, password)?;
|
||||
let mnemonic = match stored_account {
|
||||
wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(),
|
||||
@@ -571,12 +575,13 @@ fn _show_mnemonic_for_account_in_password(
|
||||
.mnemonic()
|
||||
.clone(),
|
||||
};
|
||||
Ok(mnemonic)
|
||||
Ok(mnemonic.into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::wallet_storage::{
|
||||
self,
|
||||
@@ -606,8 +611,8 @@ mod tests {
|
||||
|
||||
let all_accounts: Vec<_> = stored_login
|
||||
.unwrap_into_multiple_accounts(account_id.clone())
|
||||
.into_accounts()
|
||||
.collect();
|
||||
.inner()
|
||||
.to_vec();
|
||||
|
||||
assert_eq!(
|
||||
all_accounts,
|
||||
@@ -639,8 +644,8 @@ mod tests {
|
||||
|
||||
let all_accounts: Vec<_> = stored_login
|
||||
.unwrap_into_multiple_accounts(account_id)
|
||||
.into_accounts()
|
||||
.collect();
|
||||
.inner()
|
||||
.to_vec();
|
||||
|
||||
let expected_mn2 = bip39::Mnemonic::from_str("border hurt skull lunar goddess second danger game dismiss exhaust oven thumb dog drama onion false orchard spice tent next predict invite cherry green").unwrap();
|
||||
let expected_mn3 = bip39::Mnemonic::from_str("gentle crowd rule snap girl urge flat jump winner cluster night sand museum stock grunt quick tree acquire traffic major awake tag rack peasant").unwrap();
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
use crate::error::BackendError;
|
||||
use crate::nyxd_client;
|
||||
use crate::state::WalletState;
|
||||
use bip39::Mnemonic;
|
||||
use cosmwasm_std::Decimal;
|
||||
use nym_mixnet_contract_common::{IdentityKey, MixId, Percent};
|
||||
use nym_types::currency::DecCoin;
|
||||
use nym_types::mixnode::MixNodeCostParams;
|
||||
use nym_wallet_types::app::AppEnv;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use validator_client::nyxd::traits::MixnetQueryClient;
|
||||
use validator_client::nyxd::{tx, Coin, CosmosCoin, Gas, GasPrice};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
fn get_env_as_option(key: &str) -> Option<String> {
|
||||
match ::std::env::var(key) {
|
||||
@@ -192,3 +194,94 @@ pub async fn get_old_and_incorrect_hardcoded_fee(
|
||||
log::info!("hardcoded fee for {:?} is {:?}", operation, coin);
|
||||
guard.attempt_convert_to_display_dec_coin(coin)
|
||||
}
|
||||
|
||||
#[derive(Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct SensitiveStringWrapper(String);
|
||||
|
||||
impl<'de> Deserialize<'de> for SensitiveStringWrapper {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(SensitiveStringWrapper(String::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SensitiveStringWrapper {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
// unfortunately this serialized value will live on...
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for SensitiveStringWrapper {
|
||||
fn from(value: String) -> Self {
|
||||
SensitiveStringWrapper(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for SensitiveStringWrapper {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
// can't do it natively until https://github.com/rust-bitcoin/rust-bip39/pull/32 gets merged and released...
|
||||
pub(crate) struct ZeroizeMnemonicWrapper(bip39::Mnemonic);
|
||||
|
||||
impl From<bip39::Mnemonic> for ZeroizeMnemonicWrapper {
|
||||
fn from(value: Mnemonic) -> Self {
|
||||
ZeroizeMnemonicWrapper(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Zeroize for ZeroizeMnemonicWrapper {
|
||||
fn zeroize(&mut self) {
|
||||
// overwrite the mnemonic value with a completely random one
|
||||
// (a poor man's zeroize until bip39 crate does it properly...)
|
||||
self.0 = Mnemonic::generate(self.0.word_count()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ZeroizeMnemonicWrapper {
|
||||
fn drop(&mut self) {
|
||||
self.zeroize()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<bip39::Mnemonic> for ZeroizeMnemonicWrapper {
|
||||
fn as_ref(&self) -> &Mnemonic {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ZeroizeMnemonicWrapper {
|
||||
pub(crate) fn into_string(self) -> SensitiveStringWrapper {
|
||||
SensitiveStringWrapper(self.0.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_string(string: SensitiveStringWrapper) -> Result<Self, bip39::Error> {
|
||||
let res = string.as_ref().parse()?;
|
||||
Ok(ZeroizeMnemonicWrapper(res))
|
||||
}
|
||||
|
||||
// special care must be taken when calling this method as the mnemonic will no longer get zeroized!
|
||||
pub(crate) fn into_cloned_inner(self) -> bip39::Mnemonic {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn unchecked_clone_inner(&self) -> bip39::Mnemonic {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn generate_random() -> Self {
|
||||
ZeroizeMnemonicWrapper(Mnemonic::generate(24).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use validator_client::nyxd::bip32::DerivationPath;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::error::BackendError;
|
||||
use crate::utils::ZeroizeMnemonicWrapper;
|
||||
|
||||
use super::encryption::EncryptedData;
|
||||
use super::password::{AccountId, LoginId};
|
||||
@@ -193,6 +194,7 @@ impl StoredLogin {
|
||||
|
||||
/// Multiple stored accounts, each entry having an id and a data field.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)]
|
||||
#[zeroize(drop)]
|
||||
pub(crate) struct MultipleAccounts {
|
||||
accounts: Vec<WalletAccount>,
|
||||
}
|
||||
@@ -220,8 +222,8 @@ impl MultipleAccounts {
|
||||
.find(|account| account.mnemonic() == mnemonic)
|
||||
}
|
||||
|
||||
pub(crate) fn into_accounts(self) -> impl Iterator<Item = WalletAccount> {
|
||||
self.accounts.into_iter()
|
||||
pub(crate) fn inner(&self) -> &[WalletAccount] {
|
||||
&self.accounts
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
@@ -236,17 +238,19 @@ impl MultipleAccounts {
|
||||
pub(crate) fn add(
|
||||
&mut self,
|
||||
id: AccountId,
|
||||
mnemonic: bip39::Mnemonic,
|
||||
mnemonic: ZeroizeMnemonicWrapper,
|
||||
hd_path: DerivationPath,
|
||||
) -> Result<(), BackendError> {
|
||||
if self.get_account(&id).is_some() {
|
||||
Err(BackendError::WalletAccountIdAlreadyExistsInWalletLogin)
|
||||
} else if self.get_account_with_mnemonic(&mnemonic).is_some() {
|
||||
} else if self.get_account_with_mnemonic(mnemonic.as_ref()).is_some() {
|
||||
Err(BackendError::WalletMnemonicAlreadyExistsInWalletLogin)
|
||||
} else {
|
||||
self.accounts.push(WalletAccount::new(
|
||||
id,
|
||||
MnemonicAccount::new(mnemonic, hd_path),
|
||||
// safety: the call to `clone_inner` is fine here as the raw mnemonic will get immediately
|
||||
// passed to `MnemonicAccount` which will zeroize it on drop
|
||||
MnemonicAccount::new(mnemonic.into_cloned_inner(), hd_path),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
@@ -269,6 +273,7 @@ impl From<Vec<WalletAccount>> for MultipleAccounts {
|
||||
|
||||
/// An entry in the list of stored accounts
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)]
|
||||
#[zeroize(drop)]
|
||||
pub(crate) struct WalletAccount {
|
||||
id: AccountId,
|
||||
account: AccountData,
|
||||
|
||||
@@ -14,10 +14,11 @@ pub(crate) use crate::wallet_storage::password::{AccountId, LoginId, UserPasswor
|
||||
|
||||
use crate::error::BackendError;
|
||||
use crate::platform_constants::{STORAGE_DIR_NAME, WALLET_INFO_FILENAME};
|
||||
use chrono::Local;
|
||||
use crate::utils::ZeroizeMnemonicWrapper;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::{self, create_dir_all, OpenOptions};
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::OffsetDateTime;
|
||||
use validator_client::nyxd::bip32::DerivationPath;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -138,7 +139,7 @@ fn store_login_at_file(
|
||||
}
|
||||
|
||||
pub(crate) fn store_login_with_multiple_accounts(
|
||||
mnemonic: bip39::Mnemonic,
|
||||
mnemonic: ZeroizeMnemonicWrapper,
|
||||
hd_path: DerivationPath,
|
||||
id: LoginId,
|
||||
password: &UserPassword,
|
||||
@@ -153,7 +154,7 @@ pub(crate) fn store_login_with_multiple_accounts(
|
||||
|
||||
fn store_login_with_multiple_accounts_at_file(
|
||||
filepath: &Path,
|
||||
mnemonic: bip39::Mnemonic,
|
||||
mnemonic: ZeroizeMnemonicWrapper,
|
||||
hd_path: DerivationPath,
|
||||
id: LoginId,
|
||||
password: &UserPassword,
|
||||
@@ -184,7 +185,7 @@ fn store_login_with_multiple_accounts_at_file(
|
||||
/// account in the list of accounts associated with the encrypted entry. The inner id for this
|
||||
/// entry will be set to the same as the outer, unencrypted, id.
|
||||
pub(crate) fn append_account_to_login(
|
||||
mnemonic: bip39::Mnemonic,
|
||||
mnemonic: ZeroizeMnemonicWrapper,
|
||||
hd_path: DerivationPath,
|
||||
id: LoginId,
|
||||
inner_id: AccountId,
|
||||
@@ -200,7 +201,7 @@ pub(crate) fn append_account_to_login(
|
||||
|
||||
fn append_account_to_login_at_file(
|
||||
filepath: &Path,
|
||||
mnemonic: bip39::Mnemonic,
|
||||
mnemonic: ZeroizeMnemonicWrapper,
|
||||
hd_path: DerivationPath,
|
||||
id: LoginId,
|
||||
inner_id: AccountId,
|
||||
@@ -283,7 +284,10 @@ fn append_timestamp_to_filename(
|
||||
}
|
||||
|
||||
fn _archive_wallet_file(path: &Path) -> Result<(), BackendError> {
|
||||
let timestamp: OsString = Local::now().timestamp_millis().to_string().into();
|
||||
let timestamp: OsString = OffsetDateTime::now_utc()
|
||||
.unix_timestamp()
|
||||
.to_string()
|
||||
.into();
|
||||
let mut additional_number = 0;
|
||||
let mut new_path = append_timestamp_to_filename(path, timestamp.clone(), None)?;
|
||||
|
||||
@@ -418,12 +422,19 @@ mod tests {
|
||||
fn store_single_login() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
|
||||
store_login_at_file(&wallet_file, account1, hd_path, id1.clone(), &password).unwrap();
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.into_cloned_inner(),
|
||||
hd_path,
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let stored_wallet = load_existing_wallet_at_file(&wallet_file).unwrap();
|
||||
assert_eq!(stored_wallet.len(), 1);
|
||||
@@ -439,7 +450,7 @@ mod tests {
|
||||
fn store_single_login_with_multi() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -467,7 +478,7 @@ mod tests {
|
||||
fn store_twice_for_the_same_id_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -475,7 +486,7 @@ mod tests {
|
||||
// Store the first login
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.clone(),
|
||||
account1.unchecked_clone_inner(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
@@ -484,7 +495,13 @@ mod tests {
|
||||
|
||||
// and storing the same id again fails
|
||||
assert!(matches!(
|
||||
store_login_at_file(&wallet_file, account1, hd_path, id1, &password,),
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.into_cloned_inner(),
|
||||
hd_path,
|
||||
id1,
|
||||
&password,
|
||||
),
|
||||
Err(BackendError::WalletLoginIdAlreadyExists),
|
||||
));
|
||||
}
|
||||
@@ -493,7 +510,7 @@ mod tests {
|
||||
fn store_twice_for_the_same_id_fails_with_multiple() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -525,13 +542,20 @@ mod tests {
|
||||
fn load_with_wrong_password_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let bad_password = UserPassword::new("bad-password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
|
||||
store_login_at_file(&wallet_file, account1, hd_path, id1.clone(), &password).unwrap();
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.into_cloned_inner(),
|
||||
hd_path,
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Trying to load it with wrong password now fails
|
||||
assert!(matches!(
|
||||
@@ -544,7 +568,7 @@ mod tests {
|
||||
fn load_with_wrong_password_fails_with_multi() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let bad_password = UserPassword::new("bad-password".to_string());
|
||||
@@ -570,13 +594,20 @@ mod tests {
|
||||
fn load_with_wrong_id_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
let id2 = LoginId::new("second".to_string());
|
||||
|
||||
store_login_at_file(&wallet_file, account1, hd_path, id1, &password).unwrap();
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.into_cloned_inner(),
|
||||
hd_path,
|
||||
id1,
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Trying to load with the wrong id
|
||||
assert!(matches!(
|
||||
@@ -589,7 +620,7 @@ mod tests {
|
||||
fn load_with_wrong_id_fails_with_multi() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -609,14 +640,14 @@ mod tests {
|
||||
fn store_and_load_a_single_login() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.clone(),
|
||||
account1.unchecked_clone_inner(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
@@ -625,7 +656,7 @@ mod tests {
|
||||
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap();
|
||||
let acc = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account1, acc.mnemonic());
|
||||
assert_eq!(account1.as_ref(), acc.mnemonic());
|
||||
assert_eq!(&hd_path, acc.hd_path());
|
||||
}
|
||||
|
||||
@@ -633,7 +664,7 @@ mod tests {
|
||||
fn store_and_load_a_single_login_with_multi() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let acc1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let acc1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -654,7 +685,7 @@ mod tests {
|
||||
.get_account(&DEFAULT_FIRST_ACCOUNT_NAME.into())
|
||||
.unwrap();
|
||||
assert_eq!(account.id().as_ref(), DEFAULT_FIRST_ACCOUNT_NAME);
|
||||
assert_eq!(account.mnemonic(), &acc1);
|
||||
assert_eq!(account.mnemonic(), acc1.as_ref());
|
||||
assert_eq!(account.hd_path(), &hd_path);
|
||||
}
|
||||
|
||||
@@ -662,8 +693,8 @@ mod tests {
|
||||
fn store_a_second_login_with_a_different_password_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let bad_password = UserPassword::new("bad-password".to_string());
|
||||
@@ -672,7 +703,7 @@ mod tests {
|
||||
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1,
|
||||
account1.into_cloned_inner(),
|
||||
cosmos_hd_path.clone(),
|
||||
id1,
|
||||
&password,
|
||||
@@ -681,7 +712,13 @@ mod tests {
|
||||
|
||||
// Can't store a second login if you use different password
|
||||
assert!(matches!(
|
||||
store_login_at_file(&wallet_file, account2, cosmos_hd_path, id2, &bad_password),
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account2.into_cloned_inner(),
|
||||
cosmos_hd_path,
|
||||
id2,
|
||||
&bad_password
|
||||
),
|
||||
Err(BackendError::WalletDifferentPasswordDetected),
|
||||
));
|
||||
}
|
||||
@@ -690,8 +727,8 @@ mod tests {
|
||||
fn store_a_second_login_with_a_different_password_fails_with_multi() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let bad_password = UserPassword::new("bad-password".to_string());
|
||||
@@ -711,7 +748,7 @@ mod tests {
|
||||
assert!(matches!(
|
||||
store_login_with_multiple_accounts_at_file(
|
||||
&wallet_file,
|
||||
account2,
|
||||
account2.into(),
|
||||
hd_path,
|
||||
id2,
|
||||
&bad_password
|
||||
@@ -724,8 +761,8 @@ mod tests {
|
||||
fn store_two_mnemonic_accounts_gives_different_salts_and_iv() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let different_hd_path: DerivationPath = "m".parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
@@ -733,7 +770,14 @@ mod tests {
|
||||
let id2 = LoginId::new("second".to_string());
|
||||
|
||||
// Store the first account
|
||||
store_login_at_file(&wallet_file, account1, hd_path, id1, &password).unwrap();
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.into_cloned_inner(),
|
||||
hd_path,
|
||||
id1,
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let stored_wallet = load_existing_wallet_at_file(&wallet_file).unwrap();
|
||||
let encrypted_blob = &stored_wallet
|
||||
@@ -746,7 +790,14 @@ mod tests {
|
||||
let original_salt = encrypted_blob.salt().to_vec();
|
||||
|
||||
// Add an extra account
|
||||
store_login_at_file(&wallet_file, account2, different_hd_path, id2, &password).unwrap();
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account2.into_cloned_inner(),
|
||||
different_hd_path,
|
||||
id2,
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let loaded_accounts = load_existing_wallet_at_file(&wallet_file).unwrap();
|
||||
assert_eq!(loaded_accounts.len(), 2);
|
||||
@@ -764,8 +815,8 @@ mod tests {
|
||||
fn store_two_mnemonic_accounts_using_two_logins() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let different_hd_path: DerivationPath = "m".parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
@@ -775,7 +826,7 @@ mod tests {
|
||||
// Store the first account
|
||||
store_login_at_file(
|
||||
&wallet,
|
||||
account1.clone(),
|
||||
account1.unchecked_clone_inner(),
|
||||
cosmos_hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
@@ -784,13 +835,13 @@ mod tests {
|
||||
|
||||
let login = load_existing_login_at_file(&wallet, &id1, &password).unwrap();
|
||||
let acc = login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account1, acc.mnemonic());
|
||||
assert_eq!(account1.as_ref(), acc.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc.hd_path());
|
||||
|
||||
// Add an extra account
|
||||
store_login_at_file(
|
||||
&wallet,
|
||||
account2.clone(),
|
||||
account2.unchecked_clone_inner(),
|
||||
different_hd_path.clone(),
|
||||
id2.clone(),
|
||||
&password,
|
||||
@@ -800,12 +851,12 @@ mod tests {
|
||||
// first account should be unchanged
|
||||
let loaded_login = load_existing_login_at_file(&wallet, &id1, &password).unwrap();
|
||||
let acc1 = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account1, acc1.mnemonic());
|
||||
assert_eq!(account1.as_ref(), acc1.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
||||
|
||||
let loaded_login = load_existing_login_at_file(&wallet, &id2, &password).unwrap();
|
||||
let acc2 = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account2, acc2.mnemonic());
|
||||
assert_eq!(account2.as_ref(), acc2.mnemonic());
|
||||
assert_eq!(&different_hd_path, acc2.hd_path());
|
||||
}
|
||||
|
||||
@@ -813,8 +864,8 @@ mod tests {
|
||||
fn store_one_mnemonic_account_and_one_multi_account() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let different_hd_path: DerivationPath = "m".parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
@@ -824,7 +875,7 @@ mod tests {
|
||||
// Store the first account
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.clone(),
|
||||
account1.unchecked_clone_inner(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
@@ -833,7 +884,7 @@ mod tests {
|
||||
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap();
|
||||
let acc = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account1, acc.mnemonic());
|
||||
assert_eq!(account1.as_ref(), acc.mnemonic());
|
||||
assert_eq!(&hd_path, acc.hd_path());
|
||||
|
||||
// Add an extra account
|
||||
@@ -849,7 +900,7 @@ mod tests {
|
||||
// first account should be unchanged
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap();
|
||||
let acc1 = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account1, acc1.mnemonic());
|
||||
assert_eq!(account1.as_ref(), acc1.mnemonic());
|
||||
assert_eq!(&hd_path, acc1.hd_path());
|
||||
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap();
|
||||
@@ -859,7 +910,7 @@ mod tests {
|
||||
.get_account(&DEFAULT_FIRST_ACCOUNT_NAME.into())
|
||||
.unwrap();
|
||||
assert_eq!(account.id().as_ref(), DEFAULT_FIRST_ACCOUNT_NAME);
|
||||
assert_eq!(account.mnemonic(), &account2);
|
||||
assert_eq!(account.mnemonic(), account2.as_ref());
|
||||
assert_eq!(account.hd_path(), &different_hd_path);
|
||||
}
|
||||
|
||||
@@ -867,7 +918,7 @@ mod tests {
|
||||
fn remove_non_existent_id_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -887,8 +938,8 @@ mod tests {
|
||||
fn store_and_remove_wallet_login_information() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let different_hd_path: DerivationPath = "m".parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
@@ -898,7 +949,7 @@ mod tests {
|
||||
// Store two accounts with two different passwords
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.clone(),
|
||||
account1.unchecked_clone_inner(),
|
||||
cosmos_hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
@@ -906,7 +957,7 @@ mod tests {
|
||||
.unwrap();
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account2.clone(),
|
||||
account2.unchecked_clone_inner(),
|
||||
different_hd_path.clone(),
|
||||
id2.clone(),
|
||||
&password,
|
||||
@@ -916,12 +967,12 @@ mod tests {
|
||||
// Load and compare
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap();
|
||||
let acc1 = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account1, acc1.mnemonic());
|
||||
assert_eq!(account1.as_ref(), acc1.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
||||
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap();
|
||||
let acc2 = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account2, acc2.mnemonic());
|
||||
assert_eq!(account2.as_ref(), acc2.mnemonic());
|
||||
assert_eq!(&different_hd_path, acc2.hd_path());
|
||||
|
||||
// Delete the second account
|
||||
@@ -930,7 +981,7 @@ mod tests {
|
||||
// The first account should be unchanged
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap();
|
||||
let acc1 = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account1, acc1.mnemonic());
|
||||
assert_eq!(account1.as_ref(), acc1.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
||||
|
||||
// And we can't load the second one anymore
|
||||
@@ -957,8 +1008,8 @@ mod tests {
|
||||
fn append_account_converts_the_type() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -966,7 +1017,7 @@ mod tests {
|
||||
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.clone(),
|
||||
account1.unchecked_clone_inner(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
@@ -976,7 +1027,7 @@ mod tests {
|
||||
// Check that it's there as the correct non-multiple type
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap();
|
||||
let acc = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(acc.mnemonic(), &account1);
|
||||
assert_eq!(acc.mnemonic(), account1.as_ref());
|
||||
assert_eq!(acc.hd_path(), &hd_path);
|
||||
|
||||
append_account_to_login_at_file(
|
||||
@@ -993,8 +1044,14 @@ mod tests {
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap();
|
||||
let loaded_accounts = loaded_login.as_multiple_accounts().unwrap();
|
||||
let expected = vec![
|
||||
WalletAccount::new(id1.into(), MnemonicAccount::new(account1, hd_path.clone())),
|
||||
WalletAccount::new(id2, MnemonicAccount::new(account2, hd_path)),
|
||||
WalletAccount::new(
|
||||
id1.into(),
|
||||
MnemonicAccount::new(account1.into_cloned_inner(), hd_path.clone()),
|
||||
),
|
||||
WalletAccount::new(
|
||||
id2,
|
||||
MnemonicAccount::new(account2.into_cloned_inner(), hd_path),
|
||||
),
|
||||
]
|
||||
.into();
|
||||
assert_eq!(loaded_accounts, &expected);
|
||||
@@ -1004,10 +1061,10 @@ mod tests {
|
||||
fn append_accounts_to_existing_login() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account3 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account4 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account3 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account4 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -1017,7 +1074,7 @@ mod tests {
|
||||
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.clone(),
|
||||
account1.unchecked_clone_inner(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
@@ -1026,7 +1083,7 @@ mod tests {
|
||||
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account2.clone(),
|
||||
account2.unchecked_clone_inner(),
|
||||
hd_path.clone(),
|
||||
id2.clone(),
|
||||
&password,
|
||||
@@ -1036,7 +1093,7 @@ mod tests {
|
||||
// Check that it's there as the correct non-multiple type
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap();
|
||||
let acc2 = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(acc2.mnemonic(), &account2);
|
||||
assert_eq!(acc2.mnemonic(), account2.as_ref());
|
||||
assert_eq!(acc2.hd_path(), &hd_path);
|
||||
|
||||
// Add a third and fourth mnenonic grouped together with the second one
|
||||
@@ -1062,15 +1119,24 @@ mod tests {
|
||||
// Check that we can load all four
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap();
|
||||
let acc1 = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(acc1.mnemonic(), &account1);
|
||||
assert_eq!(acc1.mnemonic(), account1.as_ref());
|
||||
assert_eq!(acc1.hd_path(), &hd_path);
|
||||
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap();
|
||||
let loaded_accounts = loaded_login.as_multiple_accounts().unwrap();
|
||||
let expected = vec![
|
||||
WalletAccount::new(id2.into(), MnemonicAccount::new(account2, hd_path.clone())),
|
||||
WalletAccount::new(id3, MnemonicAccount::new(account3, hd_path.clone())),
|
||||
WalletAccount::new(id4, MnemonicAccount::new(account4, hd_path)),
|
||||
WalletAccount::new(
|
||||
id2.into(),
|
||||
MnemonicAccount::new(account2.into_cloned_inner(), hd_path.clone()),
|
||||
),
|
||||
WalletAccount::new(
|
||||
id3,
|
||||
MnemonicAccount::new(account3.into_cloned_inner(), hd_path.clone()),
|
||||
),
|
||||
WalletAccount::new(
|
||||
id4,
|
||||
MnemonicAccount::new(account4.into_cloned_inner(), hd_path),
|
||||
),
|
||||
]
|
||||
.into();
|
||||
assert_eq!(loaded_accounts, &expected);
|
||||
@@ -1080,10 +1146,10 @@ mod tests {
|
||||
fn append_accounts_to_existing_login_with_multi() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account3 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account4 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account3 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account4 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -1134,7 +1200,7 @@ mod tests {
|
||||
let loaded_accounts = loaded_login.as_multiple_accounts().unwrap();
|
||||
let expected = vec![WalletAccount::new(
|
||||
DEFAULT_FIRST_ACCOUNT_NAME.into(),
|
||||
MnemonicAccount::new(account1, hd_path.clone()),
|
||||
MnemonicAccount::new(account1.into_cloned_inner(), hd_path.clone()),
|
||||
)]
|
||||
.into();
|
||||
assert_eq!(loaded_accounts, &expected);
|
||||
@@ -1144,10 +1210,16 @@ mod tests {
|
||||
let expected = vec![
|
||||
WalletAccount::new(
|
||||
DEFAULT_FIRST_ACCOUNT_NAME.into(),
|
||||
MnemonicAccount::new(account2, hd_path.clone()),
|
||||
MnemonicAccount::new(account2.into_cloned_inner(), hd_path.clone()),
|
||||
),
|
||||
WalletAccount::new(
|
||||
id3,
|
||||
MnemonicAccount::new(account3.into_cloned_inner(), hd_path.clone()),
|
||||
),
|
||||
WalletAccount::new(
|
||||
id4,
|
||||
MnemonicAccount::new(account4.into_cloned_inner(), hd_path),
|
||||
),
|
||||
WalletAccount::new(id3, MnemonicAccount::new(account3, hd_path.clone())),
|
||||
WalletAccount::new(id4, MnemonicAccount::new(account4, hd_path)),
|
||||
]
|
||||
.into();
|
||||
assert_eq!(loaded_accounts, &expected);
|
||||
@@ -1157,7 +1229,7 @@ mod tests {
|
||||
fn append_the_same_mnemonic_twice_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -1182,14 +1254,21 @@ mod tests {
|
||||
fn delete_the_same_account_twice_for_a_login_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
let id2 = AccountId::new("second".to_string());
|
||||
|
||||
store_login_at_file(&wallet, account1, hd_path.clone(), id1.clone(), &password).unwrap();
|
||||
store_login_at_file(
|
||||
&wallet,
|
||||
account1.into_cloned_inner(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
append_account_to_login_at_file(
|
||||
&wallet,
|
||||
@@ -1213,8 +1292,8 @@ mod tests {
|
||||
fn delete_the_same_account_twice_for_a_login_fails_with_multi() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -1251,9 +1330,9 @@ mod tests {
|
||||
fn delete_appended_account_doesnt_affect_others() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account3 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account3 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -1262,7 +1341,7 @@ mod tests {
|
||||
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1,
|
||||
account1.into_cloned_inner(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
@@ -1271,7 +1350,7 @@ mod tests {
|
||||
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account2.clone(),
|
||||
account2.unchecked_clone_inner(),
|
||||
hd_path.clone(),
|
||||
id2.clone(),
|
||||
&password,
|
||||
@@ -1294,8 +1373,14 @@ mod tests {
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id2, &password).unwrap();
|
||||
let loaded_accounts = loaded_login.as_multiple_accounts().unwrap();
|
||||
let expected = vec![
|
||||
WalletAccount::new(id2.into(), MnemonicAccount::new(account2, hd_path.clone())),
|
||||
WalletAccount::new(id3, MnemonicAccount::new(account3, hd_path)),
|
||||
WalletAccount::new(
|
||||
id2.into(),
|
||||
MnemonicAccount::new(account2.into_cloned_inner(), hd_path.clone()),
|
||||
),
|
||||
WalletAccount::new(
|
||||
id3,
|
||||
MnemonicAccount::new(account3.into_cloned_inner(), hd_path),
|
||||
),
|
||||
]
|
||||
.into();
|
||||
assert_eq!(loaded_accounts, &expected);
|
||||
@@ -1305,8 +1390,8 @@ mod tests {
|
||||
fn remove_all_accounts_for_a_login_removes_the_file_when_empty() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -1354,9 +1439,9 @@ mod tests {
|
||||
fn remove_all_accounts_for_a_login_removes_that_login() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account3 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let account1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let account3 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -1411,7 +1496,7 @@ mod tests {
|
||||
let acc3 = loaded_login.as_multiple_accounts().unwrap();
|
||||
let expected = vec![WalletAccount::new(
|
||||
DEFAULT_FIRST_ACCOUNT_NAME.into(),
|
||||
MnemonicAccount::new(account3, hd_path),
|
||||
MnemonicAccount::new(account3.into_cloned_inner(), hd_path),
|
||||
)]
|
||||
.into();
|
||||
assert_eq!(acc3, &expected);
|
||||
@@ -1421,10 +1506,10 @@ mod tests {
|
||||
fn append_accounts_and_remove_appended_accounts() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let acc1 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let acc2 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let acc3 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let acc4 = bip39::Mnemonic::generate(24).unwrap();
|
||||
let acc1 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let acc2 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let acc3 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let acc4 = ZeroizeMnemonicWrapper::generate_random();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
@@ -1434,7 +1519,7 @@ mod tests {
|
||||
|
||||
store_login_at_file(
|
||||
&wallet,
|
||||
acc1.clone(),
|
||||
acc1.unchecked_clone_inner(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
@@ -1443,7 +1528,7 @@ mod tests {
|
||||
|
||||
store_login_at_file(
|
||||
&wallet,
|
||||
acc2.clone(),
|
||||
acc2.unchecked_clone_inner(),
|
||||
hd_path.clone(),
|
||||
id2.clone(),
|
||||
&password,
|
||||
@@ -1479,9 +1564,12 @@ mod tests {
|
||||
let expected = vec![
|
||||
WalletAccount::new(
|
||||
id2.clone().into(),
|
||||
MnemonicAccount::new(acc2, hd_path.clone()),
|
||||
MnemonicAccount::new(acc2.into_cloned_inner(), hd_path.clone()),
|
||||
),
|
||||
WalletAccount::new(
|
||||
id4.clone(),
|
||||
MnemonicAccount::new(acc4.into_cloned_inner(), hd_path.clone()),
|
||||
),
|
||||
WalletAccount::new(id4.clone(), MnemonicAccount::new(acc4, hd_path.clone())),
|
||||
]
|
||||
.into();
|
||||
assert_eq!(loaded_accounts, &expected);
|
||||
@@ -1497,7 +1585,7 @@ mod tests {
|
||||
// The first login is still available
|
||||
let loaded_login = load_existing_login_at_file(&wallet, &id1, &password).unwrap();
|
||||
let account = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(account.mnemonic(), &acc1);
|
||||
assert_eq!(account.mnemonic(), acc1.as_ref());
|
||||
assert_eq!(account.hd_path(), &hd_path);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
// The `LoginId` is the top level id in the wallet file, and is not stored encrypted
|
||||
@@ -83,11 +83,12 @@ impl fmt::Display for AccountId {
|
||||
// simple wrapper for String that will get zeroized on drop
|
||||
#[derive(Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub(crate) struct UserPassword(String);
|
||||
pub struct UserPassword(String);
|
||||
|
||||
impl UserPassword {
|
||||
pub(crate) fn new(pass: String) -> UserPassword {
|
||||
UserPassword(pass)
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new(inner: String) -> Self {
|
||||
UserPassword(inner)
|
||||
}
|
||||
|
||||
pub(crate) fn as_bytes(&self) -> &[u8] {
|
||||
@@ -95,8 +96,11 @@ impl UserPassword {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for UserPassword {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
impl<'de> Deserialize<'de> for UserPassword {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(UserPassword(String::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ bs58 = "0.4"
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
clap_complete = "4.0"
|
||||
clap_complete_fig = "4.0"
|
||||
dotenv = "0.15.0"
|
||||
dotenvy = "0.15.6"
|
||||
log = { workspace = true }
|
||||
pretty_env_logger = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
Reference in New Issue
Block a user