diff --git a/Cargo.lock b/Cargo.lock index 1008afe7c8..c7cf2a3392 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,9 +338,9 @@ dependencies = [ [[package]] name = "async-std" -version = "1.12.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +checksum = "52580991739c5cdb36cde8b2a516371c0a3b70dda36d916cc08b82372916808c" dependencies = [ "async-channel", "async-global-executor", @@ -356,6 +356,7 @@ dependencies = [ "kv-log-macro", "log", "memchr", + "num_cpus", "once_cell", "pin-project-lite", "pin-utils", @@ -382,7 +383,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -399,7 +400,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -600,13 +601,13 @@ dependencies = [ "lazy_static", "lazycell", "peeking_take_while", - "prettyplease 0.2.5", + "prettyplease 0.2.4", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -1515,7 +1516,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -1526,7 +1527,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" dependencies = [ "darling_core", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -1662,7 +1663,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -1845,7 +1846,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -2217,7 +2218,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -2457,9 +2458,9 @@ dependencies = [ [[package]] name = "hdpath" -version = "0.6.3" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa5bc9db2c17d2660f53ce217b778d06d68de13d1cd01c0f4e5de4b7918935f" +checksum = "09ae1615f843ce3981b47468f3f7c435ac17deb33c2261e64d7f1e87f5c11acc" dependencies = [ "byteorder", ] @@ -2864,7 +2865,7 @@ dependencies = [ "toml", "tonic", "tracing 0.1.37", - "uuid 1.3.3", + "uuid 1.3.2", ] [[package]] @@ -2937,6 +2938,17 @@ dependencies = [ "parity-scale-codec", ] +[[package]] +name = "impl-num-traits" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951641f13f873bff03d4bf19ae8bec531935ac0ac2cc775f84d7edfdcfed3f17" +dependencies = [ + "integer-sqrt", + "num-traits 0.2.15", + "uint", +] + [[package]] name = "impl-serde" version = "0.4.0" @@ -3022,6 +3034,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits 0.2.15", +] + [[package]] name = "io-lifetimes" version = "1.0.10" @@ -3065,9 +3086,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.63" +version = "0.3.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" dependencies = [ "wasm-bindgen", ] @@ -3180,9 +3201,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" [[package]] name = "librocksdb-sys" @@ -3367,7 +3388,7 @@ dependencies = [ [[package]] name = "masp_note_encryption" version = "0.2.0" -source = "git+https://github.com/anoma/masp?rev=cfea8c95d3f73077ca3e25380fd27e5b46e828fd#cfea8c95d3f73077ca3e25380fd27e5b46e828fd" +source = "git+https://github.com/anoma/masp?rev=9320c6b69b5d2e97134866871e960f0a31703813#9320c6b69b5d2e97134866871e960f0a31703813" dependencies = [ "borsh", "chacha20 0.9.1", @@ -3380,7 +3401,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "0.9.0" -source = "git+https://github.com/anoma/masp?rev=cfea8c95d3f73077ca3e25380fd27e5b46e828fd#cfea8c95d3f73077ca3e25380fd27e5b46e828fd" +source = "git+https://github.com/anoma/masp?rev=9320c6b69b5d2e97134866871e960f0a31703813#9320c6b69b5d2e97134866871e960f0a31703813" dependencies = [ "aes", "bip0039", @@ -3410,7 +3431,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "0.9.0" -source = "git+https://github.com/anoma/masp?rev=cfea8c95d3f73077ca3e25380fd27e5b46e828fd#cfea8c95d3f73077ca3e25380fd27e5b46e828fd" +source = "git+https://github.com/anoma/masp?rev=9320c6b69b5d2e97134866871e960f0a31703813#9320c6b69b5d2e97134866871e960f0a31703813" dependencies = [ "bellman", "blake2b_simd", @@ -3617,7 +3638,7 @@ dependencies = [ "tagptr", "thiserror", "triomphe", - "uuid 1.3.3", + "uuid 1.3.2", ] [[package]] @@ -3667,8 +3688,6 @@ dependencies = [ "rand_core 0.6.4", "rayon", "ripemd", - "rust_decimal", - "rust_decimal_macros", "serde 1.0.163", "serde_json", "sha2 0.9.9", @@ -3749,8 +3768,6 @@ dependencies = [ "rlimit", "rocksdb", "rpassword", - "rust_decimal", - "rust_decimal_macros", "serde 1.0.163", "serde_bytes", "serde_json", @@ -3790,17 +3807,20 @@ dependencies = [ "data-encoding", "derivative", "ed25519-consensus", + "eyre", "ferveo", "ferveo-common", "group-threshold-cryptography", "ibc", "ibc-proto", "ics23", + "impl-num-traits", "index-set", "itertools", "libsecp256k1 0.7.0", "masp_primitives", "namada_macros", + "num-traits 0.2.15", "pretty_assertions", "proptest", "prost", @@ -3808,8 +3828,6 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "rayon", - "rust_decimal", - "rust_decimal_macros", "serde 1.0.163", "serde_json", "sha2 0.9.9", @@ -3818,9 +3836,11 @@ dependencies = [ "tendermint-proto", "test-log", "thiserror", + "toml", "tonic-build", "tracing 0.1.37", "tracing-subscriber 0.3.17", + "uint", "zeroize", ] @@ -3856,8 +3876,6 @@ dependencies = [ "once_cell", "proptest", "proptest-state-machine", - "rust_decimal", - "rust_decimal_macros", "test-log", "thiserror", "tracing 0.1.37", @@ -3905,8 +3923,6 @@ dependencies = [ "prost", "rand 0.8.5", "regex", - "rust_decimal", - "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -3927,7 +3943,6 @@ dependencies = [ "namada_macros", "namada_proof_of_stake", "namada_vm_env", - "rust_decimal", "sha2 0.9.9", "thiserror", ] @@ -4203,7 +4218,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -4454,7 +4469,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -4557,12 +4572,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617feabb81566b593beb4886fb8c1f38064169dae4dccad0e3220160c3b37203" +checksum = "1ceca8aaf45b5c46ec7ed39fff75f57290368c1846d33d24a122ca81416ab058" dependencies = [ "proc-macro2", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -4622,9 +4637,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -5023,9 +5038,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" dependencies = [ "base64 0.21.0", "bytes", @@ -5124,7 +5139,7 @@ dependencies = [ "rkyv_derive", "seahash", "tinyvec", - "uuid 1.3.3", + "uuid 1.3.2", ] [[package]] @@ -5173,28 +5188,6 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" -[[package]] -name = "rust_decimal" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" -dependencies = [ - "arrayvec 0.7.2", - "borsh", - "num-traits 0.2.15", - "serde 1.0.163", -] - -[[package]] -name = "rust_decimal_macros" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" -dependencies = [ - "quote", - "rust_decimal", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -5570,7 +5563,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -5592,7 +5585,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -5905,9 +5898,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ "proc-macro2", "quote", @@ -6169,7 +6162,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -6341,7 +6334,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -6430,24 +6423,24 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.11" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ "serde 1.0.163", ] [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" [[package]] name = "toml_edit" -version = "0.19.9" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ "indexmap", "toml_datetime", @@ -6604,7 +6597,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] @@ -6825,9 +6818,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "universal-hash" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", "subtle 2.4.1", @@ -6870,9 +6863,9 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" [[package]] name = "uuid" -version = "1.3.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" dependencies = [ "getrandom 0.2.9", ] @@ -6885,9 +6878,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" [[package]] name = "vcpkg" @@ -7012,9 +7005,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -7022,24 +7015,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.36" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -7049,9 +7042,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.86" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7059,28 +7052,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" [[package]] name = "wasm-encoder" -version = "0.27.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e77053dc709db790691d3732cfc458adc5acc881dec524965c608effdcd9c581" +checksum = "d05d0b6fcd0aeb98adf16e7975331b3c17222aa815148f5b976370ce589d80ef" dependencies = [ "leb128", ] @@ -7323,9 +7316,9 @@ checksum = "718ed7c55c2add6548cca3ddd6383d738cd73b892df400e96b9aa876f0141d7a" [[package]] name = "wast" -version = "58.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372eecae2d10a5091c2005b32377d7ecd6feecdf2c05838056d02d8b4f07c429" +checksum = "6eb0f5ed17ac4421193c7477da05892c2edafd67f9639e3c11a82086416662dc" dependencies = [ "leb128", "memchr", @@ -7335,18 +7328,18 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.64" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d47446190e112ab1579ab40b3ad7e319d859d74e5134683f04e9f0747bf4173" +checksum = "ab9ab0d87337c3be2bb6fc5cd331c4ba9fd6bcb4ee85048a0dd59ed9ecf92e53" dependencies = [ "wast", ] [[package]] name = "web-sys" -version = "0.3.63" +version = "0.3.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721" dependencies = [ "js-sys", "wasm-bindgen", @@ -7677,7 +7670,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.15", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 317dd335e7..d078f4239a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,8 +83,8 @@ libc = "0.2.97" libloading = "0.7.2" libsecp256k1 = {git = "https://github.com/heliaxdev/libsecp256k1", rev = "bbb3bd44a49db361f21d9db80f9a087c194c0ae9", default-features = false, features = ["std", "static-context"]} # branch = "murisi/namada-integration" -masp_primitives = { git = "https://github.com/anoma/masp", rev = "cfea8c95d3f73077ca3e25380fd27e5b46e828fd" } -masp_proofs = { git = "https://github.com/anoma/masp", rev = "cfea8c95d3f73077ca3e25380fd27e5b46e828fd", default-features = false, features = ["local-prover"] } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "9320c6b69b5d2e97134866871e960f0a31703813" } +masp_proofs = { git = "https://github.com/anoma/masp", rev = "9320c6b69b5d2e97134866871e960f0a31703813", default-features = false, features = ["local-prover"] } num_cpus = "1.13.0" num-derive = "0.3.3" num-rational = "0.4.1" diff --git a/Makefile b/Makefile index 6a63da44ec..f4984d7853 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,12 @@ wasms_for_tests := wasm_for_tests/wasm_source # Paths for all the wasm templates wasm_templates := wasm/tx_template wasm/vp_template +ifdef JOBS +jobs := -j $(JOBS) +else +jobs := +endif + # TODO upgrade libp2p audit-ignores += RUSTSEC-2021-0076 @@ -35,13 +41,13 @@ crates += namada_vm_env crates += namada_vp_prelude build: - $(cargo) build + $(cargo) build $(jobs) build-test: - $(cargo) build --tests + $(cargo) build --tests $(jobs) build-release: - NAMADA_DEV=false $(cargo) build --release --package namada_apps --manifest-path Cargo.toml + NAMADA_DEV=false $(cargo) build $(jobs) --release --package namada_apps --manifest-path Cargo.toml install-release: NAMADA_DEV=false $(cargo) install --path ./apps --locked @@ -69,7 +75,7 @@ check-crates: clippy-wasm = $(cargo) +$(nightly) clippy --manifest-path $(wasm)/Cargo.toml --all-targets -- -D warnings clippy: - NAMADA_DEV=false $(cargo) +$(nightly) clippy --all-targets -- -D warnings && \ + NAMADA_DEV=false $(cargo) +$(nightly) clippy $(jobs) --all-targets -- -D warnings && \ make -C $(wasms) clippy && \ make -C $(wasms_for_tests) clippy && \ $(foreach wasm,$(wasm_templates),$(clippy-wasm) && ) true @@ -129,6 +135,7 @@ test-e2e: test-unit: $(cargo) +$(nightly) test \ $(TEST_FILTER) \ + $(jobs) \ -- --skip e2e \ -Z unstable-options --report-time @@ -136,11 +143,13 @@ test-unit-mainnet: $(cargo) +$(nightly) test \ --features "mainnet" \ $(TEST_FILTER) \ + $(jobs) \ -- --skip e2e \ -Z unstable-options --report-time test-unit-debug: $(debug-cargo) +$(nightly) test \ + $(jobs) \ $(TEST_FILTER) -- \ -- --skip e2e \ --nocapture \ diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 35b5483d7c..e7cc365d9c 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -108,8 +108,6 @@ ripemd.workspace = true rlimit.workspace = true rocksdb.workspace = true rpassword.workspace = true -rust_decimal_macros.workspace = true -rust_decimal.workspace = true serde_bytes.workspace = true serde_json = {workspace = true, features = ["raw_value"]} serde.workspace = true diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index b3078037ff..83f48cb830 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1766,12 +1766,13 @@ pub mod args { pub use namada::ledger::args::*; use namada::types::address::Address; use namada::types::chain::{ChainId, ChainIdPrefix}; + use namada::types::dec::Dec; use namada::types::key::*; use namada::types::masp::MaspValue; use namada::types::storage::{self, BlockHeight, Epoch}; use namada::types::time::DateTimeUtc; use namada::types::token; - use rust_decimal::Decimal; + use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; use super::context::*; use super::utils::*; @@ -1801,7 +1802,7 @@ pub mod args { pub const ALIAS: Arg = arg("alias"); pub const ALIAS_FORCE: ArgFlag = flag("alias-force"); pub const ALLOW_DUPLICATE_IP: ArgFlag = flag("allow-duplicate-ip"); - pub const AMOUNT: Arg = arg("amount"); + pub const AMOUNT: Arg = arg("amount"); pub const ARCHIVE_DIR: ArgOpt = arg_opt("archive-dir"); pub const BALANCE_OWNER: ArgOpt = arg_opt("owner"); pub const BASE_DIR: ArgDefault = arg_default( @@ -1820,7 +1821,7 @@ pub mod args { pub const CHANNEL_ID: Arg = arg("channel-id"); pub const CODE_PATH: Arg = arg("code-path"); pub const CODE_PATH_OPT: ArgOpt = CODE_PATH.opt(); - pub const COMMISSION_RATE: Arg = arg("commission-rate"); + pub const COMMISSION_RATE: Arg = arg("commission-rate"); pub const CONSENSUS_TIMEOUT_COMMIT: ArgDefault = arg_default( "consensus-timeout-commit", DefaultFn(|| Timeout::from_str("1s").unwrap()), @@ -1835,10 +1836,20 @@ pub mod args { pub const EXPIRATION_OPT: ArgOpt = arg_opt("expiration"); pub const FORCE: ArgFlag = flag("force"); pub const DONT_PREFETCH_WASM: ArgFlag = flag("dont-prefetch-wasm"); - pub const GAS_AMOUNT: ArgDefault = - arg_default("gas-amount", DefaultFn(|| token::Amount::from(0))); - pub const GAS_LIMIT: ArgDefault = - arg_default("gas-limit", DefaultFn(|| token::Amount::from(0))); + pub const GAS_AMOUNT: ArgDefault = arg_default( + "gas-amount", + DefaultFn(|| token::DenominatedAmount { + amount: token::Amount::default(), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }), + ); + pub const GAS_LIMIT: ArgDefault = arg_default( + "gas-limit", + DefaultFn(|| token::DenominatedAmount { + amount: token::Amount::default(), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }), + ); pub const GAS_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx("gas-token", DefaultFn(|| "NAM".parse().unwrap())); pub const GENESIS_PATH: Arg = arg("genesis-path"); @@ -1861,7 +1872,7 @@ pub mod args { pub const LEDGER_ADDRESS: Arg = arg("node"); pub const LOCALHOST: ArgFlag = flag("localhost"); pub const MASP_VALUE: Arg = arg("value"); - pub const MAX_COMMISSION_RATE_CHANGE: Arg = + pub const MAX_COMMISSION_RATE_CHANGE: Arg = arg("max-commission-rate-change"); pub const NET_ADDRESS: Arg = arg("net-address"); pub const NAMADA_START_TIME: ArgOpt = arg_opt("time"); @@ -2162,7 +2173,7 @@ pub mod args { let target = TRANSFER_TARGET.parse(matches); let token = TOKEN.parse(matches); let sub_prefix = SUB_PREFIX.parse(matches); - let amount = AMOUNT.parse(matches); + let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); let tx_code_path = PathBuf::from(TX_TRANSFER_WASM); Self { tx, @@ -2229,7 +2240,7 @@ pub mod args { receiver, token, sub_prefix, - amount, + amount: amount.amount, port_id, channel_id, timeout_height, @@ -2460,6 +2471,14 @@ pub mod args { let tx = Tx::parse(matches); let validator = VALIDATOR.parse(matches); let amount = AMOUNT.parse(matches); + let amount = amount + .canonical() + .increase_precision(NATIVE_MAX_DECIMAL_PLACES.into()) + .unwrap_or_else(|e| { + println!("Could not parse bond amount: {:?}", e); + safe_exit(1); + }) + .amount; let source = SOURCE_OPT.parse(matches); let tx_code_path = PathBuf::from(TX_BOND_WASM); Self { @@ -2500,6 +2519,14 @@ pub mod args { let tx = Tx::parse(matches); let validator = VALIDATOR.parse(matches); let amount = AMOUNT.parse(matches); + let amount = amount + .canonical() + .increase_precision(NATIVE_MAX_DECIMAL_PLACES.into()) + .unwrap_or_else(|e| { + println!("Could not parse bond amount: {:?}", e); + safe_exit(1); + }) + .amount; let source = SOURCE_OPT.parse(matches); let tx_code_path = PathBuf::from(TX_UNBOND_WASM); Self { @@ -2960,6 +2987,7 @@ pub mod args { query: self.query.to_sdk(ctx), owner: self.owner.map(|x| ctx.get_cached(&x)), token: self.token.map(|x| ctx.get(&x)), + sub_prefix: self.sub_prefix, } } } @@ -2969,10 +2997,12 @@ pub mod args { let query = Query::parse(matches); let owner = BALANCE_OWNER.parse(matches); let token = TOKEN_OPT.parse(matches); + let sub_prefix = SUB_PREFIX.parse(matches); Self { query, owner, token, + sub_prefix, } } @@ -2984,6 +3014,11 @@ pub mod args { .arg(TOKEN_OPT.def().about( "The token address that queried transfers must involve.", )) + .arg( + SUB_PREFIX.def().about( + "The token's sub prefix whose balance to query.", + ), + ) } } @@ -3381,9 +3416,10 @@ pub mod args { let ledger_address = LEDGER_ADDRESS_DEFAULT.parse(matches); let initialized_account_alias = ALIAS_OPT.parse(matches); let wallet_alias_force = WALLET_ALIAS_FORCE.parse(matches); - let fee_amount = GAS_AMOUNT.parse(matches); + let fee_amount = + InputAmount::Unvalidated(GAS_AMOUNT.parse(matches)); let fee_token = GAS_TOKEN.parse(matches); - let gas_limit = GAS_LIMIT.parse(matches).into(); + let gas_limit = GAS_LIMIT.parse(matches).amount.into(); let expiration = EXPIRATION_OPT.parse(matches); let signing_key = SIGNING_KEY_OPT.parse(matches); let signer = SIGNER.parse(matches); @@ -3964,8 +4000,8 @@ pub mod args { #[derive(Clone, Debug)] pub struct InitGenesisValidator { pub alias: String, - pub commission_rate: Decimal, - pub max_commission_rate_change: Decimal, + pub commission_rate: Dec, + pub max_commission_rate_change: Dec, pub net_address: SocketAddr, pub unsafe_dont_encrypt: bool, pub key_scheme: SchemeType, diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 8aa466420e..5c73b3f4ec 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -17,14 +17,15 @@ use itertools::Either; use masp_primitives::asset_type::AssetType; use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::{Node, ViewingKey}; -use masp_primitives::transaction::components::Amount; use masp_primitives::zip32::ExtendedFullViewingKey; use namada::core::types::transaction::governance::ProposalType; +use namada::ledger::args::InputAmount; use namada::ledger::events::Event; use namada::ledger::governance::parameters::GovParams; use namada::ledger::governance::storage as gov_storage; use namada::ledger::masp::{ - Conversions, PinnedBalanceError, ShieldedContext, ShieldedUtils, + Conversions, MaspAmount, MaspChange, PinnedBalanceError, ShieldedContext, + ShieldedUtils, }; use namada::ledger::native_vp::governance::utils::{self, Votes}; use namada::ledger::parameters::{storage as param_storage, EpochDuration}; @@ -33,7 +34,8 @@ use namada::ledger::pos::{ }; use namada::ledger::queries::RPC; use namada::ledger::rpc::{ - enriched_bonds_and_unbonds, query_epoch, TxResponse, + enriched_bonds_and_unbonds, format_denominated_amount, query_epoch, + TxResponse, }; use namada::ledger::storage::ConversionState; use namada::ledger::wallet::{AddressVpType, Wallet}; @@ -46,6 +48,7 @@ use namada::types::hash::Hash; use namada::types::key::*; use namada::types::masp::{BalanceOwner, ExtendedViewingKey, PaymentAddress}; use namada::types::storage::{BlockHeight, BlockResults, Epoch, Key, KeySeg}; +use namada::types::token::{Change, Denomination, MaspDenom, TokenAddress}; use namada::types::{storage, token}; use crate::cli::{self, args}; @@ -115,6 +118,7 @@ pub async fn query_transfers< args: args::QueryTransfers, ) { let query_token = args.token; + let sub_prefix = args.sub_prefix.map(|s| Key::parse(s).unwrap()); let query_owner = args.owner.map_or_else( || Either::Right(wallet.get_addresses().into_values().collect()), Either::Left, @@ -168,8 +172,17 @@ pub async fn query_transfers< // Check if this transfer pertains to the supplied token relevant &= match &query_token { Some(token) => { - tfer_delta.values().any(|x| x[token] != 0) - || shielded_accounts.values().any(|x| x[token] != 0) + let check = |(tok, chg): (&TokenAddress, &Change)| { + tok.sub_prefix == sub_prefix + && &tok.address == token + && !chg.is_zero() + }; + tfer_delta.values().cloned().any( + |MaspChange { ref asset, change }| check((asset, &change)), + ) || shielded_accounts + .values() + .cloned() + .any(|x| x.iter().any(check)) } None => true, }; @@ -179,34 +192,34 @@ pub async fn query_transfers< } println!("Height: {}, Index: {}, Transparent Transfer:", height, idx); // Display the transparent changes first - for (account, amt) in tfer_delta { + for (account, MaspChange { ref asset, change }) in tfer_delta { if account != masp() { print!(" {}:", account); - for (addr, val) in amt.components() { - let token_alias = lookup_alias(wallet, addr); - let sign = match val.cmp(&0) { - Ordering::Greater => "+", - Ordering::Less => "-", - Ordering::Equal => "", - }; - print!( - " {}{} {}", - sign, - token::Amount::from(val.unsigned_abs()), - token_alias - ); - } - println!(); + let token_alias = lookup_alias(wallet, &asset.address); + let sign = match change.cmp(&Change::zero()) { + Ordering::Greater => "+", + Ordering::Less => "-", + Ordering::Equal => "", + }; + print!( + " {}{} {}", + sign, + format_denominated_amount(client, asset, change.into(),) + .await, + asset.format_with_alias(&token_alias) + ); } + println!(); } // Then display the shielded changes afterwards // TODO: turn this to a display impl - for (account, amt) in shielded_accounts { + // (account, amt) + for (account, masp_change) in shielded_accounts { if fvk_map.contains_key(&account) { print!(" {}:", fvk_map[&account]); - for (addr, val) in amt.components() { - let token_alias = lookup_alias(wallet, addr); - let sign = match val.cmp(&0) { + for (token_addr, val) in masp_change { + let token_alias = lookup_alias(wallet, &token_addr.address); + let sign = match val.cmp(&Change::zero()) { Ordering::Greater => "+", Ordering::Less => "-", Ordering::Equal => "", @@ -214,8 +227,13 @@ pub async fn query_transfers< print!( " {}{} {}", sign, - token::Amount::from(val.unsigned_abs()), - token_alias + format_denominated_amount( + client, + &token_addr, + val.into(), + ) + .await, + token_addr.format_with_alias(&token_alias), ); } println!(); @@ -286,29 +304,48 @@ pub async fn query_transparent_balance< let tokens = wallet.get_addresses_with_vp_type(AddressVpType::Token); match (args.token, args.owner) { (Some(token), Some(owner)) => { - let key = match &args.sub_prefix { + let (balance_key, sub_prefix) = match &args.sub_prefix { Some(sub_prefix) => { let sub_prefix = Key::parse(sub_prefix).unwrap(); let prefix = token::multitoken_balance_prefix(&token, &sub_prefix); - token::multitoken_balance_key( - &prefix, - &owner.address().unwrap(), + ( + token::multitoken_balance_key( + &prefix, + &owner.address().unwrap(), + ), + Some(sub_prefix), ) } - None => token::balance_key(&token, &owner.address().unwrap()), + None => ( + token::balance_key(&token, &owner.address().unwrap()), + None, + ), }; let token_alias = lookup_alias(wallet, &token); - match query_storage_value::(client, &key).await { - Some(balance) => match &args.sub_prefix { - Some(sub_prefix) => { - println!( - "{} with {}: {}", - token_alias, sub_prefix, balance - ); + match query_storage_value::(client, &balance_key) + .await + { + Some(balance) => { + let balance = format_denominated_amount( + client, + &TokenAddress { + address: token, + sub_prefix, + }, + balance, + ) + .await; + match &args.sub_prefix { + Some(sub_prefix) => { + println!( + "{} with {}: {}", + token_alias, sub_prefix, balance + ); + } + None => println!("{}: {}", token_alias, balance), } - None => println!("{}: {}", token_alias, balance), - }, + } None => { println!("No {} balance found for {}", token_alias, owner) } @@ -323,11 +360,13 @@ pub async fn query_transparent_balance< .await; if let Some(balances) = balances { print_balances( + client, wallet, balances, &token, owner.address().as_ref(), - ); + ) + .await; } } } @@ -336,7 +375,7 @@ pub async fn query_transparent_balance< let balances = query_storage_prefix::(client, &prefix).await; if let Some(balances) = balances { - print_balances(wallet, balances, &token, None); + print_balances(client, wallet, balances, &token, None).await; } } (None, None) => { @@ -346,7 +385,8 @@ pub async fn query_transparent_balance< query_storage_prefix::(client, &key) .await; if let Some(balances) = balances { - print_balances(wallet, balances, &token, None); + print_balances(client, wallet, balances, &token, None) + .await; } } } @@ -383,20 +423,21 @@ pub async fn query_pinned_balance< .collect(); let _ = shielded.load().await; // Print the token balances by payment address + let pinned_error = Err(PinnedBalanceError::InvalidViewingKey); for owner in owners { - let mut balance = Err(PinnedBalanceError::InvalidViewingKey); + let mut balance = pinned_error.clone(); // Find the viewing key that can recognize payments the current payment // address for vk in &viewing_keys { balance = shielded .compute_exchanged_pinned_balance(client, owner, vk) .await; - if balance != Err(PinnedBalanceError::InvalidViewingKey) { + if balance != pinned_error { break; } } // If a suitable viewing key was not found, then demand it from the user - if balance == Err(PinnedBalanceError::InvalidViewingKey) { + if balance == pinned_error { print!("Enter the viewing key for {}: ", owner); io::stdout().flush().unwrap(); let mut vk_str = String::new(); @@ -414,42 +455,61 @@ pub async fn query_pinned_balance< .compute_exchanged_pinned_balance(client, owner, &vk) .await } + // Now print out the received quantities according to CLI arguments - match (balance, args.token.as_ref()) { - (Err(PinnedBalanceError::InvalidViewingKey), _) => println!( + match (balance, args.token.as_ref(), args.sub_prefix.as_ref()) { + (Err(PinnedBalanceError::InvalidViewingKey), _, _) => println!( "Supplied viewing key cannot decode transactions to given \ payment address." ), - (Err(PinnedBalanceError::NoTransactionPinned), _) => { + (Err(PinnedBalanceError::NoTransactionPinned), _, _) => { println!("Payment address {} has not yet been consumed.", owner) } - (Ok((balance, epoch)), Some(token)) => { - // Extract and print only the specified token from the total - let (_asset_type, balance) = - value_by_address(&balance, token.clone(), epoch); + (Ok((balance, epoch)), Some(token), sub_prefix) => { let token_alias = lookup_alias(wallet, token); - if balance == 0 { + + let token_address = TokenAddress { + address: token.clone(), + sub_prefix: sub_prefix + .map(|string| Key::parse(string).unwrap()), + }; + let total_balance = balance + .get(&(epoch, token_address.clone())) + .cloned() + .unwrap_or_default(); + + if total_balance.is_zero() { println!( "Payment address {} was consumed during epoch {}. \ Received no shielded {}", - owner, epoch, token_alias + owner, + epoch, + token_address.format_with_alias(&token_alias) ); } else { - let asset_value = token::Amount::from(balance as u64); + let formatted = format_denominated_amount( + client, + &token_address, + total_balance.into(), + ) + .await; println!( "Payment address {} was consumed during epoch {}. \ Received {} {}", - owner, epoch, asset_value, token_alias + owner, + epoch, + formatted, + token_address.format_with_alias(&token_alias), ); } } - (Ok((balance, epoch)), None) => { + (Ok((balance, epoch)), None, _) => { let mut found_any = false; - // Print balances by human-readable token names - let balance = - shielded.decode_amount(client, balance, epoch).await; - for (addr, value) in balance.components() { - let asset_value = token::Amount::from(*value as u64); + + for ((_, token_addr), value) in balance + .iter() + .filter(|((token_epoch, _), _)| *token_epoch == epoch) + { if !found_any { println!( "Payment address {} was consumed during epoch {}. \ @@ -458,13 +518,20 @@ pub async fn query_pinned_balance< ); found_any = true; } + let formatted = format_denominated_amount( + client, + token_addr, + (*value).into(), + ) + .await; + let token_alias = tokens + .get(&token_addr.address) + .map(|a| a.to_string()) + .unwrap_or_else(|| token_addr.address.to_string()); println!( - " {}: {}", - tokens - .get(addr) - .cloned() - .unwrap_or_else(|| addr.clone()), - asset_value, + " {}: {}", + token_addr.format_with_alias(&token_alias), + formatted, ); } if !found_any { @@ -479,7 +546,8 @@ pub async fn query_pinned_balance< } } -fn print_balances( +async fn print_balances( + client: &C, wallet: &Wallet, balances: impl Iterator, token: &Address, @@ -490,40 +558,59 @@ fn print_balances( let token_alias = lookup_alias(wallet, token); writeln!(w, "Token {}", token_alias).unwrap(); - - let print_num = balances - .filter_map( - |(key, balance)| match token::is_any_multitoken_balance_key(&key) { - Some((sub_prefix, owner)) => Some(( - owner.clone(), - format!( - "with {}: {}, owned by {}", - sub_prefix, - balance, - lookup_alias(wallet, owner) - ), - )), - None => token::is_any_token_balance_key(&key).map(|owner| { + let mut print_num = 0; + for (key, balance) in balances { + let (o, s) = match token::is_any_multitoken_balance_key(&key) { + Some((sub_prefix, [tok, owner])) => ( + owner.clone(), + format!( + "with {}: {}, owned by {}", + sub_prefix.clone(), + format_denominated_amount( + client, + &TokenAddress { + address: tok.clone(), + sub_prefix: Some(sub_prefix) + }, + balance + ) + .await, + lookup_alias(wallet, owner) + ), + ), + None => { + if let Some([tok, owner]) = + token::is_any_token_balance_key(&key) + { ( owner.clone(), format!( ": {}, owned by {}", - balance, + format_denominated_amount( + client, + &TokenAddress { + address: tok.clone(), + sub_prefix: None + }, + balance + ) + .await, lookup_alias(wallet, owner) ), ) - }), - }, - ) - .filter_map(|(o, s)| match target { - Some(t) if o == *t => Some(s), - Some(_) => None, - None => Some(s), - }) - .map(|s| { - writeln!(w, "{}", s).unwrap(); - }) - .count(); + } else { + continue; + } + } + }; + let s = match target { + Some(t) if o == *t => s, + Some(_) => continue, + None => s, + }; + writeln!(w, "{}", s).unwrap(); + print_num += 1; + } if print_num == 0 { match target { @@ -587,8 +674,10 @@ pub async fn query_proposal( println!("{:4}End Epoch: {}", "", end_epoch); println!("{:4}Grace Epoch: {}", "", grace_epoch); let votes = get_proposal_votes(client, start_epoch, id).await; - let total_stake = - get_total_staked_tokens(client, start_epoch).await.into(); + let total_stake = get_total_staked_tokens(client, start_epoch) + .await + .try_into() + .unwrap(); if start_epoch > current_epoch { println!("{:4}Status: pending", ""); } else if start_epoch <= current_epoch && current_epoch <= end_epoch @@ -668,23 +757,6 @@ pub async fn query_proposal( } } -/// Get the component of the given amount corresponding to the given token -pub fn value_by_address( - amt: &masp_primitives::transaction::components::Amount, - token: Address, - epoch: Epoch, -) -> (AssetType, i64) { - // Compute the unique asset identifier from the token address - let asset_type = AssetType::new( - (token, epoch.0) - .try_to_vec() - .expect("token addresses should serialize") - .as_ref(), - ) - .unwrap(); - (asset_type, amt[&asset_type]) -} - /// Query token shielded balance(s) pub async fn query_shielded_balance< C: namada::ledger::queries::Client + Sync, @@ -724,9 +796,10 @@ pub async fn query_shielded_balance< // Query the multi-asset balance at the given spending key let viewing_key = ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; - let balance: Amount = if no_conversions { + let balance: MaspAmount = if no_conversions { shielded - .compute_shielded_balance(&viewing_key) + .compute_shielded_balance(client, &viewing_key) + .await .expect("context should contain viewing key") } else { shielded @@ -734,25 +807,34 @@ pub async fn query_shielded_balance< .await .expect("context should contain viewing key") }; - // Compute the unique asset identifier from the token address - let token = token; - let asset_type = AssetType::new( - (token.clone(), epoch.0) - .try_to_vec() - .expect("token addresses should serialize") - .as_ref(), - ) - .unwrap(); + let token_alias = lookup_alias(wallet, &token); - if balance[&asset_type] == 0 { + + let token_address = TokenAddress { + address: token, + sub_prefix: args.sub_prefix.map(|k| Key::parse(k).unwrap()), + }; + + let total_balance = balance + .get(&(epoch, token_address.clone())) + .cloned() + .unwrap_or_default(); + if total_balance.is_zero() { println!( "No shielded {} balance found for given key", - token_alias + token_address.format_with_alias(&token_alias) ); } else { - let asset_value = - token::Amount::from(balance[&asset_type] as u64); - println!("{}: {}", token_alias, asset_value); + println!( + "{}: {}", + token_address.format_with_alias(&token_alias), + format_denominated_amount( + client, + &token_address, + token::Amount::from(total_balance) + ) + .await + ); } } // Here the user wants to know the balance of all tokens across users @@ -764,7 +846,8 @@ pub async fn query_shielded_balance< let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; let balance = if no_conversions { shielded - .compute_shielded_balance(&viewing_key) + .compute_shielded_balance(client, &viewing_key) + .await .expect("context should contain viewing key") } else { shielded @@ -772,58 +855,100 @@ pub async fn query_shielded_balance< .await .expect("context should contain viewing key") }; - for (asset_type, value) in balance.components() { - if !balances.contains_key(asset_type) { - balances.insert(*asset_type, Vec::new()); + for (key, value) in balance.iter() { + if !balances.contains_key(key) { + balances.insert(key.clone(), Vec::new()); } - balances.get_mut(asset_type).unwrap().push((fvk, *value)); + balances.get_mut(key).unwrap().push((fvk, *value)); } } // These are the asset types for which we have human-readable names - let mut read_tokens = HashSet::new(); + let mut read_tokens: HashMap>> = + HashMap::new(); // Print non-zero balances whose asset types can be decoded - for (asset_type, balances) in balances { - // Decode the asset type - let decoded = - shielded.decode_asset_type(client, asset_type).await; - match decoded { - Some((addr, asset_epoch)) if asset_epoch == epoch => { - // Only assets with the current timestamp count + // TODO Implement a function for this + + let mut balance_map = HashMap::new(); + for ((asset_epoch, token_addr), balances) in balances { + if asset_epoch == epoch { + // remove this from here, should not be making the + // hashtable creation any uglier + if balances.is_empty() { println!( - "Shielded Token {}:", - tokens - .get(&addr) - .cloned() - .unwrap_or_else(|| addr.clone()) + "No shielded {} balance found for any wallet key", + &token_addr ); - read_tokens.insert(addr); } - _ => continue, - } - - let mut found_any = false; - for (fvk, value) in balances { - let value = token::Amount::from(value as u64); - println!(" {}, owned by {}", value, fvk); - found_any = true; - } - if !found_any { - println!( - "No shielded {} balance found for any wallet key", - asset_type - ); + for (fvk, value) in balances { + balance_map.insert((fvk, token_addr.clone()), value); + } } } + for ( + ( + fvk, + TokenAddress { + address: addr, + sub_prefix, + }, + ), + token_balance, + ) in balance_map + { + read_tokens + .entry(addr.clone()) + .and_modify(|addr_vec| addr_vec.push(sub_prefix.clone())) + .or_insert_with(|| vec![sub_prefix.clone()]); + let token_address = TokenAddress { + address: addr, + sub_prefix, + }; + // Only assets with the current timestamp count + let alias = tokens + .get(&token_address.address) + .map(|a| a.to_string()) + .unwrap_or_else(|| token_address.address.to_string()); + println!( + "Shielded Token {}:", + token_address.format_with_alias(&alias), + ); + let formatted = format_denominated_amount( + client, + &token_address, + token_balance.into(), + ) + .await; + println!(" {}, owned by {}", formatted, fvk); + } // Print zero balances for remaining assets for token in tokens { - if !read_tokens.contains(&token) { + if let Some(sub_addrs) = read_tokens.get(&token) { let token_alias = lookup_alias(wallet, &token); - println!("Shielded Token {}:", token_alias); - println!( - "No shielded {} balance found for any wallet key", - token_alias - ); + for sub_addr in sub_addrs { + match sub_addr { + // abstract out these prints + Some(sub_addr) => { + println!( + "Shielded Token {}/{}:", + token_alias, sub_addr + ); + println!( + "No shielded {}/{} balance found for any \ + wallet key", + token_alias, sub_addr + ); + } + None => { + println!("Shielded Token {}:", token_alias,); + println!( + "No shielded {} balance found for any \ + wallet key", + token_alias + ); + } + } + } } } } @@ -832,7 +957,7 @@ pub async fn query_shielded_balance< (Some(token), false) => { // Compute the unique asset identifier from the token address let token = token; - let asset_type = AssetType::new( + let _asset_type = AssetType::new( (token.clone(), epoch.0) .try_to_vec() .expect("token addresses should serialize") @@ -842,12 +967,25 @@ pub async fn query_shielded_balance< let token_alias = lookup_alias(wallet, &token); println!("Shielded Token {}:", token_alias); let mut found_any = false; + let token_alias = lookup_alias(wallet, &token); + let token_address = TokenAddress { + address: token.clone(), + sub_prefix: args + .sub_prefix + .as_ref() + .map(|k| Key::parse(k).unwrap()), + }; + println!( + "Shielded Token {}:", + token_address.format_with_alias(&token_alias), + ); for fvk in viewing_keys { // Query the multi-asset balance at the given spending key let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; let balance = if no_conversions { shielded - .compute_shielded_balance(&viewing_key) + .compute_shielded_balance(client, &viewing_key) + .await .expect("context should contain viewing key") } else { shielded @@ -855,17 +993,24 @@ pub async fn query_shielded_balance< .await .expect("context should contain viewing key") }; - if balance[&asset_type] != 0 { - let asset_value = - token::Amount::from(balance[&asset_type] as u64); - println!(" {}, owned by {}", asset_value, fvk); - found_any = true; + + for ((_, address), val) in balance.iter() { + if !val.is_zero() { + found_any = true; + } + let formatted = format_denominated_amount( + client, + address, + (*val).into(), + ) + .await; + println!(" {}, owned by {}", formatted, fvk); } } if !found_any { println!( "No shielded {} balance found for any wallet key", - token_alias + token_address.format_with_alias(&token_alias), ); } } @@ -874,62 +1019,76 @@ pub async fn query_shielded_balance< // Query the multi-asset balance at the given spending key let viewing_key = ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; - let balance; if no_conversions { - balance = shielded - .compute_shielded_balance(&viewing_key) + let balance = shielded + .compute_shielded_balance(client, &viewing_key) + .await .expect("context should contain viewing key"); // Print balances by human-readable token names - let decoded_balance = - shielded.decode_all_amounts(client, balance).await; - print_decoded_balance_with_epoch(wallet, decoded_balance); + print_decoded_balance_with_epoch(client, wallet, balance).await; } else { - balance = shielded + let balance = shielded .compute_exchanged_balance(client, &viewing_key, epoch) .await .expect("context should contain viewing key"); // Print balances by human-readable token names - let decoded_balance = - shielded.decode_amount(client, balance, epoch).await; - print_decoded_balance(wallet, decoded_balance); + print_decoded_balance(client, wallet, balance, epoch).await; } } } } -pub fn print_decoded_balance( +pub async fn print_decoded_balance< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, wallet: &mut Wallet, - decoded_balance: Amount
, + decoded_balance: MaspAmount, + epoch: Epoch, ) { - let mut found_any = false; - for (addr, value) in decoded_balance.components() { - let asset_value = token::Amount::from(*value as u64); - println!("{} : {}", lookup_alias(wallet, addr), asset_value); - found_any = true; - } - if !found_any { + if decoded_balance.is_empty() { println!("No shielded balance found for given key"); + } else { + for ((_, token_addr), amount) in decoded_balance + .iter() + .filter(|((token_epoch, _), _)| *token_epoch == epoch) + { + println!( + "{} : {}", + token_addr.format_with_alias(&lookup_alias( + wallet, + &token_addr.address + )), + format_denominated_amount(client, token_addr, (*amount).into()) + .await, + ); + } } } -pub fn print_decoded_balance_with_epoch( +pub async fn print_decoded_balance_with_epoch< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, wallet: &mut Wallet, - decoded_balance: Amount<(Address, Epoch)>, + decoded_balance: MaspAmount, ) { let tokens = wallet.get_addresses_with_vp_type(AddressVpType::Token); - let mut found_any = false; - for ((addr, epoch), value) in decoded_balance.components() { - let asset_value = token::Amount::from(*value as u64); + if decoded_balance.is_empty() { + println!("No shielded balance found for given key"); + } + for ((epoch, token_addr), value) in decoded_balance.iter() { + let asset_value = (*value).into(); + let alias = tokens + .get(&token_addr.address) + .map(|a| a.to_string()) + .unwrap_or_else(|| token_addr.to_string()); println!( "{} | {} : {}", - tokens.get(addr).cloned().unwrap_or_else(|| addr.clone()), + token_addr.format_with_alias(&alias), epoch, - asset_value + format_denominated_amount(client, token_addr, asset_value).await, ); - found_any = true; - } - if !found_any { - println!("No shielded balance found for given key"); } } @@ -974,7 +1133,8 @@ pub async fn query_proposal_result< let total_stake = get_total_staked_tokens(client, end_epoch) .await - .into(); + .try_into() + .unwrap(); println!("Proposal: {}", id); match utils::compute_tally( votes, @@ -1080,7 +1240,8 @@ pub async fn query_proposal_result< proposal.tally_epoch, ) .await - .into(); + .try_into() + .unwrap(); match utils::compute_tally( votes, total_stake, @@ -1231,15 +1392,18 @@ pub async fn query_and_print_unbonds< } } if total_withdrawable != token::Amount::default() { - println!("Total withdrawable now: {total_withdrawable}."); + println!( + "Total withdrawable now: {}.", + total_withdrawable.to_string_native() + ); } if !not_yet_withdrawable.is_empty() { println!("Current epoch: {current_epoch}.") } for (withdraw_epoch, amount) in not_yet_withdrawable { println!( - "Amount {amount} withdrawable starting from epoch \ - {withdraw_epoch}." + "Amount {} withdrawable starting from epoch {withdraw_epoch}.", + amount.to_string_native(), ); } } @@ -1291,17 +1455,18 @@ pub async fn query_bonds( writeln!( w, " Remaining active bond from epoch {}: Δ {}", - bond.start, bond.amount + bond.start, + bond.amount.to_string_native() )?; } - if details.bonds_total_slashed != token::Amount::default() { + if details.bonds_total != token::Amount::zero() { writeln!( w, "Active (slashed) bonds total: {}", - details.bonds_total_active() + details.bonds_total_active().to_string_native() )?; } - writeln!(w, "Bonds total: {}", details.bonds_total)?; + writeln!(w, "Bonds total: {}", details.bonds_total.to_string_native())?; writeln!(w)?; if !details.data.unbonds.is_empty() { @@ -1315,22 +1480,36 @@ pub async fn query_bonds( writeln!( w, " Withdrawable from epoch {} (active from {}): Δ {}", - unbond.withdraw, unbond.start, unbond.amount + unbond.withdraw, + unbond.start, + unbond.amount.to_string_native() )?; } - writeln!(w, "Unbonded total: {}", details.unbonds_total)?; + writeln!( + w, + "Unbonded total: {}", + details.unbonds_total.to_string_native() + )?; } - writeln!(w, "Withdrawable total: {}", details.total_withdrawable)?; + writeln!( + w, + "Withdrawable total: {}", + details.total_withdrawable.to_string_native() + )?; writeln!(w)?; } if bonds_and_unbonds.bonds_total != bonds_and_unbonds.bonds_total_slashed { writeln!( w, "All bonds total active: {}", - bonds_and_unbonds.bonds_total_active() + bonds_and_unbonds.bonds_total_active().to_string_native() )?; } - writeln!(w, "All bonds total: {}", bonds_and_unbonds.bonds_total)?; + writeln!( + w, + "All bonds total: {}", + bonds_and_unbonds.bonds_total.to_string_native() + )?; if bonds_and_unbonds.unbonds_total != bonds_and_unbonds.unbonds_total_slashed @@ -1338,14 +1517,18 @@ pub async fn query_bonds( writeln!( w, "All unbonds total active: {}", - bonds_and_unbonds.unbonds_total_active() + bonds_and_unbonds.unbonds_total_active().to_string_native() )?; } - writeln!(w, "All unbonds total: {}", bonds_and_unbonds.unbonds_total)?; + writeln!( + w, + "All unbonds total: {}", + bonds_and_unbonds.unbonds_total.to_string_native() + )?; writeln!( w, "All unbonds total withdrawable: {}", - bonds_and_unbonds.total_withdrawable + bonds_and_unbonds.total_withdrawable.to_string_native() )?; Ok(()) } @@ -1369,7 +1552,10 @@ pub async fn query_bonded_stake( Some(stake) => { // TODO: show if it's in consensus set, below capacity, or // below threshold set - println!("Bonded stake of validator {validator}: {stake}",) + println!( + "Bonded stake of validator {validator}: {}", + stake.to_string_native() + ) } None => { println!("No bonded stake found for {validator}") @@ -1398,8 +1584,13 @@ pub async fn query_bonded_stake( writeln!(w, "Consensus validators:").unwrap(); for val in consensus { - writeln!(w, " {}: {}", val.address.encode(), val.bonded_stake) - .unwrap(); + writeln!( + w, + " {}: {}", + val.address.encode(), + val.bonded_stake.to_string_native() + ) + .unwrap(); } if !below_capacity.is_empty() { writeln!(w, "Below capacity validators:").unwrap(); @@ -1408,7 +1599,7 @@ pub async fn query_bonded_stake( w, " {}: {}", val.address.encode(), - val.bonded_stake + val.bonded_stake.to_string_native() ) .unwrap(); } @@ -1417,7 +1608,10 @@ pub async fn query_bonded_stake( } let total_staked_tokens = get_total_staked_tokens(client, epoch).await; - println!("Total bonded stake: {total_staked_tokens}"); + println!( + "Total bonded stake: {}", + total_staked_tokens.to_string_native() + ); } /// Query and return validator's commission rate and max commission rate change @@ -1652,7 +1846,7 @@ pub async fn query_conversions( .expect("Conversions should be defined"); // Track whether any non-sentinel conversions are found let mut conversions_found = false; - for (addr, epoch, conv, _) in conv_state.assets.values() { + for ((addr, sub, _), epoch, conv, _) in conv_state.assets.values() { let amt: masp_primitives::transaction::components::Amount = conv.clone().into(); // If the user has specified any targets, then meet them @@ -1666,8 +1860,9 @@ pub async fn query_conversions( conversions_found = true; // Print the asset to which the conversion applies print!( - "{}[{}]: ", + "{}{}[{}]: ", tokens.get(addr).cloned().unwrap_or_else(|| addr.clone()), + sub.as_ref().map(|k| format!("/{}", k)).unwrap_or_default(), epoch, ); // Now print out the components of the allowed conversion @@ -1675,13 +1870,14 @@ pub async fn query_conversions( for (asset_type, val) in amt.components() { // Look up the address and epoch of asset to facilitate pretty // printing - let (addr, epoch, _, _) = &conv_state.assets[asset_type]; + let ((addr, sub, _), epoch, _, _) = &conv_state.assets[asset_type]; // Now print out this component of the conversion print!( - "{}{} {}[{}]", + "{}{} {}{}[{}]", prefix, val, tokens.get(addr).cloned().unwrap_or_else(|| addr.clone()), + sub.as_ref().map(|k| format!("/{}", k)).unwrap_or_default(), epoch ); // Future iterations need to be prefixed with + @@ -1701,6 +1897,8 @@ pub async fn query_conversion( asset_type: AssetType, ) -> Option<( Address, + Option, + MaspDenom, Epoch, masp_primitives::transaction::components::Amount, MerklePath, @@ -1883,7 +2081,8 @@ pub async fn get_proposal_offline_votes< ) .await .unwrap_or_default() - .into(); + .try_into() + .expect("Amount out of bounds"); yay_validators.insert( proposal_vote.address, (amount, ProposalVote::Yay(VoteType::Default)), @@ -1920,7 +2119,7 @@ pub async fn get_proposal_offline_votes< }, ) in bonds_and_unbonds { - let mut delegated_amount = token::Amount::default(); + let mut delegated_amount = token::Amount::zero(); for delta in bonds { if delta.start <= proposal.tally_epoch { delegated_amount += delta.amount @@ -1934,7 +2133,7 @@ pub async fn get_proposal_offline_votes< entry.insert( validator, ( - VotePower::from(delegated_amount), + VotePower::try_from(delegated_amount).unwrap(), proposal_vote.vote.clone(), ), ); @@ -1975,7 +2174,7 @@ pub async fn get_proposal_offline_votes< // continue; // } else { // delta -= to_deduct; - // to_deduct = token::Amount::default(); + // to_deduct = token::Amount::zero(); // } // delta = apply_slashes( @@ -2052,6 +2251,10 @@ pub async fn get_total_staked_tokens< namada::ledger::rpc::get_total_staked_tokens(client, epoch).await } +/// Get the total stake of a validator at the given epoch. The total stake is a +/// sum of validator's self-bonds and delegations to their address. +/// Returns `None` when the given address is not a validator address. For a +/// validator with `0` stake, this returns `Ok(token::Amount::zero())`. async fn get_validator_stake( client: &C, epoch: Epoch, @@ -2100,3 +2303,52 @@ fn unwrap_client_response( cli::safe_exit(1) }) } + +/// Get the correct representation of the amount given the token type. +pub async fn validate_amount( + client: &C, + amount: InputAmount, + token: &Address, + sub_prefix: &Option, + force: bool, +) -> token::DenominatedAmount { + let input_amount = match amount { + InputAmount::Unvalidated(amt) => amt.canonical(), + InputAmount::Validated(amt) => return amt, + }; + let denom = unwrap_client_response::>( + RPC.vp() + .token() + .denomination(client, token, sub_prefix) + .await, + ) + .unwrap_or_else(|| { + if force { + println!( + "No denomination found for token: {token}, but --force was \ + passed. Defaulting to the provided denomination." + ); + input_amount.denom + } else { + println!( + "No denomination found for token: {token}, the input \ + arguments could not be parsed." + ); + cli::safe_exit(1); + } + }); + if denom < input_amount.denom && !force { + println!( + "The input amount contained a higher precision than allowed by \ + {token}." + ); + cli::safe_exit(1); + } else { + input_amount.increase_precision(denom).unwrap_or_else(|_| { + println!( + "The amount provided requires more the 256 bits to represent." + ); + cli::safe_exit(1); + }) + } +} diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 237cef3c92..be8936f6ee 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -19,6 +19,7 @@ use namada::ledger::{masp, pos, tx}; use namada::proof_of_stake::parameters::PosParams; use namada::proto::{Code, Data, Section, Tx}; use namada::types::address::Address; +use namada::types::dec::Dec; use namada::types::governance::{ OfflineProposal, OfflineVote, Proposal, ProposalVote, VoteType, }; @@ -30,7 +31,6 @@ use namada::types::transaction::governance::{ InitProposalData, ProposalType, VoteProposalData, }; use namada::types::transaction::{InitValidator, TxType}; -use rust_decimal::Decimal; use sha2::{Digest as Sha2Digest, Sha256}; use super::rpc; @@ -184,7 +184,7 @@ pub async fn submit_init_validator< .unwrap(); // Validate the commission rate data - if commission_rate > Decimal::ONE || commission_rate < Decimal::ZERO { + if commission_rate > Dec::one() || commission_rate < Dec::zero() { eprintln!( "The validator commission rate must not exceed 1.0 or 100%, and \ it must be 0 or positive" @@ -193,8 +193,8 @@ pub async fn submit_init_validator< safe_exit(1) } } - if max_commission_rate_change > Decimal::ONE - || max_commission_rate_change < Decimal::ZERO + if max_commission_rate_change > Dec::one() + || max_commission_rate_change < Dec::zero() { eprintln!( "The validator maximum change in commission rate per epoch must \ @@ -579,7 +579,11 @@ pub async fn submit_init_proposal( .await .unwrap_or_default(); if balance - < token::Amount::from(governance_parameters.min_proposal_fund) + < token::Amount::from_uint( + governance_parameters.min_proposal_fund, + 0, + ) + .unwrap() { eprintln!( "Address {} doesn't have enough funds.", @@ -655,7 +659,10 @@ pub async fn submit_vote_proposal( ) }) { - set.insert((address, cap.into())); + set.insert(( + address, + token::Amount::from_uint(cap, 0).unwrap(), + )); } ProposalVote::Yay(VoteType::PGFCouncil(set)) @@ -1122,3 +1129,51 @@ pub async fn submit_tx( ) -> Result { tx::submit_tx(client, to_broadcast).await } + +#[cfg(test)] +mod test_tx { + use masp_primitives::transaction::components::Amount; + use namada::ledger::masp::{make_asset_type, MaspAmount}; + use namada::types::address::testing::gen_established_address; + use namada::types::storage::DbKeySeg; + use namada::types::token::MaspDenom; + + use super::*; + + #[test] + fn test_masp_add_amount() { + let address_1 = gen_established_address(); + let prefix_1: Key = + DbKeySeg::StringSeg("eth_seg".parse().unwrap()).into(); + let prefix_2: Key = + DbKeySeg::StringSeg("crypto_kitty".parse().unwrap()).into(); + let denom_1 = MaspDenom::One; + let denom_2 = MaspDenom::Three; + let epoch = Epoch::default(); + let _masp_amount = MaspAmount::default(); + + let asset_base = make_asset_type( + Some(epoch), + &address_1, + &Some(prefix_1.clone()), + denom_1, + ); + let _asset_denom = + make_asset_type(Some(epoch), &address_1, &Some(prefix_1), denom_2); + let _asset_prefix = + make_asset_type(Some(epoch), &address_1, &Some(prefix_2), denom_1); + + let _amount_base = + Amount::from_pair(asset_base, 16).expect("Test failed"); + let _amount_denom = + Amount::from_pair(asset_base, 2).expect("Test failed"); + let _amount_prefix = + Amount::from_pair(asset_base, 4).expect("Test failed"); + + // masp_amount += amount_base; + // assert_eq!(masp_amount.get((epoch,)), Uint::zero()); + // Amount::from_pair(atype, amount) + // MaspDenom::One + // assert_eq!(zero.abs(), Uint::zero()); + } +} diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index ee63ecb1d2..95a2a6e11f 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -13,11 +13,11 @@ use flate2::Compression; use namada::ledger::wallet::Wallet; use namada::types::address; use namada::types::chain::ChainId; +use namada::types::dec::Dec; use namada::types::key::*; use prost::bytes::Bytes; use rand::prelude::ThreadRng; use rand::thread_rng; -use rust_decimal::Decimal; use serde_json::json; use sha2::{Digest, Sha256}; @@ -47,6 +47,41 @@ const DEFAULT_NETWORK_CONFIGS_SERVER: &str = /// We do pre-genesis validator set up in this directory pub const PRE_GENESIS_DIR: &str = "pre-genesis"; +/// Environment variable set to reduce the amount of printing the CLI +/// tools perform. Extra prints, while good for UI, clog up test tooling. +pub const REDUCED_CLI_PRINTING: &str = "REDUCED_CLI_PRINTING"; + +macro_rules! cli_print { + ($($arg:tt)*) => {{ + if std::env::var(REDUCED_CLI_PRINTING) + .map(|v| if v.to_lowercase().trim() == "true" { + false + } else { + true + }).unwrap_or(true) { + let mut stdout = std::io::stdout().lock(); + _ = stdout.write_all(format!("{}", std::format_args!($($arg)*)).as_bytes()); + _ = stdout.flush(); + } + }}; +} + +#[allow(unused)] +macro_rules! cli_println { + ($($arg:tt)*) => {{ + if std::env::var(REDUCED_CLI_PRINTING) + .map(|v| if v.to_lowercase().trim() == "true" { + false + } else { + true + }).unwrap_or(true) { + let mut stdout = std::io::stdout().lock(); + _ = stdout.write_all(format!("{}\n", std::format_args!($($arg)*)).as_bytes()); + _ = stdout.flush(); + } + }}; +} + /// Configure Namada to join an existing network. The chain must be released in /// the repository. pub async fn join_network( @@ -924,16 +959,11 @@ pub fn init_genesis_validator( }: args::InitGenesisValidator, ) { // Validate the commission rate data - if commission_rate > Decimal::ONE || commission_rate < Decimal::ZERO { - eprintln!( - "The validator commission rate must not exceed 1.0 or 100%, and \ - it must be 0 or positive" - ); + if commission_rate > Dec::one() { + eprintln!("The validator commission rate must not exceed 1.0 or 100%"); cli::safe_exit(1) } - if max_commission_rate_change > Decimal::ONE - || max_commission_rate_change < Decimal::ZERO - { + if max_commission_rate_change > Dec::one() { eprintln!( "The validator maximum change in commission rate per epoch must \ not exceed 1.0 or 100%" @@ -1098,6 +1128,30 @@ pub fn validator_pre_genesis_dir(base_dir: &Path, alias: &str) -> PathBuf { base_dir.join(PRE_GENESIS_DIR).join(alias) } +/// Add a spinning wheel to a message for long running commands. +/// Can be turned off for E2E tests by setting the `REDUCED_CLI_PRINTING` +/// environment variable. +pub fn with_spinny_wheel(msg: &str, func: F) -> Out +where + F: FnOnce() -> Out + Send + 'static, + Out: Send + 'static, +{ + let task = std::thread::spawn(func); + let spinny_wheel = "|/-\\"; + print!("{}", msg); + _ = std::io::stdout().flush(); + for c in spinny_wheel.chars().cycle() { + cli_print!("{}", c); + std::thread::sleep(std::time::Duration::from_secs(1)); + cli_print!("{}", (8u8 as char)); + if task.is_finished() { + break; + } + } + println!(); + task.join().unwrap() +} + fn is_valid_validator_for_current_chain( tendermint_node_pk: &common::PublicKey, genesis_config: &GenesisConfig, diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 23cc402eae..a77a34ab42 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -8,14 +8,14 @@ use derivative::Derivative; use namada::core::ledger::testnet_pow; use namada::ledger::governance::parameters::GovParams; use namada::ledger::parameters::EpochDuration; -use namada::ledger::pos::{GenesisValidator, PosParams}; +use namada::ledger::pos::{Dec, GenesisValidator, PosParams}; use namada::types::address::Address; use namada::types::chain::ProposalBytes; use namada::types::key::dkg_session_keys::DkgPublicKey; use namada::types::key::*; use namada::types::time::{DateTimeUtc, DurationSecs}; +use namada::types::token::Denomination; use namada::types::{storage, token}; -use rust_decimal::Decimal; /// Genesis configuration file format pub mod genesis_config { @@ -31,14 +31,14 @@ pub mod genesis_config { use namada::core::ledger::testnet_pow; use namada::ledger::governance::parameters::GovParams; use namada::ledger::parameters::EpochDuration; - use namada::ledger::pos::{GenesisValidator, PosParams}; + use namada::ledger::pos::{Dec, GenesisValidator, PosParams}; use namada::types::address::Address; use namada::types::chain::ProposalBytes; use namada::types::key::dkg_session_keys::DkgPublicKey; use namada::types::key::*; use namada::types::time::Rfc3339String; + use namada::types::token::Denomination; use namada::types::{storage, token}; - use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -187,9 +187,9 @@ pub mod genesis_config { pub non_staked_balance: Option, /// Commission rate charged on rewards for delegators (bounded inside /// 0-1) - pub commission_rate: Option, + pub commission_rate: Option, /// Maximum change in commission rate permitted per epoch - pub max_commission_rate_change: Option, + pub max_commission_rate_change: Option, // Filename of validator VP. (default: default validator VP) pub validator_vp: Option, // IP:port of the validator. (used in generation only) @@ -203,11 +203,13 @@ pub mod genesis_config { pub struct TokenAccountConfig { // Address of token account (default: generate). pub address: Option, + // The number of decimal places amounts of this token has + pub denom: Denomination, // Filename of token account VP. (default: token VP) pub vp: Option, // Initial balances held by accounts defined elsewhere. // XXX: u64 doesn't work with toml-rs! - pub balances: Option>, + pub balances: Option>, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -260,9 +262,9 @@ pub mod genesis_config { /// Expected number of epochs per year pub epochs_per_year: u64, /// PoS gain p - pub pos_gain_p: Decimal, + pub pos_gain_p: Dec, /// PoS gain d - pub pos_gain_d: Decimal, + pub pos_gain_d: Dec, #[cfg(not(feature = "mainnet"))] /// Fix wrapper tx fees pub wrapper_tx_fees: Option, @@ -281,26 +283,26 @@ pub mod genesis_config { pub unbonding_len: u64, // Votes per token. // XXX: u64 doesn't work with toml-rs! - pub tm_votes_per_token: Decimal, + pub tm_votes_per_token: Dec, // Reward for proposing a block. // XXX: u64 doesn't work with toml-rs! - pub block_proposer_reward: Decimal, + pub block_proposer_reward: Dec, // Reward for voting on a block. // XXX: u64 doesn't work with toml-rs! - pub block_vote_reward: Decimal, + pub block_vote_reward: Dec, // Maximum staking APY // XXX: u64 doesn't work with toml-rs! - pub max_inflation_rate: Decimal, + pub max_inflation_rate: Dec, // Target ratio of staked NAM tokens to total NAM tokens - pub target_staked_ratio: Decimal, + pub target_staked_ratio: Dec, // Portion of a validator's stake that should be slashed on a // duplicate vote. // XXX: u64 doesn't work with toml-rs! - pub duplicate_vote_min_slash_rate: Decimal, + pub duplicate_vote_min_slash_rate: Dec, // Portion of a validator's stake that should be slashed on a // light client attack. // XXX: u64 doesn't work with toml-rs! - pub light_client_attack_min_slash_rate: Decimal, + pub light_client_attack_min_slash_rate: Dec, /// Number of epochs above and below (separately) the current epoch to /// consider when doing cubic slashing pub cubic_slashing_window_length: u64, @@ -323,7 +325,9 @@ pub mod genesis_config { pos_data: GenesisValidator { address: Address::decode(config.address.as_ref().unwrap()) .unwrap(), - tokens: token::Amount::whole(config.tokens.unwrap_or_default()), + tokens: token::Amount::native_whole( + config.tokens.unwrap_or_default(), + ), consensus_key: config .consensus_public_key .as_ref() @@ -333,21 +337,13 @@ pub mod genesis_config { commission_rate: config .commission_rate .and_then(|rate| { - if rate >= Decimal::ZERO && rate <= Decimal::ONE { - Some(rate) - } else { - None - } + if rate <= Dec::one() { Some(rate) } else { None } }) .expect("Commission rate must be between 0.0 and 1.0"), max_commission_rate_change: config .max_commission_rate_change .and_then(|rate| { - if rate >= Decimal::ZERO && rate <= Decimal::ONE { - Some(rate) - } else { - None - } + if rate <= Dec::one() { Some(rate) } else { None } }) .expect( "Max commission rate change must be between 0.0 and \ @@ -372,7 +368,7 @@ pub mod genesis_config { .unwrap() .to_dkg_public_key() .unwrap(), - non_staked_balance: token::Amount::whole( + non_staked_balance: token::Amount::native_whole( config.non_staked_balance.unwrap_or_default(), ), validator_vp_code_path: validator_vp_config.filename.to_owned(), @@ -397,6 +393,7 @@ pub mod genesis_config { TokenAccount { address: Address::decode(config.address.as_ref().unwrap()).unwrap(), + denom: config.denom, vp_code_path: token_vp_config.filename.to_owned(), vp_sha256: token_vp_config .sha256 @@ -462,7 +459,9 @@ pub mod genesis_config { } } }, - token::Amount::whole(*amount), + token::Amount::from_uint(*amount, config.denom).expect( + "expected a balance that fits into 256 bits", + ), ) }) .collect(), @@ -613,8 +612,8 @@ pub mod genesis_config { epochs_per_year: parameters.epochs_per_year, pos_gain_p: parameters.pos_gain_p, pos_gain_d: parameters.pos_gain_d, - staked_ratio: Decimal::ZERO, - pos_inflation_amount: 0, + staked_ratio: Dec::zero(), + pos_inflation_amount: token::Amount::zero(), wrapper_tx_fees: parameters.wrapper_tx_fees, }; @@ -798,6 +797,8 @@ pub struct EstablishedAccount { pub struct TokenAccount { /// Address pub address: Address, + /// The number of decimal places amounts of this token has + pub denom: Denomination, /// Validity predicate code WASM pub vp_code_path: String, /// Expected SHA-256 hash of the validity predicate wasm @@ -856,13 +857,13 @@ pub struct Parameters { /// Expected number of epochs per year (read only) pub epochs_per_year: u64, /// PoS gain p (read only) - pub pos_gain_p: Decimal, + pub pos_gain_p: Dec, /// PoS gain d (read only) - pub pos_gain_d: Decimal, + pub pos_gain_d: Dec, /// PoS staked ratio (read + write for every epoch) - pub staked_ratio: Decimal, + pub staked_ratio: Dec, /// PoS inflation amount from the last epoch (read + write for every epoch) - pub pos_inflation_amount: u64, + pub pos_inflation_amount: token::Amount, /// Fixed Wrapper tx fees #[cfg(not(feature = "mainnet"))] pub wrapper_tx_fees: Option, @@ -883,7 +884,6 @@ pub fn genesis(num_validators: u64) -> Genesis { use namada::types::address::{ self, apfel, btc, dot, eth, kartoffel, nam, schnitzel, }; - use rust_decimal_macros::dec; use crate::wallet; @@ -904,15 +904,16 @@ pub fn genesis(num_validators: u64) -> Genesis { let validator = Validator { pos_data: GenesisValidator { address, - tokens: token::Amount::whole(200_000), + tokens: token::Amount::native_whole(200_000), consensus_key: consensus_keypair.ref_to(), - commission_rate: dec!(0.05), - max_commission_rate_change: dec!(0.01), + commission_rate: Dec::new(5, 2).expect("This can't fail"), + max_commission_rate_change: Dec::new(1, 2) + .expect("This can't fail"), }, account_key: account_keypair.ref_to(), protocol_key: protocol_keypair.ref_to(), dkg_public_key: dkg_keypair.public(), - non_staked_balance: token::Amount::whole(100_000), + non_staked_balance: token::Amount::native_whole(100_000), // TODO replace with https://github.com/anoma/namada/issues/25) validator_vp_code_path: vp_user_path.into(), validator_vp_sha256: Default::default(), @@ -932,15 +933,16 @@ pub fn genesis(num_validators: u64) -> Genesis { let validator = Validator { pos_data: GenesisValidator { address, - tokens: token::Amount::whole(200_000), + tokens: token::Amount::native_whole(200_000), consensus_key: consensus_keypair.ref_to(), - commission_rate: dec!(0.05), - max_commission_rate_change: dec!(0.01), + commission_rate: Dec::new(5, 2).expect("This can't fail"), + max_commission_rate_change: Dec::new(1, 2) + .expect("This can't fail"), }, account_key: account_keypair.ref_to(), protocol_key: protocol_keypair.ref_to(), dkg_public_key: dkg_keypair.public(), - non_staked_balance: token::Amount::whole(100_000), + non_staked_balance: token::Amount::native_whole(100_000), // TODO replace with https://github.com/anoma/namada/issues/25) validator_vp_code_path: vp_user_path.into(), validator_vp_sha256: Default::default(), @@ -961,11 +963,11 @@ pub fn genesis(num_validators: u64) -> Genesis { implicit_vp_sha256: Default::default(), epochs_per_year: 525_600, /* seconds in yr (60*60*24*365) div seconds * per epoch (60 = min_duration) */ - pos_gain_p: dec!(0.1), - pos_gain_d: dec!(0.1), - staked_ratio: dec!(0.0), - pos_inflation_amount: 0, - wrapper_tx_fees: Some(token::Amount::whole(0)), + pos_gain_p: Dec::new(1, 1).expect("This can't fail"), + pos_gain_d: Dec::new(1, 1).expect("This can't fail"), + staked_ratio: Dec::zero(), + pos_inflation_amount: token::Amount::zero(), + wrapper_tx_fees: Some(token::Amount::native_whole(0)), }; let albert = EstablishedAccount { address: wallet::defaults::albert_address(), @@ -998,8 +1000,8 @@ pub fn genesis(num_validators: u64) -> Genesis { let implicit_accounts = vec![ImplicitAccount { public_key: wallet::defaults::daewon_keypair().ref_to(), }]; - let default_user_tokens = token::Amount::whole(1_000_000); - let default_key_tokens = token::Amount::whole(1_000); + let default_user_tokens = token::Amount::native_whole(1_000_000); + let default_key_tokens = token::Amount::native_whole(1_000); let mut balances: HashMap = HashMap::from_iter([ // established accounts' balances (wallet::defaults::albert_address(), default_user_tokens), @@ -1026,23 +1028,24 @@ pub fn genesis(num_validators: u64) -> Genesis { } /// Deprecated function, soon to be deleted. Generates default tokens - fn tokens() -> HashMap { + fn tokens() -> HashMap { vec![ - (nam(), "NAM"), - (btc(), "BTC"), - (eth(), "ETH"), - (dot(), "DOT"), - (schnitzel(), "Schnitzel"), - (apfel(), "Apfel"), - (kartoffel(), "Kartoffel"), + (nam(), ("NAM", 6.into())), + (btc(), ("BTC", 8.into())), + (eth(), ("ETH", 18.into())), + (dot(), ("DOT", 10.into())), + (schnitzel(), ("Schnitzel", 6.into())), + (apfel(), ("Apfel", 6.into())), + (kartoffel(), ("Kartoffel", 6.into())), ] .into_iter() .collect() } let token_accounts = tokens() - .into_keys() - .map(|address| TokenAccount { + .into_iter() + .map(|(address, (_, denom))| TokenAccount { address, + denom, vp_code_path: vp_token_path.into(), vp_sha256: Default::default(), balances: balances.clone(), diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index f1e3e53ceb..76b12cf184 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use data_encoding::HEXUPPER; use namada::ledger::parameters::storage as params_storage; -use namada::ledger::pos::types::{decimal_mult_u64, into_tm_voting_power}; +use namada::ledger::pos::types::into_tm_voting_power; use namada::ledger::pos::{namada_proof_of_stake, staking_token_address}; use namada::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; use namada::ledger::storage_api::token::credit_tokens; @@ -18,10 +18,10 @@ use namada::proof_of_stake::{ write_last_block_proposer_address, }; use namada::types::address::Address; +use namada::types::dec::Dec; use namada::types::key::tm_raw_hash_to_string; use namada::types::storage::{BlockHash, BlockResults, Epoch, Header}; use namada::types::token::{total_supply_key, Amount}; -use rust_decimal::prelude::Decimal; use super::governance::execute_governance_proposals; use super::*; @@ -268,7 +268,9 @@ where .storage .write( &balance_key, - Amount::from(0).try_to_vec().unwrap(), + Amount::native_whole(0) + .try_to_vec() + .unwrap(), ) .unwrap(); tx_event["info"] = @@ -315,9 +317,15 @@ where } } DecryptedTx::Undecryptable => { + tracing::info!( + "Tx with hash {} was un-decryptable", + wrapper_hash + ); + event["info"] = "Transaction is invalid.".into(); event["log"] = "Transaction could not be decrypted.".into(); event["code"] = ErrorCodes::Undecryptable.into(); + continue; } } (event, Some(wrapper_hash)) @@ -624,19 +632,19 @@ where let epochs_per_year: u64 = self .read_storage_key(¶ms_storage::get_epochs_per_year_key()) .expect("Epochs per year should exist in storage"); - let pos_p_gain_nom: Decimal = self + let pos_p_gain_nom: Dec = self .read_storage_key(¶ms_storage::get_pos_gain_p_key()) .expect("PoS P-gain factor should exist in storage"); - let pos_d_gain_nom: Decimal = self + let pos_d_gain_nom: Dec = self .read_storage_key(¶ms_storage::get_pos_gain_d_key()) .expect("PoS D-gain factor should exist in storage"); - let pos_last_staked_ratio: Decimal = self + let pos_last_staked_ratio: Dec = self .read_storage_key(¶ms_storage::get_staked_ratio_key()) .expect("PoS staked ratio should exist in storage"); - let pos_last_inflation_amount: u64 = self + let pos_last_inflation_amount: token::Amount = self .read_storage_key(¶ms_storage::get_pos_inflation_amount_key()) - .expect("PoS inflation rate should exist in storage"); + .expect("PoS inflation amount should exist in storage"); // Read from PoS storage let total_tokens = self .read_storage_key(&total_supply_key(&staking_token_address( @@ -650,12 +658,12 @@ where // TODO: properly fetch these values (arbitrary for now) let masp_locked_supply: Amount = Amount::default(); - let masp_locked_ratio_target = Decimal::new(5, 1); - let masp_locked_ratio_last = Decimal::new(5, 1); - let masp_max_inflation_rate = Decimal::new(2, 1); - let masp_last_inflation_rate = Decimal::new(12, 2); - let masp_p_gain = Decimal::new(1, 1); - let masp_d_gain = Decimal::new(1, 1); + let masp_locked_ratio_target = Dec::new(5, 1).expect("Cannot fail"); + let masp_locked_ratio_last = Dec::new(5, 1).expect("Cannot fail"); + let masp_max_inflation_rate = Dec::new(2, 1).expect("Cannot fail"); + let masp_last_inflation_rate = Dec::new(12, 2).expect("Cannot fail"); + let masp_p_gain = Dec::new(1, 1).expect("Cannot fail"); + let masp_d_gain = Dec::new(1, 1).expect("Cannot fail"); // Run rewards PD controller let pos_controller = inflation::RewardsController { @@ -664,9 +672,7 @@ where locked_ratio_target: pos_locked_ratio_target, locked_ratio_last: pos_last_staked_ratio, max_reward_rate: pos_max_inflation_rate, - last_inflation_amount: token::Amount::from( - pos_last_inflation_amount, - ), + last_inflation_amount: pos_last_inflation_amount, p_gain_nom: pos_p_gain_nom, d_gain_nom: pos_d_gain_nom, epochs_per_year, @@ -711,15 +717,14 @@ where // // TODO: think about changing the reward to Decimal let mut reward_tokens_remaining = inflation; - let mut new_rewards_products: HashMap = + let mut new_rewards_products: HashMap = HashMap::new(); for acc in rewards_accumulator_handle().iter(&self.wl_storage)? { let (address, value) = acc?; // Get reward token amount for this validator - let fractional_claim = - value / Decimal::from(num_blocks_in_last_epoch); - let reward = decimal_mult_u64(fractional_claim, inflation); + let fractional_claim = value / num_blocks_in_last_epoch; + let reward = fractional_claim * inflation; // Get validator data at the last epoch let stake = read_validator_stake( @@ -728,25 +733,25 @@ where &address, last_epoch, )? - .map(Decimal::from) + .map(Dec::from) .unwrap_or_default(); let last_rewards_product = validator_rewards_products_handle(&address) .get(&self.wl_storage, &last_epoch)? - .unwrap_or(Decimal::ONE); + .unwrap_or_else(Dec::one); let last_delegation_product = delegator_rewards_products_handle(&address) .get(&self.wl_storage, &last_epoch)? - .unwrap_or(Decimal::ONE); + .unwrap_or_else(Dec::one); let commission_rate = validator_commission_rate_handle(&address) .get(&self.wl_storage, last_epoch, ¶ms)? .expect("Should be able to find validator commission rate"); - let new_product = last_rewards_product - * (Decimal::ONE + Decimal::from(reward) / stake); + let new_product = + last_rewards_product * (Dec::one() + Dec::from(reward) / stake); let new_delegation_product = last_delegation_product - * (Decimal::ONE - + (Decimal::ONE - commission_rate) * Decimal::from(reward) + * (Dec::one() + + (Dec::one() - commission_rate) * Dec::from(reward) / stake); new_rewards_products .insert(address, (new_product, new_delegation_product)); @@ -772,11 +777,11 @@ where let staking_token = staking_token_address(&self.wl_storage); // Mint tokens to the PoS account for the last epoch's inflation - let pos_reward_tokens = - Amount::from(inflation - reward_tokens_remaining); + let pos_reward_tokens = inflation - reward_tokens_remaining; tracing::info!( "Minting tokens for PoS rewards distribution into the PoS \ - account. Amount: {pos_reward_tokens}.", + account. Amount: {}.", + pos_reward_tokens.to_string_native(), ); credit_tokens( &mut self.wl_storage, @@ -785,11 +790,12 @@ where pos_reward_tokens, )?; - if reward_tokens_remaining > 0 { - let amount = Amount::from(reward_tokens_remaining); + if reward_tokens_remaining > token::Amount::zero() { + let amount = Amount::from_uint(reward_tokens_remaining, 0).unwrap(); tracing::info!( "Minting tokens remaining from PoS rewards distribution into \ - the Governance account. Amount: {amount}.", + the Governance account. Amount: {}.", + amount.to_string_native() ); credit_tokens( &mut self.wl_storage, @@ -907,8 +913,7 @@ mod test_finalize_block { is_validator_slashes_key, slashes_prefix, }; use namada::proof_of_stake::types::{ - decimal_mult_amount, BondId, SlashType, ValidatorState, - WeightedValidator, + BondId, SlashType, ValidatorState, WeightedValidator, }; use namada::proof_of_stake::{ enqueued_slashes_handle, get_num_consensus_validators, @@ -918,17 +923,18 @@ mod test_finalize_block { validator_slashes_handle, validator_state_handle, write_pos_params, }; use namada::proto::{Code, Data, Section, Signature}; + use namada::types::dec::POS_DECIMAL_PRECISION; use namada::types::governance::ProposalVote; use namada::types::key::tm_consensus_key_raw_hash; use namada::types::storage::Epoch; use namada::types::time::DurationSecs; - use namada::types::token::Amount; + use namada::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; use namada::types::transaction::governance::{ InitProposalData, ProposalType, VoteProposalData, }; use namada::types::transaction::{Fee, WrapperTx, MIN_FEE}; + use namada::types::uint::Uint; use namada_test_utils::TestWasms; - use rust_decimal_macros::dec; use test_log::test; use super::*; @@ -958,7 +964,10 @@ mod test_finalize_block { shell .wl_storage .storage - .write(&balance_key, Amount::whole(1000).try_to_vec().unwrap()) + .write( + &balance_key, + Amount::native_whole(1000).try_to_vec().unwrap(), + ) .unwrap(); // create some wrapper txs @@ -971,7 +980,7 @@ mod test_finalize_block { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1041,12 +1050,12 @@ mod test_finalize_block { let keypair = gen_keypair(); let mut outer_tx = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1096,12 +1105,12 @@ mod test_finalize_block { // not valid tx bytes let wrapper = WrapperTx { fee: Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, pk: keypair.ref_to(), epoch: Epoch(0), - gas_limit: 0.into(), + gas_limit: Default::default(), #[cfg(not(feature = "mainnet"))] pow_solution: None, }; @@ -1153,7 +1162,10 @@ mod test_finalize_block { shell .wl_storage .storage - .write(&balance_key, Amount::whole(1000).try_to_vec().unwrap()) + .write( + &balance_key, + Amount::native_whole(1000).try_to_vec().unwrap(), + ) .unwrap(); // create two decrypted txs @@ -1167,7 +1179,7 @@ mod test_finalize_block { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1204,7 +1216,7 @@ mod test_finalize_block { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1345,7 +1357,7 @@ mod test_finalize_block { .wl_storage .storage .db - .iter_optional_prefix(None) + .iter_prefix(None) .map(|(key, val, _gas)| (key, val)) .collect() }; @@ -1380,7 +1392,7 @@ mod test_finalize_block { let votes = vec![VoteInfo { validator: Some(Validator { address: proposer_address.clone(), - power: u64::from(val_stake) as i64, + power: u128::try_from(val_stake).expect("Test failed") as i64, }), signed_last_block: true, }]; @@ -1458,28 +1470,36 @@ mod test_finalize_block { VoteInfo { validator: Some(Validator { address: pkh1.clone(), - power: u64::from(val1.bonded_stake) as i64, + power: u128::try_from(val1.bonded_stake) + .expect("Test failed") + as i64, }), signed_last_block: true, }, VoteInfo { validator: Some(Validator { address: pkh2.clone(), - power: u64::from(val2.bonded_stake) as i64, + power: u128::try_from(val2.bonded_stake) + .expect("Test failed") + as i64, }), signed_last_block: true, }, VoteInfo { validator: Some(Validator { address: pkh3.clone(), - power: u64::from(val3.bonded_stake) as i64, + power: u128::try_from(val3.bonded_stake) + .expect("Test failed") + as i64, }), signed_last_block: true, }, VoteInfo { validator: Some(Validator { address: pkh4.clone(), - power: u64::from(val4.bonded_stake) as i64, + power: u128::try_from(val4.bonded_stake) + .expect("Test failed") + as i64, }), signed_last_block: true, }, @@ -1490,18 +1510,18 @@ mod test_finalize_block { let rewards_prod_3 = validator_rewards_products_handle(&val3.address); let rewards_prod_4 = validator_rewards_products_handle(&val4.address); - let is_decimal_equal_enough = - |target: Decimal, to_compare: Decimal| -> bool { - // also return false if to_compare > target since this should - // never happen for the use cases - if to_compare < target { - let tolerance = Decimal::new(1, 9); - let res = Decimal::ONE - to_compare / target; - res < tolerance - } else { - to_compare == target - } - }; + let is_decimal_equal_enough = |target: Dec, to_compare: Dec| -> bool { + // also return false if to_compare > target since this should + // never happen for the use cases + if to_compare < target { + let tolerance = Dec::new(1, POS_DECIMAL_PRECISION / 2) + .expect("Dec creation failed"); + let res = Dec::one() - to_compare / target; + res < tolerance + } else { + to_compare == target + } + }; // NOTE: Want to manually set the block proposer and the vote // information in a FinalizeBlock object. In non-abcipp mode, @@ -1534,7 +1554,7 @@ mod test_finalize_block { // Val1 was the proposer, so its reward should be larger than all // others, which should themselves all be equal let acc_sum = get_rewards_sum(&shell.wl_storage); - assert!(is_decimal_equal_enough(Decimal::ONE, acc_sum)); + assert!(is_decimal_equal_enough(Dec::one(), acc_sum)); let acc = get_rewards_acc(&shell.wl_storage); assert_eq!(acc.get(&val2.address), acc.get(&val3.address)); assert_eq!(acc.get(&val2.address), acc.get(&val4.address)); @@ -1553,7 +1573,7 @@ mod test_finalize_block { // should be the same as val1 now. Val3 and val4 should be equal as // well. let acc_sum = get_rewards_sum(&shell.wl_storage); - assert!(is_decimal_equal_enough(Decimal::TWO, acc_sum)); + assert!(is_decimal_equal_enough(Dec::two(), acc_sum)); let acc = get_rewards_acc(&shell.wl_storage); assert_eq!(acc.get(&val1.address), acc.get(&val2.address)); assert_eq!(acc.get(&val3.address), acc.get(&val4.address)); @@ -1567,28 +1587,36 @@ mod test_finalize_block { VoteInfo { validator: Some(Validator { address: pkh1.clone(), - power: u64::from(val1.bonded_stake) as i64, + power: u128::try_from(val1.bonded_stake) + .expect("Test failed") + as i64, }), signed_last_block: true, }, VoteInfo { validator: Some(Validator { address: pkh2, - power: u64::from(val2.bonded_stake) as i64, + power: u128::try_from(val2.bonded_stake) + .expect("Test failed") + as i64, }), signed_last_block: true, }, VoteInfo { validator: Some(Validator { address: pkh3, - power: u64::from(val3.bonded_stake) as i64, + power: u128::try_from(val3.bonded_stake) + .expect("Test failed") + as i64, }), signed_last_block: true, }, VoteInfo { validator: Some(Validator { address: pkh4, - power: u64::from(val4.bonded_stake) as i64, + power: u128::try_from(val4.bonded_stake) + .expect("Test failed") + as i64, }), signed_last_block: false, }, @@ -1602,7 +1630,7 @@ mod test_finalize_block { assert!(rewards_prod_3.is_empty(&shell.wl_storage).unwrap()); assert!(rewards_prod_4.is_empty(&shell.wl_storage).unwrap()); let acc_sum = get_rewards_sum(&shell.wl_storage); - assert!(is_decimal_equal_enough(dec!(3), acc_sum)); + assert!(is_decimal_equal_enough(Dec::new(3, 0).unwrap(), acc_sum)); let acc = get_rewards_acc(&shell.wl_storage); assert!( acc.get(&val1.address).cloned().unwrap() @@ -1662,7 +1690,7 @@ mod test_finalize_block { assert!(rp3 > rp4); } - fn get_rewards_acc(storage: &S) -> HashMap + fn get_rewards_acc(storage: &S) -> HashMap where S: StorageRead, { @@ -1670,18 +1698,18 @@ mod test_finalize_block { .iter(storage) .unwrap() .map(|elem| elem.unwrap()) - .collect::>() + .collect::>() } - fn get_rewards_sum(storage: &S) -> Decimal + fn get_rewards_sum(storage: &S) -> Dec where S: StorageRead, { let acc = get_rewards_acc(storage); if acc.is_empty() { - Decimal::ZERO + Dec::zero() } else { - acc.iter().fold(Decimal::default(), |sum, elm| sum + *elm.1) + acc.iter().fold(Dec::zero(), |sum, elm| sum + *elm.1) } } @@ -1732,7 +1760,7 @@ mod test_finalize_block { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -2029,8 +2057,8 @@ mod test_finalize_block { // Each validator has equal weight in this test, and two have been // slashed - let frac = dec!(2) / dec!(7); - let cubic_rate = dec!(9) * frac * frac; + let frac = Dec::two() / Dec::new(7, 0).unwrap(); + let cubic_rate = Dec::new(9, 0).unwrap() * frac * frac; assert_eq!(slash1.rate, cubic_rate); assert_eq!(slash2.rate, cubic_rate); @@ -2082,10 +2110,10 @@ mod test_finalize_block { let total_stake = read_total_stake(&shell.wl_storage, ¶ms, pipeline_epoch)?; - let expected_slashed = decimal_mult_amount(cubic_rate, initial_stake); + let expected_slashed = cubic_rate * initial_stake; assert_eq!(stake1, initial_stake - expected_slashed); assert_eq!(stake2, initial_stake - expected_slashed); - assert_eq!(total_stake, total_initial_stake - 2 * expected_slashed); + assert_eq!(total_stake, total_initial_stake - 2u64 * expected_slashed); // Unjail one of the validators let current_epoch = shell.wl_storage.storage.block.epoch; @@ -2219,13 +2247,13 @@ mod test_finalize_block { // Make an account with balance and delegate some tokens let delegator = address::testing::gen_implicit_address(); - let del_1_amount = token::Amount::whole(67_231); + let del_1_amount = token::Amount::native_whole(67_231); let staking_token = shell.wl_storage.storage.native_token.clone(); credit_tokens( &mut shell.wl_storage, &staking_token, &delegator, - token::Amount::whole(200_000), + token::Amount::native_whole(200_000), ) .unwrap(); namada_proof_of_stake::bond_tokens( @@ -2238,7 +2266,7 @@ mod test_finalize_block { .unwrap(); // Self-unbond - let self_unbond_1_amount = token::Amount::whole(154_654); + let self_unbond_1_amount = token::Amount::native_whole(154_654); namada_proof_of_stake::unbond_tokens( &mut shell.wl_storage, None, @@ -2281,7 +2309,7 @@ mod test_finalize_block { ); let current_epoch = advance_epoch(&mut shell, &pkh1, &votes, None); println!("\nUnbonding in epoch 2"); - let del_unbond_1_amount = token::Amount::whole(18_000); + let del_unbond_1_amount = token::Amount::native_whole(18_000); namada_proof_of_stake::unbond_tokens( &mut shell.wl_storage, Some(&delegator), @@ -2327,7 +2355,7 @@ mod test_finalize_block { let current_epoch = advance_epoch(&mut shell, &pkh1, &votes, None); println!("\nBonding in epoch 3"); - let self_bond_1_amount = token::Amount::whole(9_123); + let self_bond_1_amount = token::Amount::native_whole(9_123); namada_proof_of_stake::bond_tokens( &mut shell.wl_storage, None, @@ -2346,7 +2374,7 @@ mod test_finalize_block { let current_epoch = advance_epoch(&mut shell, &pkh1, &votes, None); assert_eq!(current_epoch.0, 4_u64); - let self_unbond_2_amount = token::Amount::whole(15_000); + let self_unbond_2_amount = token::Amount::native_whole(15_000); namada_proof_of_stake::unbond_tokens( &mut shell.wl_storage, None, @@ -2367,7 +2395,7 @@ mod test_finalize_block { println!("Delegating in epoch 5"); // Delegate - let del_2_amount = token::Amount::whole(8_144); + let del_2_amount = token::Amount::native_whole(8_144); namada_proof_of_stake::bond_tokens( &mut shell.wl_storage, Some(&delegator), @@ -2432,7 +2460,7 @@ mod test_finalize_block { .unwrap(); assert_eq!(enqueued_slash.epoch, misbehavior_epoch); assert_eq!(enqueued_slash.r#type, SlashType::DuplicateVote); - assert_eq!(enqueued_slash.rate, Decimal::ZERO); + assert_eq!(enqueued_slash.rate, Dec::zero()); let last_slash = namada_proof_of_stake::read_validator_last_slash_epoch( &shell.wl_storage, @@ -2583,16 +2611,18 @@ mod test_finalize_block { ) .unwrap(); - let vp_frac_3 = Decimal::from(val_stake_3) / Decimal::from(tot_stake_3); - let vp_frac_4 = Decimal::from(val_stake_4) / Decimal::from(tot_stake_4); - let tot_frac = dec!(2) * vp_frac_3 + vp_frac_4; - let cubic_rate = - std::cmp::min(Decimal::ONE, dec!(9) * tot_frac * tot_frac); + let vp_frac_3 = Dec::from(val_stake_3) / Dec::from(tot_stake_3); + let vp_frac_4 = Dec::from(val_stake_4) / Dec::from(tot_stake_4); + let tot_frac = Dec::two() * vp_frac_3 + vp_frac_4; + let cubic_rate = std::cmp::min( + Dec::one(), + Dec::new(9, 0).unwrap() * tot_frac * tot_frac, + ); dbg!(&cubic_rate); - let equal_enough = |rate1: Decimal, rate2: Decimal| -> bool { - let tolerance = dec!(0.000000001); - (rate1 - rate2).abs() < tolerance + let equal_enough = |rate1: Dec, rate2: Dec| -> bool { + let tolerance = Dec::new(1, 9).unwrap(); + rate1.abs_diff(&rate2) < tolerance }; // There should be 2 slashes processed for the validator, each with rate @@ -2621,32 +2651,35 @@ mod test_finalize_block { // self_unbond_2_amount` (since this self-bond was enacted then unbonded // all after the infraction). Thus, the additional deltas to be // deducted is the (infraction stake - this) * rate - let slash_rate_3 = std::cmp::min(Decimal::ONE, dec!(2) * cubic_rate); - let exp_slashed_during_processing_9 = decimal_mult_amount( - slash_rate_3, - initial_stake + del_1_amount + let slash_rate_3 = std::cmp::min(Dec::one(), Dec::two() * cubic_rate); + let exp_slashed_during_processing_9 = slash_rate_3 + * (initial_stake + del_1_amount - self_unbond_1_amount - del_unbond_1_amount + self_bond_1_amount - - self_unbond_2_amount, - ); - assert_eq!( - pre_stake_10 - post_stake_10, - exp_slashed_during_processing_9 + - self_unbond_2_amount); + assert!( + ((pre_stake_10 - post_stake_10).change() + - exp_slashed_during_processing_9.change()) + .abs() + < Uint::from(1000) ); // Check that we can compute the stake at the pipeline epoch // NOTE: may be off. by 1 namnam due to rounding; - let exp_pipeline_stake = decimal_mult_amount( - Decimal::ONE - slash_rate_3, - initial_stake + del_1_amount - - self_unbond_1_amount - - del_unbond_1_amount - + self_bond_1_amount - - self_unbond_2_amount, - ) + del_2_amount; + let exp_pipeline_stake = (Dec::one() - slash_rate_3) + * Dec::from( + initial_stake + del_1_amount + - self_unbond_1_amount + - del_unbond_1_amount + + self_bond_1_amount + - self_unbond_2_amount, + ) + + Dec::from(del_2_amount); + assert!( - (exp_pipeline_stake.change() - post_stake_10.change()).abs() <= 1 + exp_pipeline_stake.abs_diff(&Dec::from(post_stake_10)) + <= Dec::new(1, NATIVE_MAX_DECIMAL_PLACES).unwrap() ); // Check the balance of the Slash Pool @@ -2807,13 +2840,12 @@ mod test_finalize_block { // TODO: decimal mult issues should be resolved with PR 1282 assert!( (del_details.bonds[0].slashed_amount.unwrap().change() - - decimal_mult_amount( - std::cmp::min(Decimal::ONE, dec!(3) * cubic_rate), - del_1_amount - del_unbond_1_amount - ) - .change()) + - std::cmp::min( + Dec::one(), + Dec::new(3, 0).unwrap() * cubic_rate + ) * (del_1_amount.change() - del_unbond_1_amount.change())) .abs() - <= 2 + <= Uint::from(2) ); assert_eq!(del_details.bonds[1].start, Epoch(7)); assert_eq!(del_details.bonds[1].amount, del_2_amount); @@ -2830,13 +2862,18 @@ mod test_finalize_block { // TODO: not sure why this is correct??? (with + self_bond_1_amount - // self_unbond_2_amount) // TODO: Make sure this is sound and what we expect - assert_eq!( - self_details.bonds[0].slashed_amount, - Some(decimal_mult_amount( - std::cmp::min(Decimal::ONE, dec!(3) * cubic_rate), - initial_stake - self_unbond_1_amount + self_bond_1_amount - - self_unbond_2_amount - )) + assert!( + (self_details.bonds[0].slashed_amount.unwrap().change() + - (std::cmp::min( + Dec::one(), + Dec::new(3, 0).unwrap() * cubic_rate + ) * (initial_stake - self_unbond_1_amount + + self_bond_1_amount + - self_unbond_2_amount)) + .change()) + <= Amount::from_uint(1000, NATIVE_MAX_DECIMAL_PLACES) + .unwrap() + .change() ); // Check delegation unbonds @@ -2846,13 +2883,11 @@ mod test_finalize_block { assert_eq!(del_details.unbonds[0].amount, del_unbond_1_amount); assert!( (del_details.unbonds[0].slashed_amount.unwrap().change() - - decimal_mult_amount( - std::cmp::min(Decimal::ONE, dec!(2) * cubic_rate), - del_unbond_1_amount - ) - .change()) + - (std::cmp::min(Dec::one(), Dec::two() * cubic_rate) + * del_unbond_1_amount) + .change()) .abs() - <= 1 + <= Uint::from(1) ); // Check self-unbonds @@ -2871,10 +2906,10 @@ mod test_finalize_block { ); assert_eq!( self_details.unbonds[1].slashed_amount, - Some(decimal_mult_amount( - std::cmp::min(Decimal::ONE, dec!(3) * cubic_rate), - self_unbond_2_amount - self_bond_1_amount - )) + Some( + std::cmp::min(Dec::one(), Dec::new(3, 0).unwrap() * cubic_rate) + * (self_unbond_2_amount - self_bond_1_amount) + ) ); assert_eq!(self_details.unbonds[2].amount, self_bond_1_amount); assert_eq!(self_details.unbonds[2].slashed_amount, None); @@ -2892,7 +2927,7 @@ mod test_finalize_block { .unwrap(); let exp_del_withdraw_slashed_amount = - decimal_mult_amount(slash_rate_3, del_unbond_1_amount); + slash_rate_3 * del_unbond_1_amount; assert_eq!( del_withdraw, del_unbond_1_amount - exp_del_withdraw_slashed_amount @@ -2967,7 +3002,7 @@ mod test_finalize_block { VoteInfo { validator: Some(Validator { address: pkh, - power: u64::from(val.bonded_stake) as i64, + power: u128::try_from(val.bonded_stake).unwrap() as i64, }), signed_last_block: true, } diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 9ff5ebc279..df0fa1ec5e 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -68,7 +68,8 @@ where let total_stake = read_total_stake(&shell.wl_storage, ¶ms, proposal_end_epoch) .map_err(|msg| Error::BadProposal(id, msg.to_string()))?; - let total_stake = VotePower::from(u64::from(total_stake)); + let total_stake = VotePower::try_from(total_stake) + .expect("Voting power exceeds NAM supply"); let tally_result = compute_tally(votes, total_stake, &proposal_type) .map_err(|msg| Error::BadProposal(id, msg.to_string()))? .result; diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index e7ab680e8a..cf7eda3884 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -7,12 +7,12 @@ use namada::core::ledger::testnet_pow; use namada::ledger::parameters::{self, Parameters}; use namada::ledger::pos::{into_tm_voting_power, staking_token_address}; use namada::ledger::storage_api::token::{ - credit_tokens, read_balance, read_total_supply, + credit_tokens, read_balance, read_total_supply, write_denom, }; use namada::ledger::storage_api::{ResultExt, StorageRead, StorageWrite}; +use namada::types::dec::Dec; use namada::types::hash::Hash as CodeHash; use namada::types::key::*; -use rust_decimal::Decimal; use super::*; use crate::facade::tendermint_proto::abci; @@ -251,7 +251,7 @@ where // withdrawal limit defaults to 1000 NAM when not set let withdrawal_limit = genesis .faucet_withdrawal_limit - .unwrap_or_else(|| token::Amount::whole(1_000)); + .unwrap_or_else(|| token::Amount::native_whole(1_000)); testnet_pow::init_faucet_storage( &mut self.wl_storage, &address, @@ -273,11 +273,21 @@ where // Initialize genesis token accounts for genesis::TokenAccount { address, + denom, vp_code_path, vp_sha256, balances, } in genesis.token_accounts { + // associate a token with its denomination. + write_denom( + &mut self.wl_storage, + &address, + // TODO: Should we support multi-tokens at genesis? + None, + denom, + ) + .unwrap(); let vp_code_hash = read_wasm_hash(&self.wl_storage, vp_code_path.clone())?.ok_or( Error::LoadingWasm(format!( @@ -386,13 +396,19 @@ where read_balance(&self.wl_storage, &staking_token, &address::POS) .unwrap(); - tracing::info!("Genesis total native tokens: {total_nam}."); - tracing::info!("Total staked tokens: {total_staked_nam}."); + tracing::info!( + "Genesis total native tokens: {}.", + total_nam.to_string_native() + ); + tracing::info!( + "Total staked tokens: {}.", + total_staked_nam.to_string_native() + ); // Set the ratio of staked to total NAM tokens in the parameters storage parameters::update_staked_ratio_parameter( &mut self.wl_storage, - &(Decimal::from(total_staked_nam) / Decimal::from(total_nam)), + &(Dec::from(total_staked_nam) / Dec::from(total_nam)), ) .expect("unable to set staked ratio of NAM in storage"); @@ -479,7 +495,7 @@ mod test { .wl_storage .storage .db - .iter_optional_prefix(None) + .iter_prefix(None) .map(|(key, val, _gas)| (key, val)) .collect() }; diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 6797d9a06c..6a2b297a86 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -961,7 +961,7 @@ where &self.wl_storage, ) .expect("Must be able to read wrapper tx fees parameter"); - fees.unwrap_or(token::Amount::whole(MIN_FEE)) + fees.unwrap_or_else(|| token::Amount::native_whole(MIN_FEE)) } #[cfg(not(feature = "mainnet"))] @@ -1239,12 +1239,12 @@ mod test_utils { // enqueue a wrapper tx let mut wrapper = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: native_token, }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1312,7 +1312,7 @@ mod test_mempool_validate { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1349,7 +1349,7 @@ mod test_mempool_validate { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1366,7 +1366,7 @@ mod test_mempool_validate { // we mount a malleability attack to try and remove the fee let mut new_wrapper = invalid_wrapper.header().wrapper().expect("Test failed"); - new_wrapper.fee.amount = 0.into(); + new_wrapper.fee.amount = Default::default(); invalid_wrapper.update_header(TxType::Wrapper(Box::new(new_wrapper))); let mut result = shell.mempool_validate( @@ -1409,12 +1409,13 @@ mod test_mempool_validate { let mut wrapper = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 100.into(), + amount: token::Amount::from_uint(100, 0) + .expect("This can't fail"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); diff --git a/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/apps/src/lib/node/ledger/shell/prepare_proposal.rs index 9a9aa58491..5d655cb059 100644 --- a/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -320,7 +320,7 @@ mod test_prepare_proposal { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -356,12 +356,12 @@ mod test_prepare_proposal { for i in 0..2 { let mut tx = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -430,7 +430,7 @@ mod test_prepare_proposal { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -482,7 +482,7 @@ mod test_prepare_proposal { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -523,7 +523,7 @@ mod test_prepare_proposal { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -576,7 +576,7 @@ mod test_prepare_proposal { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -599,7 +599,7 @@ mod test_prepare_proposal { }, &keypair_2, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -641,7 +641,7 @@ mod test_prepare_proposal { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index 2837ac6600..7a6d8b660d 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -185,7 +185,7 @@ where /// 5. More decrypted txs than expected /// 6. A transaction could not be decrypted /// 7. Not enough block space was available for some tx - /// 8: Replay attack + /// 8: Replay attack /// /// INVARIANT: Any changes applied in this method must be reverted if the /// proposal is rejected (unless we can simply overwrite them in the @@ -537,12 +537,12 @@ mod test_process_proposal { let keypair = gen_keypair(); let mut outer_tx = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -582,12 +582,12 @@ mod test_process_proposal { let keypair = gen_keypair(); let mut outer_tx = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 100.into(), + amount: Amount::from_uint(100, 0).expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -602,7 +602,7 @@ mod test_process_proposal { let mut new_tx = outer_tx.clone(); if let TxType::Wrapper(wrapper) = &mut new_tx.header.tx_type { // we mount a malleability attack to try and remove the fee - wrapper.fee.amount = 0.into(); + wrapper.fee.amount = Default::default(); } else { panic!("Test failed") }; @@ -640,18 +640,18 @@ mod test_process_proposal { .write_log .write( &get_wrapper_tx_fees_key(), - token::Amount::whole(MIN_FEE).try_to_vec().unwrap(), + token::Amount::native_whole(MIN_FEE).try_to_vec().unwrap(), ) .unwrap(); let keypair = gen_keypair(); let mut outer_tx = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 1.into(), + amount: Amount::from_uint(1, 0).expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -700,25 +700,25 @@ mod test_process_proposal { shell .wl_storage .write_log - .write(&balance_key, Amount::whole(99).try_to_vec().unwrap()) + .write(&balance_key, Amount::native_whole(99).try_to_vec().unwrap()) .unwrap(); shell .wl_storage .write_log .write( &get_wrapper_tx_fees_key(), - token::Amount::whole(MIN_FEE).try_to_vec().unwrap(), + token::Amount::native_whole(MIN_FEE).try_to_vec().unwrap(), ) .unwrap(); let mut outer_tx = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::whole(1_000_100), + amount: Amount::native_whole(1_000_100), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -769,7 +769,7 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -823,12 +823,12 @@ mod test_process_proposal { let mut tx = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -871,12 +871,12 @@ mod test_process_proposal { let mut tx = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -915,12 +915,12 @@ mod test_process_proposal { // not valid tx bytes let wrapper = WrapperTx { fee: Fee { - amount: 0.into(), + amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, pk: keypair.ref_to(), epoch: Epoch(0), - gas_limit: 0.into(), + gas_limit: Default::default(), #[cfg(not(feature = "mainnet"))] pow_solution: None, }; @@ -1022,12 +1022,12 @@ mod test_process_proposal { let mut wrapper = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1089,17 +1089,20 @@ mod test_process_proposal { shell .wl_storage .storage - .write(&balance_key, Amount::whole(1000).try_to_vec().unwrap()) + .write( + &balance_key, + Amount::native_whole(1000).try_to_vec().unwrap(), + ) .unwrap(); let mut wrapper = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1152,12 +1155,12 @@ mod test_process_proposal { let mut wrapper = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1220,7 +1223,10 @@ mod test_process_proposal { shell .wl_storage .storage - .write(&balance_key, Amount::whole(1000).try_to_vec().unwrap()) + .write( + &balance_key, + Amount::native_whole(1000).try_to_vec().unwrap(), + ) .unwrap(); // Add unshielded balance for fee payment @@ -1231,17 +1237,20 @@ mod test_process_proposal { shell .wl_storage .storage - .write(&balance_key, Amount::whole(1000).try_to_vec().unwrap()) + .write( + &balance_key, + Amount::native_whole(1000).try_to_vec().unwrap(), + ) .unwrap(); let mut wrapper = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1259,12 +1268,12 @@ mod test_process_proposal { new_wrapper.update_header(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair_2, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1307,12 +1316,12 @@ mod test_process_proposal { let mut wrapper = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1371,12 +1380,12 @@ mod test_process_proposal { let wrong_chain_id = ChainId("Wrong chain id".to_string()); let mut wrapper = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: token::Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1432,12 +1441,12 @@ mod test_process_proposal { let mut wrapper = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: token::Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -1475,12 +1484,12 @@ mod test_process_proposal { let mut wrapper = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount: token::Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); diff --git a/apps/src/lib/node/ledger/storage/rocksdb.rs b/apps/src/lib/node/ledger/storage/rocksdb.rs index 6108cdf289..9854f213cb 100644 --- a/apps/src/lib/node/ledger/storage/rocksdb.rs +++ b/apps/src/lib/node/ledger/storage/rocksdb.rs @@ -413,7 +413,7 @@ impl RocksDB { let batch = Mutex::new(batch); tracing::info!("Restoring previous hight subspace diffs"); - self.iter_optional_prefix(None).par_bridge().try_for_each( + self.iter_prefix(None).par_bridge().try_for_each( |(key, _value, _gas)| -> Result<()> { // Restore previous height diff if present, otherwise delete the // subspace key @@ -1243,7 +1243,7 @@ impl DB for RocksDB { impl<'iter> DBIter<'iter> for RocksDB { type PrefixIter = PersistentPrefixIterator<'iter>; - fn iter_optional_prefix( + fn iter_prefix( &'iter self, prefix: Option<&Key>, ) -> PersistentPrefixIterator<'iter> { @@ -1289,11 +1289,8 @@ fn iter_subspace_prefix<'iter>( .get_column_family(SUBSPACE_CF) .expect("{SUBSPACE_CF} column family should exist"); let db_prefix = "".to_owned(); - let prefix_string = match prefix { - Some(prefix) => prefix.to_string(), - None => "".to_string(), - }; - iter_prefix(db, subspace_cf, db_prefix, prefix_string) + let prefix = prefix.map(|k| k.to_string()).unwrap_or_default(); + iter_prefix(db, subspace_cf, db_prefix, prefix) } fn iter_diffs_prefix( diff --git a/core/Cargo.toml b/core/Cargo.toml index 45e1bc7976..0fd4ed8e2c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -62,6 +62,7 @@ chrono.workspace = true data-encoding.workspace = true derivative.workspace = true ed25519-consensus.workspace = true +eyre.workspace = true ferveo = {optional = true, git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} tpke = {package = "group-threshold-cryptography", optional = true, git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} @@ -69,9 +70,11 @@ tpke = {package = "group-threshold-cryptography", optional = true, git = "https: ibc = {git = "https://github.com/heliaxdev/cosmos-ibc-rs.git", rev = "e71bc2cc79f8c2b32e970d95312f251398c93d9e", default-features = false, features = ["serde"], optional = true} ibc-proto = {git = "https://github.com/heliaxdev/ibc-proto-rs.git", rev = "6f4038fcf4981f1ed70771d1cd89931267f917af", default-features = false, optional = true} ics23.workspace = true +impl-num-traits = "0.1.2" index-set.workspace = true itertools.workspace = true libsecp256k1.workspace = true +num-traits.workspace = true masp_primitives.workspace = true proptest = {version = "1.2.0", optional = true} prost.workspace = true @@ -79,8 +82,6 @@ prost-types.workspace = true rand = {version = "0.8", optional = true} rand_core = {version = "0.6", optional = true} rayon = {version = "=1.5.3", optional = true} -rust_decimal.workspace = true -rust_decimal_macros.workspace = true serde.workspace = true serde_json.workspace = true sha2.workspace = true @@ -89,6 +90,7 @@ tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev thiserror.workspace = true tracing.workspace = true zeroize.workspace = true +uint = "0.9.5" [dev-dependencies] assert_matches.workspace = true @@ -99,6 +101,7 @@ rand.workspace = true rand_core.workspace = true test-log.workspace = true tracing-subscriber.workspace = true +toml.workspace = true [build-dependencies] tonic-build.workspace = true diff --git a/core/src/ledger/governance/parameters.rs b/core/src/ledger/governance/parameters.rs index 9ae820d96c..2d247bc24f 100644 --- a/core/src/ledger/governance/parameters.rs +++ b/core/src/ledger/governance/parameters.rs @@ -79,7 +79,7 @@ impl GovParams { } = self; let min_proposal_fund_key = gov_storage::get_min_proposal_fund_key(); - let amount = Amount::whole(*min_proposal_fund); + let amount = Amount::native_whole(*min_proposal_fund); storage.write(&min_proposal_fund_key, amount)?; let max_proposal_code_size_key = diff --git a/core/src/ledger/ibc/actions.rs b/core/src/ledger/ibc/actions.rs new file mode 100644 index 0000000000..e925a98d92 --- /dev/null +++ b/core/src/ledger/ibc/actions.rs @@ -0,0 +1,1605 @@ +//! Functions to handle IBC modules + +use std::str::FromStr; + +use sha2::Digest; +use thiserror::Error; + +use crate::ibc::applications::ics20_fungible_token_transfer::msgs::transfer::MsgTransfer; +use crate::ibc::clients::ics07_tendermint::consensus_state::ConsensusState as TmConsensusState; +use crate::ibc::core::ics02_client::client_consensus::{ + AnyConsensusState, ConsensusState, +}; +use crate::ibc::core::ics02_client::client_state::{ + AnyClientState, ClientState, +}; +use crate::ibc::core::ics02_client::client_type::ClientType; +use crate::ibc::core::ics02_client::events::{ + Attributes as ClientAttributes, CreateClient, UpdateClient, UpgradeClient, +}; +use crate::ibc::core::ics02_client::header::{AnyHeader, Header}; +use crate::ibc::core::ics02_client::height::Height; +use crate::ibc::core::ics02_client::msgs::create_client::MsgCreateAnyClient; +use crate::ibc::core::ics02_client::msgs::update_client::MsgUpdateAnyClient; +use crate::ibc::core::ics02_client::msgs::upgrade_client::MsgUpgradeAnyClient; +use crate::ibc::core::ics02_client::msgs::ClientMsg; +use crate::ibc::core::ics03_connection::connection::{ + ConnectionEnd, Counterparty as ConnCounterparty, State as ConnState, +}; +use crate::ibc::core::ics03_connection::events::{ + Attributes as ConnectionAttributes, OpenAck as ConnOpenAck, + OpenConfirm as ConnOpenConfirm, OpenInit as ConnOpenInit, + OpenTry as ConnOpenTry, +}; +use crate::ibc::core::ics03_connection::msgs::conn_open_ack::MsgConnectionOpenAck; +use crate::ibc::core::ics03_connection::msgs::conn_open_confirm::MsgConnectionOpenConfirm; +use crate::ibc::core::ics03_connection::msgs::conn_open_init::MsgConnectionOpenInit; +use crate::ibc::core::ics03_connection::msgs::conn_open_try::MsgConnectionOpenTry; +use crate::ibc::core::ics03_connection::msgs::ConnectionMsg; +use crate::ibc::core::ics04_channel::channel::{ + ChannelEnd, Counterparty as ChanCounterparty, Order, State as ChanState, +}; +use crate::ibc::core::ics04_channel::commitment::PacketCommitment; +use crate::ibc::core::ics04_channel::events::{ + AcknowledgePacket, CloseConfirm as ChanCloseConfirm, + CloseInit as ChanCloseInit, OpenAck as ChanOpenAck, + OpenConfirm as ChanOpenConfirm, OpenInit as ChanOpenInit, + OpenTry as ChanOpenTry, SendPacket, TimeoutPacket, WriteAcknowledgement, +}; +use crate::ibc::core::ics04_channel::msgs::acknowledgement::MsgAcknowledgement; +use crate::ibc::core::ics04_channel::msgs::chan_close_confirm::MsgChannelCloseConfirm; +use crate::ibc::core::ics04_channel::msgs::chan_close_init::MsgChannelCloseInit; +use crate::ibc::core::ics04_channel::msgs::chan_open_ack::MsgChannelOpenAck; +use crate::ibc::core::ics04_channel::msgs::chan_open_confirm::MsgChannelOpenConfirm; +use crate::ibc::core::ics04_channel::msgs::chan_open_init::MsgChannelOpenInit; +use crate::ibc::core::ics04_channel::msgs::chan_open_try::MsgChannelOpenTry; +use crate::ibc::core::ics04_channel::msgs::recv_packet::MsgRecvPacket; +use crate::ibc::core::ics04_channel::msgs::timeout::MsgTimeout; +use crate::ibc::core::ics04_channel::msgs::timeout_on_close::MsgTimeoutOnClose; +use crate::ibc::core::ics04_channel::msgs::{ChannelMsg, PacketMsg}; +use crate::ibc::core::ics04_channel::packet::{Packet, Sequence}; +use crate::ibc::core::ics23_commitment::commitment::CommitmentPrefix; +use crate::ibc::core::ics24_host::error::ValidationError as Ics24Error; +use crate::ibc::core::ics24_host::identifier::{ + ChannelId, ClientId, ConnectionId, PortChannelId, PortId, +}; +use crate::ibc::core::ics26_routing::msgs::Ics26Envelope; +use crate::ibc::events::IbcEvent; +#[cfg(any(feature = "ibc-mocks-abcipp", feature = "ibc-mocks"))] +use crate::ibc::mock::client_state::{MockClientState, MockConsensusState}; +use crate::ibc::timestamp::Timestamp; +use crate::ledger::ibc::data::{ + Error as IbcDataError, FungibleTokenPacketData, IbcMessage, PacketAck, + PacketReceipt, +}; +use crate::ledger::ibc::storage; +use crate::ledger::storage_api; +use crate::tendermint::Time; +use crate::tendermint_proto::{Error as ProtoError, Protobuf}; +use crate::types::address::{Address, InternalAddress}; +use crate::types::ibc::IbcEvent as NamadaIbcEvent; +use crate::types::storage::{BlockHeight, Key}; +use crate::types::time::Rfc3339String; +use crate::types::token::{self, Amount}; + +const COMMITMENT_PREFIX: &[u8] = b"ibc"; + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum Error { + #[error("Invalid client error: {0}")] + ClientId(Ics24Error), + #[error("Invalid port error: {0}")] + PortId(Ics24Error), + #[error("Updating a client error: {0}")] + ClientUpdate(String), + #[error("IBC data error: {0}")] + IbcData(IbcDataError), + #[error("Decoding IBC data error: {0}")] + Decoding(ProtoError), + #[error("Client error: {0}")] + Client(String), + #[error("Connection error: {0}")] + Connection(String), + #[error("Channel error: {0}")] + Channel(String), + #[error("Counter error: {0}")] + Counter(String), + #[error("Sequence error: {0}")] + Sequence(String), + #[error("Time error: {0}")] + Time(String), + #[error("Invalid transfer message: {0}")] + TransferMessage(token::TransferError), + #[error("Sending a token error: {0}")] + SendingToken(String), + #[error("Receiving a token error: {0}")] + ReceivingToken(String), + #[error("IBC storage error: {0}")] + IbcStorage(storage::Error), +} + +// This is needed to use `ibc::Handler::Error` with `IbcActions` in +// `tx_prelude/src/ibc.rs` +impl From for storage_api::Error { + fn from(err: Error) -> Self { + storage_api::Error::new(err) + } +} + +/// for handling IBC modules +pub type Result = std::result::Result; + +/// IBC trait to be implemented in integration that can read and write +pub trait IbcActions { + /// IBC action error + type Error: From; + + /// Read IBC-related data + fn read_ibc_data( + &self, + key: &Key, + ) -> std::result::Result>, Self::Error>; + + /// Write IBC-related data + fn write_ibc_data( + &mut self, + key: &Key, + data: impl AsRef<[u8]>, + ) -> std::result::Result<(), Self::Error>; + + /// Delete IBC-related data + fn delete_ibc_data( + &mut self, + key: &Key, + ) -> std::result::Result<(), Self::Error>; + + /// Emit an IBC event + fn emit_ibc_event( + &mut self, + event: NamadaIbcEvent, + ) -> std::result::Result<(), Self::Error>; + + /// Transfer token + fn transfer_token( + &mut self, + src: &Key, + dest: &Key, + amount: Amount, + ) -> std::result::Result<(), Self::Error>; + + /// Get the current height of this chain + fn get_height(&self) -> std::result::Result; + + /// Get the current time of the tendermint header of this chain + fn get_header_time( + &self, + ) -> std::result::Result; + + /// dispatch according to ICS26 routing + fn dispatch_ibc_action( + &mut self, + tx_data: &[u8], + ) -> std::result::Result<(), Self::Error> { + let ibc_msg = IbcMessage::decode(tx_data).map_err(Error::IbcData)?; + match &ibc_msg.0 { + Ics26Envelope::Ics2Msg(ics02_msg) => match ics02_msg { + ClientMsg::CreateClient(msg) => self.create_client(msg), + ClientMsg::UpdateClient(msg) => self.update_client(msg), + ClientMsg::Misbehaviour(_msg) => todo!(), + ClientMsg::UpgradeClient(msg) => self.upgrade_client(msg), + }, + Ics26Envelope::Ics3Msg(ics03_msg) => match ics03_msg { + ConnectionMsg::ConnectionOpenInit(msg) => { + self.init_connection(msg) + } + ConnectionMsg::ConnectionOpenTry(msg) => { + self.try_connection(msg) + } + ConnectionMsg::ConnectionOpenAck(msg) => { + self.ack_connection(msg) + } + ConnectionMsg::ConnectionOpenConfirm(msg) => { + self.confirm_connection(msg) + } + }, + Ics26Envelope::Ics4ChannelMsg(ics04_msg) => match ics04_msg { + ChannelMsg::ChannelOpenInit(msg) => self.init_channel(msg), + ChannelMsg::ChannelOpenTry(msg) => self.try_channel(msg), + ChannelMsg::ChannelOpenAck(msg) => self.ack_channel(msg), + ChannelMsg::ChannelOpenConfirm(msg) => { + self.confirm_channel(msg) + } + ChannelMsg::ChannelCloseInit(msg) => { + self.close_init_channel(msg) + } + ChannelMsg::ChannelCloseConfirm(msg) => { + self.close_confirm_channel(msg) + } + }, + Ics26Envelope::Ics4PacketMsg(ics04_msg) => match ics04_msg { + PacketMsg::AckPacket(msg) => self.acknowledge_packet(msg), + PacketMsg::RecvPacket(msg) => self.receive_packet(msg), + PacketMsg::ToPacket(msg) => self.timeout_packet(msg), + PacketMsg::ToClosePacket(msg) => { + self.timeout_on_close_packet(msg) + } + }, + Ics26Envelope::Ics20Msg(msg) => self.send_token(msg), + } + } + + /// Create a new client + fn create_client( + &mut self, + msg: &MsgCreateAnyClient, + ) -> std::result::Result<(), Self::Error> { + let counter_key = storage::client_counter_key(); + let counter = self.get_and_inc_counter(&counter_key)?; + let client_type = msg.client_state.client_type(); + let client_id = client_id(client_type, counter)?; + // client type + let client_type_key = storage::client_type_key(&client_id); + self.write_ibc_data(&client_type_key, client_type.as_str().as_bytes())?; + // client state + let client_state_key = storage::client_state_key(&client_id); + self.write_ibc_data( + &client_state_key, + msg.client_state + .encode_vec() + .expect("encoding shouldn't fail"), + )?; + // consensus state + let height = msg.client_state.latest_height(); + let consensus_state_key = + storage::consensus_state_key(&client_id, height); + self.write_ibc_data( + &consensus_state_key, + msg.consensus_state + .encode_vec() + .expect("encoding shouldn't fail"), + )?; + + self.set_client_update_time(&client_id)?; + + let event = make_create_client_event(&client_id, msg) + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Update a client + fn update_client( + &mut self, + msg: &MsgUpdateAnyClient, + ) -> std::result::Result<(), Self::Error> { + // get and update the client + let client_id = msg.client_id.clone(); + let client_state_key = storage::client_state_key(&client_id); + let value = + self.read_ibc_data(&client_state_key)?.ok_or_else(|| { + Error::Client(format!( + "The client to be updated doesn't exist: ID {}", + client_id + )) + })?; + let client_state = + AnyClientState::decode_vec(&value).map_err(Error::Decoding)?; + let (new_client_state, new_consensus_state) = + update_client(client_state, msg.header.clone())?; + + let height = new_client_state.latest_height(); + self.write_ibc_data( + &client_state_key, + new_client_state + .encode_vec() + .expect("encoding shouldn't fail"), + )?; + let consensus_state_key = + storage::consensus_state_key(&client_id, height); + self.write_ibc_data( + &consensus_state_key, + new_consensus_state + .encode_vec() + .expect("encoding shouldn't fail"), + )?; + + self.set_client_update_time(&client_id)?; + + let event = make_update_client_event(&client_id, msg) + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Upgrade a client + fn upgrade_client( + &mut self, + msg: &MsgUpgradeAnyClient, + ) -> std::result::Result<(), Self::Error> { + let client_state_key = storage::client_state_key(&msg.client_id); + let height = msg.client_state.latest_height(); + let consensus_state_key = + storage::consensus_state_key(&msg.client_id, height); + self.write_ibc_data( + &client_state_key, + msg.client_state + .encode_vec() + .expect("encoding shouldn't fail"), + )?; + self.write_ibc_data( + &consensus_state_key, + msg.consensus_state + .encode_vec() + .expect("encoding shouldn't fail"), + )?; + + self.set_client_update_time(&msg.client_id)?; + + let event = make_upgrade_client_event(&msg.client_id, msg) + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Initialize a connection for ConnectionOpenInit + fn init_connection( + &mut self, + msg: &MsgConnectionOpenInit, + ) -> std::result::Result<(), Self::Error> { + let counter_key = storage::connection_counter_key(); + let counter = self.get_and_inc_counter(&counter_key)?; + // new connection + let conn_id = connection_id(counter); + let conn_key = storage::connection_key(&conn_id); + let connection = init_connection(msg); + self.write_ibc_data( + &conn_key, + connection.encode_vec().expect("encoding shouldn't fail"), + )?; + + let event = make_open_init_connection_event(&conn_id, msg) + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Initialize a connection for ConnectionOpenTry + fn try_connection( + &mut self, + msg: &MsgConnectionOpenTry, + ) -> std::result::Result<(), Self::Error> { + let counter_key = storage::connection_counter_key(); + let counter = self.get_and_inc_counter(&counter_key)?; + // new connection + let conn_id = connection_id(counter); + let conn_key = storage::connection_key(&conn_id); + let connection = try_connection(msg); + self.write_ibc_data( + &conn_key, + connection.encode_vec().expect("encoding shouldn't fail"), + )?; + + let event = make_open_try_connection_event(&conn_id, msg) + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Open the connection for ConnectionOpenAck + fn ack_connection( + &mut self, + msg: &MsgConnectionOpenAck, + ) -> std::result::Result<(), Self::Error> { + let conn_key = storage::connection_key(&msg.connection_id); + let value = self.read_ibc_data(&conn_key)?.ok_or_else(|| { + Error::Connection(format!( + "The connection to be opened doesn't exist: ID {}", + msg.connection_id + )) + })?; + let mut connection = + ConnectionEnd::decode_vec(&value).map_err(Error::Decoding)?; + open_connection(&mut connection); + let mut counterparty = connection.counterparty().clone(); + counterparty.connection_id = + Some(msg.counterparty_connection_id.clone()); + connection.set_counterparty(counterparty); + self.write_ibc_data( + &conn_key, + connection.encode_vec().expect("encoding shouldn't fail"), + )?; + + let event = make_open_ack_connection_event(msg).try_into().unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Open the connection for ConnectionOpenConfirm + fn confirm_connection( + &mut self, + msg: &MsgConnectionOpenConfirm, + ) -> std::result::Result<(), Self::Error> { + let conn_key = storage::connection_key(&msg.connection_id); + let value = self.read_ibc_data(&conn_key)?.ok_or_else(|| { + Error::Connection(format!( + "The connection to be opend doesn't exist: ID {}", + msg.connection_id + )) + })?; + let mut connection = + ConnectionEnd::decode_vec(&value).map_err(Error::Decoding)?; + open_connection(&mut connection); + self.write_ibc_data( + &conn_key, + connection.encode_vec().expect("encoding shouldn't fail"), + )?; + + let event = make_open_confirm_connection_event(msg).try_into().unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Initialize a channel for ChannelOpenInit + fn init_channel( + &mut self, + msg: &MsgChannelOpenInit, + ) -> std::result::Result<(), Self::Error> { + self.bind_port(&msg.port_id)?; + let counter_key = storage::channel_counter_key(); + let counter = self.get_and_inc_counter(&counter_key)?; + let channel_id = channel_id(counter); + let port_channel_id = port_channel_id(msg.port_id.clone(), channel_id); + let channel_key = storage::channel_key(&port_channel_id); + self.write_ibc_data( + &channel_key, + msg.channel.encode_vec().expect("encoding shouldn't fail"), + )?; + + let event = make_open_init_channel_event(&channel_id, msg) + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Initialize a channel for ChannelOpenTry + fn try_channel( + &mut self, + msg: &MsgChannelOpenTry, + ) -> std::result::Result<(), Self::Error> { + self.bind_port(&msg.port_id)?; + let counter_key = storage::channel_counter_key(); + let counter = self.get_and_inc_counter(&counter_key)?; + let channel_id = channel_id(counter); + let port_channel_id = port_channel_id(msg.port_id.clone(), channel_id); + let channel_key = storage::channel_key(&port_channel_id); + self.write_ibc_data( + &channel_key, + msg.channel.encode_vec().expect("encoding shouldn't fail"), + )?; + + let event = make_open_try_channel_event(&channel_id, msg) + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Open the channel for ChannelOpenAck + fn ack_channel( + &mut self, + msg: &MsgChannelOpenAck, + ) -> std::result::Result<(), Self::Error> { + let port_channel_id = + port_channel_id(msg.port_id.clone(), msg.channel_id); + let channel_key = storage::channel_key(&port_channel_id); + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { + Error::Channel(format!( + "The channel to be opened doesn't exist: Port/Channel {}", + port_channel_id + )) + })?; + let mut channel = + ChannelEnd::decode_vec(&value).map_err(Error::Decoding)?; + channel.set_counterparty_channel_id(msg.counterparty_channel_id); + open_channel(&mut channel); + self.write_ibc_data( + &channel_key, + channel.encode_vec().expect("encoding shouldn't fail"), + )?; + + let event = make_open_ack_channel_event(msg, &channel)? + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Open the channel for ChannelOpenConfirm + fn confirm_channel( + &mut self, + msg: &MsgChannelOpenConfirm, + ) -> std::result::Result<(), Self::Error> { + let port_channel_id = + port_channel_id(msg.port_id.clone(), msg.channel_id); + let channel_key = storage::channel_key(&port_channel_id); + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { + Error::Channel(format!( + "The channel to be opened doesn't exist: Port/Channel {}", + port_channel_id + )) + })?; + let mut channel = + ChannelEnd::decode_vec(&value).map_err(Error::Decoding)?; + open_channel(&mut channel); + self.write_ibc_data( + &channel_key, + channel.encode_vec().expect("encoding shouldn't fail"), + )?; + + let event = make_open_confirm_channel_event(msg, &channel)? + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Close the channel for ChannelCloseInit + fn close_init_channel( + &mut self, + msg: &MsgChannelCloseInit, + ) -> std::result::Result<(), Self::Error> { + let port_channel_id = + port_channel_id(msg.port_id.clone(), msg.channel_id); + let channel_key = storage::channel_key(&port_channel_id); + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { + Error::Channel(format!( + "The channel to be closed doesn't exist: Port/Channel {}", + port_channel_id + )) + })?; + let mut channel = + ChannelEnd::decode_vec(&value).map_err(Error::Decoding)?; + close_channel(&mut channel); + self.write_ibc_data( + &channel_key, + channel.encode_vec().expect("encoding shouldn't fail"), + )?; + + let event = make_close_init_channel_event(msg, &channel)? + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Close the channel for ChannelCloseConfirm + fn close_confirm_channel( + &mut self, + msg: &MsgChannelCloseConfirm, + ) -> std::result::Result<(), Self::Error> { + let port_channel_id = + port_channel_id(msg.port_id.clone(), msg.channel_id); + let channel_key = storage::channel_key(&port_channel_id); + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { + Error::Channel(format!( + "The channel to be closed doesn't exist: Port/Channel {}", + port_channel_id + )) + })?; + let mut channel = + ChannelEnd::decode_vec(&value).map_err(Error::Decoding)?; + close_channel(&mut channel); + self.write_ibc_data( + &channel_key, + channel.encode_vec().expect("encoding shouldn't fail"), + )?; + + let event = make_close_confirm_channel_event(msg, &channel)? + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Send a packet + fn send_packet( + &mut self, + port_channel_id: PortChannelId, + data: Vec, + timeout_height: Height, + timeout_timestamp: Timestamp, + ) -> std::result::Result<(), Self::Error> { + // get and increment the next sequence send + let seq_key = storage::next_sequence_send_key(&port_channel_id); + let sequence = self.get_and_inc_sequence(&seq_key)?; + + // get the channel for the destination info. + let channel_key = storage::channel_key(&port_channel_id); + let channel = self + .read_ibc_data(&channel_key)? + .expect("cannot get the channel to be closed"); + let channel = + ChannelEnd::decode_vec(&channel).expect("cannot get the channel"); + let counterparty = channel.counterparty(); + + // make a packet + let packet = Packet { + sequence, + source_port: port_channel_id.port_id.clone(), + source_channel: port_channel_id.channel_id, + destination_port: counterparty.port_id.clone(), + destination_channel: *counterparty + .channel_id() + .expect("the counterparty channel should exist"), + data, + timeout_height, + timeout_timestamp, + }; + // store the commitment of the packet + let commitment_key = storage::commitment_key( + &port_channel_id.port_id, + &port_channel_id.channel_id, + packet.sequence, + ); + let commitment = commitment(&packet); + self.write_ibc_data(&commitment_key, commitment.into_vec())?; + + let event = make_send_packet_event(packet).try_into().unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Receive a packet + fn receive_packet( + &mut self, + msg: &MsgRecvPacket, + ) -> std::result::Result<(), Self::Error> { + // check the packet data + let packet_ack = + if let Ok(data) = serde_json::from_slice(&msg.packet.data) { + match self.receive_token(&msg.packet, &data) { + Ok(_) => PacketAck::result_success(), + Err(_) => PacketAck::result_error( + "receiving a token failed".to_string(), + ), + } + } else { + PacketAck::result_error("unknown packet data".to_string()) + }; + + // store the receipt + let receipt_key = storage::receipt_key( + &msg.packet.destination_port, + &msg.packet.destination_channel, + msg.packet.sequence, + ); + self.write_ibc_data(&receipt_key, PacketReceipt::default().as_bytes())?; + + // store the ack + let ack_key = storage::ack_key( + &msg.packet.destination_port, + &msg.packet.destination_channel, + msg.packet.sequence, + ); + let ack = packet_ack.encode_to_vec(); + let ack_commitment = sha2::Sha256::digest(&ack).to_vec(); + self.write_ibc_data(&ack_key, ack_commitment)?; + + // increment the next sequence receive + let port_channel_id = port_channel_id( + msg.packet.destination_port.clone(), + msg.packet.destination_channel, + ); + let seq_key = storage::next_sequence_recv_key(&port_channel_id); + self.get_and_inc_sequence(&seq_key)?; + + let event = make_write_ack_event(msg.packet.clone(), ack) + .try_into() + .unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Receive a acknowledgement + fn acknowledge_packet( + &mut self, + msg: &MsgAcknowledgement, + ) -> std::result::Result<(), Self::Error> { + let ack = PacketAck::try_from(msg.acknowledgement.clone()) + .map_err(Error::IbcData)?; + if !ack.is_success() { + if let Ok(data) = serde_json::from_slice(&msg.packet.data) { + self.refund_token(&msg.packet, &data)?; + } + } + + let commitment_key = storage::commitment_key( + &msg.packet.source_port, + &msg.packet.source_channel, + msg.packet.sequence, + ); + self.delete_ibc_data(&commitment_key)?; + + // get and increment the next sequence ack + let port_channel_id = port_channel_id( + msg.packet.source_port.clone(), + msg.packet.source_channel, + ); + let seq_key = storage::next_sequence_ack_key(&port_channel_id); + self.get_and_inc_sequence(&seq_key)?; + + let event = make_ack_event(msg.packet.clone()).try_into().unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Receive a timeout + fn timeout_packet( + &mut self, + msg: &MsgTimeout, + ) -> std::result::Result<(), Self::Error> { + // check the packet data + if let Ok(data) = serde_json::from_slice(&msg.packet.data) { + self.refund_token(&msg.packet, &data)?; + } + + // delete the commitment of the packet + let commitment_key = storage::commitment_key( + &msg.packet.source_port, + &msg.packet.source_channel, + msg.packet.sequence, + ); + self.delete_ibc_data(&commitment_key)?; + + // close the channel + let port_channel_id = port_channel_id( + msg.packet.source_port.clone(), + msg.packet.source_channel, + ); + let channel_key = storage::channel_key(&port_channel_id); + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { + Error::Channel(format!( + "The channel to be closed doesn't exist: Port/Channel {}", + port_channel_id + )) + })?; + let mut channel = + ChannelEnd::decode_vec(&value).map_err(Error::Decoding)?; + if channel.order_matches(&Order::Ordered) { + close_channel(&mut channel); + self.write_ibc_data( + &channel_key, + channel.encode_vec().expect("encoding shouldn't fail"), + )?; + } + + let event = make_timeout_event(msg.packet.clone()).try_into().unwrap(); + self.emit_ibc_event(event)?; + + Ok(()) + } + + /// Receive a timeout for TimeoutOnClose + fn timeout_on_close_packet( + &mut self, + msg: &MsgTimeoutOnClose, + ) -> std::result::Result<(), Self::Error> { + // check the packet data + if let Ok(data) = serde_json::from_slice(&msg.packet.data) { + self.refund_token(&msg.packet, &data)?; + } + + // delete the commitment of the packet + let commitment_key = storage::commitment_key( + &msg.packet.source_port, + &msg.packet.source_channel, + msg.packet.sequence, + ); + self.delete_ibc_data(&commitment_key)?; + + // close the channel + let port_channel_id = port_channel_id( + msg.packet.source_port.clone(), + msg.packet.source_channel, + ); + let channel_key = storage::channel_key(&port_channel_id); + let value = self.read_ibc_data(&channel_key)?.ok_or_else(|| { + Error::Channel(format!( + "The channel to be closed doesn't exist: Port/Channel {}", + port_channel_id + )) + })?; + let mut channel = + ChannelEnd::decode_vec(&value).map_err(Error::Decoding)?; + if channel.order_matches(&Order::Ordered) { + close_channel(&mut channel); + self.write_ibc_data( + &channel_key, + channel.encode_vec().expect("encoding shouldn't fail"), + )?; + } + + Ok(()) + } + + /// Set the timestamp and the height for the client update + fn set_client_update_time( + &mut self, + client_id: &ClientId, + ) -> std::result::Result<(), Self::Error> { + let time = Time::parse_from_rfc3339(&self.get_header_time()?.0) + .map_err(|e| { + Error::Time(format!("The time of the header is invalid: {}", e)) + })?; + let key = storage::client_update_timestamp_key(client_id); + self.write_ibc_data( + &key, + time.encode_vec().expect("encoding shouldn't fail"), + )?; + + // the revision number is always 0 + let height = Height::new(0, self.get_height()?.0); + let height_key = storage::client_update_height_key(client_id); + // write the current height as u64 + self.write_ibc_data( + &height_key, + height.encode_vec().expect("Encoding shouldn't fail"), + )?; + + Ok(()) + } + + /// Get and increment the counter + fn get_and_inc_counter( + &mut self, + key: &Key, + ) -> std::result::Result { + let value = self.read_ibc_data(key)?.ok_or_else(|| { + Error::Counter(format!("The counter doesn't exist: {}", key)) + })?; + let value: [u8; 8] = value.try_into().map_err(|_| { + Error::Counter(format!("The counter value wasn't u64: Key {}", key)) + })?; + let counter = u64::from_be_bytes(value); + self.write_ibc_data(key, (counter + 1).to_be_bytes())?; + Ok(counter) + } + + /// Get and increment the sequence + fn get_and_inc_sequence( + &mut self, + key: &Key, + ) -> std::result::Result { + let index = match self.read_ibc_data(key)? { + Some(v) => { + let index: [u8; 8] = v.try_into().map_err(|_| { + Error::Sequence(format!( + "The sequence index wasn't u64: Key {}", + key + )) + })?; + u64::from_be_bytes(index) + } + // when the sequence has never been used, returns the initial value + None => 1, + }; + self.write_ibc_data(key, (index + 1).to_be_bytes())?; + Ok(index.into()) + } + + /// Bind a new port + fn bind_port( + &mut self, + port_id: &PortId, + ) -> std::result::Result<(), Self::Error> { + let port_key = storage::port_key(port_id); + match self.read_ibc_data(&port_key)? { + Some(_) => {} + None => { + // create a new capability and claim it + let index_key = storage::capability_index_key(); + let cap_index = self.get_and_inc_counter(&index_key)?; + self.write_ibc_data(&port_key, cap_index.to_be_bytes())?; + let cap_key = storage::capability_key(cap_index); + self.write_ibc_data(&cap_key, port_id.as_bytes())?; + } + } + Ok(()) + } + + /// Send the specified token by escrowing or burning + fn send_token( + &mut self, + msg: &MsgTransfer, + ) -> std::result::Result<(), Self::Error> { + let mut data = FungibleTokenPacketData::from(msg.clone()); + if let Some(hash) = storage::token_hash_from_denom(&data.denom) + .map_err(Error::IbcStorage)? + { + let denom_key = storage::ibc_denom_key(hash); + let denom_bytes = + self.read_ibc_data(&denom_key)?.ok_or_else(|| { + Error::SendingToken(format!( + "No original denom: denom_key {}", + denom_key + )) + })?; + let denom = std::str::from_utf8(&denom_bytes).map_err(|e| { + Error::SendingToken(format!( + "Decoding the denom failed: denom_key {}, error {}", + denom_key, e + )) + })?; + data.denom = denom.to_string(); + } + let token = storage::token(&data.denom).map_err(Error::IbcStorage)?; + let amount = Amount::from_str(&data.amount, 0).map_err(|e| { + Error::SendingToken(format!( + "Invalid amount: amount {}, error {}", + data.amount, e + )) + })?; + + let source_addr = Address::decode(&data.sender).map_err(|e| { + Error::SendingToken(format!( + "Invalid sender address: sender {}, error {}", + data.sender, e + )) + })?; + + // check the denomination field + let prefix = format!( + "{}/{}/", + msg.source_port.clone(), + msg.source_channel.clone() + ); + let (source, target) = if data.denom.starts_with(&prefix) { + // the receiver's chain was the source + // transfer from the origin-specific account of the token + let key_prefix = storage::ibc_token_prefix(&data.denom) + .map_err(Error::IbcStorage)?; + let src = token::multitoken_balance_key(&key_prefix, &source_addr); + + let key_prefix = storage::ibc_account_prefix( + &msg.source_port, + &msg.source_channel, + &token, + ); + let burn = token::multitoken_balance_key( + &key_prefix, + &Address::Internal(InternalAddress::IbcBurn), + ); + (src, burn) + } else { + // this chain is the source + // escrow the amount of the token + let src = if data.denom == token.to_string() { + token::balance_key(&token, &source_addr) + } else { + let key_prefix = storage::ibc_token_prefix(&data.denom) + .map_err(Error::IbcStorage)?; + token::multitoken_balance_key(&key_prefix, &source_addr) + }; + + let key_prefix = storage::ibc_account_prefix( + &msg.source_port, + &msg.source_channel, + &token, + ); + let escrow = token::multitoken_balance_key( + &key_prefix, + &Address::Internal(InternalAddress::IbcEscrow), + ); + (src, escrow) + }; + self.transfer_token(&source, &target, amount)?; + + // send a packet + let port_channel_id = + port_channel_id(msg.source_port.clone(), msg.source_channel); + let packet_data = serde_json::to_vec(&data) + .expect("encoding the packet data shouldn't fail"); + self.send_packet( + port_channel_id, + packet_data, + msg.timeout_height, + msg.timeout_timestamp, + ) + } + + /// Receive the specified token by unescrowing or minting + fn receive_token( + &mut self, + packet: &Packet, + data: &FungibleTokenPacketData, + ) -> std::result::Result<(), Self::Error> { + let token = storage::token(&data.denom).map_err(Error::IbcStorage)?; + let amount = Amount::from_str(&data.amount, 0).map_err(|e| { + Error::ReceivingToken(format!( + "Invalid amount: amount {}, error {}", + data.amount, e + )) + })?; + // The receiver should be an address because the origin-specific account + // key should be assigned internally + let dest_addr = Address::decode(&data.receiver).map_err(|e| { + Error::ReceivingToken(format!( + "Invalid receiver address: receiver {}, error {}", + data.receiver, e + )) + })?; + + let prefix = format!( + "{}/{}/", + packet.source_port.clone(), + packet.source_channel.clone() + ); + let (source, target) = match data.denom.strip_prefix(&prefix) { + Some(denom) => { + // unescrow the token because this chain was the source + let escrow_prefix = storage::ibc_account_prefix( + &packet.destination_port, + &packet.destination_channel, + &token, + ); + let escrow = token::multitoken_balance_key( + &escrow_prefix, + &Address::Internal(InternalAddress::IbcEscrow), + ); + let dest = if denom == token.to_string() { + token::balance_key(&token, &dest_addr) + } else { + let key_prefix = storage::ibc_token_prefix(denom) + .map_err(Error::IbcStorage)?; + token::multitoken_balance_key(&key_prefix, &dest_addr) + }; + (escrow, dest) + } + None => { + // mint the token because the sender chain is the source + let key_prefix = storage::ibc_account_prefix( + &packet.destination_port, + &packet.destination_channel, + &token, + ); + let mint = token::multitoken_balance_key( + &key_prefix, + &Address::Internal(InternalAddress::IbcMint), + ); + + // prefix the denom with the this chain port and channel + let denom = format!( + "{}/{}/{}", + &packet.destination_port, + &packet.destination_channel, + &data.denom + ); + let key_prefix = storage::ibc_token_prefix(&denom) + .map_err(Error::IbcStorage)?; + let dest = + token::multitoken_balance_key(&key_prefix, &dest_addr); + + // store the prefixed denom + let token_hash = storage::calc_hash(&denom); + let denom_key = storage::ibc_denom_key(token_hash); + self.write_ibc_data(&denom_key, denom.as_bytes())?; + + (mint, dest) + } + }; + self.transfer_token(&source, &target, amount)?; + + Ok(()) + } + + /// Refund the specified token by unescrowing or minting + fn refund_token( + &mut self, + packet: &Packet, + data: &FungibleTokenPacketData, + ) -> std::result::Result<(), Self::Error> { + let token = storage::token(&data.denom).map_err(Error::IbcStorage)?; + let amount = Amount::from_str(&data.amount, 0).map_err(|e| { + Error::ReceivingToken(format!( + "Invalid amount: amount {}, error {}", + data.amount, e + )) + })?; + + let dest_addr = Address::decode(&data.sender).map_err(|e| { + Error::SendingToken(format!( + "Invalid sender address: sender {}, error {}", + data.sender, e + )) + })?; + + let prefix = format!( + "{}/{}/", + packet.source_port.clone(), + packet.source_channel.clone() + ); + let (source, target) = if data.denom.starts_with(&prefix) { + // mint the token because the amount was burned + let key_prefix = storage::ibc_account_prefix( + &packet.source_port, + &packet.source_channel, + &token, + ); + let mint = token::multitoken_balance_key( + &key_prefix, + &Address::Internal(InternalAddress::IbcMint), + ); + let key_prefix = storage::ibc_token_prefix(&data.denom) + .map_err(Error::IbcStorage)?; + let dest = token::multitoken_balance_key(&key_prefix, &dest_addr); + (mint, dest) + } else { + // unescrow the token because the acount was escrowed + let dest = if data.denom == token.to_string() { + token::balance_key(&token, &dest_addr) + } else { + let key_prefix = storage::ibc_token_prefix(&data.denom) + .map_err(Error::IbcStorage)?; + token::multitoken_balance_key(&key_prefix, &dest_addr) + }; + + let key_prefix = storage::ibc_account_prefix( + &packet.source_port, + &packet.source_channel, + &token, + ); + let escrow = token::multitoken_balance_key( + &key_prefix, + &Address::Internal(InternalAddress::IbcEscrow), + ); + (escrow, dest) + }; + self.transfer_token(&source, &target, amount)?; + + Ok(()) + } +} + +/// Update a client with the given state and headers +pub fn update_client( + client_state: AnyClientState, + header: AnyHeader, +) -> Result<(AnyClientState, AnyConsensusState)> { + match client_state { + AnyClientState::Tendermint(cs) => match header { + AnyHeader::Tendermint(h) => { + let new_client_state = cs.with_header(h.clone()).wrap_any(); + let new_consensus_state = TmConsensusState::from(h).wrap_any(); + Ok((new_client_state, new_consensus_state)) + } + #[cfg(any(feature = "ibc-mocks-abcipp", feature = "ibc-mocks"))] + _ => Err(Error::ClientUpdate( + "The header type is mismatched".to_owned(), + )), + }, + #[cfg(any(feature = "ibc-mocks-abcipp", feature = "ibc-mocks"))] + AnyClientState::Mock(_) => match header { + AnyHeader::Mock(h) => Ok(( + MockClientState::new(h).wrap_any(), + MockConsensusState::new(h).wrap_any(), + )), + _ => Err(Error::ClientUpdate( + "The header type is mismatched".to_owned(), + )), + }, + } +} + +/// Returns a new client ID +pub fn client_id(client_type: ClientType, counter: u64) -> Result { + ClientId::new(client_type, counter).map_err(Error::ClientId) +} + +/// Returns a new connection ID +pub fn connection_id(counter: u64) -> ConnectionId { + ConnectionId::new(counter) +} + +/// Make a connection end from the init message +pub fn init_connection(msg: &MsgConnectionOpenInit) -> ConnectionEnd { + ConnectionEnd::new( + ConnState::Init, + msg.client_id.clone(), + msg.counterparty.clone(), + vec![msg.version.clone().unwrap_or_default()], + msg.delay_period, + ) +} + +/// Make a connection end from the try message +pub fn try_connection(msg: &MsgConnectionOpenTry) -> ConnectionEnd { + ConnectionEnd::new( + ConnState::TryOpen, + msg.client_id.clone(), + msg.counterparty.clone(), + msg.counterparty_versions.clone(), + msg.delay_period, + ) +} + +/// Open the connection +pub fn open_connection(conn: &mut ConnectionEnd) { + conn.set_state(ConnState::Open); +} + +/// Returns a new channel ID +pub fn channel_id(counter: u64) -> ChannelId { + ChannelId::new(counter) +} + +/// Open the channel +pub fn open_channel(channel: &mut ChannelEnd) { + channel.set_state(ChanState::Open); +} + +/// Close the channel +pub fn close_channel(channel: &mut ChannelEnd) { + channel.set_state(ChanState::Closed); +} + +/// Returns a port ID +pub fn port_id(id: &str) -> Result { + PortId::from_str(id).map_err(Error::PortId) +} + +/// Returns a pair of port ID and channel ID +pub fn port_channel_id( + port_id: PortId, + channel_id: ChannelId, +) -> PortChannelId { + PortChannelId { + port_id, + channel_id, + } +} + +/// Returns a sequence +pub fn sequence(index: u64) -> Sequence { + Sequence::from(index) +} + +/// Make a packet from MsgTransfer +pub fn packet_from_message( + msg: &MsgTransfer, + sequence: Sequence, + counterparty: &ChanCounterparty, +) -> Packet { + Packet { + sequence, + source_port: msg.source_port.clone(), + source_channel: msg.source_channel, + destination_port: counterparty.port_id.clone(), + destination_channel: *counterparty + .channel_id() + .expect("the counterparty channel should exist"), + data: serde_json::to_vec(&FungibleTokenPacketData::from(msg.clone())) + .expect("encoding the packet data shouldn't fail"), + timeout_height: msg.timeout_height, + timeout_timestamp: msg.timeout_timestamp, + } +} + +/// Returns a commitment from the given packet +pub fn commitment(packet: &Packet) -> PacketCommitment { + let timeout = packet.timeout_timestamp.nanoseconds().to_be_bytes(); + let revision_number = packet.timeout_height.revision_number.to_be_bytes(); + let revision_height = packet.timeout_height.revision_height.to_be_bytes(); + let data = sha2::Sha256::digest(&packet.data); + let input = [ + &timeout, + &revision_number, + &revision_height, + data.as_slice(), + ] + .concat(); + sha2::Sha256::digest(&input).to_vec().into() +} + +/// Returns a counterparty of a connection +pub fn connection_counterparty( + client_id: ClientId, + conn_id: ConnectionId, +) -> ConnCounterparty { + ConnCounterparty::new(client_id, Some(conn_id), commitment_prefix()) +} + +/// Returns a counterparty of a channel +pub fn channel_counterparty( + port_id: PortId, + channel_id: ChannelId, +) -> ChanCounterparty { + ChanCounterparty::new(port_id, Some(channel_id)) +} + +/// Returns Namada commitment prefix +pub fn commitment_prefix() -> CommitmentPrefix { + CommitmentPrefix::try_from(COMMITMENT_PREFIX.to_vec()) + .expect("the conversion shouldn't fail") +} + +/// Makes CreateClient event +pub fn make_create_client_event( + client_id: &ClientId, + msg: &MsgCreateAnyClient, +) -> IbcEvent { + let attributes = ClientAttributes { + client_id: client_id.clone(), + client_type: msg.client_state.client_type(), + consensus_height: msg.client_state.latest_height(), + ..Default::default() + }; + IbcEvent::CreateClient(CreateClient::from(attributes)) +} + +/// Makes UpdateClient event +pub fn make_update_client_event( + client_id: &ClientId, + msg: &MsgUpdateAnyClient, +) -> IbcEvent { + let attributes = ClientAttributes { + client_id: client_id.clone(), + client_type: msg.header.client_type(), + consensus_height: msg.header.height(), + ..Default::default() + }; + IbcEvent::UpdateClient(UpdateClient::from(attributes)) +} + +/// Makes UpgradeClient event +pub fn make_upgrade_client_event( + client_id: &ClientId, + msg: &MsgUpgradeAnyClient, +) -> IbcEvent { + let attributes = ClientAttributes { + client_id: client_id.clone(), + client_type: msg.client_state.client_type(), + consensus_height: msg.client_state.latest_height(), + ..Default::default() + }; + IbcEvent::UpgradeClient(UpgradeClient::from(attributes)) +} + +/// Makes OpenInitConnection event +pub fn make_open_init_connection_event( + conn_id: &ConnectionId, + msg: &MsgConnectionOpenInit, +) -> IbcEvent { + let attributes = ConnectionAttributes { + connection_id: Some(conn_id.clone()), + client_id: msg.client_id.clone(), + counterparty_connection_id: msg.counterparty.connection_id().cloned(), + counterparty_client_id: msg.counterparty.client_id().clone(), + ..Default::default() + }; + ConnOpenInit::from(attributes).into() +} + +/// Makes OpenTryConnection event +pub fn make_open_try_connection_event( + conn_id: &ConnectionId, + msg: &MsgConnectionOpenTry, +) -> IbcEvent { + let attributes = ConnectionAttributes { + connection_id: Some(conn_id.clone()), + client_id: msg.client_id.clone(), + counterparty_connection_id: msg.counterparty.connection_id().cloned(), + counterparty_client_id: msg.counterparty.client_id().clone(), + ..Default::default() + }; + ConnOpenTry::from(attributes).into() +} + +/// Makes OpenAckConnection event +pub fn make_open_ack_connection_event(msg: &MsgConnectionOpenAck) -> IbcEvent { + let attributes = ConnectionAttributes { + connection_id: Some(msg.connection_id.clone()), + counterparty_connection_id: Some( + msg.counterparty_connection_id.clone(), + ), + ..Default::default() + }; + ConnOpenAck::from(attributes).into() +} + +/// Makes OpenConfirmConnection event +pub fn make_open_confirm_connection_event( + msg: &MsgConnectionOpenConfirm, +) -> IbcEvent { + let attributes = ConnectionAttributes { + connection_id: Some(msg.connection_id.clone()), + ..Default::default() + }; + ConnOpenConfirm::from(attributes).into() +} + +/// Makes OpenInitChannel event +pub fn make_open_init_channel_event( + channel_id: &ChannelId, + msg: &MsgChannelOpenInit, +) -> IbcEvent { + let connection_id = match msg.channel.connection_hops().get(0) { + Some(c) => c.clone(), + None => ConnectionId::default(), + }; + let attributes = ChanOpenInit { + height: Height::default(), + port_id: msg.port_id.clone(), + channel_id: Some(*channel_id), + connection_id, + counterparty_port_id: msg.channel.counterparty().port_id().clone(), + counterparty_channel_id: msg + .channel + .counterparty() + .channel_id() + .cloned(), + }; + attributes.into() +} + +/// Makes OpenTryChannel event +pub fn make_open_try_channel_event( + channel_id: &ChannelId, + msg: &MsgChannelOpenTry, +) -> IbcEvent { + let connection_id = match msg.channel.connection_hops().get(0) { + Some(c) => c.clone(), + None => ConnectionId::default(), + }; + let attributes = ChanOpenTry { + height: Height::default(), + port_id: msg.port_id.clone(), + channel_id: Some(*channel_id), + connection_id, + counterparty_port_id: msg.channel.counterparty().port_id().clone(), + counterparty_channel_id: msg + .channel + .counterparty() + .channel_id() + .cloned(), + }; + attributes.into() +} + +/// Makes OpenAckChannel event +pub fn make_open_ack_channel_event( + msg: &MsgChannelOpenAck, + channel: &ChannelEnd, +) -> Result { + let conn_id = get_connection_id_from_channel(channel)?; + let counterparty = channel.counterparty(); + let attributes = ChanOpenAck { + height: Height::default(), + port_id: msg.port_id.clone(), + channel_id: Some(msg.channel_id), + counterparty_channel_id: Some(msg.counterparty_channel_id), + connection_id: conn_id.clone(), + counterparty_port_id: counterparty.port_id().clone(), + }; + Ok(attributes.into()) +} + +/// Makes OpenConfirmChannel event +pub fn make_open_confirm_channel_event( + msg: &MsgChannelOpenConfirm, + channel: &ChannelEnd, +) -> Result { + let conn_id = get_connection_id_from_channel(channel)?; + let counterparty = channel.counterparty(); + let attributes = ChanOpenConfirm { + height: Height::default(), + port_id: msg.port_id.clone(), + channel_id: Some(msg.channel_id), + connection_id: conn_id.clone(), + counterparty_port_id: counterparty.port_id().clone(), + counterparty_channel_id: counterparty.channel_id().cloned(), + }; + Ok(attributes.into()) +} + +/// Makes CloseInitChannel event +pub fn make_close_init_channel_event( + msg: &MsgChannelCloseInit, + channel: &ChannelEnd, +) -> Result { + let conn_id = get_connection_id_from_channel(channel)?; + let counterparty = channel.counterparty(); + let attributes = ChanCloseInit { + height: Height::default(), + port_id: msg.port_id.clone(), + channel_id: msg.channel_id, + connection_id: conn_id.clone(), + counterparty_port_id: counterparty.port_id().clone(), + counterparty_channel_id: counterparty.channel_id().cloned(), + }; + Ok(attributes.into()) +} + +/// Makes CloseConfirmChannel event +pub fn make_close_confirm_channel_event( + msg: &MsgChannelCloseConfirm, + channel: &ChannelEnd, +) -> Result { + let conn_id = get_connection_id_from_channel(channel)?; + let counterparty = channel.counterparty(); + let attributes = ChanCloseConfirm { + height: Height::default(), + port_id: msg.port_id.clone(), + channel_id: Some(msg.channel_id), + connection_id: conn_id.clone(), + counterparty_port_id: counterparty.port_id.clone(), + counterparty_channel_id: counterparty.channel_id().cloned(), + }; + Ok(attributes.into()) +} + +fn get_connection_id_from_channel( + channel: &ChannelEnd, +) -> Result<&ConnectionId> { + channel.connection_hops().get(0).ok_or_else(|| { + Error::Channel("No connection for the channel".to_owned()) + }) +} + +/// Makes SendPacket event +pub fn make_send_packet_event(packet: Packet) -> IbcEvent { + IbcEvent::SendPacket(SendPacket { + height: packet.timeout_height, + packet, + }) +} + +/// Makes WriteAcknowledgement event +pub fn make_write_ack_event(packet: Packet, ack: Vec) -> IbcEvent { + IbcEvent::WriteAcknowledgement(WriteAcknowledgement { + // this height is not used + height: Height::default(), + packet, + ack, + }) +} + +/// Makes AcknowledgePacket event +pub fn make_ack_event(packet: Packet) -> IbcEvent { + IbcEvent::AcknowledgePacket(AcknowledgePacket { + // this height is not used + height: Height::default(), + packet, + }) +} + +/// Makes TimeoutPacket event +pub fn make_timeout_event(packet: Packet) -> IbcEvent { + IbcEvent::TimeoutPacket(TimeoutPacket { + // this height is not used + height: Height::default(), + packet, + }) +} diff --git a/core/src/ledger/ibc/context/transfer_mod.rs b/core/src/ledger/ibc/context/transfer_mod.rs index 73961a1e28..ad0aa75800 100644 --- a/core/src/ledger/ibc/context/transfer_mod.rs +++ b/core/src/ledger/ibc/context/transfer_mod.rs @@ -480,7 +480,9 @@ where ChannelError::Other { description: format!( "Sending a coin failed: from {}, to {}, amount {}", - src, dest, amount + src, + dest, + amount.to_string_native() ), }, )) @@ -517,7 +519,9 @@ where ChannelError::Other { description: format!( "Sending a coin failed: from {}, to {}, amount {}", - src, dest, amount + src, + dest, + amount.to_string_native() ), }, )) @@ -554,7 +558,9 @@ where ChannelError::Other { description: format!( "Sending a coin failed: from {}, to {}, amount {}", - src, dest, amount + src, + dest, + amount.to_string_native() ), }, )) diff --git a/core/src/ledger/ibc/storage.rs b/core/src/ledger/ibc/storage.rs index bee874bb0e..3f0eb94d2a 100644 --- a/core/src/ledger/ibc/storage.rs +++ b/core/src/ledger/ibc/storage.rs @@ -22,7 +22,7 @@ use crate::types::storage::{self, DbKeySeg, Key, KeySeg}; const CLIENTS_COUNTER: &str = "clients/counter"; const CONNECTIONS_COUNTER: &str = "connections/counter"; const CHANNELS_COUNTER: &str = "channelEnds/counter"; -const DENOM: &str = "denom"; +const DENOM: &str = "ibc_denom"; /// Key segment for a multitoken related to IBC pub const MULTITOKEN_STORAGE_KEY: &str = "ibc"; @@ -82,7 +82,7 @@ pub fn ibc_prefix(key: &Key) -> Option { "receipts" => IbcPrefix::Receipt, "acks" => IbcPrefix::Ack, "event" => IbcPrefix::Event, - "denom" => IbcPrefix::Denom, + "ibc_denom" => IbcPrefix::Denom, _ => IbcPrefix::Unknown, }) } diff --git a/core/src/ledger/parameters/mod.rs b/core/src/ledger/parameters/mod.rs index bb9ae99577..ab72527522 100644 --- a/core/src/ledger/parameters/mod.rs +++ b/core/src/ledger/parameters/mod.rs @@ -2,7 +2,6 @@ pub mod storage; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use rust_decimal::Decimal; use thiserror::Error; use super::storage::types; @@ -10,6 +9,7 @@ use super::storage_api::{self, ResultExt, StorageRead, StorageWrite}; use crate::ledger::storage::{self as ledger_storage}; use crate::types::address::{Address, InternalAddress}; use crate::types::chain::ProposalBytes; +use crate::types::dec::Dec; use crate::types::hash::Hash; use crate::types::time::DurationSecs; use crate::types::token; @@ -45,13 +45,13 @@ pub struct Parameters { /// Expected number of epochs per year (read only) pub epochs_per_year: u64, /// PoS gain p (read only) - pub pos_gain_p: Decimal, + pub pos_gain_p: Dec, /// PoS gain d (read only) - pub pos_gain_d: Decimal, + pub pos_gain_d: Dec, /// PoS staked ratio (read + write for every epoch) - pub staked_ratio: Decimal, + pub staked_ratio: Dec, /// PoS inflation amount from the last epoch (read + write for every epoch) - pub pos_inflation_amount: u64, + pub pos_inflation_amount: token::Amount, #[cfg(not(feature = "mainnet"))] /// Faucet account for free token withdrawal pub faucet_account: Option
, @@ -188,7 +188,7 @@ impl Parameters { { let wrapper_tx_fees_key = storage::get_wrapper_tx_fees_key(); let wrapper_tx_fees = - wrapper_tx_fees.unwrap_or(token::Amount::whole(100)); + wrapper_tx_fees.unwrap_or(token::Amount::native_whole(100)); storage.write(&wrapper_tx_fees_key, wrapper_tx_fees)?; } Ok(()) @@ -276,7 +276,7 @@ where /// cost. pub fn update_pos_gain_p_parameter( storage: &mut S, - value: &Decimal, + value: &Dec, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -289,7 +289,7 @@ where /// cost. pub fn update_pos_gain_d_parameter( storage: &mut S, - value: &Decimal, + value: &Dec, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -302,7 +302,7 @@ where /// gas cost. pub fn update_staked_ratio_parameter( storage: &mut S, - value: &Decimal, + value: &Dec, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -435,28 +435,28 @@ where // read PoS gain P let pos_gain_p_key = storage::get_pos_gain_p_key(); let value = storage.read(&pos_gain_p_key)?; - let pos_gain_p: Decimal = value + let pos_gain_p = value .ok_or(ReadError::ParametersMissing) .into_storage_result()?; // read PoS gain D let pos_gain_d_key = storage::get_pos_gain_d_key(); let value = storage.read(&pos_gain_d_key)?; - let pos_gain_d: Decimal = value + let pos_gain_d = value .ok_or(ReadError::ParametersMissing) .into_storage_result()?; // read staked ratio let staked_ratio_key = storage::get_staked_ratio_key(); let value = storage.read(&staked_ratio_key)?; - let staked_ratio: Decimal = value + let staked_ratio = value .ok_or(ReadError::ParametersMissing) .into_storage_result()?; // read PoS inflation rate let pos_inflation_key = storage::get_pos_inflation_amount_key(); let value = storage.read(&pos_inflation_key)?; - let pos_inflation_amount: u64 = value + let pos_inflation_amount = value .ok_or(ReadError::ParametersMissing) .into_storage_result()?; diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 3945ba936a..4dec02d4f5 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -9,7 +9,8 @@ use masp_primitives::merkle_tree::FrozenCommitmentTree; use masp_primitives::sapling::Node; use crate::types::address::Address; -use crate::types::storage::Epoch; +use crate::types::storage::{Epoch, Key}; +use crate::types::token::MaspDenom; /// A representation of the conversion state #[derive(Debug, Default, BorshSerialize, BorshDeserialize)] @@ -19,7 +20,16 @@ pub struct ConversionState { /// The tree currently containing all the conversions pub tree: FrozenCommitmentTree, /// Map assets to their latest conversion and position in Merkle tree - pub assets: BTreeMap, + #[allow(clippy::type_complexity)] + pub assets: BTreeMap< + AssetType, + ( + (Address, Option, MaspDenom), + Epoch, + AllowedConversion, + usize, + ), + >, } // This is only enabled when "wasm-runtime" is on, because we're using rayon @@ -49,54 +59,70 @@ where let masp_rewards = address::masp_rewards(); // The total transparent value of the rewards being distributed - let mut total_reward = token::Amount::from(0); + let mut total_reward = token::Amount::native_whole(0); // Construct MASP asset type for rewards. Always timestamp reward tokens // with the zeroth epoch to minimize the number of convert notes clients // have to use. This trick works under the assumption that reward tokens // from different epochs are exactly equivalent. - let reward_asset_bytes = (address::nam(), 0u64) - .try_to_vec() - .expect("unable to serialize address and epoch"); - let reward_asset = AssetType::new(reward_asset_bytes.as_ref()) - .expect("unable to derive asset identifier"); + let reward_asset = + encode_asset_type(address::nam(), &None, MaspDenom::Zero, Epoch(0)); // Conversions from the previous to current asset for each address - let mut current_convs = BTreeMap::::new(); + let mut current_convs = + BTreeMap::<(Address, Option, MaspDenom), AllowedConversion>::new(); // Reward all tokens according to above reward rates - for (addr, reward) in &masp_rewards { - // Dispence a transparent reward in parallel to the shielded rewards - let addr_bal: token::Amount = wl_storage - .read(&token::balance_key(addr, &masp_addr))? - .unwrap_or_default(); + for ((addr, sub_prefix), reward) in &masp_rewards { + // Dispense a transparent reward in parallel to the shielded rewards + let addr_bal: token::Amount = match sub_prefix { + None => wl_storage + .read(&token::balance_key(addr, &masp_addr))? + .unwrap_or_default(), + Some(sub) => wl_storage + .read(&token::multitoken_balance_key( + &token::multitoken_balance_prefix(addr, sub), + &masp_addr, + ))? + .unwrap_or_default(), + }; // The reward for each reward.1 units of the current asset is // reward.0 units of the reward token // Since floor(a) + floor(b) <= floor(a+b), there will always be // enough rewards to reimburse users total_reward += (addr_bal * *reward).0; - // Provide an allowed conversion from previous timestamp. The - // negative sign allows each instance of the old asset to be - // cancelled out/replaced with the new asset - let old_asset = - encode_asset_type(addr.clone(), wl_storage.storage.last_epoch); - let new_asset = - encode_asset_type(addr.clone(), wl_storage.storage.block.epoch); - current_convs.insert( - addr.clone(), - (MaspAmount::from_pair(old_asset, -(reward.1 as i64)).unwrap() - + MaspAmount::from_pair(new_asset, reward.1).unwrap() - + MaspAmount::from_pair(reward_asset, reward.0).unwrap()) - .into(), - ); - // Add a conversion from the previous asset type - wl_storage.storage.conversion_state.assets.insert( - old_asset, - ( + for denom in token::MaspDenom::iter() { + // Provide an allowed conversion from previous timestamp. The + // negative sign allows each instance of the old asset to be + // cancelled out/replaced with the new asset + let old_asset = encode_asset_type( addr.clone(), + sub_prefix, + denom, wl_storage.storage.last_epoch, - MaspAmount::zero().into(), - 0, - ), - ); + ); + let new_asset = encode_asset_type( + addr.clone(), + sub_prefix, + denom, + wl_storage.storage.block.epoch, + ); + current_convs.insert( + (addr.clone(), sub_prefix.clone(), denom), + (MaspAmount::from_pair(old_asset, -(reward.1 as i64)).unwrap() + + MaspAmount::from_pair(new_asset, reward.1).unwrap() + + MaspAmount::from_pair(reward_asset, reward.0).unwrap()) + .into(), + ); + // Add a conversion from the previous asset type + wl_storage.storage.conversion_state.assets.insert( + old_asset, + ( + (addr.clone(), sub_prefix.clone(), denom), + wl_storage.storage.last_epoch, + MaspAmount::zero().into(), + 0, + ), + ); + } } // Try to distribute Merkle leaf updating as evenly as possible across @@ -119,9 +145,9 @@ where .into_par_iter() .with_min_len(notes_per_thread_min) .with_max_len(notes_per_thread_max) - .map(|(idx, (addr, _epoch, conv, pos))| { + .map(|(idx, (asset, _epoch, conv, pos))| { // Use transitivity to update conversion - *conv += current_convs[addr].clone(); + *conv += current_convs[asset].clone(); // Update conversion position to leaf we are about to create *pos = idx; // The merkle tree need only provide the conversion commitment, @@ -162,20 +188,26 @@ where // Add purely decoding entries to the assets map. These will be // overwritten before the creation of the next commitment tree - for addr in masp_rewards.keys() { - // Add the decoding entry for the new asset type. An uncommited - // node position is used since this is not a conversion. - let new_asset = - encode_asset_type(addr.clone(), wl_storage.storage.block.epoch); - wl_storage.storage.conversion_state.assets.insert( - new_asset, - ( + for (addr, sub_prefix) in masp_rewards.keys() { + for denom in token::MaspDenom::iter() { + // Add the decoding entry for the new asset type. An uncommited + // node position is used since this is not a conversion. + let new_asset = encode_asset_type( addr.clone(), + sub_prefix, + denom, wl_storage.storage.block.epoch, - MaspAmount::zero().into(), - wl_storage.storage.conversion_state.tree.size(), - ), - ); + ); + wl_storage.storage.conversion_state.assets.insert( + new_asset, + ( + (addr.clone(), sub_prefix.clone(), denom), + wl_storage.storage.block.epoch, + MaspAmount::zero().into(), + wl_storage.storage.conversion_state.tree.size(), + ), + ); + } } // Save the current conversion state in order to avoid computing @@ -195,8 +227,21 @@ where } /// Construct MASP asset type with given epoch for given token -pub fn encode_asset_type(addr: Address, epoch: Epoch) -> AssetType { - let new_asset_bytes = (addr, epoch.0) +pub fn encode_asset_type( + addr: Address, + sub_prefix: &Option, + denom: MaspDenom, + epoch: Epoch, +) -> AssetType { + let new_asset_bytes = ( + addr, + sub_prefix + .as_ref() + .map(|k| k.to_string()) + .unwrap_or_default(), + denom, + epoch.0, + ) .try_to_vec() .expect("unable to serialize address and epoch"); AssetType::new(new_asset_bytes.as_ref()) diff --git a/core/src/ledger/storage/mockdb.rs b/core/src/ledger/storage/mockdb.rs index e53178b157..9955456830 100644 --- a/core/src/ledger/storage/mockdb.rs +++ b/core/src/ledger/storage/mockdb.rs @@ -514,21 +514,10 @@ impl DB for MockDB { impl<'iter> DBIter<'iter> for MockDB { type PrefixIter = MockPrefixIterator; - fn iter_optional_prefix( - &'iter self, - prefix: Option<&Key>, - ) -> MockPrefixIterator { + fn iter_prefix(&'iter self, prefix: Option<&Key>) -> MockPrefixIterator { let db_prefix = "subspace/".to_owned(); - let prefix = format!( - "{}{}", - db_prefix, - match prefix { - None => "".to_string(), - Some(prefix) => { - prefix.to_string() - } - } - ); + let prefix_str = prefix.map(|k| k.to_string()).unwrap_or_default(); + let prefix = format!("{}{}", db_prefix, prefix_str); let iter = self.0.borrow().clone().into_iter(); MockPrefixIterator::new(MockIterator { prefix, iter }, db_prefix) } diff --git a/core/src/ledger/storage/mod.rs b/core/src/ledger/storage/mod.rs index 137c7b9e53..b60ad4ff8c 100644 --- a/core/src/ledger/storage/mod.rs +++ b/core/src/ledger/storage/mod.rs @@ -340,20 +340,7 @@ pub trait DBIter<'iter> { /// /// Read account subspace key value pairs with the given prefix from the DB, /// ordered by the storage keys. - fn iter_prefix(&'iter self, prefix: &Key) -> Self::PrefixIter { - self.iter_optional_prefix(Some(prefix)) - } - - /// Iterate over all subspace keys - fn iter_all(&'iter self) -> Self::PrefixIter { - self.iter_optional_prefix(None) - } - - /// Iterate over subspace keys, with optional prefix - fn iter_optional_prefix( - &'iter self, - prefix: Option<&Key>, - ) -> Self::PrefixIter; + fn iter_prefix(&'iter self, prefix: Option<&Key>) -> Self::PrefixIter; /// Read results subspace key value pairs from the DB fn iter_results(&'iter self) -> Self::PrefixIter; @@ -600,10 +587,7 @@ where &self, prefix: &Key, ) -> (>::PrefixIter, u64) { - ( - self.db.iter_optional_prefix(Some(prefix)), - prefix.len() as _, - ) + (self.db.iter_prefix(Some(prefix)), prefix.len() as _) } /// Returns a prefix iterator and the gas cost @@ -1072,11 +1056,11 @@ mod tests { use chrono::{TimeZone, Utc}; use proptest::prelude::*; use proptest::test_runner::Config; - use rust_decimal_macros::dec; use super::testing::*; use super::*; use crate::ledger::parameters::{self, Parameters}; + use crate::types::dec::Dec; use crate::types::time::{self, Duration}; prop_compose! { @@ -1155,10 +1139,10 @@ mod tests { tx_whitelist: vec![], implicit_vp_code_hash: Hash::zero(), epochs_per_year: 100, - pos_gain_p: dec!(0.1), - pos_gain_d: dec!(0.1), - staked_ratio: dec!(0.1), - pos_inflation_amount: 0, + pos_gain_p: Dec::new(1,1).expect("Cannot fail"), + pos_gain_d: Dec::new(1,1).expect("Cannot fail"), + staked_ratio: Dec::new(1,1).expect("Cannot fail"), + pos_inflation_amount: token::Amount::zero(), #[cfg(not(feature = "mainnet"))] faucet_account: None, #[cfg(not(feature = "mainnet"))] diff --git a/core/src/ledger/storage/wl_storage.rs b/core/src/ledger/storage/wl_storage.rs index 4ca76aa379..cdb29668bf 100644 --- a/core/src/ledger/storage/wl_storage.rs +++ b/core/src/ledger/storage/wl_storage.rs @@ -236,7 +236,7 @@ where D: DB + for<'iter_> DBIter<'iter_>, H: StorageHasher, { - let storage_iter = storage.db.iter_optional_prefix(Some(prefix)).peekable(); + let storage_iter = storage.db.iter_prefix(Some(prefix)).peekable(); let write_log_iter = write_log.iter_prefix_pre(prefix).peekable(); ( PrefixIter { @@ -261,7 +261,7 @@ where D: DB + for<'iter_> DBIter<'iter_>, H: StorageHasher, { - let storage_iter = storage.db.iter_optional_prefix(Some(prefix)).peekable(); + let storage_iter = storage.db.iter_prefix(Some(prefix)).peekable(); let write_log_iter = write_log.iter_prefix_post(prefix).peekable(); ( PrefixIter { diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index 8cccc2d3a6..880d748274 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -3,6 +3,8 @@ use super::{StorageRead, StorageWrite}; use crate::ledger::storage_api; use crate::types::address::Address; +use crate::types::storage::DbKeySeg::StringSeg; +use crate::types::storage::Key; use crate::types::token; pub use crate::types::token::{ balance_key, is_balance_key, is_total_supply_key, total_supply_key, Amount, @@ -36,6 +38,45 @@ where Ok(balance) } +/// Read the denomination of a given token, if any. Note that native +/// transparent tokens do not have this set and instead use the constant +/// [`token::NATIVE_MAX_DECIMAL_PLACES`]. +pub fn read_denom( + storage: &S, + token: &Address, + sub_prefix: Option<&Key>, +) -> storage_api::Result> +where + S: StorageRead, +{ + if let Some(sub_prefix) = sub_prefix { + if sub_prefix.segments.contains(&StringSeg("ibc".to_string())) { + return Ok(Some(token::NATIVE_MAX_DECIMAL_PLACES.into())); + } + } + let key = token::denom_key(token, sub_prefix); + storage.read(&key).map(|opt_denom| { + Some( + opt_denom + .unwrap_or_else(|| token::NATIVE_MAX_DECIMAL_PLACES.into()), + ) + }) +} + +/// Write the denomination of a given token. +pub fn write_denom( + storage: &mut S, + token: &Address, + sub_prefix: Option<&Key>, + denom: token::Denomination, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = token::denom_key(token, sub_prefix); + storage.write(&key, denom) +} + /// Transfer `token` from `src` to `dest`. Returns an `Err` if `src` has /// insufficient balance or if the transfer the `dest` would overflow (This can /// only happen if the total supply does't fit in `token::Amount`). diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index e350ed4ee7..37fb5b7286 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -22,8 +22,9 @@ use crate::tendermint_proto::abci::ResponseDeliverTx; use crate::types::address::Address; use crate::types::chain::ChainId; use crate::types::key::*; -use crate::types::storage::Epoch; +use crate::types::storage::{Epoch, Key}; use crate::types::time::DateTimeUtc; +use crate::types::token::MaspDenom; #[cfg(feature = "ferveo-tpke")] use crate::types::token::Transfer; #[cfg(feature = "ferveo-tpke")] @@ -478,7 +479,7 @@ pub struct MaspBuilder { pub target: crate::types::hash::Hash, /// The decoded set of asset types used by the transaction. Useful for /// offline wallets trying to display AssetTypes. - pub asset_types: HashSet<(Address, Epoch)>, + pub asset_types: HashSet<(Address, Option, MaspDenom, Epoch)>, /// Track how Info objects map to descriptors and outputs #[serde( serialize_with = "borsh_serde::", diff --git a/core/src/types/address.rs b/core/src/types/address.rs index 439adddc2d..32a5d1c6cd 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -16,6 +16,8 @@ use thiserror::Error; use crate::ibc::signer::Signer; use crate::types::key; use crate::types::key::PublicKeyHash; +use crate::types::storage::Key; +use crate::types::token::Denomination; /// The length of an established [`Address`] encoded with Borsh. pub const ESTABLISHED_ADDRESS_BYTES_LEN: usize = 21; @@ -627,18 +629,34 @@ pub fn masp_tx_key() -> crate::types::key::common::SecretKey { common::SecretKey::try_from_slice(bytes.as_ref()).unwrap() } +/// Temporary helper for testing, a hash map of tokens addresses with their +/// informal currency codes and number of decimal places. +pub fn tokens() -> HashMap { + vec![ + (nam(), ("NAM", 6.into())), + (btc(), ("BTC", 8.into())), + (eth(), ("ETH", 18.into())), + (dot(), ("DOT", 10.into())), + (schnitzel(), ("Schnitzel", 6.into())), + (apfel(), ("Apfel", 6.into())), + (kartoffel(), ("Kartoffel", 6.into())), + ] + .into_iter() + .collect() +} + /// Temporary helper for testing, a hash map of tokens addresses with their /// MASP XAN incentive schedules. If the reward is (a, b) then a rewarded tokens /// are dispensed for every b possessed tokens. -pub fn masp_rewards() -> HashMap { +pub fn masp_rewards() -> HashMap<(Address, Option), (u64, u64)> { vec![ - (nam(), (0, 100)), - (btc(), (1, 100)), - (eth(), (2, 100)), - (dot(), (3, 100)), - (schnitzel(), (4, 100)), - (apfel(), (5, 100)), - (kartoffel(), (6, 100)), + ((nam(), None), (0, 100)), + ((btc(), None), (1, 100)), + ((eth(), None), (2, 100)), + ((dot(), None), (3, 100)), + ((schnitzel(), None), (4, 100)), + ((apfel(), None), (5, 100)), + ((kartoffel(), None), (6, 100)), ] .into_iter() .collect() diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs new file mode 100644 index 0000000000..230d63d86e --- /dev/null +++ b/core/src/types/dec.rs @@ -0,0 +1,620 @@ +//! A non-negative fixed precision decimal type for computation primarily in the +//! PoS module. For rounding, any computation that exceeds the specified +//! precision is truncated down to the closest value with the specified +//! precision. + +use std::fmt::{Debug, Display, Formatter}; +use std::ops::{Add, AddAssign, Div, Mul, Sub}; +use std::str::FromStr; + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use eyre::eyre; +use serde::{Deserialize, Serialize}; + +use super::token::NATIVE_MAX_DECIMAL_PLACES; +use crate::types::token::{Amount, Change}; +use crate::types::uint::{Uint, I256}; + +/// The number of Dec places for PoS rational calculations +pub const POS_DECIMAL_PRECISION: u8 = 12; + +#[derive(thiserror::Error, Debug)] +#[error(transparent)] +/// Generic error [`Dec`] operations can return +pub struct Error(#[from] eyre::Error); + +/// Generic result type for fallible [`Dec`] operations +pub type Result = std::result::Result; + +/// A 256 bit number with [`POS_DECIMAL_PRECISION`] number of Dec places. +/// +/// To be precise, an instance X of this type should be interpeted as the Dec +/// X * 10 ^ (-[`POS_DECIMAL_PRECISION`]) +#[derive( + Clone, + Copy, + Default, + BorshSerialize, + BorshDeserialize, + BorshSchema, + PartialEq, + Serialize, + Deserialize, + Eq, + PartialOrd, + Ord, + Hash, +)] +#[serde(try_from = "String")] +#[serde(into = "String")] +pub struct Dec(pub I256); + +impl std::ops::Deref for Dec { + type Target = I256; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for Dec { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Dec { + /// Division with truncation (TODO: better description) + pub fn trunc_div(&self, rhs: &Self) -> Option { + let is_neg = self.0.is_negative() ^ rhs.0.is_negative(); + let inner_uint = self.0.abs(); + let inner_rhs_uint = rhs.0.abs(); + match inner_uint + .fixed_precision_div(&inner_rhs_uint, POS_DECIMAL_PRECISION) + { + Some(res) => { + let res = I256::try_from(res).ok()?; + if is_neg { + Some(Self(-res)) + } else { + Some(Self(res)) + } + } + None => None, + } + } + + /// The representation of 0 + pub fn zero() -> Self { + Self(I256::zero()) + } + + /// The representation of 1 + pub fn one() -> Self { + Self(I256( + Uint::one() * Uint::exp10(POS_DECIMAL_PRECISION as usize), + )) + } + + /// The representation of 2 + pub fn two() -> Self { + Self::one() + Self::one() + } + + /// Create a new [`Dec`] using a mantissa and a scale. + pub fn new(mantissa: i128, scale: u8) -> Option { + if scale > POS_DECIMAL_PRECISION { + None + } else { + let abs = u64::try_from(mantissa.abs()).ok()?; + match Uint::exp10((POS_DECIMAL_PRECISION - scale) as usize) + .checked_mul(Uint::from(abs)) + { + Some(res) => { + if mantissa.is_negative() { + Some(Self(-I256(res))) + } else { + Some(Self(I256(res))) + } + } + None => None, + } + } + } + + /// Get the non-negative difference between two [`Dec`]s. + pub fn abs_diff(&self, other: &Self) -> Self { + if self > other { + *self - *other + } else { + *other - *self + } + } + + /// Convert the Dec type into a I256 with truncation + pub fn to_i256(&self) -> I256 { + self.0 / Uint::exp10(POS_DECIMAL_PRECISION as usize) + } + + /// Convert the Dec type into a Uint with truncation + pub fn to_uint(&self) -> Option { + if self.is_negative() { + None + } else { + Some(self.0.abs() / Uint::exp10(POS_DECIMAL_PRECISION as usize)) + } + } + + /// Do subtraction of two [`Dec`]s If and only if the value is + /// greater + pub fn checked_sub(&self, other: &Self) -> Option { + if self > other { + Some(*self - *other) + } else { + None + } + } + + /// Return if the [`Dec`] is negative + pub fn is_negative(&self) -> bool { + self.0.is_negative() + } +} + +impl FromStr for Dec { + type Err = Error; + + fn from_str(s: &str) -> Result { + let ((large, small), is_neg) = if let Some(strip) = s.strip_prefix('-') + { + (strip.split_once('.').unwrap_or((s, "0")), true) + } else { + (s.split_once('.').unwrap_or((s, "0")), false) + }; + + let num_large = Uint::from_str_radix(large, 10).map_err(|e| { + eyre!("Could not parse {} as an integer: {}", large, e) + })?; + + // In theory we could allow this, but it is aesthetically offensive. + // Thus we don't. + if small.is_empty() { + return Err(eyre!( + "Failed to parse Dec from string as there were no numbers \ + following the decimal point." + ) + .into()); + } + + let trimmed = small + .trim_end_matches('0') + .chars() + .take(POS_DECIMAL_PRECISION as usize) + .collect::(); + let decimal_part = if trimmed.is_empty() { + Uint::zero() + } else { + Uint::from_str_radix(&trimmed, 10).map_err(|e| { + eyre!("Could not parse .{} as decimals: {}", small, e) + })? * Uint::exp10(POS_DECIMAL_PRECISION as usize - trimmed.len()) + }; + let int_part = Uint::exp10(POS_DECIMAL_PRECISION as usize) + .checked_mul(num_large) + .ok_or_else(|| { + eyre!( + "The number {} is too large to fit in the Dec type.", + num_large + ) + })?; + let inner = I256::try_from(int_part + decimal_part) + .map_err(|e| eyre!("Could not convert Uint to I256: {}", e))?; + if is_neg { + Ok(Dec(-inner)) + } else { + Ok(Dec(inner)) + } + } +} + +impl TryFrom for Dec { + type Error = Error; + + fn try_from(value: String) -> Result { + Self::from_str(&value) + } +} + +impl From for Dec { + fn from(amt: Amount) -> Self { + match I256::try_from(amt.raw_amount()).ok() { + Some(raw) => Self( + raw * Uint::exp10( + (POS_DECIMAL_PRECISION - NATIVE_MAX_DECIMAL_PLACES) + as usize, + ), + ), + None => Self::zero(), + } + } +} + +impl TryFrom for Dec { + type Error = Error; + + fn try_from(value: Uint) -> std::result::Result { + let i256 = I256::try_from(value) + .map_err(|e| eyre!("Could not convert Uint to I256: {}", e))?; + Ok(Self(i256 * Uint::exp10(POS_DECIMAL_PRECISION as usize))) + } +} + +impl From for Dec { + fn from(num: u64) -> Self { + Self(I256::from(num) * Uint::exp10(POS_DECIMAL_PRECISION as usize)) + } +} + +impl From for Dec { + fn from(num: usize) -> Self { + Self::from(num as u64) + } +} + +impl From for Dec { + fn from(num: i128) -> Self { + Self(I256::from(num) * Uint::exp10(POS_DECIMAL_PRECISION as usize)) + } +} + +impl TryFrom for Dec { + type Error = Box; + + fn try_from(num: u128) -> std::result::Result { + Ok(Self( + I256::try_from(Uint::from(num))? + * Uint::exp10(POS_DECIMAL_PRECISION as usize), + )) + } +} + +impl TryFrom for i128 { + type Error = std::io::Error; + + fn try_from(value: Dec) -> std::result::Result { + value.0.try_into() + } +} + +// Is error handling needed for this? +impl From for Dec { + fn from(num: I256) -> Self { + Self(num * Uint::exp10(POS_DECIMAL_PRECISION as usize)) + } +} + +impl From for String { + fn from(value: Dec) -> String { + value.to_string() + } +} + +impl Add for Dec { + type Output = Self; + + fn add(self, rhs: Dec) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Add for Dec { + type Output = Self; + + fn add(self, rhs: u64) -> Self::Output { + Self(self.0 + I256::from(rhs)) + } +} + +impl AddAssign for Dec { + fn add_assign(&mut self, rhs: Dec) { + *self = *self + rhs; + } +} + +impl Sub for Dec { + type Output = Self; + + fn sub(self, rhs: Dec) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl Mul for Dec { + type Output = Dec; + + fn mul(self, rhs: u64) -> Self::Output { + Self(self.0 * Uint::from(rhs)) + } +} + +impl Mul for Dec { + type Output = Dec; + + fn mul(self, rhs: u128) -> Self::Output { + Self(self.0 * Uint::from(rhs)) + } +} + +impl Mul for Dec { + type Output = Amount; + + fn mul(self, rhs: Amount) -> Self::Output { + if !self.is_negative() { + (rhs * self.0.abs()) / 10u64.pow(POS_DECIMAL_PRECISION as u32) + } else { + panic!("aaa"); + } + } +} + +impl Mul for Dec { + type Output = Change; + + fn mul(self, rhs: Change) -> Self::Output { + let tot = rhs * self.0; + let denom = Uint::from(10u64.pow(POS_DECIMAL_PRECISION as u32)); + tot / denom + } +} + +// TODO: is some checked arithmetic needed here to prevent overflows? +impl Mul for Dec { + type Output = Self; + + fn mul(self, rhs: Dec) -> Self::Output { + let prod = self.0 * rhs.0; + Self(prod / Uint::exp10(POS_DECIMAL_PRECISION as usize)) + } +} + +impl Div for Dec { + type Output = Self; + + /// Unchecked fixed precision division. + /// + /// # Panics: + /// + /// * Denominator is zero + /// * Scaling the left hand side by 10^([`POS_DECIMAL_PRECISION`]) + /// overflows 256 bits + fn div(self, rhs: Dec) -> Self::Output { + self.trunc_div(&rhs).unwrap() + } +} + +impl Div for Dec { + type Output = Self; + + fn div(self, rhs: u64) -> Self::Output { + Self(self.0 / Uint::from(rhs)) + } +} + +impl Display for Dec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let is_neg = self.is_negative(); + let mut string = self.0.abs().to_string(); + if string.len() > POS_DECIMAL_PRECISION as usize { + let idx = string.len() - POS_DECIMAL_PRECISION as usize; + string.insert(idx, '.'); + } else { + let mut str_pre = "0.".to_string(); + for _ in 0..(POS_DECIMAL_PRECISION as usize - string.len()) { + str_pre.push('0'); + } + str_pre.push_str(string.as_str()); + string = str_pre; + }; + let stripped_string = string.trim_end_matches(['.', '0']); + if stripped_string.is_empty() { + f.write_str("0") + } else if is_neg { + let stripped_string = format!("-{}", stripped_string); + f.write_str(stripped_string.as_str()) + } else { + f.write_str(stripped_string) + } + } +} + +impl Debug for Dec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.to_string()) + } +} + +#[cfg(test)] +mod test_dec { + use super::*; + use crate::types::token::{Amount, Change}; + + #[derive(Debug, Serialize, Deserialize)] + struct SerializerTest { + dec: Dec, + } + + #[test] + fn dump_toml() { + let serializer = SerializerTest { + dec: Dec::new(3, 0).unwrap(), + }; + println!("{:?}", toml::to_string(&serializer)); + } + + /// Fill in tests later + #[test] + fn test_dec_basics() { + assert_eq!( + Dec::one() + Dec::new(3, 0).unwrap() / Dec::new(5, 0).unwrap(), + Dec::new(16, 1).unwrap() + ); + assert_eq!(Dec::new(1, 0).expect("Test failed"), Dec::one()); + assert_eq!(Dec::new(2, 0).expect("Test failed"), Dec::two()); + assert_eq!( + Dec(I256(Uint::from(1653))), + Dec::new(1653, POS_DECIMAL_PRECISION).expect("Test failed") + ); + assert_eq!( + Dec(I256::from(-48756)), + Dec::new(-48756, POS_DECIMAL_PRECISION).expect("Test failed") + ); + assert_eq!( + Dec::new(123456789, 4) + .expect("Test failed") + .to_uint() + .unwrap(), + Uint::from(12345) + ); + assert_eq!( + Dec::new(-123456789, 4).expect("Test failed").to_i256(), + I256::from(-12345) + ); + assert_eq!( + Dec::new(123, 4).expect("Test failed").to_uint().unwrap(), + Uint::zero() + ); + assert_eq!( + Dec::new(123, 4).expect("Test failed").to_i256(), + I256::zero() + ); + assert_eq!( + Dec::from_str("4876.3855") + .expect("Test failed") + .to_uint() + .unwrap(), + Uint::from(4876) + ); + assert_eq!( + Dec::from_str("4876.3855").expect("Test failed").to_i256(), + I256::from(4876) + ); + + // Fixed precision division is more thoroughly tested for the `Uint` + // type. These are sanity checks that the precision is correct. + assert_eq!( + Dec::new(1, POS_DECIMAL_PRECISION).expect("Test failed") + / Dec::new(1, POS_DECIMAL_PRECISION).expect("Test failed"), + Dec::one(), + ); + assert_eq!( + Dec::new(1, POS_DECIMAL_PRECISION).expect("Test failed") + / (Dec::new(1, 0).expect("Test failed") + Dec::one()), + Dec::zero(), + ); + assert_eq!( + Dec::new(1, POS_DECIMAL_PRECISION).expect("Test failed") + / Dec::two(), + Dec::zero(), + ); + + // Test Dec * Dec multiplication + assert!(Dec::new(32353, POS_DECIMAL_PRECISION + 1u8).is_none()); + let dec1 = Dec::new(12345654321, 12).expect("Test failed"); + let dec2 = Dec::new(9876789, 12).expect("Test failed"); + let exp_prod = Dec::new(121935, 12).expect("Test failed"); + let exp_quot = Dec::new(1249966393025101, 12).expect("Test failed"); + assert_eq!(dec1 * dec2, exp_prod); + assert_eq!(dec1 / dec2, exp_quot); + } + + /// Test the `Dec` and `Amount` interplay + #[test] + fn test_dec_and_amount() { + let amt = Amount::from(1018u64); + let dec = Dec::from_str("2.76").unwrap(); + + debug_assert_eq!( + Dec::from(amt), + Dec::new(1018, 6).expect("Test failed") + ); + debug_assert_eq!(dec * amt, Amount::from(2809u64)); + + let chg = -amt.change(); + debug_assert_eq!(dec * chg, Change::from(-2809i64)); + } + + #[test] + fn test_into() { + assert_eq!( + Dec::from(u64::MAX), + Dec::from_str("18446744073709551615.000000000000") + .expect("only 104 bits") + ) + } + + /// Test that parsing from string is correct. + #[test] + fn test_dec_from_string() { + // Fewer than six decimal places and non-zero integer part + assert_eq!( + Dec::from_str("3.14").expect("Test failed"), + Dec::new(314, 2).expect("Test failed"), + ); + + // more than 12 decimal places and zero integer part + assert_eq!( + Dec::from_str("0.1234567654321").expect("Test failed"), + Dec::new(123456765432, 12).expect("Test failed"), + ); + + // No zero before the decimal + assert_eq!( + Dec::from_str(".333333").expect("Test failed"), + Dec::new(333333, 6).expect("Test failed"), + ); + + // No decimal places + assert_eq!( + Dec::from_str("50").expect("Test failed"), + Dec::new(50, 0).expect("Test failed"), + ); + + // Test zero representations + assert_eq!(Dec::from_str("0").expect("Test failed"), Dec::zero()); + assert_eq!(Dec::from_str("0.0").expect("Test failed"), Dec::zero()); + assert_eq!(Dec::from_str(".0").expect("Test failed"), Dec::zero()); + + // Error conditions + + // Test that a decimal point must be followed by numbers + assert!(Dec::from_str("0.").is_err()); + // Test that multiple decimal points get caught + assert!(Dec::from_str("1.2.3").is_err()); + // Test that negative numbers are rejected + assert!(Dec::from_str("-1").is_err()); + // Test that non-numerics are caught + assert!(Dec::from_str("DEADBEEF.12").is_err()); + assert!(Dec::from_str("23.DEADBEEF").is_err()); + // Test that we catch strings overflowing 256 bits + let mut yuge = String::from("1"); + for _ in 0..80 { + yuge.push('0'); + } + assert!(Dec::from_str(&yuge).is_err()); + } + + /// Test that parsing from string is correct. + #[test] + fn test_dec_from_serde() { + assert_eq!( + serde_json::from_str::(r#""0.667""#).expect("all good"), + Dec::from_str("0.667").expect("should work") + ); + + let dec = Dec::from_str("0.667").unwrap(); + assert_eq!( + dec, + serde_json::from_str::(&serde_json::to_string(&dec).unwrap()) + .unwrap() + ); + } +} diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs index dc17d07e22..d3450dccd1 100644 --- a/core/src/types/governance.rs +++ b/core/src/types/governance.rs @@ -4,7 +4,6 @@ use std::collections::{BTreeMap, HashSet}; use std::fmt::{self, Display}; use borsh::{BorshDeserialize, BorshSerialize}; -use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -13,10 +12,13 @@ use crate::types::hash::Hash; use crate::types::key::common::{self, Signature}; use crate::types::key::SigScheme; use crate::types::storage::Epoch; -use crate::types::token::{Amount, SCALE}; +use crate::types::token::{ + Amount, DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES, +}; +use crate::types::uint::Uint; /// Type alias for vote power -pub type VotePower = u128; +pub type VotePower = Uint; /// A PGF cocuncil composed of the address and spending cap pub type Council = (Address, Amount); @@ -85,7 +87,8 @@ impl Display for ProposalVote { writeln!( f, "Council: {}, spending cap: {}", - address, spending_cap + address, + spending_cap.to_string_native() )? } @@ -109,6 +112,7 @@ pub enum ProposalVoteParseError { } /// The type of the tally +#[derive(Clone, Debug)] pub enum Tally { /// Default proposal Default, @@ -119,6 +123,7 @@ pub enum Tally { } /// The result of a proposal +#[derive(Clone, Debug)] pub enum TallyResult { /// Proposal was accepted with the associated value Passed(Tally), @@ -140,19 +145,30 @@ pub struct ProposalResult { impl Display for ProposalResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let percentage = Decimal::checked_div( - self.total_yay_power.into(), - self.total_voting_power.into(), - ) - .unwrap_or_default(); + let percentage = DenominatedAmount { + amount: Amount::from_uint( + self.total_yay_power + .fixed_precision_div(&self.total_voting_power, 4) + .unwrap_or_default(), + 0, + ) + .unwrap(), + denom: 2.into(), + }; write!( f, - "{} with {} yay votes over {} ({:.2}%)", + "{} with {} yay votes over {} ({}%)", self.result, - self.total_yay_power / SCALE as u128, - self.total_voting_power / SCALE as u128, - percentage.checked_mul(100.into()).unwrap_or_default() + DenominatedAmount { + amount: Amount::from_uint(self.total_yay_power, 0).unwrap(), + denom: NATIVE_MAX_DECIMAL_PLACES.into() + }, + DenominatedAmount { + amount: Amount::from_uint(self.total_voting_power, 0).unwrap(), + denom: NATIVE_MAX_DECIMAL_PLACES.into() + }, + percentage ) } } @@ -165,7 +181,8 @@ impl Display for TallyResult { Tally::PGFCouncil((council, cap)) => write!( f, "passed with PGF council address: {}, spending cap: {}", - council, cap + council, + cap.to_string_native() ), }, TallyResult::Rejected => write!(f, "rejected"), diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs index 0550060498..8bbd73eaf2 100644 --- a/core/src/types/mod.rs +++ b/core/src/types/mod.rs @@ -2,6 +2,7 @@ pub mod address; pub mod chain; +pub mod dec; pub mod governance; pub mod hash; pub mod ibc; @@ -12,4 +13,5 @@ pub mod storage; pub mod time; pub mod token; pub mod transaction; +pub mod uint; pub mod validity_predicate; diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 68ba40552d..c30319dedd 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1,19 +1,25 @@ //! A basic fungible token -use std::fmt::Display; +use std::fmt::{Display, Formatter}; use std::iter::Sum; -use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; +use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use rust_decimal::prelude::{Decimal, ToPrimitive}; +use data_encoding::BASE32HEX_NOPAD; use serde::{Deserialize, Serialize}; use thiserror::Error; +use super::dec::POS_DECIMAL_PRECISION; use crate::ibc::applications::transfer::Amount as IbcAmount; +use crate::ledger::storage_api::token::read_denom; +use crate::ledger::storage_api::StorageRead; use crate::types::address::{masp, Address, DecodeError as AddressError}; +use crate::types::dec::Dec; use crate::types::hash::Hash; +use crate::types::storage; use crate::types::storage::{DbKeySeg, Key, KeySeg}; +use crate::types::uint::{self, Uint, I256}; /// Amount in micro units. For different granularity another representation /// might be more appropriate. @@ -32,95 +38,341 @@ use crate::types::storage::{DbKeySeg, Key, KeySeg}; Hash, )] pub struct Amount { - micro: u64, + raw: Uint, } -/// Maximum decimal places in a token [`Amount`] and [`Change`]. -pub const MAX_DECIMAL_PLACES: u32 = 6; -/// Decimal scale of token [`Amount`] and [`Change`]. -pub const SCALE: u64 = 1_000_000; +/// Maximum decimal places in a native token [`Amount`] and [`Change`]. +/// For non-native (e.g. ERC20 tokens) one must read the `denom_key` storage +/// key. +pub const NATIVE_MAX_DECIMAL_PLACES: u8 = 6; -/// The largest value that can be represented by this integer type -pub const MAX_AMOUNT: Amount = Amount { micro: u64::MAX }; +/// Decimal scale of a native token [`Amount`] and [`Change`]. +/// For non-native (e.g. ERC20 tokens) one must read the `denom_key` storage +/// key. +pub const NATIVE_SCALE: u64 = 1_000_000; /// A change in tokens amount -pub type Change = i128; +pub type Change = I256; impl Amount { - /// Returns whether an amount is zero. - pub fn is_zero(&self) -> bool { - self.micro == 0 - } - /// Get the amount as a [`Change`] pub fn change(&self) -> Change { - self.micro as Change + self.raw.try_into().unwrap() } /// Spend a given amount. - /// Panics when given `amount` > `self.micro` amount. + /// Panics when given `amount` > `self.raw` amount. pub fn spend(&mut self, amount: &Amount) { - self.micro = self.micro.checked_sub(amount.micro).unwrap(); + self.raw = self.raw.checked_sub(amount.raw).unwrap(); } /// Receive a given amount. - /// Panics on overflow. + /// Panics on overflow and when [`uint::MAX_SIGNED_VALUE`] is exceeded. pub fn receive(&mut self, amount: &Amount) { - self.micro = self.micro.checked_add(amount.micro).unwrap(); + self.raw = self.raw.checked_add(amount.raw).unwrap(); } - /// Create a new amount from whole number of tokens - pub const fn whole(amount: u64) -> Self { + /// Create a new amount of native token from whole number of tokens + pub fn native_whole(amount: u64) -> Self { Self { - micro: amount * SCALE, + raw: Uint::from(amount) * NATIVE_SCALE, } } + /// Get the raw [`Uint`] value, which represents namnam + pub fn raw_amount(&self) -> Uint { + self.raw + } + /// Create a new amount with the maximum value pub fn max() -> Self { - Self { micro: u64::MAX } + Self { + raw: uint::MAX_VALUE, + } + } + + /// Create a new amount with the maximum signed value + pub fn max_signed() -> Self { + Self { + raw: uint::MAX_SIGNED_VALUE, + } + } + + /// Zero [`Amount`]. + pub fn zero() -> Self { + Self::default() + } + + /// Check if [`Amount`] is zero. + pub fn is_zero(&self) -> bool { + self.raw == Uint::from(0) } - /// Checked addition. Returns `None` on overflow. + /// Checked addition. Returns `None` on overflow or if + /// the amount exceed [`uint::MAX_VALUE`] pub fn checked_add(&self, amount: Amount) -> Option { - self.micro - .checked_add(amount.micro) - .map(|result| Self { micro: result }) + self.raw.checked_add(amount.raw).and_then(|result| { + if result <= uint::MAX_VALUE { + Some(Self { raw: result }) + } else { + None + } + }) + } + + /// Checked addition. Returns `None` on overflow or if + /// the amount exceed [`uint::MAX_SIGNED_VALUE`] + pub fn checked_signed_add(&self, amount: Amount) -> Option { + self.raw.checked_add(amount.raw).and_then(|result| { + if result <= uint::MAX_SIGNED_VALUE { + Some(Self { raw: result }) + } else { + None + } + }) } /// Checked subtraction. Returns `None` on underflow pub fn checked_sub(&self, amount: Amount) -> Option { - self.micro - .checked_sub(amount.micro) - .map(|result| Self { micro: result }) + self.raw + .checked_sub(amount.raw) + .map(|result| Self { raw: result }) } - /// Create amount from Change - /// - /// # Panics - /// - /// Panics if the change is negative or overflows `u64`. + /// Create amount from the absolute value of `Change`. pub fn from_change(change: Change) -> Self { + Self { raw: change.abs() } + } + + /// Given a string and a denomination, parse an amount from string. + pub fn from_str( + string: impl AsRef, + denom: impl Into, + ) -> Result { + DenominatedAmount::from_str(string.as_ref())? + .increase_precision(denom.into().into()) + .map(Into::into) + } + + /// Attempt to convert an unsigned integer to an `Amount` with the + /// specified precision. + pub fn from_uint( + uint: impl Into, + denom: impl Into, + ) -> Result { + let denom = denom.into(); + match Uint::from(10) + .checked_pow(Uint::from(denom)) + .and_then(|scaling| scaling.checked_mul(uint.into())) + { + Some(amount) => Ok(Self { raw: amount }), + None => Err(AmountParseError::ConvertToDecimal), + } + } + + /// Given a u64 and [`MaspDenom`], construct the corresponding + /// amount. + pub fn from_masp_denominated(val: u64, denom: MaspDenom) -> Self { + let mut raw = [0u64; 4]; + raw[denom as usize] = val; + Self { raw: Uint(raw) } + } + + /// Get a string representation of a native token amount. + pub fn to_string_native(&self) -> String { + DenominatedAmount { + amount: *self, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + } + .to_string_precise() + } + + /// Add denomination info if it exists in storage. + pub fn denominated( + &self, + token: &Address, + sub_prefix: Option<&Key>, + storage: &impl StorageRead, + ) -> Option { + let denom = read_denom(storage, token, sub_prefix) + .expect("Should be able to read storage"); + denom.map(|denom| DenominatedAmount { + amount: *self, + denom, + }) + } + + /// Convert to an [`Amount`] under the assumption that the input + /// string encodes all necessary decimal places. + pub fn from_string_precise(string: &str) -> Result { + DenominatedAmount::from_str(string).map(|den| den.amount) + } +} + +/// Given a number represented as `M*B^D`, then +/// `M` is the matissa, `B` is the base and `D` +/// is the denomination, represented by this stuct. +#[derive( + Debug, + Copy, + Clone, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +#[serde(transparent)] +pub struct Denomination(pub u8); + +impl From for Denomination { + fn from(denom: u8) -> Self { + Self(denom) + } +} + +impl From for u8 { + fn from(denom: Denomination) -> Self { + denom.0 + } +} + +/// An amount with its denomination. +#[derive( + Debug, + Copy, + Clone, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, + BorshSchema, +)] +pub struct DenominatedAmount { + /// The mantissa + pub amount: Amount, + /// The number of decimal places in base ten. + pub denom: Denomination, +} + +impl DenominatedAmount { + /// A precise string representation. The number of + /// decimal places in this string gives the denomination. + /// This not true of the string produced by the `Display` + /// trait. + pub fn to_string_precise(&self) -> String { + let decimals = self.denom.0 as usize; + let mut string = self.amount.raw.to_string(); + if string.len() > decimals { + string.insert(string.len() - decimals, '.'); + } else { + for _ in string.len()..decimals { + string.insert(0, '0'); + } + string.insert(0, '.'); + string.insert(0, '0'); + } + string + } + + /// Find the minimal precision that holds this value losslessly. + /// This equates to stripping trailing zeros after the decimal + /// place. + pub fn canonical(self) -> Self { + let mut value = self.amount.raw; + let ten = Uint::from(10); + let mut denom = self.denom.0; + for _ in 0..self.denom.0 { + let (div, rem) = value.div_mod(ten); + if rem == Uint::zero() { + value = div; + denom -= 1; + } + } Self { - micro: change as u64, + amount: Amount { raw: value }, + denom: denom.into(), + } + } + + /// Attempt to increase the precision of an amount. Can fail + /// if the resulting amount does not fit into 256 bits. + pub fn increase_precision( + self, + denom: Denomination, + ) -> Result { + if denom.0 < self.denom.0 { + return Err(AmountParseError::PrecisionDecrease); } + Uint::from(10) + .checked_pow(Uint::from(denom.0 - self.denom.0)) + .and_then(|scaling| self.amount.raw.checked_mul(scaling)) + .map(|amount| Self { + amount: Amount { raw: amount }, + denom, + }) + .ok_or(AmountParseError::PrecisionOverflow) } +} - /// Convert the amount to [`Decimal`] ignoring its scale (i.e. as an integer - /// in micro units). - pub fn as_dec_unscaled(&self) -> Decimal { - Into::::into(self.micro) +impl Display for DenominatedAmount { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let string = self.to_string_precise(); + let string = string.trim_end_matches(&['0']); + let string = string.trim_end_matches(&['.']); + f.write_str(string) } +} - /// Convert from a [`Decimal`] that's not scaled (i.e. an integer - /// in micro units). - /// - /// # Panics - /// - /// Panics if the given decimal is not an integer that fits `u64`. - pub fn from_dec_unscaled(micro: Decimal) -> Self { - let res = micro.to_u64().unwrap(); - Self { micro: res } +impl FromStr for DenominatedAmount { + type Err = AmountParseError; + + fn from_str(s: &str) -> Result { + let precision = s.find('.').map(|pos| s.len() - pos - 1); + let digits = s + .chars() + .filter_map(|c| { + if c.is_numeric() { + c.to_digit(10).map(Uint::from) + } else { + None + } + }) + .rev() + .collect::>(); + if digits.len() != s.len() && precision.is_none() + || digits.len() != s.len() - 1 && precision.is_some() + { + return Err(AmountParseError::NotNumeric); + } + if digits.len() > 77 { + return Err(AmountParseError::ScaleTooLarge( + digits.len() as u32, + 77, + )); + } + let mut value = Uint::default(); + let ten = Uint::from(10); + for (pow, digit) in digits.into_iter().enumerate() { + value = ten + .checked_pow(Uint::from(pow)) + .and_then(|scaling| scaling.checked_mul(digit)) + .and_then(|scaled| value.checked_add(scaled)) + .ok_or(AmountParseError::InvalidRange)?; + } + let denom = Denomination(precision.unwrap_or_default() as u8); + Ok(Self { + amount: Amount { raw: value }, + denom, + }) } } @@ -132,12 +384,37 @@ impl serde::Serialize for Amount { where S: serde::Serializer, { - let amount_string = self.to_string(); + let amount_string = self.raw.to_string(); serde::Serialize::serialize(&amount_string, serializer) } } impl<'de> serde::Deserialize<'de> for Amount { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let amount_string: String = + serde::Deserialize::deserialize(deserializer)?; + let amt = DenominatedAmount::from_str(&amount_string).unwrap(); + Ok(amt.amount) + } +} + +impl serde::Serialize for DenominatedAmount { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + let amount_string = self.to_string_precise(); + serde::Serialize::serialize(&amount_string, serializer) + } +} + +impl<'de> serde::Deserialize<'de> for DenominatedAmount { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, @@ -149,34 +426,56 @@ impl<'de> serde::Deserialize<'de> for Amount { } } -impl From for Decimal { - fn from(amount: Amount) -> Self { - Into::::into(amount.micro) +impl<'a> From<&'a DenominatedAmount> for &'a Amount { + fn from(denom: &'a DenominatedAmount) -> Self { + &denom.amount } } -impl From for Amount { - fn from(micro: Decimal) -> Self { - let res = micro.to_u64().unwrap(); - Self { micro: res } +impl From for Amount { + fn from(denom: DenominatedAmount) -> Self { + denom.amount } } +// Treats the u64 as a value of the raw amount (namnam) impl From for Amount { - fn from(micro: u64) -> Self { - Self { micro } + fn from(val: u64) -> Amount { + Amount { + raw: Uint::from(val), + } } } -impl From for u64 { - fn from(amount: Amount) -> Self { - amount.micro +impl From for Amount { + fn from(dec: Dec) -> Amount { + if !dec.is_negative() { + Amount { + raw: dec.0.abs() / Uint::exp10(POS_DECIMAL_PRECISION as usize), + } + } else { + panic!( + "The Dec value is negative and cannot be multiplied by an \ + Amount" + ) + } } } -impl From for u128 { - fn from(amount: Amount) -> Self { - u128::from(amount.micro) +impl TryFrom for u128 { + type Error = std::io::Error; + + fn try_from(value: Amount) -> Result { + let Uint(arr) = value.raw; + for word in arr.iter().skip(2) { + if *word != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Integer overflow when casting to u128", + )); + } + } + Ok(value.raw.low_u128()) } } @@ -184,44 +483,84 @@ impl Add for Amount { type Output = Amount; fn add(mut self, rhs: Self) -> Self::Output { - self.micro += rhs.micro; + self.raw += rhs.raw; self } } +impl Add for Amount { + type Output = Self; + + fn add(self, rhs: u64) -> Self::Output { + Self { + raw: self.raw + Uint::from(rhs), + } + } +} + impl Mul for Amount { type Output = Amount; fn mul(mut self, rhs: u64) -> Self::Output { - self.micro *= rhs; + self.raw *= rhs; + self + } +} + +impl Mul for u64 { + type Output = Amount; + + fn mul(self, mut rhs: Amount) -> Self::Output { + rhs.raw *= self; + rhs + } +} + +impl Mul for Amount { + type Output = Amount; + + fn mul(mut self, rhs: Uint) -> Self::Output { + self.raw *= rhs; + self + } +} + +impl Mul for Amount { + type Output = Amount; + + fn mul(mut self, rhs: Amount) -> Self::Output { + self.raw *= rhs.raw; self } } /// A combination of Euclidean division and fractions: -/// x*(a,b) = (a*(x//b), x%b) +/// x*(a,b) = (a*(x//b), x%b). impl Mul<(u64, u64)> for Amount { type Output = (Amount, Amount); fn mul(mut self, rhs: (u64, u64)) -> Self::Output { - let ant = Amount::from((self.micro / rhs.1) * rhs.0); - self.micro %= rhs.1; - (ant, self) + let amt = Amount { + raw: (self.raw / rhs.1) * rhs.0, + }; + self.raw %= rhs.1; + (amt, self) } } -impl Mul for u64 { - type Output = Amount; +impl Div for Amount { + type Output = Self; - fn mul(mut self, rhs: Amount) -> Self::Output { - self *= rhs.micro; - Self::Output::from(self) + fn div(self, rhs: u64) -> Self::Output { + Self { + raw: self.raw / Uint::from(rhs), + } } } impl AddAssign for Amount { fn add_assign(&mut self, rhs: Self) { - self.micro += rhs.micro + self.raw += rhs.raw } } @@ -229,14 +568,14 @@ impl Sub for Amount { type Output = Amount; fn sub(mut self, rhs: Self) -> Self::Output { - self.micro -= rhs.micro; + self.raw -= rhs.raw; self } } impl SubAssign for Amount { fn sub_assign(&mut self, rhs: Self) { - self.micro -= rhs.micro + self.raw -= rhs.raw } } @@ -251,69 +590,130 @@ impl KeySeg for Amount { where Self: Sized, { - let micro = u64::parse(string)?; - Ok(Self { micro }) + let bytes = BASE32HEX_NOPAD.decode(string.as_ref()).map_err(|err| { + storage::Error::ParseKeySeg(format!( + "Failed parsing {} with {}", + string, err + )) + })?; + Ok(Amount { + raw: Uint::from_big_endian(&bytes), + }) } fn raw(&self) -> String { - self.micro.raw() + let mut buf = [0u8; 32]; + self.raw.to_big_endian(&mut buf); + BASE32HEX_NOPAD.encode(&buf) } fn to_db_key(&self) -> DbKeySeg { - self.micro.to_db_key() + DbKeySeg::StringSeg(self.raw()) } } #[allow(missing_docs)] #[derive(Error, Debug)] pub enum AmountParseError { - #[error("Error decoding token amount: {0}")] - InvalidDecimal(rust_decimal::Error), #[error( "Error decoding token amount, too many decimal places: {0}. Maximum \ - {MAX_DECIMAL_PLACES}" + {1}" + )] + ScaleTooLarge(u32, u8), + #[error( + "Error decoding token amount, the value is not within invalid range." )] - ScaleTooLarge(u32), - #[error("Error decoding token amount, the value is within invalid range.")] InvalidRange, + #[error("Error converting amount to decimal, number too large.")] + ConvertToDecimal, + #[error( + "Could not convert from string, expected an unsigned 256-bit integer." + )] + FromString, + #[error("Could not parse string as a correctly formatted number.")] + NotNumeric, + #[error("This amount cannot handle the requested precision in 256 bits.")] + PrecisionOverflow, + #[error("More precision given in the amount than requested.")] + PrecisionDecrease, } -impl FromStr for Amount { - type Err = AmountParseError; - - fn from_str(s: &str) -> Result { - match rust_decimal::Decimal::from_str(s) { - Ok(decimal) => { - let scale = decimal.scale(); - if scale > MAX_DECIMAL_PLACES { - return Err(AmountParseError::ScaleTooLarge(scale)); - } - let whole = - decimal * rust_decimal::Decimal::new(SCALE as i64, 0); - let micro: u64 = - rust_decimal::prelude::ToPrimitive::to_u64(&whole) - .ok_or(AmountParseError::InvalidRange)?; - Ok(Self { micro }) - } - Err(err) => Err(AmountParseError::InvalidDecimal(err)), - } +impl From for Change { + fn from(amount: Amount) -> Self { + amount.raw.try_into().unwrap() } } -impl Display for Amount { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let decimal = rust_decimal::Decimal::from_i128_with_scale( - self.micro as i128, - MAX_DECIMAL_PLACES, - ) - .normalize(); - write!(f, "{}", decimal) +impl From for Amount { + fn from(change: Change) -> Self { + Amount { raw: change.abs() } } } -impl From for Change { +impl From for Uint { fn from(amount: Amount) -> Self { - amount.micro as i128 + amount.raw + } +} + +/// The four possible u64 words in a [`Uint`]. +/// Used for converting to MASP amounts. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, +)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum MaspDenom { + Zero = 0, + One, + Two, + Three, +} + +impl From for MaspDenom { + fn from(denom: u8) -> Self { + match denom { + 0 => Self::Zero, + 1 => Self::One, + 2 => Self::Two, + 3 => Self::Three, + _ => panic!("Possible MASP denominations must be between 0 and 3"), + } + } +} + +impl MaspDenom { + /// Iterator over the possible denominations + pub fn iter() -> impl Iterator { + // 0, 1, 2, 3 + (0u8..4).map(Self::from) + } + + /// Get the corresponding u64 word from the input uint256. + pub fn denominate<'a>(&self, amount: impl Into<&'a Amount>) -> u64 { + let amount = amount.into(); + amount.raw.0[*self as usize] + } + + /// Get the corresponding u64 word from the input uint256. + pub fn denominate_i128(&self, amount: &Change) -> i128 { + let val = amount.abs().0[*self as usize] as i128; + if Change::is_negative(amount) { + -val + } else { + val + } } } @@ -322,16 +722,20 @@ impl TryFrom for Amount { fn try_from(amount: IbcAmount) -> Result { // TODO: https://github.com/anoma/namada/issues/1089 - if amount > u64::MAX.into() { - return Err(AmountParseError::InvalidRange); - } - Self::from_str(&amount.to_string()) + // TODO: OVERFLOW CHECK PLEASE (PATCH IBC TO ALLOW GETTING + // IBCAMOUNT::MAX OR SIMILAR) if amount > u64::MAX.into() { + // return Err(AmountParseError::InvalidRange); + //} + DenominatedAmount::from_str(&amount.to_string()) + .map(|a| a.amount * NATIVE_SCALE) } } /// Key segment for a balance key pub const BALANCE_STORAGE_KEY: &str = "balance"; -/// Key segment for head shielded transaction pointer key +/// Key segment for a denomination key +pub const DENOM_STORAGE_KEY: &str = "denomination"; +/// Key segment for head shielded transaction pointer keys pub const HEAD_TX_KEY: &str = "head-tx"; /// Key segment prefix for shielded transaction key pub const TX_KEY_PREFIX: &str = "tx-"; @@ -341,6 +745,54 @@ pub const CONVERSION_KEY_PREFIX: &str = "conv"; pub const PIN_KEY_PREFIX: &str = "pin-"; const TOTAL_SUPPLY_STORAGE_KEY: &str = "total_supply"; +/// A fully qualified (multi-) token address. +#[derive( + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + Hash, + BorshSerialize, + BorshDeserialize, +)] +pub struct TokenAddress { + /// The address of the (multi-) token + pub address: Address, + /// If it is a mutli-token, this indicates the sub-token. + pub sub_prefix: Option, +} + +impl TokenAddress { + /// A function for displaying a [`TokenAddress`]. Takes a + /// human readable name of the token as input. + pub fn format_with_alias(&self, alias: &str) -> String { + format!( + "{}{}", + alias, + self.sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default() + ) + } +} + +impl Display for TokenAddress { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let formatted = format!( + "{}{}", + self.address, + self.sub_prefix + .as_ref() + .map(|k| format!("/{}", k)) + .unwrap_or_default() + ); + f.write_str(&formatted) + } +} + /// Obtain a storage key for user's balance. pub fn balance_key(token_addr: &Address, owner: &Address) -> Key { Key::from(token_addr.to_db_key()) @@ -391,18 +843,41 @@ pub fn is_balance_key<'a>( } /// Check if the given storage key is balance key for unspecified token. If it -/// is, returns the owner. -pub fn is_any_token_balance_key(key: &Key) -> Option<&Address> { +/// is, returns the token and owner address. +pub fn is_any_token_balance_key(key: &Key) -> Option<[&Address; 2]> { match &key.segments[..] { [ - DbKeySeg::AddressSeg(_), + DbKeySeg::AddressSeg(token), DbKeySeg::StringSeg(key), DbKeySeg::AddressSeg(owner), - ] if key == BALANCE_STORAGE_KEY => Some(owner), + ] if key == BALANCE_STORAGE_KEY => Some([token, owner]), _ => None, } } +/// Obtain a storage key denomination of a token. +pub fn denom_key(token_addr: &Address, sub_prefix: Option<&Key>) -> Key { + match sub_prefix { + Some(sub) => Key::from(token_addr.to_db_key()) + .join(sub) + .push(&DENOM_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key"), + None => Key::from(token_addr.to_db_key()) + .push(&DENOM_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key"), + } +} + +/// Check if the given storage key is a denomination key for the given token. +pub fn is_denom_key(token_addr: &Address, key: &Key) -> bool { + matches!(&key.segments[..], + [ + DbKeySeg::AddressSeg(addr), + .., + DbKeySeg::StringSeg(key), + ] if key == DENOM_STORAGE_KEY && addr == token_addr) +} + /// Check if the given storage key is a masp key pub fn is_masp_key(key: &Key) -> bool { matches!(&key.segments[..], @@ -440,14 +915,27 @@ pub fn is_multitoken_balance_key<'a>( } /// Check if the given storage key is multitoken balance key for unspecified -/// token. If it is, returns the sub prefix and the owner. -pub fn is_any_multitoken_balance_key(key: &Key) -> Option<(Key, &Address)> { +/// token. If it is, returns the sub prefix and the token and owner addresses. +pub fn is_any_multitoken_balance_key( + key: &Key, +) -> Option<(Key, [&Address; 2])> { match key.segments.first() { - Some(DbKeySeg::AddressSeg(_)) => multitoken_balance_owner(key), + Some(DbKeySeg::AddressSeg(token)) => multitoken_balance_owner(key) + .map(|(sub, owner)| (sub, [token, owner])), _ => None, } } +/// Check if the given storage key is token or multitoken balance key for +/// unspecified token. If it is, returns the token and owner addresses. +pub fn is_any_token_or_multitoken_balance_key( + key: &Key, +) -> Option<[&Address; 2]> { + is_any_multitoken_balance_key(key) + .map(|a| a.1) + .or_else(|| is_any_token_balance_key(key)) +} + fn multitoken_balance_owner(key: &Key) -> Option<(Key, &Address)> { let len = key.segments.len(); if len < 4 { @@ -494,7 +982,7 @@ pub struct Transfer { /// Source token's sub prefix pub sub_prefix: Option, /// The amount of tokens - pub amount: Amount, + pub amount: DenominatedAmount, /// The unused storage location at which to place TxId pub key: Option, /// Shielded transaction part @@ -514,43 +1002,65 @@ pub enum TransferError { #[cfg(test)] mod tests { - use proptest::prelude::*; - use super::*; - proptest! { - /// The upper limit is set to `2^51`, because then the float is - /// starting to lose precision. - #[test] - fn test_token_amount_decimal_conversion(raw_amount in 0..2_u64.pow(51)) { - let amount = Amount::from(raw_amount); - // A round-trip conversion to and from Decimal should be an identity - let decimal = Decimal::from(amount); - let identity = Amount::from(decimal); - assert_eq!(amount, identity); - } - } - #[test] fn test_token_display() { - let max = Amount::from(u64::MAX); + let max = Amount::from_uint(u64::MAX, 0).expect("Test failed"); + assert_eq!("18446744073709.551615", max.to_string_native()); + let max = DenominatedAmount { + amount: max, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; assert_eq!("18446744073709.551615", max.to_string()); - let whole = Amount::from(u64::MAX / SCALE * SCALE); + let whole = + Amount::from_uint(u64::MAX / NATIVE_SCALE * NATIVE_SCALE, 0) + .expect("Test failed"); + assert_eq!("18446744073709.000000", whole.to_string_native()); + let whole = DenominatedAmount { + amount: whole, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; assert_eq!("18446744073709", whole.to_string()); - let trailing_zeroes = Amount::from(123000); + let trailing_zeroes = + Amount::from_uint(123000, 0).expect("Test failed"); + assert_eq!("0.123000", trailing_zeroes.to_string_native()); + let trailing_zeroes = DenominatedAmount { + amount: trailing_zeroes, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; assert_eq!("0.123", trailing_zeroes.to_string()); - let zero = Amount::from(0); + let zero = Amount::default(); + assert_eq!("0.000000", zero.to_string_native()); + let zero = DenominatedAmount { + amount: zero, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; assert_eq!("0", zero.to_string()); + + let amount = DenominatedAmount { + amount: Amount::from_uint(1120, 0).expect("Test failed"), + denom: 3u8.into(), + }; + assert_eq!("1.12", amount.to_string()); + assert_eq!("1.120", amount.to_string_precise()); + + let amount = DenominatedAmount { + amount: Amount::from_uint(1120, 0).expect("Test failed"), + denom: 5u8.into(), + }; + assert_eq!("0.0112", amount.to_string()); + assert_eq!("0.01120", amount.to_string_precise()); } #[test] fn test_amount_checked_sub() { - let max = Amount::from(u64::MAX); - let one = Amount::from(1); - let zero = Amount::from(0); + let max = Amount::native_whole(u64::MAX); + let one = Amount::native_whole(1); + let zero = Amount::native_whole(0); assert_eq!(zero.checked_sub(zero), Some(zero)); assert_eq!(zero.checked_sub(one), None); @@ -561,28 +1071,102 @@ mod tests { assert_eq!(max.checked_sub(max), Some(zero)); } + #[test] + fn test_serialization_round_trip() { + let amount: Amount = serde_json::from_str(r#""1000000000""#).unwrap(); + assert_eq!( + amount, + Amount { + raw: Uint::from(1000000000) + } + ); + let serialized = serde_json::to_string(&amount).unwrap(); + assert_eq!(serialized, r#""1000000000""#); + } + #[test] fn test_amount_checked_add() { - let max = Amount::from(u64::MAX); - let one = Amount::from(1); - let zero = Amount::from(0); + let max = Amount::max(); + let max_signed = Amount::max_signed(); + let one = Amount::native_whole(1); + let zero = Amount::native_whole(0); assert_eq!(zero.checked_add(zero), Some(zero)); + assert_eq!(zero.checked_signed_add(zero), Some(zero)); assert_eq!(zero.checked_add(one), Some(one)); assert_eq!(zero.checked_add(max - one), Some(max - one)); + assert_eq!( + zero.checked_signed_add(max_signed - one), + Some(max_signed - one) + ); assert_eq!(zero.checked_add(max), Some(max)); + assert_eq!(zero.checked_signed_add(max_signed), Some(max_signed)); assert_eq!(max.checked_add(zero), Some(max)); + assert_eq!(max.checked_signed_add(zero), None); assert_eq!(max.checked_add(one), None); assert_eq!(max.checked_add(max), None); + + assert_eq!(max_signed.checked_add(zero), Some(max_signed)); + assert_eq!(max_signed.checked_add(one), Some(max_signed + one)); + assert_eq!(max_signed.checked_signed_add(max_signed), None); + } + + #[test] + fn test_amount_from_string() { + assert!(Amount::from_str("1.12", 1).is_err()); + assert!(Amount::from_str("0.0", 0).is_err()); + assert!(Amount::from_str("1.12", 80).is_err()); + assert!(Amount::from_str("1.12.1", 3).is_err()); + assert!(Amount::from_str("1.1a", 3).is_err()); + assert_eq!( + Amount::zero(), + Amount::from_str("0.0", 1).expect("Test failed") + ); + assert_eq!( + Amount::zero(), + Amount::from_str(".0", 1).expect("Test failed") + ); + + let amount = Amount::from_str("1.12", 3).expect("Test failed"); + assert_eq!(amount, Amount::from_uint(1120, 0).expect("Test failed")); + let amount = Amount::from_str(".34", 3).expect("Test failed"); + assert_eq!(amount, Amount::from_uint(340, 0).expect("Test failed")); + let amount = Amount::from_str("0.34", 3).expect("Test failed"); + assert_eq!(amount, Amount::from_uint(340, 0).expect("Test failed")); + let amount = Amount::from_str("34", 1).expect("Test failed"); + assert_eq!(amount, Amount::from_uint(340, 0).expect("Test failed")); + } + + #[test] + fn test_from_masp_denominated() { + let uint = Uint([15u64, 16, 17, 18]); + let original = Amount::from_uint(uint, 0).expect("Test failed"); + for denom in MaspDenom::iter() { + let word = denom.denominate(&original); + assert_eq!(word, denom as u64 + 15u64); + let amount = Amount::from_masp_denominated(word, denom); + let raw = Uint::from(amount).0; + let mut expected = [0u64; 4]; + expected[denom as usize] = word; + assert_eq!(raw, expected); + } + } + + #[test] + fn test_key_seg() { + let original = Amount::from_uint(1234560000, 0).expect("Test failed"); + let key = original.raw(); + let amount = Amount::parse(key).expect("Test failed"); + assert_eq!(amount, original); } #[test] fn test_amount_is_zero() { - let zero = Amount::from(0); + let zero = Amount::zero(); assert!(zero.is_zero()); - let non_zero = Amount::from(1); + let non_zero = Amount::from_uint(1, 0).expect("Test failed"); assert!(!non_zero.is_zero()); } } @@ -596,12 +1180,12 @@ pub mod testing { /// Generate an arbitrary token amount pub fn arb_amount() -> impl Strategy { - any::().prop_map(Amount::from) + any::().prop_map(|val| Amount::from_uint(val, 0).unwrap()) } /// Generate an arbitrary token amount up to and including given `max` value pub fn arb_amount_ceiled(max: u64) -> impl Strategy { - (0..=max).prop_map(Amount::from) + (0..=max).prop_map(|val| Amount::from_uint(val, 0).unwrap()) } /// Generate an arbitrary non-zero token amount up to and including given @@ -609,6 +1193,6 @@ pub mod testing { pub fn arb_amount_non_zero_ceiled( max: u64, ) -> impl Strategy { - (1..=max).prop_map(Amount::from) + (1..=max).prop_map(|val| Amount::from_uint(val, 0).unwrap()) } } diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 5b9069b731..d4104920ab 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -21,13 +21,13 @@ pub use decrypted::*; #[cfg(feature = "ferveo-tpke")] pub use encrypted::EncryptionKey; pub use protocol::UpdateDkgSessionKey; -use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; pub use wrapper::*; use crate::ledger::gas::VpsGas; use crate::types::address::Address; +use crate::types::dec::Dec; use crate::types::hash::Hash; use crate::types::ibc::IbcEvent; use crate::types::key::*; @@ -192,10 +192,10 @@ pub struct InitValidator { /// Serialization of the public session key used in the DKG pub dkg_key: crate::types::key::dkg_session_keys::DkgPublicKey, /// The initial commission rate charged for delegation rewards - pub commission_rate: Decimal, + pub commission_rate: Dec, /// The maximum change allowed per epoch to the commission rate. This is /// immutable once set here. - pub max_commission_rate_change: Decimal, + pub max_commission_rate_change: Dec, /// The VP code for validator account pub validator_vp_code_hash: Hash, } @@ -362,7 +362,7 @@ mod test_process_tx { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -398,7 +398,7 @@ mod test_process_tx { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); diff --git a/core/src/types/transaction/pos.rs b/core/src/types/transaction/pos.rs index 8119eb2310..d334047ffe 100644 --- a/core/src/types/transaction/pos.rs +++ b/core/src/types/transaction/pos.rs @@ -1,10 +1,10 @@ //! Types used for PoS system transactions use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use crate::types::address::Address; +use crate::types::dec::Dec; use crate::types::token; /// A bond is a validator's self-bond or a delegation from non-validator to a @@ -72,5 +72,5 @@ pub struct CommissionChange { /// Validator address pub validator: Address, /// The new commission rate - pub new_rate: Decimal, + pub new_rate: Dec, } diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index a92d5720ee..08e8f7d2aa 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -2,11 +2,14 @@ /// to enable encrypted txs inside of normal txs. /// *Not wasm compatible* pub mod wrapper_tx { + use std::fmt::Formatter; + pub use ark_bls12_381::Bls12_381 as EllipticCurve; #[cfg(feature = "ferveo-tpke")] pub use ark_ec::{AffineCurve, PairingEngine}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; - use serde::{Deserialize, Serialize}; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sha2::{Digest, Sha256}; use thiserror::Error; @@ -14,6 +17,7 @@ pub mod wrapper_tx { use crate::types::key::*; use crate::types::storage::Epoch; use crate::types::token::Amount; + use crate::types::uint::Uint; /// Minimum fee amount in micro NAMs pub const MIN_FEE: u64 = 100; @@ -56,6 +60,7 @@ pub mod wrapper_tx { /// amount of the fee pub amount: Amount, /// address of the token + /// TODO: This should support multi-tokens pub token: Address, } @@ -67,52 +72,95 @@ pub mod wrapper_tx { /// This struct only stores the multiple of GAS_LIMIT_RESOLUTION, /// not the raw amount #[derive( + Default, Debug, Clone, PartialEq, - Serialize, - Deserialize, BorshSerialize, BorshDeserialize, BorshSchema, Eq, )] - #[serde(from = "u64")] - #[serde(into = "u64")] pub struct GasLimit { - multiplier: u64, + multiplier: Uint, + } + + impl Serialize for GasLimit { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let limit = Uint::from(self).to_string(); + Serialize::serialize(&limit, serializer) + } + } + + impl<'de> Deserialize<'de> for GasLimit { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct GasLimitVisitor; + + impl<'a> serde::de::Visitor<'a> for GasLimitVisitor { + type Value = GasLimit; + + fn expecting( + &self, + formatter: &mut Formatter, + ) -> std::fmt::Result { + formatter.write_str( + "A string representing 256-bit unsigned integer", + ) + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + let uint = Uint::from_dec_str(v) + .map_err(|e| E::custom(e.to_string()))?; + Ok(GasLimit::from(uint)) + } + } + deserializer.deserialize_any(GasLimitVisitor) + } } impl GasLimit { /// We refund unused gas up to GAS_LIMIT_RESOLUTION - pub fn refund_amount(&self, used_gas: u64) -> Amount { - if used_gas < (u64::from(self) - GAS_LIMIT_RESOLUTION) { - // we refund only up to GAS_LIMIT_RESOLUTION - GAS_LIMIT_RESOLUTION - } else if used_gas >= u64::from(self) { - // Gas limit was under estimated, no refund - 0 - } else { - // compute refund - u64::from(self) - used_gas - } - .into() + pub fn refund_amount(&self, used_gas: Uint) -> Amount { + Amount::from_uint( + if used_gas + < (Uint::from(self) - Uint::from(GAS_LIMIT_RESOLUTION)) + { + // we refund only up to GAS_LIMIT_RESOLUTION + Uint::from(GAS_LIMIT_RESOLUTION) + } else if used_gas >= Uint::from(self) { + // Gas limit was under estimated, no refund + Uint::from(0) + } else { + // compute refund + Uint::from(self) - used_gas + }, + 0, + ) + .unwrap() } } /// Round the input number up to the next highest multiple /// of GAS_LIMIT_RESOLUTION - impl From for GasLimit { - fn from(amount: u64) -> GasLimit { - // we could use the ceiling function but this way avoids casts to - // floats - if GAS_LIMIT_RESOLUTION * (amount / GAS_LIMIT_RESOLUTION) < amount { + impl From for GasLimit { + fn from(amount: Uint) -> GasLimit { + let gas_limit_resolution = Uint::from(GAS_LIMIT_RESOLUTION); + if gas_limit_resolution * (amount / gas_limit_resolution) < amount { GasLimit { - multiplier: (amount / GAS_LIMIT_RESOLUTION) + 1, + multiplier: (amount / gas_limit_resolution) + 1, } } else { GasLimit { - multiplier: (amount / GAS_LIMIT_RESOLUTION), + multiplier: (amount / gas_limit_resolution), } } } @@ -122,20 +170,20 @@ pub mod wrapper_tx { /// of GAS_LIMIT_RESOLUTION impl From for GasLimit { fn from(amount: Amount) -> GasLimit { - GasLimit::from(u64::from(amount)) + GasLimit::from(Uint::from(amount)) } } /// Get back the gas limit as a raw number - impl From<&GasLimit> for u64 { - fn from(limit: &GasLimit) -> u64 { + impl From<&GasLimit> for Uint { + fn from(limit: &GasLimit) -> Uint { limit.multiplier * GAS_LIMIT_RESOLUTION } } /// Get back the gas limit as a raw number - impl From for u64 { - fn from(limit: GasLimit) -> u64 { + impl From for Uint { + fn from(limit: GasLimit) -> Uint { limit.multiplier * GAS_LIMIT_RESOLUTION } } @@ -143,7 +191,8 @@ pub mod wrapper_tx { /// Get back the gas limit as a raw number, viewed as an Amount impl From for Amount { fn from(limit: GasLimit) -> Amount { - Amount::from(limit.multiplier * GAS_LIMIT_RESOLUTION) + Amount::from_uint(limit.multiplier * GAS_LIMIT_RESOLUTION, 0) + .unwrap() } } @@ -221,10 +270,12 @@ pub mod wrapper_tx { /// Test that serializing converts GasLimit to u64 correctly #[test] fn test_gas_limit_roundtrip() { - let limit = GasLimit { multiplier: 1 }; + let limit = GasLimit { + multiplier: 1.into(), + }; // Test serde roundtrip let js = serde_json::to_string(&limit).expect("Test failed"); - assert_eq!(js, format!("{}", GAS_LIMIT_RESOLUTION)); + assert_eq!(js, format!(r#""{}""#, GAS_LIMIT_RESOLUTION)); let new_limit: GasLimit = serde_json::from_str(&js).expect("Test failed"); assert_eq!(new_limit, limit); @@ -243,35 +294,52 @@ pub mod wrapper_tx { /// multiple #[test] fn test_deserialize_not_multiple_of_resolution() { - let js = serde_json::to_string(&(GAS_LIMIT_RESOLUTION + 1)) - .expect("Test failed"); + let js = format!(r#""{}""#, &(GAS_LIMIT_RESOLUTION + 1)); let limit: GasLimit = serde_json::from_str(&js).expect("Test failed"); - assert_eq!(limit, GasLimit { multiplier: 2 }); + assert_eq!( + limit, + GasLimit { + multiplier: 2.into() + } + ); } /// Test that refund is calculated correctly #[test] fn test_gas_limit_refund() { - let limit = GasLimit { multiplier: 1 }; - let refund = limit.refund_amount(GAS_LIMIT_RESOLUTION - 1); - assert_eq!(refund, Amount::from(1u64)); + let limit = GasLimit { + multiplier: 1.into(), + }; + let refund = + limit.refund_amount(Uint::from(GAS_LIMIT_RESOLUTION - 1)); + assert_eq!(refund, Amount::from_uint(1, 0).expect("Test failed")); } /// Test that we don't refund more than GAS_LIMIT_RESOLUTION #[test] fn test_gas_limit_too_high_no_refund() { - let limit = GasLimit { multiplier: 2 }; - let refund = limit.refund_amount(GAS_LIMIT_RESOLUTION - 1); - assert_eq!(refund, Amount::from(GAS_LIMIT_RESOLUTION)); + let limit = GasLimit { + multiplier: 2.into(), + }; + let refund = + limit.refund_amount(Uint::from(GAS_LIMIT_RESOLUTION - 1)); + assert_eq!( + refund, + Amount::from_uint(GAS_LIMIT_RESOLUTION, 0) + .expect("Test failed") + ); } /// Test that if gas usage was underestimated, we issue no refund #[test] fn test_gas_limit_too_low_no_refund() { - let limit = GasLimit { multiplier: 1 }; - let refund = limit.refund_amount(GAS_LIMIT_RESOLUTION + 1); - assert_eq!(refund, Amount::from(0u64)); + let limit = GasLimit { + multiplier: 1.into(), + }; + let refund = + limit.refund_amount(Uint::from(GAS_LIMIT_RESOLUTION + 1)); + assert_eq!(refund, Amount::default()); } } @@ -303,7 +371,7 @@ pub mod wrapper_tx { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -336,7 +404,7 @@ pub mod wrapper_tx { }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); @@ -366,12 +434,12 @@ pub mod wrapper_tx { // the signed tx let mut tx = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 10.into(), + amount: Amount::from_uint(10, 0).expect("Test failed"), token: nam(), }, &keypair, Epoch(0), - 0.into(), + Default::default(), #[cfg(not(feature = "mainnet"))] None, )))); diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs new file mode 100644 index 0000000000..a3e147ca7c --- /dev/null +++ b/core/src/types/uint.rs @@ -0,0 +1,547 @@ +#![allow(clippy::assign_op_pattern)] +//! An unsigned 256 integer type. Used for, among other things, +//! the backing type of token amounts. +use std::cmp::Ordering; +use std::fmt::Debug; +use std::ops::{Add, AddAssign, BitXor, Div, Mul, Neg, Rem, Sub, SubAssign}; + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use impl_num_traits::impl_uint_num_traits; +use serde::{Deserialize, Serialize}; +use uint::construct_uint; + +use crate::types::token; +use crate::types::token::{Amount, AmountParseError, MaspDenom}; + +construct_uint! { + /// Namada native type to replace for unsigned 256 bit + /// integers. + #[derive( + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + BorshSchema, + )] + + pub struct Uint(4); +} + +impl_uint_num_traits!(Uint, 4); + +/// The maximum 256 bit integer +pub const MAX_VALUE: Uint = Uint([u64::MAX; 4]); + +impl Uint { + /// Divide two [`Uint`]s with scaled to allow the `denom` number + /// of decimal places. + /// + /// This method is checked and will return `None` if + /// * `self` * 10^(`denom`) overflows 256 bits + /// * `other` is zero (`checked_div` will return `None`). + pub fn fixed_precision_div(&self, rhs: &Self, denom: u8) -> Option { + let lhs = Uint::from(10) + .checked_pow(Uint::from(denom)) + .and_then(|res| res.checked_mul(*self))?; + lhs.checked_div(*rhs) + } + + /// Compute the two's complement of a number. + fn negate(&self) -> Self { + Self( + self.0 + .into_iter() + .map(|byte| byte.bitxor(u64::MAX)) + .collect::>() + .try_into() + .expect("This cannot fail"), + ) + .overflowing_add(Uint::from(1u64)) + .0 + .canonical() + } + + /// There are two valid representations of zero: plus and + /// minus. We only allow the positive representation. + fn canonical(self) -> Self { + if self == MINUS_ZERO { + Self::zero() + } else { + self + } + } +} + +/// The maximum absolute value a [`I256`] may have. +/// Note the the last digit is 2^63 - 1. We add this cap so +/// we can use two's complement. +pub const MAX_SIGNED_VALUE: Uint = + Uint([u64::MAX, u64::MAX, u64::MAX, 9223372036854775807]); + +const MINUS_ZERO: Uint = Uint([0u64, 0u64, 0u64, 9223372036854775808]); + +/// A signed 256 big integer. +#[derive( + Copy, + Clone, + Debug, + Default, + PartialEq, + Eq, + Hash, + BorshSerialize, + BorshDeserialize, + BorshSchema, +)] +pub struct I256(pub Uint); + +impl I256 { + /// Check if the amount is not negative (greater + /// than or equal to zero) + pub fn non_negative(&self) -> bool { + self.0.0[3].leading_zeros() > 0 + } + + /// Check if the amount is negative (less than zero) + pub fn is_negative(&self) -> bool { + !self.non_negative() + } + + /// Check if the amount is positive (greater than zero) + pub fn is_positive(&self) -> bool { + self.non_negative() && !self.is_zero() + } + + /// Get the absolute value + pub fn abs(&self) -> Uint { + if self.non_negative() { + self.0 + } else { + self.0.negate() + } + } + + /// Check if this value is zero + pub fn is_zero(&self) -> bool { + self.0 == Uint::zero() + } + + /// Gives the zero value of an I256 + pub fn zero() -> I256 { + Self(Uint::zero()) + } + + /// Get a string representation of `self` as a + /// native token amount. + pub fn to_string_native(&self) -> String { + let mut sign = if !self.non_negative() { + String::from("-") + } else { + String::new() + }; + sign.push_str(&token::Amount::from(*self).to_string_native()); + sign + } + + /// Adds two [`I256`]'s if the absolute value does + /// not exceed [`MAX_SIGNED_VALUE`], else returns `None`. + pub fn checked_add(&self, other: &Self) -> Option { + if self.non_negative() == other.non_negative() { + self.abs().checked_add(other.abs()).and_then(|val| { + Self::try_from(val) + .ok() + .map(|val| if !self.non_negative() { -val } else { val }) + }) + } else { + Some(*self + *other) + } + } + + /// Subtracts two [`I256`]'s if the absolute value does + /// not exceed [`MAX_SIGNED_VALUE`], else returns `None`. + pub fn checked_sub(&self, other: &Self) -> Option { + self.checked_add(&other.neg()) + } + + /// Changed the inner Uint into a canonical representation. + fn canonical(self) -> Self { + Self(self.0.canonical()) + } + + /// the maximum I256 value + pub fn maximum() -> Self { + Self(MAX_SIGNED_VALUE) + } + + /// Attempt to convert a MASP-denominated integer to an I256 + /// using the given denomination. + pub fn from_masp_denominated( + value: impl Into, + denom: MaspDenom, + ) -> Result { + let value = value.into(); + let is_negative = value < 0; + let value = value.unsigned_abs(); + let mut result = [0u64; 4]; + result[denom as usize] = value as u64; + let result = Uint(result); + if result <= MAX_SIGNED_VALUE { + if is_negative { + Ok(Self(result.negate()).canonical()) + } else { + Ok(Self(result).canonical()) + } + } else { + Err(AmountParseError::InvalidRange) + } + } +} + +impl From for I256 { + fn from(val: u64) -> Self { + I256::try_from(Uint::from(val)) + .expect("A u64 will always fit in this type") + } +} + +impl TryFrom for I256 { + type Error = Box; + + fn try_from(value: Uint) -> Result { + if value <= MAX_SIGNED_VALUE { + Ok(Self(value)) + } else { + Err("The given integer is too large to be represented asa \ + SignedUint" + .into()) + } + } +} + +impl Neg for I256 { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(self.0.negate()) + } +} + +impl PartialOrd for I256 { + fn partial_cmp(&self, other: &Self) -> Option { + match (self.non_negative(), other.non_negative()) { + (true, false) => Some(Ordering::Greater), + (false, true) => Some(Ordering::Less), + (true, true) => { + let this = self.abs(); + let that = other.abs(); + this.0.partial_cmp(&that.0) + } + (false, false) => { + let this = self.abs(); + let that = other.abs(); + that.0.partial_cmp(&this.0) + } + } + } +} + +impl Ord for I256 { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap() + } +} + +impl Add for I256 { + type Output = Self; + + fn add(self, rhs: I256) -> Self::Output { + match (self.non_negative(), rhs.non_negative()) { + (true, true) => Self(self.0 + rhs.0), + (false, false) => -Self(self.abs() + rhs.abs()), + (true, false) => { + if self.0 >= rhs.abs() { + Self(self.0 - rhs.abs()) + } else { + -Self(rhs.abs() - self.0) + } + } + (false, true) => { + if rhs.0 >= self.abs() { + Self(rhs.0 - self.abs()) + } else { + -Self(self.abs() - rhs.0) + } + } + } + .canonical() + } +} + +impl AddAssign for I256 { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl Sub for I256 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + self + (-rhs) + } +} + +impl SubAssign for I256 { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +// NOTE: watch the overflow +impl Mul for I256 { + type Output = Self; + + fn mul(self, rhs: Uint) -> Self::Output { + let is_neg = self.is_negative(); + let prod = self.abs() * rhs; + if is_neg { -Self(prod) } else { Self(prod) } + } +} + +impl Mul for I256 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + if rhs.is_negative() { + -self * rhs.abs() + } else { + self * rhs.abs() + } + } +} + +impl Div for I256 { + type Output = Self; + + fn div(self, rhs: Uint) -> Self::Output { + let is_neg = self.is_negative(); + let quot = self + .abs() + .fixed_precision_div(&rhs, 0u8) + .unwrap_or_default(); + if is_neg { -Self(quot) } else { Self(quot) } + } +} + +impl Div for I256 { + type Output = Self; + + fn div(self, rhs: I256) -> Self::Output { + if rhs.is_negative() { + -(self / rhs.abs()) + } else { + self / rhs.abs() + } + } +} + +impl Rem for I256 { + type Output = Self; + + fn rem(self, rhs: Self) -> Self::Output { + if self.is_negative() { + -(Self(self.abs() % rhs.abs())) + } else { + Self(self.abs() % rhs.abs()) + } + } +} +impl From for I256 { + fn from(val: i128) -> Self { + if val < 0 { + let abs = Self((-val).into()); + -abs + } else { + Self(val.into()) + } + } +} + +impl From for I256 { + fn from(val: i64) -> Self { + Self::from(val as i128) + } +} + +impl From for I256 { + fn from(val: i32) -> Self { + Self::from(val as i128) + } +} + +impl std::iter::Sum for I256 { + fn sum>(iter: I) -> Self { + iter.fold(I256::zero(), |acc, amt| acc + amt) + } +} + +impl TryFrom for i128 { + type Error = std::io::Error; + + fn try_from(value: I256) -> Result { + if !value.non_negative() { + Ok(-(u128::try_from(Amount::from_change(value))? as i128)) + } else { + Ok(u128::try_from(Amount::from_change(value))? as i128) + } + } +} + +#[cfg(test)] +mod test_uint { + use super::*; + + /// Test that dividing two [`Uint`]s with the specified precision + /// works correctly and performs correct checks. + #[test] + fn test_fixed_precision_div() { + let zero = Uint::zero(); + let two = Uint::from(2); + let three = Uint::from(3); + + assert_eq!( + zero.fixed_precision_div(&two, 10).expect("Test failed"), + zero + ); + assert!(two.fixed_precision_div(&zero, 3).is_none()); + assert_eq!( + three.fixed_precision_div(&two, 1).expect("Test failed"), + Uint::from(15) + ); + assert_eq!( + two.fixed_precision_div(&three, 2).expect("Test failed"), + Uint::from(66) + ); + assert_eq!( + two.fixed_precision_div(&three, 3).expect("Satan lives"), + Uint::from(666) + ); + assert!(two.fixed_precision_div(&three, 77).is_none()); + assert!(Uint::from(20).fixed_precision_div(&three, 76).is_none()); + } + + /// Test that adding one to the max signed + /// value gives zero. + #[test] + fn test_max_signed_value() { + let signed = I256::try_from(MAX_SIGNED_VALUE).expect("Test failed"); + let one = I256::try_from(Uint::from(1u64)).expect("Test failed"); + let overflow = signed + one; + assert_eq!( + overflow, + I256::try_from(Uint::zero()).expect("Test failed") + ); + assert!(signed.checked_add(&one).is_none()); + assert!((-signed).checked_sub(&one).is_none()); + } + + /// Sanity on our constants and that the minus zero representation + /// is not allowed. + #[test] + fn test_minus_zero_not_allowed() { + let larger = Uint([0, 0, 0, 2u64.pow(63)]); + let smaller = Uint([u64::MAX, u64::MAX, u64::MAX, 2u64.pow(63) - 1]); + assert!(larger > smaller); + assert_eq!(smaller, MAX_SIGNED_VALUE); + assert_eq!(larger, MINUS_ZERO); + assert!(I256::try_from(MINUS_ZERO).is_err()); + let zero = Uint::zero(); + assert_eq!(zero, zero.negate()); + } + + /// Test that we correctly reserve the right bit for indicating the + /// sign. + #[test] + fn test_non_negative() { + let zero = I256::try_from(Uint::zero()).expect("Test failed"); + assert!(zero.non_negative()); + assert!((-zero).non_negative()); + let negative = I256(Uint([1u64, 0, 0, 2u64.pow(63)])); + assert!(!negative.non_negative()); + assert!((-negative).non_negative()); + let positive = I256(MAX_SIGNED_VALUE); + assert!(positive.non_negative()); + assert!(!(-positive).non_negative()); + } + + /// Test that the absolute value is computed correctly. + #[test] + fn test_abs() { + let zero = I256::try_from(Uint::zero()).expect("Test failed"); + let neg_one = I256(Uint::max_value()); + let neg_eight = I256(Uint::max_value() - Uint::from(7)); + let two = I256(Uint::from(2)); + let ten = I256(Uint::from(10)); + + assert_eq!(zero.abs(), Uint::zero()); + assert_eq!(neg_one.abs(), Uint::from(1)); + assert_eq!(neg_eight.abs(), Uint::from(8)); + assert_eq!(two.abs(), Uint::from(2)); + assert_eq!(ten.abs(), Uint::from(10)); + } + + /// Test that the string representation is created correctly. + #[test] + fn test_to_string_native() { + let native_scaling = Uint::exp10(6); + let zero = I256::try_from(Uint::zero()).expect("Test failed"); + let neg_one = -I256(native_scaling); + let neg_eight = -I256(Uint::from(8) * native_scaling); + let two = I256(Uint::from(2) * native_scaling); + let ten = I256(Uint::from(10) * native_scaling); + + assert_eq!(zero.to_string_native(), "0.000000"); + assert_eq!(neg_one.to_string_native(), "-1.000000"); + assert_eq!(neg_eight.to_string_native(), "-8.000000"); + assert_eq!(two.to_string_native(), "2.000000"); + assert_eq!(ten.to_string_native(), "10.000000"); + } + + /// Test that we correctly handle arithmetic with two's complement + #[test] + fn test_arithmetic() { + let zero = I256::try_from(Uint::zero()).expect("Test failed"); + let neg_one = I256(Uint::max_value()); + let neg_eight = I256(Uint::max_value() - Uint::from(7)); + let two = I256(Uint::from(2)); + let ten = I256(Uint::from(10)); + + assert_eq!(zero + neg_one, neg_one); + assert_eq!(neg_one - zero, neg_one); + assert_eq!(zero - neg_one, I256(Uint::one())); + assert_eq!(two - neg_eight, ten); + assert_eq!(two + ten, I256(Uint::from(12))); + assert_eq!(ten - two, -neg_eight); + assert_eq!(two - ten, neg_eight); + assert_eq!(neg_eight + neg_one, -I256(Uint::from(9))); + assert_eq!(neg_one - neg_eight, I256(Uint::from(7))); + assert_eq!(neg_eight - neg_one, -I256(Uint::from(7))); + assert_eq!(neg_eight - two, -ten); + assert!((two - two).is_zero()); + } + + /// Test that ordering is correctly implemented + #[test] + fn test_ord() { + let this = Amount::from_uint(1, 0).unwrap().change(); + let that = Amount::native_whole(1000).change(); + assert!(this <= that); + assert!(-this <= that); + assert!(-this >= -that); + assert!(this >= -that); + assert!(that >= this); + assert!(that >= -this); + assert!(-that <= -this); + assert!(-that <= this); + } +} diff --git a/genesis/dev.toml b/genesis/dev.toml index 80748ec403..b6eb070b42 100644 --- a/genesis/dev.toml +++ b/genesis/dev.toml @@ -17,9 +17,9 @@ non_staked_balance = 100000 # VP for the validator account validator_vp = "vp_validator" # Commission rate for rewards -commission_rate = 0.05 +commission_rate = "0.05" # Maximum change per epoch in the commission rate -max_commission_rate_change = 0.01 +max_commission_rate_change = "0.01" # Public IP:port address net_address = "127.0.0.1:26656" @@ -27,6 +27,7 @@ net_address = "127.0.0.1:26656" [token.NAM] address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" +denom = 8 vp = "vp_token" [token.NAM.balances] # In token balances, we can use: @@ -35,7 +36,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m atest1v4ehgw36x3qng3jzggu5yvpsxgcngv2xgguy2dpkgvu5x33kx3pr2w2zgep5xwfkxscrxs2pj8075p = 1000000 atest1v4ehgw36xvcyyvejgvenxs34g3zygv3jxqunjd6rxyeyys3sxy6rwvfkx4qnj33hg9qnvse4lsfctw = 1000000 # 2. An alias of any account -bertha = 1000000 +Bertha = "1000000" # 3. A public key of a validator or an established account from which the # address of the implicit account is derived) "bertha.public_key" = 100 @@ -43,6 +44,7 @@ bertha = 1000000 [token.BTC] address = "atest1v4ehgw36xdzryve5gsc52veeg5cnsv2yx5eygvp38qcrvd29xy6rys6p8yc5xvp4xfpy2v694wgwcp" +denom = 8 vp = "vp_token" [token.BTC.balances] atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 @@ -52,6 +54,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m [token.ETH] address = "atest1v4ehgw36xqmr2d3nx3ryvd2xxgmrq33j8qcns33sxezrgv6zxdzrydjrxveygd2yxumrsdpsf9jc2p" +denom = 18 vp = "vp_token" [token.ETH.balances] atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 @@ -61,6 +64,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m [token.DOT] address = "atest1v4ehgw36gg6nvs2zgfpyxsfjgc65yv6pxy6nwwfsxgungdzrggeyzv35gveyxsjyxymyz335hur2jn" +denom = 10 vp = "vp_token" [token.DOT.balances] atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 @@ -70,6 +74,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m [token.schnitzel] address = "atest1v4ehgw36xue5xvf5xvuyzvpjx5un2v3k8qeyvd3cxdqns32p89rrxd6xx9zngvpegccnzs699rdnnt" +denom = 6 vp = "vp_token" [token.schnitzel.balances] atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 @@ -79,6 +84,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m [token.apfel] address = "atest1v4ehgw36gfryydj9g3p5zv3kg9znyd358ycnzsfcggc5gvecgc6ygs2rxv6ry3zpg4zrwdfeumqcz9" +denom = 6 vp = "vp_token" [token.apfel.balances] atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 @@ -88,6 +94,7 @@ a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6m [token.kartoffel] address = "atest1v4ehgw36gep5ysecxq6nyv3jg3zygv3e89qn2vp48pryxsf4xpznvve5gvmy23fs89pryvf5a6ht90" +denom = 6 public_key = "" vp = "vp_token" [token.kartoffel.balances] @@ -160,21 +167,21 @@ pipeline_len = 2 # for a fault in epoch 'n' up through epoch 'n + unbonding_len'. unbonding_len = 21 # Votes per fundamental staking token (namnam) -tm_votes_per_token = 1 +tm_votes_per_token = "1" # Reward for proposing a block. -block_proposer_reward = 0.125 +block_proposer_reward = "0.125" # Reward for voting on a block. -block_vote_reward = 0.1 +block_vote_reward = "0.1" # Maximum inflation rate per annum (10%) -max_inflation_rate = 0.1 +max_inflation_rate = "0.1" # Targeted ratio of staked tokens to total tokens in the supply -target_staked_ratio = 0.6667 +target_staked_ratio = "0.6667" # Portion of a validator's stake that should be slashed on a duplicate # vote. -duplicate_vote_min_slash_rate = 0.001 +duplicate_vote_min_slash_rate = "0.001" # Portion of a validator's stake that should be slashed on a light # client attack. -light_client_attack_min_slash_rate = 0.001 +light_client_attack_min_slash_rate = "0.001" # Number of epochs above and below (separately) the current epoch to # consider when doing cubic slashing cubic_slashing_window_length = 1 diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index f9860e6529..86b0072223 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -5,7 +5,7 @@ genesis_time = "2021-09-30T10:00:00Z" native_token = "NAM" faucet_pow_difficulty = 1 -faucet_withdrawal_limit = "1_000" +faucet_withdrawal_limit = "1000000000" [validator.validator-0] # Validator's staked NAM at genesis. @@ -15,11 +15,11 @@ non_staked_balance = 1000000000000 # VP for the validator account validator_vp = "vp_validator" # Commission rate for rewards -commission_rate = 0.05 +commission_rate = "0.05" # Maximum change per epoch in the commission rate -max_commission_rate_change = 0.01 +max_commission_rate_change = "0.01" # Public IP:port address. -# We set the port to be the default+1000, so that if a local node was running at +# We set the port to be the default+1000, so that if a local node was running at # the same time as the E2E tests, it wouldn't affect them. net_address = "127.0.0.1:27656" @@ -27,79 +27,86 @@ net_address = "127.0.0.1:27656" [token.NAM] address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" +denom = 6 vp = "vp_token" [token.NAM.balances] -Albert = 1000000 -"Albert.public_key" = 100 -Bertha = 1000000 -"Bertha.public_key" = 2000 -Christel = 1000000 -"Christel.public_key" = 100 -Daewon = 1000000 -faucet = 9223372036 -"faucet.public_key" = 100 -"validator-0.public_key" = 100 +Albert = "1000000" +"Albert.public_key" = "100" +Bertha = "1000000" +"Bertha.public_key" = "2000" +Christel = "1000000" +"Christel.public_key" = "100" +Daewon = "1000000" +faucet = "9223372036854" +"faucet.public_key" = "100" +"validator-0.public_key" = "100" [token.BTC] address = "atest1v4ehgw36xdzryve5gsc52veeg5cnsv2yx5eygvp38qcrvd29xy6rys6p8yc5xvp4xfpy2v694wgwcp" +denom = 8 vp = "vp_token" [token.BTC.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" [token.ETH] address = "atest1v4ehgw36xqmr2d3nx3ryvd2xxgmrq33j8qcns33sxezrgv6zxdzrydjrxveygd2yxumrsdpsf9jc2p" +denom = 18 vp = "vp_token" [token.ETH.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" [token.DOT] address = "atest1v4ehgw36gg6nvs2zgfpyxsfjgc65yv6pxy6nwwfsxgungdzrggeyzv35gveyxsjyxymyz335hur2jn" +denom = 10 vp = "vp_token" -[token.Dot.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +[token.DOT.balances] +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" [token.Schnitzel] address = "atest1v4ehgw36xue5xvf5xvuyzvpjx5un2v3k8qeyvd3cxdqns32p89rrxd6xx9zngvpegccnzs699rdnnt" +denom = 6 vp = "vp_token" [token.Schnitzel.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" [token.Apfel] address = "atest1v4ehgw36gfryydj9g3p5zv3kg9znyd358ycnzsfcggc5gvecgc6ygs2rxv6ry3zpg4zrwdfeumqcz9" +denom = 6 vp = "vp_token" [token.Apfel.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" [token.Kartoffel] address = "atest1v4ehgw36gep5ysecxq6nyv3jg3zygv3e89qn2vp48pryxsf4xpznvve5gvmy23fs89pryvf5a6ht90" public_key = "" +denom = 6 vp = "vp_token" [token.Kartoffel.balances] -Albert = 1000000 -Bertha = 1000000 -Christel = 1000000 -Daewon = 1000000 -faucet = 9223372036854 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +faucet = "9223372036854" # Some established accounts present at genesis. [established.faucet] @@ -164,9 +171,9 @@ implicit_vp = "vp_implicit" # Expected number of epochs per year (also sets the min duration of an epoch in seconds) epochs_per_year = 31_536_000 # The P gain factor in the Proof of Stake rewards controller -pos_gain_p = 0.1 +pos_gain_p = "0.1" # The D gain factor in the Proof of Stake rewards controller -pos_gain_d = 0.1 +pos_gain_d = "0.1" # Proof of stake parameters. [pos_params] @@ -179,21 +186,21 @@ pipeline_len = 2 # for a fault in epoch 'n' up through epoch 'n + unbonding_len'. unbonding_len = 3 # Votes per fundamental staking token (namnam) -tm_votes_per_token = 0.1 +tm_votes_per_token = "0.1" # Reward for proposing a block. -block_proposer_reward = 0.125 +block_proposer_reward = "0.125" # Reward for voting on a block. -block_vote_reward = 0.1 +block_vote_reward = "0.1" # Maximum inflation rate per annum (10%) -max_inflation_rate = 0.1 +max_inflation_rate = "0.1" # Targeted ratio of staked tokens to total tokens in the supply -target_staked_ratio = 0.6667 +target_staked_ratio = "0.6667" # Portion of a validator's stake that should be slashed on a duplicate # vote. -duplicate_vote_min_slash_rate = 0.001 +duplicate_vote_min_slash_rate = "0.001" # Portion of a validator's stake that should be slashed on a light # client attack. -light_client_attack_min_slash_rate = 0.001 +light_client_attack_min_slash_rate = "0.001" # Number of epochs above and below (separately) the current epoch to # consider when doing cubic slashing cubic_slashing_window_length = 1 diff --git a/proof_of_stake/Cargo.toml b/proof_of_stake/Cargo.toml index 2e9bd22757..a5b407df36 100644 --- a/proof_of_stake/Cargo.toml +++ b/proof_of_stake/Cargo.toml @@ -27,8 +27,6 @@ data-encoding.workspace = true derivative.workspace = true once_cell.workspace = true proptest = {version = "1.2.0", optional = true} -rust_decimal_macros.workspace = true -rust_decimal.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/proof_of_stake/proptest-regressions/tests/state_machine.txt b/proof_of_stake/proptest-regressions/tests/state_machine.txt index fd37a6f64a..4c02bc0ede 100644 --- a/proof_of_stake/proptest-regressions/tests/state_machine.txt +++ b/proof_of_stake/proptest-regressions/tests/state_machine.txt @@ -5,3 +5,4 @@ # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. cc 3076c8509d56c546d5915febcf429f218ab79a7bac34c75c288f531b88110bc3 # shrinks to (initial_state, transitions) = (AbstractPosState { epoch: Epoch(0), params: PosParams { max_validator_slots: 4, pipeline_len: 2, unbonding_len: 4, tm_votes_per_token: 0.0614, block_proposer_reward: 0.125, block_vote_reward: 0.1, max_inflation_rate: 0.1, target_staked_ratio: 0.6667, duplicate_vote_min_slash_rate: 0.001, light_client_attack_min_slash_rate: 0.001, cubic_slashing_window_length: 1 }, genesis_validators: [GenesisValidator { address: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, tokens: Amount { micro: 9185807 }, consensus_key: Ed25519(PublicKey(VerificationKey("ee1aa49a4459dfe813a3cf6eb882041230c7b2558469de81f87c9bf23bf10a03"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6, tokens: Amount { micro: 5025206 }, consensus_key: Ed25519(PublicKey(VerificationKey("17888c2ca502371245e5e35d5bcf35246c3bc36878e859938c9ead3c54db174f"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc, tokens: Amount { micro: 4424807 }, consensus_key: Ed25519(PublicKey(VerificationKey("478243aed376da313d7cf3a60637c264cb36acc936efb341ff8d3d712092d244"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, tokens: Amount { micro: 4119410 }, consensus_key: Ed25519(PublicKey(VerificationKey("c5bbbb60e412879bbec7bb769804fa8e36e68af10d5477280b63deeaca931bed"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, tokens: Amount { micro: 3619078 }, consensus_key: Ed25519(PublicKey(VerificationKey("4f44e6c7bdfed3d9f48d86149ee3d29382cae8c83ca253e06a70be54a301828b"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, tokens: Amount { micro: 2691447 }, consensus_key: Ed25519(PublicKey(VerificationKey("ff87a0b0a3c7c0ce827e9cada5ff79e75a44a0633bfcb5b50f99307ddb26b337"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, tokens: Amount { micro: 224944 }, consensus_key: Ed25519(PublicKey(VerificationKey("191fc38f134aaf1b7fdb1f86330b9d03e94bd4ba884f490389de964448e89b3f"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }, GenesisValidator { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, tokens: Amount { micro: 142614 }, consensus_key: Ed25519(PublicKey(VerificationKey("e2e8aa145e1ec5cb01ebfaa40e10e12f0230c832fd8135470c001cb86d77de00"))), commission_rate: 0.05, max_commission_rate_change: 0.01 }], bonds: {BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }: {Epoch(0): 142614}, BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }: {Epoch(0): 4119410}, BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }: {Epoch(0): 9185807}, BondId { source: Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6, validator: Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6 }: {Epoch(0): 5025206}, BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }: {Epoch(0): 2691447}, BondId { source: Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc, validator: Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc }: {Epoch(0): 4424807}, BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }: {Epoch(0): 224944}, BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }: {Epoch(0): 3619078}}, validator_stakes: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 142614, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 4119410, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 9185807, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: 5025206, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 2691447, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: 4424807, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 224944, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3619078}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 142614, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 4119410, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 9185807, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: 5025206, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 2691447, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: 4424807, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 224944, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3619078}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 142614, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 4119410, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 9185807, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: 5025206, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 2691447, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: 4424807, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 224944, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3619078}}, consensus_set: {Epoch(0): {Amount { micro: 4119410 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 4424807 }: [Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc], Amount { micro: 5025206 }: [Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6], Amount { micro: 9185807 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(1): {Amount { micro: 4119410 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 4424807 }: [Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc], Amount { micro: 5025206 }: [Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6], Amount { micro: 9185807 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(2): {Amount { micro: 4119410 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { micro: 4424807 }: [Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc], Amount { micro: 5025206 }: [Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6], Amount { micro: 9185807 }: [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}}, below_capacity_set: {Epoch(0): {ReverseOrdTokenAmount(Amount { micro: 142614 }): [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6], ReverseOrdTokenAmount(Amount { micro: 224944 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 2691447 }): [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], ReverseOrdTokenAmount(Amount { micro: 3619078 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}, Epoch(1): {ReverseOrdTokenAmount(Amount { micro: 142614 }): [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6], ReverseOrdTokenAmount(Amount { micro: 224944 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 2691447 }): [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], ReverseOrdTokenAmount(Amount { micro: 3619078 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}, Epoch(2): {ReverseOrdTokenAmount(Amount { micro: 142614 }): [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6], ReverseOrdTokenAmount(Amount { micro: 224944 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { micro: 2691447 }): [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], ReverseOrdTokenAmount(Amount { micro: 3619078 }): [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd]}}, validator_states: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: BelowCapacity, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: BelowCapacity, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: BelowCapacity, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: Consensus, Established: atest1v4ehgw36gfzrydfsx9zryv6pxcmng32xg9zyvve3xveyxvf58pzyzd2p8qmr23fsggensve3v7a7y6: Consensus, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: BelowCapacity, Established: atest1v4ehgw36gvcn23zyx3zngw2pgv6nxvfjx9pyyv2p8ye5vvpjxcenvv3ng3przvpnxqur2vzpkrazgc: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: BelowCapacity}}, unbonds: {}, validator_slashes: {}, enqueued_slashes: {}, validator_last_slash_epochs: {}, unbond_records: {} }, [InitValidator { address: Established: atest1v4ehgw36xgunxvj9xqmny3jyxycnzdzxxqeng33ngvunqsfsx5mnwdfjgvenvwfk89prwdpjd0cjrk, consensus_key: Ed25519(PublicKey(VerificationKey("bea04de1e5be8ca0ae27be8ad935df8d757e96c1e067e96aedeba0ded0df997d"))), commission_rate: 0.39428, max_commission_rate_change: 0.12485 }]) +cc c0ffe7b368967ea0c456da20046f7d8a78c232c066ea116d3a123c945b7882fb # shrinks to (initial_state, transitions) = (AbstractPosState { epoch: Epoch(0), params: PosParams { max_validator_slots: 4, pipeline_len: 2, unbonding_len: 7, tm_votes_per_token: Dec(900700.000000), block_proposer_reward: Dec(125000.000000), block_vote_reward: Dec(100000.000000), max_inflation_rate: Dec(100000.000000), target_staked_ratio: Dec(666700.000000), duplicate_vote_min_slash_rate: Dec(1000.000000), light_client_attack_min_slash_rate: Dec(1000.000000), cubic_slashing_window_length: 1 }, genesis_validators: [GenesisValidator { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, tokens: Amount { raw: 8937727 }, consensus_key: Ed25519(PublicKey(VerificationKey("e2e8aa145e1ec5cb01ebfaa40e10e12f0230c832fd8135470c001cb86d77de00"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, tokens: Amount { raw: 8738693 }, consensus_key: Ed25519(PublicKey(VerificationKey("ff87a0b0a3c7c0ce827e9cada5ff79e75a44a0633bfcb5b50f99307ddb26b337"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, tokens: Amount { raw: 8373784 }, consensus_key: Ed25519(PublicKey(VerificationKey("c5bbbb60e412879bbec7bb769804fa8e36e68af10d5477280b63deeaca931bed"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, tokens: Amount { raw: 3584214 }, consensus_key: Ed25519(PublicKey(VerificationKey("4f44e6c7bdfed3d9f48d86149ee3d29382cae8c83ca253e06a70be54a301828b"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, tokens: Amount { raw: 553863 }, consensus_key: Ed25519(PublicKey(VerificationKey("ee1aa49a4459dfe813a3cf6eb882041230c7b2558469de81f87c9bf23bf10a03"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }, GenesisValidator { address: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, tokens: Amount { raw: 218044 }, consensus_key: Ed25519(PublicKey(VerificationKey("191fc38f134aaf1b7fdb1f86330b9d03e94bd4ba884f490389de964448e89b3f"))), commission_rate: Dec(50000.000000), max_commission_rate_change: Dec(10000.000000) }], bonds: {BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }: {Epoch(0): 8.937727}, BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }: {Epoch(0): 8.373784}, BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }: {Epoch(0): 0.553863}, BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }: {Epoch(0): 8.738693}, BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }: {Epoch(0): 0.218044}, BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }: {Epoch(0): 3.584214}}, validator_stakes: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 8.937727, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 8.373784, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 0.553863, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 8.738693, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 0.218044, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3.584214}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 8.937727, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 8.373784, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 0.553863, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 8.738693, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 0.218044, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3.584214}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: 8.937727, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: 8.373784, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: 0.553863, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: 8.738693, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: 0.218044, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: 3.584214}}, consensus_set: {Epoch(0): {Amount { raw: 3584214 }: [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd], Amount { raw: 8373784 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { raw: 8738693 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { raw: 8937727 }: [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6]}, Epoch(1): {Amount { raw: 3584214 }: [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd], Amount { raw: 8373784 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { raw: 8738693 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { raw: 8937727 }: [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6]}, Epoch(2): {Amount { raw: 3584214 }: [Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd], Amount { raw: 8373784 }: [Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3], Amount { raw: 8738693 }: [Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk], Amount { raw: 8937727 }: [Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6]}}, below_capacity_set: {Epoch(0): {ReverseOrdTokenAmount(Amount { raw: 218044 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { raw: 553863 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(1): {ReverseOrdTokenAmount(Amount { raw: 218044 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { raw: 553863 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}, Epoch(2): {ReverseOrdTokenAmount(Amount { raw: 218044 }): [Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv], ReverseOrdTokenAmount(Amount { raw: 553863 }): [Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6]}}, validator_states: {Epoch(0): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: Consensus}, Epoch(1): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: Consensus}, Epoch(2): {Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6: Consensus, Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3: Consensus, Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6: BelowCapacity, Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk: Consensus, Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv: BelowCapacity, Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd: Consensus}}, unbonds: {}, validator_slashes: {}, enqueued_slashes: {}, validator_last_slash_epochs: {}, unbond_records: {} }, [Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 267 } }, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7610143 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 9863718 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 7102818 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 63132 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 9663084 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2694963 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7453740 } }, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 14974324 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2628172 } }, NextEpoch, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 282055 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 11228090 } }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 2027105 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2034080 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 3329590 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 854661 } }, Misbehavior { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, slash_type: DuplicateVote, infraction_epoch: Epoch(1), height: 0 }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 227931 } }, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 2701887 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 1776100 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 3717491 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 5281559 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 2426117 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2005749 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7883312 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7300122 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 3388459 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 195542 } }, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2251455 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 1237777 } }, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 691613 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 1244599 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2645543 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 8384136 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 590662 } }, NextEpoch, InitValidator { address: Established: atest1v4ehgw368qcrqd2ygvmyyvf4g9qnvv3kxucrwv3hxg6ryve4x56r233cxucnysjrxsmygdj9yer4pz, consensus_key: Ed25519(PublicKey(VerificationKey("afa2335747c0249f66eca84e88fba1a0e3ccec6a8f6f97f3177a42ffbb216492"))), commission_rate: Dec(195450.000000), max_commission_rate_change: Dec(954460.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 1687952 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 12754717 } }, Misbehavior { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, slash_type: LightClientAttack, infraction_epoch: Epoch(4), height: 0 }, Bond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 8952712 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 519835 } }, UnjailValidator { address: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 2207493 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 236124 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 71122 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 1158688 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 267618 } }, InitValidator { address: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, consensus_key: Ed25519(PublicKey(VerificationKey("822cfec1ec829a50306424ac3d11115e880b952f5f54ac9a624277898991ee70"))), commission_rate: Dec(614520.000000), max_commission_rate_change: Dec(369920.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 8634884 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 8660668 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 8436873 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 515615 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 46481 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 4153966 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2272563 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 7491749 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 1921487 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 8316111 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 11873152 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 4728535 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 2828807 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 655500 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 234416 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 330322 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 222600 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 2538059 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 168498 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 510701 } }, Misbehavior { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk, slash_type: DuplicateVote, infraction_epoch: Epoch(8), height: 0 }, InitValidator { address: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r, consensus_key: Ed25519(PublicKey(VerificationKey("afc853489cf37abedeb6a97d036f3dc60934194af7169a2cc15fb3f85e4e287c"))), commission_rate: Dec(52690.000000), max_commission_rate_change: Dec(56470.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 7098849 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 2180088 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 243441 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 1621261 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 7650954 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 1201023 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 9702706 } }, InitValidator { address: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, consensus_key: Ed25519(PublicKey(VerificationKey("f8506f129faaf3bac1397ad0ab3bfa6d1a00d5c1064c4fafe740f2844be8fb04"))), commission_rate: Dec(575190.000000), max_commission_rate_change: Dec(602710.000000) }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 347187 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 5536481 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xc6nvvf4g9znxvf3xdrrgvfexuen2dek8qmnqse58q6ygdpkxeznz3j9xyeyydfht747xe, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 1859243 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 1907757 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 3007741 } }, Misbehavior { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, slash_type: DuplicateVote, infraction_epoch: Epoch(9), height: 0 }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 8226972 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 602759 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 8350223 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 3787232 } }, InitValidator { address: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595, consensus_key: Ed25519(PublicKey(VerificationKey("0b88c50c1b9b5b1e83c89110e388908dc3cc18ce0551494ab1c82bece24b2714"))), commission_rate: Dec(674000.000000), max_commission_rate_change: Dec(247230.000000) }, Bond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 1391049 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36gve5zdf4gccygv6zxgcnxwzrgv65x32rg4zrxv34g9prvs2pxqmnzve5xvuns33czq9awp, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 4008194 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 9368360 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 9140634 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 600383 } }, Misbehavior { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, slash_type: DuplicateVote, infraction_epoch: Epoch(7), height: 0 }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 8599835 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 345454 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 12448069 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 5151682 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 1862578 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 10904134 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 773655 } }, Bond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 8927299 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 1288039 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2861830 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 445593 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 8204875 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 602527 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 5812026 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 211165 } }, NextEpoch, Bond { id: BondId { source: Implicit: atest1d9khqw36xsun2decx9p52v2xg5cr2vphxym5vve58yerqve5x5c5yve3gepyzs3ngycy233eufckzz, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 350302 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 4560437 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xqunjdeegge5xdpcg5mnqwzp8yerzde58pq5g3pcxu6yvvphg3zr23z9gg6yvs3cmzdz9u, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 3515009 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 4956849 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xsun2decx9p52v2xg5cr2vphxym5vve58yerqve5x5c5yve3gepyzs3ngycy233eufckzz, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 290427 } }, NextEpoch, Unbond { id: BondId { source: Implicit: atest1d9khqw36gve5zdf4gccygv6zxgcnxwzrgv65x32rg4zrxv34g9prvs2pxqmnzve5xvuns33czq9awp, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 3261985 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 8946479 } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, NextEpoch, InitValidator { address: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3, consensus_key: Ed25519(PublicKey(VerificationKey("a856fc650a2404e2d0c152d89c1c221bd9056a6103980e1d821b0cbae213ff44"))), commission_rate: Dec(324920.000000), max_commission_rate_change: Dec(512260.000000) }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 82795 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 128956 } }, Bond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 2043203 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 6764953 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g5eyzwf3xqc5gwzxg3pnq3jpgsenxwp3x56rjvz9x5crwsf3gerrgwphxqen2sjz4hscvd, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 6413168 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 6384185 } }, Misbehavior { address: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595, slash_type: LightClientAttack, infraction_epoch: Epoch(13), height: 0 }, Bond { id: BondId { source: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 8314982 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xscrsve3geqnwd2x8qmrzwpe89z5zsekgvenqwp5x4p5ydzp8qmrz3zpgcmnydjptyfc40, validator: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3 }, amount: Amount { raw: 9139532 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 34693 } }, Bond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 9487215 } }, NextEpoch, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 799953 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xscrsve3geqnwd2x8qmrzwpe89z5zsekgvenqwp5x4p5ydzp8qmrz3zpgcmnydjptyfc40, validator: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3 }, amount: Amount { raw: 3334636 } }, NextEpoch, Withdraw { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 7942329 } }, NextEpoch, Unbond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 878389 } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, UnjailValidator { address: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, UnjailValidator { address: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, Bond { id: BondId { source: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 5376602 } }, UnjailValidator { address: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, Unbond { id: BondId { source: Implicit: atest1d9khqw36xc6nvvf4g9znxvf3xdrrgvfexuen2dek8qmnqse58q6ygdpkxeznz3j9xyeyydfht747xe, validator: Established: atest1v4ehgw36gsm5xvzygg65zvjpxpprw32z89q5y334gvenzdf5x5e5zsjpgfrygwpc8qcnswf32ad0uk }, amount: Amount { raw: 1118174 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 286221 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 73579 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 2010212 } }, Bond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 4276553 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 54860 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 145154 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3, validator: Established: atest1v4ehgw36g3qnv3fnxvu5z3jpx5urjsesxs6ny3pcgs652333x3pn2wzyx4rrqwpngveny32p9qxcv3 }, amount: Amount { raw: 1941194 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 93 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw3689rrqdp58pznydecgyu5xs3cxdznvd6xxsmng32zxumrxvpj8qenydejgfzygwzxlu6r7s, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 }, amount: Amount { raw: 9992596 } }, Bond { id: BondId { source: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, validator: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv }, amount: Amount { raw: 504024 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 5640962 } }, InitValidator { address: Established: atest1v4ehgw368qmnzsfeg5urqw2p8pq5gsf4ggcnqdz9xvc5vsfjxc6nvsekgsmyv3jp8ym52wph0hm33r, consensus_key: Ed25519(PublicKey(VerificationKey("2bccbdf7490f98b2e258a399b75c74bd1b71e9f6f4cc2160edbe3186e23d30e4"))), commission_rate: Dec(427420.000000), max_commission_rate_change: Dec(574220.000000) }, Misbehavior { address: Established: atest1v4ehgw36x5unyvphgc6yx32rgvcyvd35g3p5y3zx89znzd6zxgerqsjp89qnqvzyxsenyvehtufkzv, slash_type: DuplicateVote, infraction_epoch: Epoch(12), height: 0 }, Bond { id: BondId { source: Implicit: atest1d9khqw368pq5g3f3gceygvpjxuenyveexary2wzx8ycnw3zpg9zrvvp4xger2dzyxuunwvjz4n93ww, validator: Established: atest1v4ehgw36gc6njdpcxycnwv2zx9zrsdjxg9zrqvjzxuurxve5x3rryde48pqnjsekg3przs2z8dz595 }, amount: Amount { raw: 4019468 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36xscrsve3geqnwd2x8qmrzwpe89z5zsekgvenqwp5x4p5ydzp8qmrz3zpgcmnydjptyfc40, validator: Established: atest1v4ehgw36gvcrgdeex5ensvfkgccyxve3x3pnys6xxpzr2s6rxuurv3j9g4pyysjzxq6ygdzyt2wxa3 }, amount: Amount { raw: 5683219 } }, Bond { id: BondId { source: Implicit: atest1d9khqw368pz5zd3sgeqnxve4g9ryv3zzggerqdf3xqmrywfng4zrs3pkx5enydesg5mr2v6p4v8rst, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 6886837 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 7852494 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 749047 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gdp52wp4xv6yyd3nx9pnysfn89znjsen8quyvwfkgycnjs29x9ryxveh8prygsfecye5dj, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 9097957 } }, Bond { id: BondId { source: Established: atest1v4ehgw36g9rryv3sx5c5v33sgsmrsd3egerrgdenx3zy2sfex4prvsehxcurydjx8qu5zdz9f2npes, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 6781624 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw36gve5zdf4gccygv6zxgcnxwzrgv65x32rg4zrxv34g9prvs2pxqmnzve5xvuns33czq9awp, validator: Established: atest1v4ehgw36xsuy2vzx89pygd35gsurs3f3xsenz3pnxgmnws29xfrrzvp3xeq5yvjygsmnz33crlu8uu }, amount: Amount { raw: 123577 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gvmrzsf58yurxsjxgfqnqv6yg56nwv69xv6yv3zpx9znv3jpg4p5zdpnxpznzv3hq7q2az, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 1515359 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 9136180 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368yenjvpjxcu5vv33x3zrqw2zgg6nsvzrx9prxd2pgsmyxwfjxgunvs3exerrydp3csdkvr, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 190090 } }, Unbond { id: BondId { source: Implicit: atest1d9khqw368pz5zd3sgeqnxve4g9ryv3zzggerqdf3xqmrywfng4zrs3pkx5enydesg5mr2v6p4v8rst, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 2817512 } }, NextEpoch, Bond { id: BondId { source: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6, validator: Established: atest1v4ehgw36g3pyvvekx3q52dzr8q6ngvee8pzrzv2xgscr2sfh8ymyzwfjxdzrwv3jxuur2s2ydfjhs6 }, amount: Amount { raw: 5207922 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36x5uyvv2px4pr2d3cgdpry3zzxq6nsd6yg5mnwsjzgcervdpegsunqd3kgy6ygvpjyvyhzj, validator: Established: atest1v4ehgw368qcrqd2ygvmyyvf4g9qnvv3kxucrwv3hxg6ryve4x56r233cxucnysjrxsmygdj9yer4pz }, amount: Amount { raw: 70961 } }, Bond { id: BondId { source: Established: atest1v4ehgw36gdzns33sgsmr2wz9x4rrxdenx3zyysfcxcmry32pgeznjw2zx4zrysjxgeryxsfc2etu33, validator: Established: atest1v4ehgw36ggcrz3zygyunqsfjggmnq33h8ycnsdphxepnsve4gerrss2pgfp5z3psgccrj33klenl5r }, amount: Amount { raw: 9056961 } }, Unbond { id: BondId { source: Established: atest1v4ehgw36gvmrzsf58yurxsjxgfqnqv6yg56nwv69xv6yv3zpx9znv3jpg4p5zdpnxpznzv3hq7q2az, validator: Established: atest1v4ehgw36xgm5ydpkxq6nxdzxxveyg3jygceyzwpnx4prvwpnx5ey2wpnx9zrj3phxvcnjwzpn29wcd }, amount: Amount { raw: 1451932 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36gcunwdzyxpz5xs2rxuuyxvfcgfznzd3hg9zrzdfnx5crwv69ggcnvsjpgc65gd33uuymj8, validator: Established: atest1v4ehgw36xucy2dfcxdzrxvpjx5uygwzrxpzrjs3jx4p5vvjrxdq5yvpjx5e5zs3jxdqng3pcplv2ch }, amount: Amount { raw: 1463719 } }, Withdraw { id: BondId { source: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6, validator: Established: atest1v4ehgw36gcur2v2p89z5ys6xgdqngvjxxuu52v3excm52sejx9znwdpjgfq5vv6rxgurwvzxn85ca6 } }, Bond { id: BondId { source: Implicit: atest1d9khqw36x5uyvv2px4pr2d3cgdpry3zzxq6nsd6yg5mnwsjzgcervdpegsunqd3kgy6ygvpjyvyhzj, validator: Established: atest1v4ehgw368qcrqd2ygvmyyvf4g9qnvv3kxucrwv3hxg6ryve4x56r233cxucnysjrxsmygdj9yer4pz }, amount: Amount { raw: 792907 } }, InitValidator { address: Established: atest1v4ehgw36xy65xd3cgvcyxsesgsunys3hgg6nyvekxgerz3fjxaprqvfhxser2wphg5mnjdzpf7edt5, consensus_key: Ed25519(PublicKey(VerificationKey("8f6eeade76a7ce1ccf1d3138807774696d51fcf2c8879e53aa2b082e34eec42b"))), commission_rate: Dec(592790.000000), max_commission_rate_change: Dec(854710.000000) }]) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 1308d92e11..a18ddb1a5e 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -39,6 +39,7 @@ use namada_core::ledger::storage_api::{ self, OptionExt, ResultExt, StorageRead, StorageWrite, }; use namada_core::types::address::{Address, InternalAddress}; +use namada_core::types::dec::Dec; use namada_core::types::key::{ common, tm_consensus_key_raw_hash, PublicKeyTmRawHash, }; @@ -47,28 +48,29 @@ use namada_core::types::token; use once_cell::unsync::Lazy; use parameters::PosParams; use rewards::PosRewardsCalculator; -use rust_decimal::Decimal; -use rust_decimal_macros::dec; use storage::{ bonds_for_source_prefix, bonds_prefix, consensus_keys_key, - decimal_mult_amount, get_validator_address_from_bond, into_tm_voting_power, - is_bond_key, is_unbond_key, is_validator_slashes_key, - last_block_proposer_key, params_key, slashes_prefix, - unbonds_for_source_prefix, unbonds_prefix, validator_address_raw_hash_key, - validator_last_slash_key, validator_max_commission_rate_change_key, + get_validator_address_from_bond, into_tm_voting_power, is_bond_key, + is_unbond_key, is_validator_slashes_key, last_block_proposer_key, + params_key, slashes_prefix, unbonds_for_source_prefix, unbonds_prefix, + validator_address_raw_hash_key, validator_max_commission_rate_change_key, BondDetails, BondsAndUnbondsDetail, BondsAndUnbondsDetails, - ReverseOrdTokenAmount, RewardsAccumulator, SlashedAmount, UnbondDetails, - ValidatorUnbondRecords, + ReverseOrdTokenAmount, RewardsAccumulator, UnbondDetails, }; use thiserror::Error; use types::{ - decimal_mult_i128, BelowCapacityValidatorSet, BelowCapacityValidatorSets, - BondId, Bonds, CommissionRates, ConsensusValidator, ConsensusValidatorSet, - ConsensusValidatorSets, EpochedSlashes, GenesisValidator, Position, - RewardsProducts, Slash, SlashType, Slashes, TotalDeltas, Unbonds, - ValidatorAddresses, ValidatorConsensusKeys, ValidatorDeltas, - ValidatorPositionAddresses, ValidatorSetPositions, ValidatorSetUpdate, - ValidatorState, ValidatorStates, VoteInfo, WeightedValidator, + BelowCapacityValidatorSet, BelowCapacityValidatorSets, BondId, Bonds, + CommissionRates, ConsensusValidator, ConsensusValidatorSet, + ConsensusValidatorSets, GenesisValidator, Position, RewardsProducts, Slash, + SlashType, Slashes, TotalDeltas, Unbonds, ValidatorConsensusKeys, + ValidatorDeltas, ValidatorPositionAddresses, ValidatorSetPositions, + ValidatorSetUpdate, ValidatorState, ValidatorStates, VoteInfo, + WeightedValidator, +}; + +use crate::storage::{ + validator_last_slash_key, EpochedSlashes, SlashedAmount, + ValidatorAddresses, ValidatorUnbondRecords, }; /// Address of the PoS account implemented as a native VP @@ -130,7 +132,7 @@ pub enum UnbondError { #[error( "Trying to withdraw more tokens ({0}) than the amount bonded ({0})" )] - UnbondAmountGreaterThanBond(token::Amount, token::Amount), + UnbondAmountGreaterThanBond(String, String), #[error("No bonds found for the validator {0}")] ValidatorHasNoBonds(Address), #[error("Voting power not found for the validator {0}")] @@ -169,9 +171,9 @@ pub enum SlashError { #[derive(Error, Debug)] pub enum CommissionRateChangeError { #[error("Unexpected negative commission rate {0} for validator {1}")] - NegativeRate(Decimal, Address), + NegativeRate(Dec, Address), #[error("Rate change of {0} is too large for validator {1}")] - RateChangeTooLarge(Decimal, Address), + RateChangeTooLarge(Dec, Address), #[error( "There is no maximum rate change written in storage for validator {0}" )] @@ -527,7 +529,7 @@ where pub fn read_validator_max_commission_rate_change( storage: &S, validator: &Address, -) -> storage_api::Result> +) -> storage_api::Result> where S: StorageRead, { @@ -539,7 +541,7 @@ where pub fn write_validator_max_commission_rate_change( storage: &mut S, validator: &Address, - change: Decimal, + change: Dec, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -862,7 +864,10 @@ where S: StorageRead + StorageWrite, { let amount = amount.change(); - tracing::debug!("Bonding token amount {amount} at epoch {current_epoch}."); + tracing::debug!( + "Bonding token amount {} at epoch {current_epoch}", + amount.to_string_native() + ); let params = read_pos_params(storage)?; let pipeline_epoch = current_epoch + params.pipeline_len; if let Some(source) = source { @@ -898,8 +903,12 @@ where let delta = bond_handle .get_delta_val(storage, ep, ¶ms)? .unwrap_or_default(); - if delta != 0 { - tracing::debug!("bond ∆ at epoch {}: {}", ep, delta); + if !delta.is_zero() { + tracing::debug!( + "bond ∆ at epoch {}: {}", + ep, + delta.to_string_native() + ); } } @@ -924,8 +933,12 @@ where let delta = bond_handle .get_delta_val(storage, ep, ¶ms)? .unwrap_or_default(); - if delta != 0 { - tracing::debug!("bond ∆ at epoch {}: {}", ep, delta); + if !delta.is_zero() { + tracing::debug!( + "bond ∆ at epoch {}: {}", + ep, + delta.to_string_native() + ); } } @@ -1083,7 +1096,7 @@ fn update_validator_set( where S: StorageRead + StorageWrite, { - if token_change == 0_i128 { + if token_change.is_zero() { return Ok(()); } let epoch = current_epoch + params.pipeline_len; @@ -1523,7 +1536,10 @@ where S: StorageRead + StorageWrite, { let amount = amount.change(); - tracing::debug!("Unbonding token amount {amount} at epoch {current_epoch}"); + tracing::debug!( + "Unbonding token amount {} at epoch {current_epoch}", + amount.to_string_native() + ); let params = read_pos_params(storage)?; let pipeline_epoch = current_epoch + params.pipeline_len; @@ -1565,8 +1581,12 @@ where let delta = bonds_handle .get_delta_val(storage, ep, ¶ms)? .unwrap_or_default(); - if delta != 0 { - tracing::debug!("bond ∆ at epoch {}: {}", ep, delta); + if !delta.is_zero() { + tracing::debug!( + "bond ∆ at epoch {}: {}", + ep, + delta.to_string_native() + ); } } @@ -1576,8 +1596,9 @@ where .unwrap_or_default(); if amount > remaining_at_pipeline { return Err(UnbondError::UnbondAmountGreaterThanBond( - token::Amount::from_change(amount), - token::Amount::from_change(remaining_at_pipeline), + token::Amount::from_change(amount).to_string_native(), + token::Amount::from_change(remaining_at_pipeline) + .to_string_native(), ) .into()); } @@ -1666,13 +1687,17 @@ where let delta = bonds_handle .get_delta_val(storage, ep, ¶ms)? .unwrap_or_default(); - if delta != 0 { - tracing::debug!("bond ∆ at epoch {}: {}", ep, delta); + if !delta.is_zero() { + tracing::debug!( + "bond ∆ at epoch {}: {}", + ep, + delta.to_string_native() + ); } } tracing::debug!( "Token change including slashes on unbond = {}", - -amount_after_slashing + (-amount_after_slashing).to_string_native() ); // Update the validator set at the pipeline offset. Since unbonding from a @@ -1720,7 +1745,7 @@ where fn get_slashed_amount( params: &PosParams, amount: token::Amount, - slashes: &BTreeMap, + slashes: &BTreeMap, ) -> storage_api::Result { // println!("FN `get_slashed_amount`"); @@ -1751,7 +1776,7 @@ fn get_slashed_amount( computed_amounts.remove(item.0); } computed_amounts.push(SlashedAmount { - amount: decimal_mult_amount(*slash_rate, updated_amount), + amount: *slash_rate * updated_amount, epoch: *infraction_epoch, }); } @@ -1800,8 +1825,8 @@ pub fn become_validator( address: &Address, consensus_key: &common::PublicKey, current_epoch: Epoch, - commission_rate: Decimal, - max_commission_rate_change: Decimal, + commission_rate: Dec, + max_commission_rate_change: Dec, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -1894,7 +1919,8 @@ where ) = unbond?; tracing::debug!( - "Unbond delta ({start_epoch}..{withdraw_epoch}), amount {amount}", + "Unbond delta ({start_epoch}..{withdraw_epoch}), amount {}", + amount.to_string_native() ); // TODO: adding slash rates in same epoch, applying cumulatively in dif @@ -1919,13 +1945,14 @@ where let amount_after_slashing = get_slashed_amount(¶ms, amount, &slashes_for_this_unbond)?; - // total_slashed += - // amount - token::Amount::from_change(amount_after_slashing); - withdrawable_amount += - token::Amount::from_change(amount_after_slashing); + // total_slashed += amount - token::Amount::from(amount_after_slashing); + withdrawable_amount += token::Amount::from(amount_after_slashing); unbonds_to_remove.push((withdraw_epoch, start_epoch)); } - tracing::debug!("Withdrawing total {withdrawable_amount}"); + tracing::debug!( + "Withdrawing total {}", + withdrawable_amount.to_string_native() + ); // Remove the unbond data from storage for (withdraw_epoch, start_epoch) in unbonds_to_remove { @@ -1964,19 +1991,19 @@ where pub fn change_validator_commission_rate( storage: &mut S, validator: &Address, - new_rate: Decimal, + new_rate: Dec, current_epoch: Epoch, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { - if new_rate < Decimal::ZERO { - return Err(CommissionRateChangeError::NegativeRate( - new_rate, - validator.clone(), - ) - .into()); - } + // if new_rate < Uint::zero() { + // return Err(CommissionRateChangeError::NegativeRate( + // new_rate, + // validator.clone(), + // ) + // .into()); + // } let max_change = read_validator_max_commission_rate_change(storage, validator)?; @@ -2000,8 +2027,16 @@ where let rate_before_pipeline = commission_handle .get(storage, pipeline_epoch.prev(), ¶ms)? .expect("Could not find a rate in given epoch"); - let change_from_prev = new_rate - rate_before_pipeline; - if change_from_prev.abs() > max_change.unwrap() { + + // TODO: change this back if we use `Dec` type with a signed integer + // let change_from_prev = new_rate - rate_before_pipeline; + // if change_from_prev.abs() > max_change.unwrap() { + let change_from_prev = if new_rate > rate_before_pipeline { + new_rate - rate_before_pipeline + } else { + rate_before_pipeline - new_rate + }; + if change_from_prev > max_change.unwrap() { return Err(CommissionRateChangeError::RateChangeTooLarge( change_from_prev, validator.clone(), @@ -2033,8 +2068,8 @@ where tracing::error!( "PoS system transfer error, the source doesn't have \ sufficient balance. It has {}, but {} is required", - src_balance, - amount + src_balance.to_string_native(), + amount.to_string_native(), ); } src_balance.spend(&amount); @@ -2103,10 +2138,10 @@ where // TODO: review this logic carefully, apply rewards let slashes = find_validator_slashes(storage, &bond_id.validator)?; let slash_rates = slashes.into_iter().fold( - BTreeMap::::new(), + BTreeMap::::new(), |mut map, slash| { let tot_rate = map.entry(slash.epoch).or_default(); - *tot_rate = cmp::min(Decimal::ONE, *tot_rate + slash.rate); + *tot_rate = cmp::min(Dec::one(), *tot_rate + slash.rate); map }, ); @@ -2121,17 +2156,17 @@ where continue; } - total += token::Amount::from_change(delta); - total_active += token::Amount::from_change(delta); + total += token::Amount::from(delta); + total_active += token::Amount::from(delta); for (slash_epoch, rate) in &slash_rates { if *slash_epoch < bond_epoch { continue; } // TODO: think about truncation - let current_slashed = decimal_mult_i128(*rate, delta); + let current_slashed = *rate * delta; total_active - .checked_sub(token::Amount::from_change(current_slashed)) + .checked_sub(token::Amount::from(current_slashed)) .unwrap_or_default(); } } @@ -2172,7 +2207,8 @@ where ) = validator.unwrap(); tracing::debug!( - "Consensus validator address {address}, stake {cur_stake}" + "Consensus validator address {address}, stake {}", + cur_stake.to_string_native() ); // Check if the validator was consensus in the previous epoch with @@ -2230,7 +2266,7 @@ where ); Some(ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key, - bonded_stake: cur_stake.into(), + bonded_stake: cur_stake, })) }); let cur_below_capacity_validators = @@ -2252,7 +2288,8 @@ where let cur_stake = token::Amount::from(cur_stake); tracing::debug!( - "Below-capacity validator address {address}, stake {cur_stake}" + "Below-capacity validator address {address}, stake {}", + cur_stake.to_string_native() ); let prev_validator_stake = validator_deltas_handle(&address) @@ -2516,7 +2553,7 @@ where } let change: token::Change = BorshDeserialize::try_from_slice(&val_bytes).ok()?; - if change == 0 { + if change.is_zero() { return None; } return Some((bond_id, start, change)); @@ -2689,14 +2726,14 @@ fn make_bond_details( .cloned() .unwrap_or_default(); let amount = token::Amount::from_change(change); - let mut slash_rates_by_epoch = BTreeMap::::new(); + let mut slash_rates_by_epoch = BTreeMap::::new(); let validator_slashes = applied_slashes.entry(validator.clone()).or_default(); for slash in slashes { if slash.epoch >= start { let cur_rate = slash_rates_by_epoch.entry(slash.epoch).or_default(); - *cur_rate = cmp::min(Decimal::ONE, *cur_rate + slash.rate); + *cur_rate = cmp::min(Dec::one(), *cur_rate + slash.rate); if !prev_applied_slashes.iter().any(|s| s == slash) { validator_slashes.push(slash.clone()); @@ -2733,7 +2770,7 @@ fn make_unbond_details( .get(validator) .cloned() .unwrap_or_default(); - let mut slash_rates_by_epoch = BTreeMap::::new(); + let mut slash_rates_by_epoch = BTreeMap::::new(); let validator_slashes = applied_slashes.entry(validator.clone()).or_default(); @@ -2748,7 +2785,7 @@ fn make_unbond_details( .unwrap_or_default() { let cur_rate = slash_rates_by_epoch.entry(slash.epoch).or_default(); - *cur_rate = cmp::min(Decimal::ONE, *cur_rate + slash.rate); + *cur_rate = cmp::min(Dec::one(), *cur_rate + slash.rate); if !prev_applied_slashes.iter().any(|s| s == slash) { validator_slashes.push(slash.clone()); @@ -2833,7 +2870,7 @@ where debug_assert_eq!( into_tm_voting_power( params.tm_votes_per_token, - stake_from_deltas + stake_from_deltas, ), i64::try_from(validator_vp).unwrap_or_default(), ); @@ -2848,8 +2885,8 @@ where let rewards_calculator = PosRewardsCalculator { proposer_reward: params.block_proposer_reward, signer_reward: params.block_vote_reward, - signing_stake: u64::from(total_signing_stake), - total_stake: u64::from(total_consensus_stake), + signing_stake: total_signing_stake, + total_stake: total_consensus_stake, }; let coeffs = rewards_calculator .get_reward_coeffs() @@ -2866,10 +2903,9 @@ where // Compute the fractional block rewards for each consensus validator and // update the reward accumulators - let consensus_stake_unscaled: Decimal = - total_consensus_stake.as_dec_unscaled(); - let signing_stake_unscaled: Decimal = total_signing_stake.as_dec_unscaled(); - let mut values: HashMap = HashMap::new(); + let consensus_stake_unscaled: Dec = total_consensus_stake.into(); + let signing_stake_unscaled: Dec = total_signing_stake.into(); + let mut values: HashMap = HashMap::new(); for validator in consensus_validators.iter(storage)? { let ( NestedSubKey::Data { @@ -2887,8 +2923,8 @@ where continue; } - let mut rewards_frac = Decimal::default(); - let stake_unscaled: Decimal = stake.as_dec_unscaled(); + let mut rewards_frac = Dec::zero(); + let stake_unscaled: Dec = stake.into(); // println!( // "NAMADA VALIDATOR STAKE (LOGGING BLOCK REWARDS) OF EPOCH {} = // {}", epoch, stake @@ -2927,25 +2963,25 @@ pub fn compute_cubic_slash_rate( storage: &S, params: &PosParams, infraction_epoch: Epoch, -) -> storage_api::Result +) -> storage_api::Result where S: StorageRead, { // println!("COMPUTING CUBIC SLASH RATE"); - let mut sum_vp_fraction = Decimal::ZERO; + let mut sum_vp_fraction = Dec::zero(); let (start_epoch, end_epoch) = params.cubic_slash_epoch_window(infraction_epoch); for epoch in Epoch::iter_bounds_inclusive(start_epoch, end_epoch) { let consensus_stake = - Decimal::from(get_total_consensus_stake(storage, epoch)?); + Dec::from(get_total_consensus_stake(storage, epoch)?); // println!("Consensus stake in epoch {}: {}", epoch, consensus_stake); let processing_epoch = epoch + params.slash_processing_epoch_offset(); let slashes = enqueued_slashes_handle().at(&processing_epoch); - let infracting_stake = slashes - .iter(storage)? - .map(|res| { + let infracting_stake = slashes.iter(storage)?.fold( + Ok(Dec::zero()), + |acc: storage_api::Result, res| { let ( NestedSubKey::Data { key: validator, @@ -2959,16 +2995,20 @@ where .unwrap_or_default(); // println!("Val {} stake: {}", &validator, validator_stake); - Ok(Decimal::from(validator_stake)) + if let Ok(inner) = acc { + Ok(inner + Dec::from(validator_stake)) + } else { + acc + } // TODO: does something more complex need to be done // here in the event some of these slashes correspond to // the same validator? - }) - .sum::>()?; + }, + )?; sum_vp_fraction += infracting_stake / consensus_stake; } // println!("sum_vp_fraction: {}", sum_vp_fraction); - Ok(dec!(9) * sum_vp_fraction * sum_vp_fraction) + Ok(Dec::new(9, 0).unwrap() * sum_vp_fraction * sum_vp_fraction) } /// Record a slash for a misbehavior that has been received from Tendermint and @@ -2991,7 +3031,7 @@ where epoch: evidence_epoch, block_height: evidence_block_height, r#type: slash_type, - rate: Decimal::ZERO, // Let the rate be 0 initially before processing + rate: Dec::zero(), // Let the rate be 0 initially before processing }; // Need `+1` because we process at the beginning of a new epoch let processing_epoch = @@ -3172,7 +3212,7 @@ where debug_assert_eq!(enqueued_slash.epoch, infraction_epoch); let slash_rate = cmp::min( - Decimal::ONE, + Dec::one(), cmp::max( enqueued_slash.r#type.get_slash_rate(¶ms), cubic_slash_rate, @@ -3213,10 +3253,10 @@ where "Validator {} stake at infraction epoch {} = {}", &validator, infraction_epoch, - validator_stake_at_infraction + validator_stake_at_infraction.to_string_native() ); - let mut total_rate = Decimal::ZERO; + let mut total_rate = Dec::zero(); for enqueued_slash in &enqueued_slashes { // Add this slash to the list of validator's slashes in storage @@ -3225,7 +3265,7 @@ where total_rate += enqueued_slash.rate; } - total_rate = cmp::min(Decimal::ONE, total_rate); + total_rate = cmp::min(Dec::one(), total_rate); // Find the total amount deducted from the deltas due to unbonds that // became active after the infraction epoch, accounting for slashes @@ -3248,7 +3288,7 @@ where let (start, unbond_amount) = unbond?; tracing::debug!( "UnbondRecord: amount = {}, start_epoch {}", - &unbond_amount, + unbond_amount.to_string_native(), &start ); if start <= infraction_epoch { @@ -3283,7 +3323,7 @@ where tracing::debug!( "Total unbonded (epoch {}) w slashing = {}", epoch, - total_unbonded + total_unbonded.to_string_native() ); } @@ -3300,7 +3340,7 @@ where tracing::debug!( "Epoch {}\nLast slash = {}", current_epoch + offset, - last_slash + last_slash.to_string_native() ); let mut recent_unbonds = token::Change::default(); let unbonds = @@ -3310,7 +3350,7 @@ where let (start, unbond_amount) = unbond?; tracing::debug!( "UnbondRecord: amount = {}, start_epoch {}", - &unbond_amount, + unbond_amount.to_string_native(), &start ); if start <= infraction_epoch { @@ -3345,18 +3385,14 @@ where tracing::debug!( "Total unbonded (offset {}) w slashing = {}", offset, - total_unbonded + total_unbonded.to_string_native() ); } - let this_slash = decimal_mult_amount( - total_rate, - validator_stake_at_infraction - total_unbonded, - ) - .change(); + let this_slash = total_rate + * (validator_stake_at_infraction - total_unbonded).change(); let diff_slashed_amount = last_slash - this_slash; last_slash = this_slash; - // println!("This slash = {}", this_slash); // println!("Diff slashed amount = {}", diff_slashed_amount); // total_slashed -= diff_slashed_amount; @@ -3397,7 +3433,7 @@ where tracing::debug!( "Deltas change = {} at offset {} for validator {}", - delta, + delta.to_string_native(), offset, &validator ); @@ -3553,11 +3589,11 @@ fn find_slashes_in_range( start: Epoch, end: Option, validator: &Address, -) -> storage_api::Result> +) -> storage_api::Result> where S: StorageRead, { - let mut slashes = BTreeMap::::new(); + let mut slashes = BTreeMap::::new(); for slash in validator_slashes_handle(validator).iter(storage)? { let slash = slash?; if start <= slash.epoch @@ -3568,7 +3604,7 @@ where // &slash.epoch, &slash.rate // ); let cur_rate = slashes.entry(slash.epoch).or_default(); - *cur_rate = cmp::min(*cur_rate + slash.rate, Decimal::ONE); + *cur_rate = cmp::min(*cur_rate + slash.rate, Dec::one()); } } Ok(slashes) diff --git a/proof_of_stake/src/parameters.rs b/proof_of_stake/src/parameters.rs index 0e54e0f7f2..080d8b89d5 100644 --- a/proof_of_stake/src/parameters.rs +++ b/proof_of_stake/src/parameters.rs @@ -1,10 +1,9 @@ //! Proof-of-Stake system parameters use borsh::{BorshDeserialize, BorshSerialize}; +use namada_core::types::dec::Dec; use namada_core::types::storage::Epoch; -use rust_decimal::prelude::ToPrimitive; -use rust_decimal::Decimal; -use rust_decimal_macros::dec; +use namada_core::types::uint::Uint; use thiserror::Error; /// Proof-of-Stake system parameters, set at genesis and can only be changed via @@ -24,22 +23,22 @@ pub struct PosParams { /// The voting power per fundamental unit of the staking token (namnam). /// Used in validators' voting power calculation to interface with /// tendermint. - pub tm_votes_per_token: Decimal, + pub tm_votes_per_token: Dec, /// Amount of tokens rewarded to a validator for proposing a block - pub block_proposer_reward: Decimal, + pub block_proposer_reward: Dec, /// Amount of tokens rewarded to each validator that voted on a block /// proposal - pub block_vote_reward: Decimal, + pub block_vote_reward: Dec, /// Maximum staking rewards rate per annum - pub max_inflation_rate: Decimal, + pub max_inflation_rate: Dec, /// Target ratio of staked NAM tokens to total NAM tokens - pub target_staked_ratio: Decimal, + pub target_staked_ratio: Dec, /// Fraction of validator's stake that should be slashed on a duplicate /// vote. - pub duplicate_vote_min_slash_rate: Decimal, + pub duplicate_vote_min_slash_rate: Dec, /// Fraction of validator's stake that should be slashed on a light client /// attack. - pub light_client_attack_min_slash_rate: Decimal, + pub light_client_attack_min_slash_rate: Dec, /// Number of epochs above and below (separately) the current epoch to /// consider when doing cubic slashing pub cubic_slashing_window_length: u64, @@ -53,17 +52,18 @@ impl Default for PosParams { unbonding_len: 21, // 1 voting power per 1 fundamental token (10^6 per NAM or 1 per // namnam) - tm_votes_per_token: dec!(1.0), - block_proposer_reward: dec!(0.125), - block_vote_reward: dec!(0.1), + tm_votes_per_token: Dec::one(), + block_proposer_reward: Dec::new(125, 3).expect("Test failed"), + block_vote_reward: Dec::new(1, 1).expect("Test failed"), // PoS inflation of 10% - max_inflation_rate: dec!(0.1), + max_inflation_rate: Dec::new(1, 1).expect("Test failed"), // target staked ratio of 2/3 - target_staked_ratio: dec!(0.6667), + target_staked_ratio: Dec::new(6667, 4).expect("Test failed"), // slash 0.1% - duplicate_vote_min_slash_rate: dec!(0.001), + duplicate_vote_min_slash_rate: Dec::new(1, 3).expect("Test failed"), // slash 0.1% - light_client_attack_min_slash_rate: dec!(0.001), + light_client_attack_min_slash_rate: Dec::new(1, 3) + .expect("Test failed"), cubic_slashing_window_length: 1, } } @@ -76,9 +76,9 @@ pub enum ValidationError { "Maximum total voting power is too large: got {0}, expected at most \ {MAX_TOTAL_VOTING_POWER}" )] - TotalVotingPowerTooLarge(u64), + TotalVotingPowerTooLarge(Uint), #[error("Votes per token cannot be greater than 1, got {0}")] - VotesPerTokenGreaterThanOne(Decimal), + VotesPerTokenGreaterThanOne(Dec), #[error("Pipeline length must be >= 2, got {0}")] PipelineLenTooShort(u64), #[error( @@ -118,28 +118,26 @@ impl PosParams { // Check maximum total voting power cannot get larger than what // Tendermint allows - // - // TODO: decide if this is still a check we want to do (in its current - // state with our latest voting power conventions, it will fail - // always) - let max_total_voting_power = Decimal::from(self.max_validator_slots) - * self.tm_votes_per_token - * Decimal::from(TOKEN_MAX_AMOUNT); + let max_total_voting_power = (self.tm_votes_per_token + * TOKEN_MAX_AMOUNT + * self.max_validator_slots) + .to_uint() + .expect("Cannot fail"); match i64::try_from(max_total_voting_power) { Ok(max_total_voting_power_i64) => { if max_total_voting_power_i64 > MAX_TOTAL_VOTING_POWER { errors.push(ValidationError::TotalVotingPowerTooLarge( - max_total_voting_power.to_u64().unwrap(), + max_total_voting_power, )) } } Err(_) => errors.push(ValidationError::TotalVotingPowerTooLarge( - max_total_voting_power.to_u64().unwrap(), + max_total_voting_power, )), } // Check that there is no more than 1 vote per token - if self.tm_votes_per_token > dec!(1.0) { + if self.tm_votes_per_token > Dec::one() { errors.push(ValidationError::VotesPerTokenGreaterThanOne( self.tm_votes_per_token, )) @@ -197,8 +195,8 @@ mod tests { /// Testing helpers #[cfg(any(test, feature = "testing"))] pub mod testing { + use namada_core::types::dec::Dec; use proptest::prelude::*; - use rust_decimal::Decimal; use super::*; @@ -210,13 +208,13 @@ pub mod testing { // `unbonding_len` > `pipeline_len` unbonding_len in pipeline_len + 1..pipeline_len + 8, pipeline_len in Just(pipeline_len), - tm_votes_per_token in 1..10_001_u64) + tm_votes_per_token in 1..10_001_i128) -> PosParams { PosParams { max_validator_slots, pipeline_len, unbonding_len, - tm_votes_per_token: Decimal::from(tm_votes_per_token) / dec!(10_000), + tm_votes_per_token: Dec::new(tm_votes_per_token, 4).expect("Test failed"), // The rest of the parameters that are not being used in the PoS // VP are constant for now ..Default::default() @@ -224,10 +222,10 @@ pub mod testing { } } - /// Get an arbitrary rate - a Decimal value between 0 and 1 inclusive, with + /// Get an arbitrary rate - a Dec value between 0 and 1 inclusive, with /// some fixed precision - pub fn arb_rate() -> impl Strategy { - (0..=100_000_u64) - .prop_map(|num| Decimal::from(num) / Decimal::from(100_000_u64)) + pub fn arb_rate() -> impl Strategy { + (0..=100_000_i128) + .prop_map(|num| Dec::new(num, 5).expect("Test failed")) } } diff --git a/proof_of_stake/src/pos_queries.rs b/proof_of_stake/src/pos_queries.rs index 251350e414..6873531cd9 100644 --- a/proof_of_stake/src/pos_queries.rs +++ b/proof_of_stake/src/pos_queries.rs @@ -123,9 +123,8 @@ where pub fn get_total_voting_power(self, epoch: Option) -> token::Amount { self.get_consensus_validators(epoch) .iter() - .map(|validator| u64::from(validator.bonded_stake)) - .sum::() - .into() + .map(|validator| validator.bonded_stake) + .sum::() } /// Simple helper function for the ledger to get balances diff --git a/proof_of_stake/src/rewards.rs b/proof_of_stake/src/rewards.rs index 6f830d9c52..26d1b91442 100644 --- a/proof_of_stake/src/rewards.rs +++ b/proof_of_stake/src/rewards.rs @@ -1,10 +1,13 @@ //! PoS rewards distribution. -use rust_decimal::Decimal; -use rust_decimal_macros::dec; +use namada_core::types::dec::Dec; +use namada_core::types::token::Amount; +use namada_core::types::uint::{Uint, I256}; use thiserror::Error; -const MIN_PROPOSER_REWARD: Decimal = dec!(0.01); +/// This is equal to 0.01. +const MIN_PROPOSER_REWARD: Dec = + Dec(I256(Uint([10000000000u64, 0u64, 0u64, 0u64]))); /// Errors during rewards calculation #[derive(Debug, Error)] @@ -16,8 +19,8 @@ pub enum RewardsError { least 2/3 of the total bonded stake)." )] InsufficientVotes { - votes_needed: u64, - signing_stake: u64, + votes_needed: Uint, + signing_stake: Uint, }, /// rewards coefficients are not set #[error("Rewards coefficients are not properly set.")] @@ -28,9 +31,9 @@ pub enum RewardsError { #[derive(Debug, Copy, Clone)] #[allow(missing_docs)] pub struct PosRewards { - pub proposer_coeff: Decimal, - pub signer_coeff: Decimal, - pub active_val_coeff: Decimal, + pub proposer_coeff: Dec, + pub signer_coeff: Dec, + pub active_val_coeff: Dec, } /// Holds relevant PoS parameters and is used to calculate the coefficients for @@ -38,13 +41,13 @@ pub struct PosRewards { #[derive(Debug, Copy, Clone)] pub struct PosRewardsCalculator { /// Rewards fraction that goes to the block proposer - pub proposer_reward: Decimal, + pub proposer_reward: Dec, /// Rewards fraction that goes to the block signers - pub signer_reward: Decimal, + pub signer_reward: Dec, /// Total stake of validators who signed the block - pub signing_stake: u64, + pub signing_stake: Amount, /// Total stake of the whole consensus set - pub total_stake: u64, + pub total_stake: Amount, } impl PosRewardsCalculator { @@ -64,18 +67,18 @@ impl PosRewardsCalculator { if signing_stake < votes_needed { return Err(RewardsError::InsufficientVotes { - votes_needed, - signing_stake, + votes_needed: votes_needed.into(), + signing_stake: signing_stake.into(), }); } // Logic for determining the coefficients. - let proposer_coeff = proposer_reward - * Decimal::from(signing_stake - votes_needed) - / Decimal::from(total_stake) - + MIN_PROPOSER_REWARD; + let proposer_coeff = + Dec::from(proposer_reward * (signing_stake - votes_needed)) + / Dec::from(total_stake) + + MIN_PROPOSER_REWARD; let signer_coeff = signer_reward; - let active_val_coeff = dec!(1.0) - proposer_coeff - signer_coeff; + let active_val_coeff = Dec::one() - proposer_coeff - signer_coeff; let coeffs = PosRewards { proposer_coeff, @@ -87,7 +90,7 @@ impl PosRewardsCalculator { } /// Implement as ceiling of (2/3) * validator set stake - fn get_min_required_votes(&self) -> u64 { - ((2 * self.total_stake) + 3 - 1) / 3 + fn get_min_required_votes(&self) -> Amount { + ((self.total_stake * 2u64) + (3u64 - 1u64)) / 3u64 } } diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 95b8d91fac..2f58e39b33 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -13,16 +13,16 @@ use namada_core::types::address::testing::{ address_from_simple_seed, arb_established_address, }; use namada_core::types::address::{Address, EstablishedAddressGen}; +use namada_core::types::dec::Dec; use namada_core::types::key::common::{PublicKey, SecretKey}; use namada_core::types::key::testing::{ arb_common_keypair, common_sk_from_simple_seed, }; use namada_core::types::storage::{BlockHeight, Epoch}; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_core::types::{address, key, token}; use proptest::prelude::*; use proptest::test_runner::Config; -use rust_decimal::Decimal; -use rust_decimal_macros::dec; // Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see // `tracing` logs from tests use test_log::test; @@ -30,10 +30,10 @@ use test_log::test; use crate::parameters::testing::arb_pos_params; use crate::parameters::PosParams; use crate::types::{ - decimal_mult_amount, into_tm_voting_power, BondDetails, BondId, - BondsAndUnbondsDetails, ConsensusValidator, GenesisValidator, Position, - ReverseOrdTokenAmount, SlashType, UnbondDetails, ValidatorSetUpdate, - ValidatorState, WeightedValidator, + into_tm_voting_power, BondDetails, BondId, BondsAndUnbondsDetails, + ConsensusValidator, GenesisValidator, Position, ReverseOrdTokenAmount, + SlashType, UnbondDetails, ValidatorSetUpdate, ValidatorState, + WeightedValidator, }; use crate::{ become_validator, below_capacity_validator_set_handle, bond_handle, @@ -244,7 +244,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { read_total_stake(&s, ¶ms, pipeline_epoch).unwrap(); // Self-bond - let amount_self_bond = token::Amount::from(100_500_000); + let amount_self_bond = token::Amount::from_uint(100_500_000, 0).unwrap(); credit_tokens(&mut s, &staking_token, &validator.address, amount_self_bond) .unwrap(); bond_tokens( @@ -348,7 +348,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { // Get a non-validating account with tokens let delegator = address::testing::gen_implicit_address(); - let amount_del = token::Amount::from(201_000_000); + let amount_del = token::Amount::from_uint(201_000_000, 0).unwrap(); credit_tokens(&mut s, &staking_token, &delegator, amount_del).unwrap(); let balance_key = token::balance_key(&staking_token, &delegator); let balance = s @@ -484,7 +484,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { // Unbond the self-bond with an amount that will remove all of the self-bond // executed after genesis and some of the genesis bond let amount_self_unbond: token::Amount = - amount_self_bond + (u64::from(validator.tokens) / 2).into(); + amount_self_bond + (validator.tokens / 2); // When the difference is 0, only the non-genesis self-bond is unbonded let unbonded_genesis_self_bond = amount_self_unbond - amount_self_bond != token::Amount::default(); @@ -628,7 +628,7 @@ fn test_bonds_aux(params: PosParams, validators: Vec) { ); // Unbond delegation - let amount_undel = token::Amount::from(1_000_000); + let amount_undel = token::Amount::from_uint(1_000_000, 0).unwrap(); unbond_tokens( &mut s, Some(&delegator), @@ -799,8 +799,8 @@ fn test_become_validator_aux( &new_validator, &consensus_key, current_epoch, - Decimal::new(5, 2), - Decimal::new(5, 2), + Dec::new(5, 2).expect("Dec creation failed"), + Dec::new(5, 2).expect("Dec creation failed"), ) .unwrap(); assert!(is_validator(&s, &new_validator).unwrap()); @@ -822,7 +822,7 @@ fn test_become_validator_aux( // Self-bond to the new validator let staking_token = staking_token_address(&s); - let amount = token::Amount::from(100_500_000); + let amount = token::Amount::from_uint(100_500_000, 0).unwrap(); credit_tokens(&mut s, &staking_token, &new_validator, amount).unwrap(); bond_tokens(&mut s, None, &new_validator, amount, current_epoch).unwrap(); @@ -909,7 +909,8 @@ fn test_slashes_with_unbonding_aux( let val_addr = &validator.address; let val_tokens = validator.tokens; println!( - "Validator that will misbehave addr {val_addr}, tokens {val_tokens}" + "Validator that will misbehave addr {val_addr}, tokens {}", + val_tokens.to_string_native() ); // Genesis @@ -958,8 +959,8 @@ fn test_slashes_with_unbonding_aux( } // Unbond half of the tokens - let unbond_amount = decimal_mult_amount(dec!(0.5), val_tokens); - println!("Going to unbond {unbond_amount}"); + let unbond_amount = Dec::new(5, 1).unwrap() * val_tokens; + println!("Going to unbond {}", unbond_amount.to_string_native()); let unbond_epoch = current_epoch; unbond_tokens(&mut s, None, val_addr, unbond_amount, unbond_epoch).unwrap(); @@ -1004,7 +1005,7 @@ fn test_slashes_with_unbonding_aux( let val_balance_post = read_balance(&s, &token, val_addr).unwrap(); let withdrawn_tokens = val_balance_post - val_balance_pre; - println!("Withdrew {withdrawn_tokens} tokens"); + println!("Withdrew {} tokens", withdrawn_tokens.to_string_native()); assert_eq!(exp_withdraw_from_details, withdrawn_tokens); @@ -1020,17 +1021,17 @@ fn test_slashes_with_unbonding_aux( .rate; println!("Slash 0 rate {slash_rate_0}, slash 1 rate {slash_rate_1}"); - let expected_withdrawn_amount = decimal_mult_amount( - dec!(1) - slash_rate_1, - decimal_mult_amount(dec!(1) - slash_rate_0, unbond_amount), + let expected_withdrawn_amount = Dec::from( + (Dec::one() - slash_rate_1) + * (Dec::one() - slash_rate_0) + * unbond_amount, ); // Allow some rounding error, 1 NAMNAM per each slash - let rounding_error_tolerance = 2; + let rounding_error_tolerance = + Dec::new(2, NATIVE_MAX_DECIMAL_PLACES).unwrap(); assert!( - dbg!( - (expected_withdrawn_amount.change() - withdrawn_tokens.change()) - .abs() - ) <= rounding_error_tolerance + dbg!(expected_withdrawn_amount.abs_diff(&Dec::from(withdrawn_tokens))) + <= rounding_error_tolerance ); // TODO: finish once implemented @@ -1118,20 +1119,27 @@ fn test_validator_sets() { // Start with two genesis validators with 1 NAM stake let epoch = Epoch::default(); - let ((val1, pk1), stake1) = (gen_validator(), token::Amount::whole(1)); - let ((val2, pk2), stake2) = (gen_validator(), token::Amount::whole(1)); - let ((val3, pk3), stake3) = (gen_validator(), token::Amount::whole(10)); - let ((val4, pk4), stake4) = (gen_validator(), token::Amount::whole(1)); - let ((val5, pk5), stake5) = (gen_validator(), token::Amount::whole(100)); - let ((val6, pk6), stake6) = (gen_validator(), token::Amount::whole(1)); - let ((val7, pk7), stake7) = (gen_validator(), token::Amount::whole(1)); - println!("val1: {val1}, {pk1}, {stake1}"); - println!("val2: {val2}, {pk2}, {stake2}"); - println!("val3: {val3}, {pk3}, {stake3}"); - println!("val4: {val4}, {pk4}, {stake4}"); - println!("val5: {val5}, {pk5}, {stake5}"); - println!("val6: {val6}, {pk6}, {stake6}"); - println!("val7: {val7}, {pk7}, {stake7}"); + let ((val1, pk1), stake1) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val2, pk2), stake2) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val3, pk3), stake3) = + (gen_validator(), token::Amount::native_whole(10)); + let ((val4, pk4), stake4) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val5, pk5), stake5) = + (gen_validator(), token::Amount::native_whole(100)); + let ((val6, pk6), stake6) = + (gen_validator(), token::Amount::native_whole(1)); + let ((val7, pk7), stake7) = + (gen_validator(), token::Amount::native_whole(1)); + println!("val1: {val1}, {pk1}, {}", stake1.to_string_native()); + println!("val2: {val2}, {pk2}, {}", stake2.to_string_native()); + println!("val3: {val3}, {pk3}, {}", stake3.to_string_native()); + println!("val4: {val4}, {pk4}, {}", stake4.to_string_native()); + println!("val5: {val5}, {pk5}, {}", stake5.to_string_native()); + println!("val6: {val6}, {pk6}, {}", stake6.to_string_native()); + println!("val7: {val7}, {pk7}, {}", stake7.to_string_native()); init_genesis( &mut s, @@ -1141,15 +1149,17 @@ fn test_validator_sets() { address: val1.clone(), tokens: stake1, consensus_key: pk1.clone(), - commission_rate: Decimal::new(1, 1), - max_commission_rate_change: Decimal::new(1, 1), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), }, GenesisValidator { address: val2.clone(), tokens: stake2, consensus_key: pk2.clone(), - commission_rate: Decimal::new(1, 1), - max_commission_rate_change: Decimal::new(1, 1), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), }, ] .into_iter(), @@ -1300,7 +1310,7 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk3, - bonded_stake: stake3.into(), + bonded_stake: stake3, }) ); @@ -1355,16 +1365,16 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk5, - bonded_stake: stake5.into(), + bonded_stake: stake5, }) ); assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk2)); // Unbond some stake from val1, it should be be swapped with the greatest // below-capacity validator val2 into the below-capacity set - let unbond = token::Amount::from(500_000); + let unbond = token::Amount::from_uint(500_000, 0).unwrap(); let stake1 = stake1 - unbond; - println!("val1 {val1} new stake {stake1}"); + println!("val1 {val1} new stake {}", stake1.to_string_native()); // Because `update_validator_set` and `update_validator_deltas` are // effective from pipeline offset, we use pipeline epoch for the rest of the // checks @@ -1558,16 +1568,16 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk4.clone(), - bonded_stake: stake4.into(), + bonded_stake: stake4, }) ); assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk1)); // Bond some stake to val6, it should be be swapped with the lowest // consensus validator val2 into the consensus set - let bond = token::Amount::from(500_000); + let bond = token::Amount::from_uint(500_000, 0).unwrap(); let stake6 = stake6 + bond; - println!("val6 {val6} new stake {stake6}"); + println!("val6 {val6} new stake {}", stake6.to_string_native()); update_validator_set(&mut s, ¶ms, &val6, bond.change(), epoch).unwrap(); update_validator_deltas( &mut s, @@ -1681,7 +1691,7 @@ fn test_validator_sets() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk6, - bonded_stake: stake6.into(), + bonded_stake: stake6, }) ); assert_eq!(tm_updates[1], ValidatorSetUpdate::Deactivated(pk4)); @@ -1700,7 +1710,7 @@ fn test_validator_sets_swap() { let params = PosParams { max_validator_slots: 2, // Set 0.1 votes per token - tm_votes_per_token: dec!(0.1), + tm_votes_per_token: Dec::new(1, 1).expect("Dec creation failed"), ..Default::default() }; let addr_seed = "seed"; @@ -1752,14 +1762,17 @@ fn test_validator_sets_swap() { // Start with two genesis validators, one with 1 voting power and other 0 let epoch = Epoch::default(); // 1M voting power - let ((val1, pk1), stake1) = (gen_validator(), token::Amount::whole(10)); + let ((val1, pk1), stake1) = + (gen_validator(), token::Amount::native_whole(10)); // 0 voting power - let ((val2, pk2), stake2) = (gen_validator(), token::Amount::from(5)); + let ((val2, pk2), stake2) = + (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); // 0 voting power - let ((val3, pk3), stake3) = (gen_validator(), token::Amount::from(5)); - println!("val1: {val1}, {pk1}, {stake1}"); - println!("val2: {val2}, {pk2}, {stake2}"); - println!("val3: {val3}, {pk3}, {stake3}"); + let ((val3, pk3), stake3) = + (gen_validator(), token::Amount::from_uint(5, 0).unwrap()); + println!("val1: {val1}, {pk1}, {}", stake1.to_string_native()); + println!("val2: {val2}, {pk2}, {}", stake2.to_string_native()); + println!("val3: {val3}, {pk3}, {}", stake3.to_string_native()); init_genesis( &mut s, @@ -1769,15 +1782,17 @@ fn test_validator_sets_swap() { address: val1, tokens: stake1, consensus_key: pk1, - commission_rate: Decimal::new(1, 1), - max_commission_rate_change: Decimal::new(1, 1), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), }, GenesisValidator { address: val2.clone(), tokens: stake2, consensus_key: pk2, - commission_rate: Decimal::new(1, 1), - max_commission_rate_change: Decimal::new(1, 1), + commission_rate: Dec::new(1, 1).expect("Dec creation failed"), + max_commission_rate_change: Dec::new(1, 1) + .expect("Dec creation failed"), }, ] .into_iter(), @@ -1796,9 +1811,9 @@ fn test_validator_sets_swap() { // Add 2 bonds, one for val2 and greater one for val3 let bonds_epoch_1 = pipeline_epoch; - let bond2 = token::Amount::from(1); + let bond2 = token::Amount::from_uint(1, 0).unwrap(); let stake2 = stake2 + bond2; - let bond3 = token::Amount::from(4); + let bond3 = token::Amount::from_uint(4, 0).unwrap(); let stake3 = stake3 + bond3; assert!(stake2 < stake3); @@ -1835,7 +1850,7 @@ fn test_validator_sets_swap() { // Add 2 more bonds, same amount for `val2` and val3` let bonds_epoch_2 = pipeline_epoch; - let bonds = token::Amount::whole(1); + let bonds = token::Amount::native_whole(1); let stake2 = stake2 + bonds; let stake3 = stake3 + bonds; assert!(stake2 < stake3); @@ -1893,7 +1908,7 @@ fn test_validator_sets_swap() { tm_updates[0], ValidatorSetUpdate::Consensus(ConsensusValidator { consensus_key: pk3, - bonded_stake: stake3.into(), + bonded_stake: stake3, }) ); } @@ -1932,7 +1947,11 @@ fn arb_genesis_validators( size: Range, ) -> impl Strategy> { let tokens: Vec<_> = (0..size.end) - .map(|_| (1..=10_000_000_u64).prop_map(token::Amount::from)) + .map(|_| { + (1..=10_000_000_u64).prop_map(|val| { + token::Amount::from_uint(val, 0).expect("This cannot fail") + }) + }) .collect(); (size, tokens).prop_map(|(size, token_amounts)| { // use unique seeds to generate validators' address and consensus key @@ -1944,8 +1963,9 @@ fn arb_genesis_validators( let consensus_sk = common_sk_from_simple_seed(seed); let consensus_key = consensus_sk.to_public(); - let commission_rate = Decimal::new(5, 2); - let max_commission_rate_change = Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).expect("Test failed"); + let max_commission_rate_change = + Dec::new(1, 2).expect("Test failed"); GenesisValidator { address, tokens, diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index df3c85be13..443daa10eb 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -9,16 +9,16 @@ use namada_core::ledger::storage_api::collections::lazy_map::NestedSubKey; use namada_core::ledger::storage_api::token::read_balance; use namada_core::ledger::storage_api::{token, StorageRead}; use namada_core::types::address::{self, Address}; +use namada_core::types::dec::Dec; use namada_core::types::key; use namada_core::types::key::common::PublicKey; use namada_core::types::storage::Epoch; +use namada_core::types::token::Change; use proptest::prelude::*; use proptest::test_runner::Config; use proptest_state_machine::{ prop_state_machine, ReferenceStateMachine, StateMachineTest, }; -use rust_decimal::Decimal; -use rust_decimal_macros::dec; // Use `RUST_LOG=info` (or another tracing level) and `--nocapture` to see // `tracing` logs from tests use test_log::test; @@ -27,9 +27,8 @@ use super::arb_genesis_validators; use crate::parameters::testing::{arb_pos_params, arb_rate}; use crate::parameters::PosParams; use crate::types::{ - decimal_mult_amount, decimal_mult_i128, BondId, GenesisValidator, - ReverseOrdTokenAmount, Slash, SlashType, SlashedAmount, ValidatorState, - WeightedValidator, + BondId, GenesisValidator, ReverseOrdTokenAmount, Slash, SlashType, + SlashedAmount, ValidatorState, WeightedValidator, }; use crate::{ below_capacity_validator_set_handle, consensus_validator_set_handle, @@ -104,8 +103,8 @@ enum Transition { InitValidator { address: Address, consensus_key: PublicKey, - commission_rate: Decimal, - max_commission_rate_change: Decimal, + commission_rate: Dec, + max_commission_rate_change: Dec, }, Bond { id: BondId, @@ -169,7 +168,7 @@ impl StateMachineTest for ConcretePosState { &crate::ADDRESS, ) .unwrap(); - println!("PoS balance: {}", pos_balance); + println!("PoS balance: {}", pos_balance.to_string_native()); match transition { Transition::NextEpoch => { println!("\nCONCRETE Next epoch"); @@ -727,9 +726,11 @@ impl ConcretePosState { { assert!( consensus_stake >= below_cap_stake, - "Consensus validator {consensus_addr} with stake \ - {consensus_stake} and below-capacity {below_cap_addr} \ - with stake {below_cap_stake} should be swapped." + "Consensus validator {consensus_addr} with stake {} and \ + below-capacity {below_cap_addr} with stake {} should be \ + swapped.", + consensus_stake.to_string_native(), + below_cap_stake.to_string_native() ); } } @@ -837,7 +838,7 @@ impl ConcretePosState { if let Some(slash) = slash { assert_eq!(slash.epoch, infraction_epoch); assert_eq!(slash.r#type, slash_type); - assert_eq!(slash.rate, Decimal::ZERO); + assert_eq!(slash.rate, Dec::zero()); } else { panic!("Could not find the slash enqueued"); } @@ -934,10 +935,10 @@ impl ConcretePosState { tracing::debug!( "Consensus val {}, stake: {} ({})", &validator, - u64::from(bonded_stake), - deltas_stake + bonded_stake.to_string_native(), + deltas_stake.to_string_native(), ); - assert!(deltas_stake >= 0); + assert!(!deltas_stake.is_negative()); assert_eq!( bonded_stake, token::Amount::from_change(deltas_stake) @@ -987,8 +988,8 @@ impl ConcretePosState { tracing::debug!( "Below-cap val {}, stake: {} ({})", &validator, - u64::from(bonded_stake), - deltas_stake + bonded_stake.to_string_native(), + deltas_stake.to_string_native(), ); assert_eq!( bonded_stake, @@ -1064,7 +1065,11 @@ impl ConcretePosState { .get_sum(&self.s, epoch, params) .unwrap() .unwrap_or_default(); - tracing::debug!("Jailed val {}, stake {}", &val, stake); + tracing::debug!( + "Jailed val {}, stake {}", + &val, + stake.to_string_native() + ); assert_eq!( state, @@ -1274,11 +1279,13 @@ impl ReferenceStateMachine for AbstractPosState { let arb_unbondable = prop::sample::select(unbondable); let arb_unbond = arb_unbondable.prop_flat_map(|(id, deltas_sum)| { + let deltas_sum = i128::try_from(deltas_sum).unwrap(); // Generate an amount to unbond, up to the sum assert!(deltas_sum > 0); (0..deltas_sum).prop_map(move |to_unbond| { let id = id.clone(); - let amount = token::Amount::from_change(to_unbond); + let amount = + token::Amount::from_change(Change::from(to_unbond)); Transition::Unbond { id, amount } }) }); @@ -1333,7 +1340,7 @@ impl ReferenceStateMachine for AbstractPosState { .validator_stakes .entry(pipeline) .or_default() - .insert(address.clone(), 0_i128); + .insert(address.clone(), 0_i128.into()); // Insert into validator set at pipeline let consensus_set = @@ -1370,7 +1377,11 @@ impl ReferenceStateMachine for AbstractPosState { state.debug_validators(); } Transition::Bond { id, amount } => { - println!("\nABSTRACT Bond {} tokens, id = {}", amount, id); + println!( + "\nABSTRACT Bond {} tokens, id = {}", + amount.to_string_native(), + id + ); if *amount != token::Amount::default() { let change = token::Change::from(*amount); @@ -1391,7 +1402,11 @@ impl ReferenceStateMachine for AbstractPosState { state.debug_validators(); } Transition::Unbond { id, amount } => { - println!("\nABSTRACT Unbond {} tokens, id = {}", amount, id); + println!( + "\nABSTRACT Unbond {} tokens, id = {}", + amount.to_string_native(), + id + ); if *amount != token::Amount::default() { let change = token::Change::from(*amount); @@ -1448,7 +1463,7 @@ impl ReferenceStateMachine for AbstractPosState { epoch: *infraction_epoch, block_height: *height, r#type: *slash_type, - rate: Decimal::ZERO, + rate: Dec::zero(), }; // Enqueue the slash for future processing @@ -1923,7 +1938,7 @@ impl AbstractPosState { let bond = bonds.entry(pipeline_epoch).or_default(); *bond += change; // Remove fully unbonded entries - if *bond == 0 { + if bond.is_zero() { bonds.remove(&pipeline_epoch); } // Update total_bonded @@ -1965,27 +1980,37 @@ impl AbstractPosState { tracing::debug!("Bonds before decrementing"); for (start, amnt) in bonds.iter() { - tracing::debug!("Bond epoch {} - amnt {}", start, amnt); + tracing::debug!( + "Bond epoch {} - amnt {}", + start, + amnt.to_string_native() + ); } for (bond_epoch, bond_amnt) in bonds.iter_mut().rev() { - tracing::debug!("remaining {}", remaining); - tracing::debug!("Bond epoch {} - amnt {}", bond_epoch, bond_amnt); + tracing::debug!("remaining {}", remaining.to_string_native()); + tracing::debug!( + "Bond epoch {} - amnt {}", + bond_epoch, + bond_amnt.to_string_native() + ); let to_unbond = cmp::min(*bond_amnt, remaining); - tracing::debug!("to_unbond (init) = {}", to_unbond); + tracing::debug!( + "to_unbond (init) = {}", + to_unbond.to_string_native() + ); *bond_amnt -= to_unbond; *unbonds += token::Amount::from_change(to_unbond); - let slashes_for_this_bond: BTreeMap = - validator_slashes - .iter() - .cloned() - .filter(|s| *bond_epoch <= s.epoch) - .fold(BTreeMap::new(), |mut acc, s| { - let cur = acc.entry(s.epoch).or_default(); - *cur += s.rate; - acc - }); + let slashes_for_this_bond: BTreeMap = validator_slashes + .iter() + .cloned() + .filter(|s| *bond_epoch <= s.epoch) + .fold(BTreeMap::new(), |mut acc, s| { + let cur = acc.entry(s.epoch).or_default(); + *cur += s.rate; + acc + }); tracing::debug!( "Slashes for this bond{:?}", slashes_for_this_bond.clone() @@ -1999,21 +2024,25 @@ impl AbstractPosState { .change(); tracing::debug!( "Cur amnt after slashing = {}", - &amount_after_slashing + &amount_after_slashing.to_string_native() ); let amt = unbond_records.entry(*bond_epoch).or_default(); *amt += token::Amount::from_change(to_unbond); remaining -= to_unbond; - if remaining == 0 { + if remaining.is_zero() { break; } } tracing::debug!("Bonds after decrementing"); for (start, amnt) in bonds.iter() { - tracing::debug!("Bond epoch {} - amnt {}", start, amnt); + tracing::debug!( + "Bond epoch {} - amnt {}", + start, + amnt.to_string_native() + ); } let pipeline_state = self @@ -2228,10 +2257,10 @@ impl AbstractPosState { tracing::debug!( "Val {} stake at infraction {}", validator, - stake_at_infraction + stake_at_infraction.to_string_native(), ); - let mut total_rate = Decimal::ZERO; + let mut total_rate = Dec::zero(); for slash in slashes { debug_assert_eq!(slash.epoch, infraction_epoch); @@ -2253,7 +2282,7 @@ impl AbstractPosState { total_rate += rate; } - total_rate = cmp::min(total_rate, Decimal::ONE); + total_rate = cmp::min(total_rate, Dec::one()); tracing::debug!("Total rate: {}", total_rate); let mut total_unbonded = token::Amount::default(); @@ -2272,7 +2301,7 @@ impl AbstractPosState { for (start, unbond_amount) in unbond_records { tracing::debug!( "UnbondRecord: amount = {}, start_epoch {}", - &unbond_amount, + &unbond_amount.to_string_native(), &start ); if start <= infraction_epoch { @@ -2293,7 +2322,7 @@ impl AbstractPosState { }) .cloned() .fold( - BTreeMap::::new(), + BTreeMap::::new(), |mut acc, s| { let cur = acc.entry(s.epoch).or_default(); @@ -2318,7 +2347,7 @@ impl AbstractPosState { tracing::debug!( "Total unbonded (epoch {}) w slashing = {}", epoch, - total_unbonded + total_unbonded.to_string_native() ); } sum_post_bonds += self @@ -2336,7 +2365,7 @@ impl AbstractPosState { tracing::debug!( "Epoch {}\nLast slash = {}", self.epoch + offset, - last_slash + last_slash.to_string_native(), ); let mut recent_unbonds = token::Change::default(); let unbond_records = self @@ -2349,7 +2378,7 @@ impl AbstractPosState { for (start, unbond_amount) in unbond_records { tracing::debug!( "UnbondRecord: amount = {}, start_epoch {}", - &unbond_amount, + unbond_amount.to_string_native(), &start ); if start <= infraction_epoch { @@ -2370,7 +2399,7 @@ impl AbstractPosState { }) .cloned() .fold( - BTreeMap::::new(), + BTreeMap::::new(), |mut acc, s| { let cur = acc.entry(s.epoch).or_default(); @@ -2396,23 +2425,24 @@ impl AbstractPosState { tracing::debug!( "Total unbonded (offset {}) w slashing = {}", offset, - total_unbonded + total_unbonded.to_string_native() ); } tracing::debug!( "stake at infraction {}", - stake_at_infraction + stake_at_infraction.to_string_native(), ); - tracing::debug!("total unbonded {}", total_unbonded); - let this_slash = decimal_mult_i128( - total_rate, - stake_at_infraction - total_unbonded.change(), + tracing::debug!( + "total unbonded {}", + total_unbonded.to_string_native() ); + let this_slash = total_rate + * (stake_at_infraction - total_unbonded.change()); let diff_slashed_amount = last_slash - this_slash; tracing::debug!( "Offset {} diff_slashed_amount {}", offset, - diff_slashed_amount + diff_slashed_amount.to_string_native(), ); last_slash = this_slash; // total_unbonded = token::Amount::default(); @@ -2435,7 +2465,10 @@ impl AbstractPosState { .unwrap_or_default() - recent_unbonds; - tracing::debug!("\nUnslashable bonds = {}", sum_post_bonds); + tracing::debug!( + "\nUnslashable bonds = {}", + sum_post_bonds.to_string_native() + ); let validator_stake_at_offset = self .validator_stakes .entry(self.epoch + offset) @@ -2448,18 +2481,18 @@ impl AbstractPosState { tracing::debug!( "Val stake pre (epoch {}) = {}", self.epoch + offset, - validator_stake_at_offset + validator_stake_at_offset.to_string_native(), ); tracing::debug!( "Slashable stake at offset = {}", - slashable_stake_at_offset + slashable_stake_at_offset.to_string_native(), ); let change = cmp::max( -slashable_stake_at_offset, diff_slashed_amount, ); - tracing::debug!("Change = {}", change); + tracing::debug!("Change = {}", change.to_string_native()); *validator_stake_at_offset += change; for os in (offset + 1)..=self.params.pipeline_len { @@ -2481,7 +2514,7 @@ impl AbstractPosState { tracing::debug!( "New stake at epoch {} = {}", self.epoch + os, - offset_stake + offset_stake.to_string_native() ); } } @@ -2576,7 +2609,7 @@ impl AbstractPosState { } /// Compute the cubic slashing rate for the current epoch - fn cubic_slash_rate(&self) -> Decimal { + fn cubic_slash_rate(&self) -> Dec { let infraction_epoch = self.epoch - self.params.unbonding_len - 1_u64 @@ -2592,7 +2625,7 @@ impl AbstractPosState { let epoch_end = infraction_epoch + window_width; // Calculate cubic slashing rate with the abstract state - let mut vp_frac_sum = Decimal::default(); + let mut vp_frac_sum = Dec::zero(); for epoch in Epoch::iter_bounds_inclusive(epoch_start, epoch_end) { let consensus_stake = self.consensus_set.get(&epoch).unwrap().iter().fold( @@ -2604,7 +2637,7 @@ impl AbstractPosState { tracing::debug!( "Consensus stake in epoch {}: {}", epoch, - consensus_stake + consensus_stake.to_string_native() ); let processing_epoch = epoch @@ -2626,18 +2659,21 @@ impl AbstractPosState { "Val {} stake epoch {}: {}", &validator, epoch, - val_stake + val_stake.to_string_native(), ); - vp_frac_sum += Decimal::from(slashes.len()) - * Decimal::from(val_stake) - / Decimal::from(consensus_stake); + vp_frac_sum += Dec::from(slashes.len()) + * Dec::from(val_stake) + / Dec::from(consensus_stake); } } } - let vp_frac_sum = cmp::min(Decimal::ONE, vp_frac_sum); + let vp_frac_sum = cmp::min(Dec::one(), vp_frac_sum); tracing::debug!("vp_frac_sum: {}", vp_frac_sum); - cmp::min(dec!(9) * vp_frac_sum * vp_frac_sum, Decimal::ONE) + cmp::min( + Dec::new(9, 0).unwrap() * vp_frac_sum * vp_frac_sum, + Dec::one(), + ) } fn debug_validators(&self) { @@ -2669,8 +2705,8 @@ impl AbstractPosState { tracing::debug!( "Consensus val {}, stake {} ({}) - ({:?})", val, - u64::from(*amount), - deltas_stake, + amount.to_string_native(), + deltas_stake.to_string_native(), val_state ); debug_assert_eq!( @@ -2703,8 +2739,8 @@ impl AbstractPosState { tracing::debug!( "Below-cap val {}, stake {} ({}) - ({:?})", val, - u64::from(token::Amount::from(*amount)), - deltas_stake, + token::Amount::from(*amount).to_string_native(), + deltas_stake.to_string_native(), val_state ); debug_assert_eq!( @@ -2743,7 +2779,11 @@ impl AbstractPosState { .get(&addr) .cloned() .unwrap_or_default(); - tracing::debug!("Jailed val {}, stake {}", &addr, &stake); + tracing::debug!( + "Jailed val {}, stake {}", + &addr, + &stake.to_string_native() + ); } } } @@ -2818,7 +2858,8 @@ fn arb_self_bond( // Bond up to 10 tokens (10M micro units) to avoid overflows pub fn arb_bond_amount() -> impl Strategy { - (1_u64..10_000_000).prop_map(token::Amount::from) + (1_u64..10_000_000) + .prop_map(|val| token::Amount::from_uint(val, 0).unwrap()) } /// Arbitrary validator misbehavior @@ -2855,7 +2896,7 @@ fn arb_slash(state: &AbstractPosState) -> impl Strategy { } fn compute_amount_after_slashing( - slashes: &BTreeMap, + slashes: &BTreeMap, amount: token::Amount, unbonding_len: u64, cubic_slash_window_len: u64, @@ -2880,7 +2921,7 @@ fn compute_amount_after_slashing( computed_amounts.remove(idx); } computed_amounts.push(SlashedAmount { - amount: decimal_mult_amount(*slash_rate, updated_amount), + amount: *slash_rate * updated_amount, epoch: *infraction_epoch, }); } diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index bf81ad2286..ff36d73aac 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -16,14 +16,17 @@ use namada_core::ledger::storage_api::collections::{ }; use namada_core::ledger::storage_api::{self, StorageRead}; use namada_core::types::address::Address; +use namada_core::types::dec::Dec; use namada_core::types::key::common; use namada_core::types::storage::{Epoch, KeySeg}; use namada_core::types::token; +use namada_core::types::token::Amount; pub use rev_order::ReverseOrdTokenAmount; -use rust_decimal::prelude::{Decimal, ToPrimitive}; use crate::parameters::PosParams; +// TODO: replace `POS_MAX_DECIMAL_PLACES` with +// core::types::token::NATIVE_MAX_DECIMAL_PLACES?? const U64_MAX: u64 = u64::MAX; // TODO: add this to the spec @@ -122,7 +125,7 @@ pub type TotalDeltas = crate::epoched::EpochedDelta< /// Epoched validator commission rate pub type CommissionRates = - crate::epoched::Epoched; + crate::epoched::Epoched; /// Epoched validator's bonds pub type Bonds = crate::epoched::EpochedDelta< @@ -181,17 +184,17 @@ pub struct SlashedAmount { /// Commission rate and max commission rate change per epoch for a validator pub struct CommissionPair { /// Validator commission rate - pub commission_rate: Decimal, + pub commission_rate: Dec, /// Validator max commission rate change per epoch - pub max_commission_change_per_epoch: Decimal, + pub max_commission_change_per_epoch: Dec, } /// Epoched rewards products -pub type RewardsProducts = LazyMap; +pub type RewardsProducts = LazyMap; /// Consensus validator rewards accumulator (for tracking the fractional block /// rewards owed over the course of an epoch) -pub type RewardsAccumulator = LazyMap; +pub type RewardsAccumulator = LazyMap; // -------------------------------------------------------------------------------------------- @@ -215,9 +218,9 @@ pub struct GenesisValidator { /// A public key used for signing validator's consensus actions pub consensus_key: common::PublicKey, /// Commission rate charged on rewards for delegators (bounded inside 0-1) - pub commission_rate: Decimal, + pub commission_rate: Dec, /// Maximum change in commission rate permitted per epoch - pub max_commission_rate_change: Decimal, + pub max_commission_rate_change: Dec, } /// An update of the consensus and below-capacity validator set. @@ -236,7 +239,7 @@ pub struct ConsensusValidator { /// A public key used for signing validator's consensus actions pub consensus_key: common::PublicKey, /// Total bonded stake of the validator - pub bonded_stake: u64, + pub bonded_stake: token::Amount, } /// ID of a bond and/or an unbond. @@ -287,7 +290,8 @@ impl Display for WeightedValidator { write!( f, "{} with bonded stake {}", - self.address, self.bonded_stake + self.address, + self.bonded_stake.to_string_native() ) } } @@ -392,7 +396,7 @@ pub struct Slash { /// A type of slashable event. pub r#type: SlashType, /// The cubic slashing rate for this validator - pub rate: Decimal, + pub rate: Dec, } /// Slashes applied to validator, to punish byzantine behavior by removing @@ -489,7 +493,7 @@ impl Display for BondId { impl SlashType { /// Get the slash rate applicable to the given slash type from the PoS /// parameters. - pub fn get_slash_rate(&self, params: &PosParams) -> Decimal { + pub fn get_slash_rate(&self, params: &PosParams) -> Dec { match self { SlashType::DuplicateVote => params.duplicate_vote_min_slash_rate, SlashType::LightClientAttack => { @@ -508,52 +512,13 @@ impl Display for SlashType { } } -/// Multiply a value of type Decimal with one of type u64 and then return the -/// truncated u64 -pub fn decimal_mult_u64(dec: Decimal, int: u64) -> u64 { - let prod = dec * Decimal::from(int); - // truncate the number to the floor - prod.to_u64().expect("Product is out of bounds") -} - -/// Multiply a value of type Decimal with one of type i128 and then return the -/// truncated i128 -pub fn decimal_mult_i128(dec: Decimal, int: i128) -> i128 { - let prod = dec * Decimal::from(int); - // truncate the number to the floor - prod.to_i128().expect("Product is out of bounds") -} - -/// Multiply a value of type Decimal with one of type i128 and then convert it -/// to an Amount type -pub fn mult_change_to_amount( - dec: Decimal, - change: token::Change, -) -> token::Amount { - let prod = dec * Decimal::from(change); - // truncate the number to the floor - token::Amount::from(prod.to_u64().expect("Product is out of bounds")) -} - -/// Multiply a value of type Decimal with one of type Amount and then return the -/// truncated Amount -pub fn decimal_mult_amount( - dec: Decimal, - amount: token::Amount, -) -> token::Amount { - let prod = dec * Decimal::from(amount); - // truncate the number to the floor - token::Amount::from(prod.to_u64().expect("Product is out of bounds")) -} - /// Calculate voting power in the tendermint context (which is stored as i64) /// from the number of tokens -pub fn into_tm_voting_power( - votes_per_token: Decimal, - tokens: impl Into, -) -> i64 { - let prod = decimal_mult_u64(votes_per_token, tokens.into()); - i64::try_from(prod).expect("Invalid voting power") +pub fn into_tm_voting_power(votes_per_token: Dec, tokens: Amount) -> i64 { + let pow = votes_per_token + * u128::try_from(tokens).expect("Voting power out of bounds"); + i64::try_from(pow.to_uint().expect("Cant fail")) + .expect("Invalid voting power") } #[cfg(test)] diff --git a/proof_of_stake/src/types/rev_order.rs b/proof_of_stake/src/types/rev_order.rs index 807795e10e..57619941d4 100644 --- a/proof_of_stake/src/types/rev_order.rs +++ b/proof_of_stake/src/types/rev_order.rs @@ -23,7 +23,7 @@ impl From for ReverseOrdTokenAmount { /// Invert the token amount fn invert(amount: token::Amount) -> token::Amount { - token::MAX_AMOUNT - amount + token::Amount::max_signed() - amount } impl KeySeg for ReverseOrdTokenAmount { @@ -46,15 +46,16 @@ impl KeySeg for ReverseOrdTokenAmount { impl std::fmt::Display for ReverseOrdTokenAmount { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) + f.write_str(&self.0.to_string_native()) } } impl std::str::FromStr for ReverseOrdTokenAmount { - type Err = ::Err; + type Err = token::AmountParseError; fn from_str(s: &str) -> Result { - let amount = token::Amount::from_str(s)?; + let amount = + token::Amount::from_str(s, token::NATIVE_MAX_DECIMAL_PLACES)?; Ok(Self(amount)) } } diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 3b43d464bd..89c143e1be 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -98,16 +98,18 @@ derivation-path.workspace = true derivative.workspace = true itertools.workspace = true loupe = {version = "0.1.3", optional = true} +masp_primitives.workspace = true +masp_proofs = { workspace = true, features = ["download-params"] } orion.workspace = true parity-wasm = {version = "0.45.0", features = ["sign_ext"], optional = true} paste.workspace = true proptest = {version = "1.2.0", optional = true} prost.workspace = true pwasm-utils = {git = "https://github.com/heliaxdev/wasm-utils", tag = "v0.20.0", features = ["sign_ext"], optional = true} +rand = {version = "0.8", default-features = false, optional = true} +rand_core = {version = "0.6", default-features = false, optional = true} rayon = {version = "=1.5.3", optional = true} ripemd.workspace = true -rust_decimal.workspace = true -rust_decimal_macros.workspace = true serde.workspace = true serde_json.workspace = true sha2.workspace = true @@ -127,10 +129,6 @@ wasmer-engine-dylib = {version = "=2.2.0", optional = true} wasmer-engine-universal = {version = "=2.2.0", optional = true} wasmer-vm = {version = "2.2.0", optional = true} wasmparser.workspace = true -masp_primitives.workspace = true -masp_proofs = { workspace = true, features = ["download-params"] } -rand = {version = "0.8", default-features = false, optional = true} -rand_core = {version = "0.6", default-features = false, optional = true} zeroize.workspace = true [dev-dependencies] diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 58863756bd..0ad90faf0a 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; use namada_core::types::chain::ChainId; +use namada_core::types::dec::Dec; use namada_core::types::time::DateTimeUtc; -use rust_decimal::Decimal; use zeroize::Zeroizing; use crate::ibc::core::ics24_host::identifier::{ChannelId, PortId}; @@ -96,12 +96,23 @@ pub struct TxTransfer { /// Transferred token address pub sub_prefix: Option, /// Transferred token amount - pub amount: token::Amount, + pub amount: InputAmount, /// Native token address pub native_token: C::NativeAddress, /// Path to the TX WASM code file pub tx_code_path: PathBuf, } +/// An amount read in by the cli +#[derive(Copy, Clone, Debug)] +pub enum InputAmount { + /// An amount whose representation has been validated + /// against the allowed representation in storage + Validated(token::DenominatedAmount), + /// The parsed amount read in from the cli. It has + /// not yet been validated against the allowed + /// representation in storage. + Unvalidated(token::DenominatedAmount), +} /// IBC transfer transaction arguments #[derive(Clone, Debug)] @@ -112,7 +123,7 @@ pub struct TxIbcTransfer { pub source: C::Address, /// Transfer target address pub receiver: String, - /// Transferred token address + /// Transferred token addres s pub token: C::Address, /// Transferred token address pub sub_prefix: Option, @@ -161,9 +172,9 @@ pub struct TxInitValidator { /// Protocol key pub protocol_key: Option, /// Commission rate - pub commission_rate: Decimal, + pub commission_rate: Dec, /// Maximum commission rate change - pub max_commission_rate_change: Decimal, + pub max_commission_rate_change: Dec, /// Path to the VP WASM code file pub validator_vp_code_path: PathBuf, /// Path to the TX WASM code file @@ -293,6 +304,8 @@ pub struct QueryTransfers { pub owner: Option, /// Address of a token pub token: Option, + /// sub-prefix if querying a multi-token + pub sub_prefix: Option, } /// Query PoS bond(s) @@ -325,7 +338,7 @@ pub struct TxCommissionRateChange { /// Validator address (should be self) pub validator: C::Address, /// Value to which the tx changes the commission rate - pub rate: Decimal, + pub rate: Dec, /// Path to the TX WASM code file pub tx_code_path: PathBuf, } @@ -408,7 +421,7 @@ pub struct Tx { /// wallet. pub wallet_alias_force: bool, /// The amount being payed to include the transaction - pub fee_amount: token::Amount, + pub fee_amount: InputAmount, /// The token in which the fee is being paid pub fee_token: C::Address, /// The max amount of gas used to process tx diff --git a/shared/src/ledger/ibc/vp/context.rs b/shared/src/ledger/ibc/vp/context.rs index c8e74979bd..0da78bba53 100644 --- a/shared/src/ledger/ibc/vp/context.rs +++ b/shared/src/ledger/ibc/vp/context.rs @@ -11,7 +11,9 @@ use namada_core::ledger::storage_api::StorageRead; use namada_core::types::address::{Address, InternalAddress}; use namada_core::types::ibc::IbcEvent; use namada_core::types::storage::{BlockHeight, Header, Key}; -use namada_core::types::token::{is_any_token_balance_key, Amount}; +use namada_core::types::token::{ + is_any_token_balance_key, is_any_token_or_multitoken_balance_key, Amount, +}; use super::Error; use crate::ledger::native_vp::CtxPreStorageRead; @@ -119,10 +121,12 @@ where dest: &Key, amount: Amount, ) -> Result<(), Self::Error> { - let src_owner = is_any_token_balance_key(src); + let src_owner = is_any_token_or_multitoken_balance_key(src); let mut src_bal = match src_owner { - Some(Address::Internal(InternalAddress::IbcMint)) => Amount::max(), - Some(Address::Internal(InternalAddress::IbcBurn)) => { + Some([_, Address::Internal(InternalAddress::IbcMint)]) => { + Amount::max() + } + Some([_, Address::Internal(InternalAddress::IbcBurn)]) => { unreachable!("Invalid transfer from IBC burn address") } _ => match self.read(src)? { @@ -135,7 +139,7 @@ where src_bal.spend(&amount); let dest_owner = is_any_token_balance_key(dest); let mut dest_bal = match dest_owner { - Some(Address::Internal(InternalAddress::IbcMint)) => { + Some([_, Address::Internal(InternalAddress::IbcMint)]) => { unreachable!("Invalid transfer to IBC mint address") } _ => match self.read(dest)? { diff --git a/shared/src/ledger/ibc/vp/mod.rs b/shared/src/ledger/ibc/vp/mod.rs index 518e7d540a..dae26cf4e0 100644 --- a/shared/src/ledger/ibc/vp/mod.rs +++ b/shared/src/ledger/ibc/vp/mod.rs @@ -18,6 +18,7 @@ use namada_core::ledger::storage::write_log::StorageModification; use namada_core::ledger::storage::{self as ledger_storage, StorageHasher}; use namada_core::proto::Tx; use namada_core::types::address::{Address, InternalAddress}; +use namada_core::types::dec::Dec; use namada_core::types::storage::Key; use namada_proof_of_stake::read_pos_params; use thiserror::Error; @@ -224,19 +225,19 @@ pub fn get_dummy_header() -> crate::types::storage::Header { #[cfg(any(test, feature = "testing"))] pub fn get_dummy_genesis_validator() -> namada_proof_of_stake::types::GenesisValidator { - use rust_decimal::prelude::Decimal; - use crate::core::types::address::testing::established_address_1; use crate::types::key::testing::common_sk_from_simple_seed; use crate::types::token::Amount; let address = established_address_1(); - let tokens = Amount::whole(1); + let tokens = Amount::native_whole(1); let consensus_sk = common_sk_from_simple_seed(0); let consensus_key = consensus_sk.to_public(); - let commission_rate = Decimal::new(1, 1); - let max_commission_rate_change = Decimal::new(1, 1); + let commission_rate = + Dec::new(1, 1).expect("expected 0.1 to be a valid decimal"); + let max_commission_rate_change = + Dec::new(1, 1).expect("expected 0.1 to be a valid decimal"); namada_proof_of_stake::types::GenesisValidator { address, tokens, @@ -2011,7 +2012,7 @@ mod tests { // init balance let sender = established_address_1(); let balance_key = balance_key(&nam(), &sender); - let amount = Amount::whole(100); + let amount = Amount::native_whole(100); wl_storage .write_log .write(&balance_key, amount.try_to_vec().unwrap()) @@ -2476,7 +2477,7 @@ mod tests { // init the escrow balance let balance_key = balance_key(&nam(), &Address::Internal(InternalAddress::IbcEscrow)); - let amount = Amount::whole(100); + let amount = Amount::native_whole(100); wl_storage .write_log .write(&balance_key, amount.try_to_vec().unwrap()) @@ -2629,7 +2630,7 @@ mod tests { // init the escrow balance let balance_key = balance_key(&nam(), &Address::Internal(InternalAddress::IbcEscrow)); - let amount = Amount::whole(100); + let amount = Amount::native_whole(100); wl_storage .write_log .write(&balance_key, amount.try_to_vec().unwrap()) diff --git a/shared/src/ledger/ibc/vp/token.rs b/shared/src/ledger/ibc/vp/token.rs index da0043046f..18234abd35 100644 --- a/shared/src/ledger/ibc/vp/token.rs +++ b/shared/src/ledger/ibc/vp/token.rs @@ -97,11 +97,14 @@ where .filter(|k| { matches!( token::is_any_token_balance_key(k), - Some(Address::Internal( - InternalAddress::IbcEscrow - | InternalAddress::IbcBurn - | InternalAddress::IbcMint - )) + Some([ + _, + Address::Internal( + InternalAddress::IbcEscrow + | InternalAddress::IbcBurn + | InternalAddress::IbcMint + ) + ]) ) }) .cloned() @@ -126,7 +129,7 @@ where changes.insert(sub_prefix, change + this_change); } } - if changes.iter().all(|(_, c)| *c == 0) { + if changes.iter().all(|(_, c)| c.is_zero()) { return Ok(true); } else { return Err(Error::TokenTransfer( @@ -292,7 +295,7 @@ where )? .unwrap_or_default(); // the previous balance of the mint address should be the maximum - Amount::max().change() - post.change() + Amount::max_signed().change() - post.change() }; if change == amount.change() { @@ -343,7 +346,7 @@ where )? .unwrap_or_default(); // the previous balance of the mint address should be the maximum - Amount::max().change() - post.change() + Amount::max_signed().change() - post.change() }; if change == amount.change() { diff --git a/shared/src/ledger/inflation.rs b/shared/src/ledger/inflation.rs index 2779dc92f6..1c4850d2b1 100644 --- a/shared/src/ledger/inflation.rs +++ b/shared/src/ledger/inflation.rs @@ -2,9 +2,7 @@ //! proof-of-stake, providing liquity to shielded asset pools, and public goods //! funding. -use rust_decimal::prelude::ToPrimitive; -use rust_decimal::Decimal; -use rust_decimal_macros::dec; +use namada_core::types::dec::Dec; use crate::types::token; @@ -21,8 +19,8 @@ pub enum RewardsType { /// Holds the PD controller values that should be updated in storage #[allow(missing_docs)] pub struct ValsToUpdate { - pub locked_ratio: Decimal, - pub inflation: u64, + pub locked_ratio: Dec, + pub inflation: token::Amount, } /// PD controller used to dynamically adjust the rewards rates @@ -33,17 +31,17 @@ pub struct RewardsController { /// Total token supply pub total_tokens: token::Amount, /// PD target locked ratio - pub locked_ratio_target: Decimal, + pub locked_ratio_target: Dec, /// PD last locked ratio - pub locked_ratio_last: Decimal, + pub locked_ratio_last: Dec, /// Maximum reward rate - pub max_reward_rate: Decimal, + pub max_reward_rate: Dec, /// Last inflation amount pub last_inflation_amount: token::Amount, /// Nominal proportional gain - pub p_gain_nom: Decimal, + pub p_gain_nom: Dec, /// Nominal derivative gain - pub d_gain_nom: Decimal, + pub d_gain_nom: Dec, /// Number of epochs per year pub epochs_per_year: u64, } @@ -63,9 +61,13 @@ impl RewardsController { epochs_per_year, } = self; - let locked: Decimal = u64::from(locked_tokens).into(); - let total: Decimal = u64::from(total_tokens).into(); - let epochs_py: Decimal = (epochs_per_year).into(); + // Token amounts must be expressed in terms of the raw amount (namnam) + // to properly run the PD controller + let locked = + Dec::try_from(locked_tokens.raw_amount()).expect("Should not fail"); + let total = + Dec::try_from(total_tokens.raw_amount()).expect("Should not fail"); + let epochs_py: Dec = epochs_per_year.into(); let locked_ratio = locked / total; let max_inflation = total * max_reward_rate / epochs_py; @@ -76,16 +78,16 @@ impl RewardsController { let delta_error = locked_ratio_last - locked_ratio; let control_val = p_gain * error - d_gain * delta_error; - let last_inflation_amount = Decimal::from(last_inflation_amount); + let last_inflation_amount = Dec::from(last_inflation_amount); let new_inflation_amount = last_inflation_amount + control_val; - let inflation = if new_inflation_amount > max_inflation { + let inflation = if last_inflation_amount + control_val > max_inflation { max_inflation - } else if new_inflation_amount > dec!(0.0) { + } else if new_inflation_amount > Dec::zero() { new_inflation_amount } else { - dec!(0.0) + Dec::zero() }; - let inflation: u64 = inflation.to_u64().unwrap(); + let inflation = token::Amount::from(inflation); ValsToUpdate { locked_ratio, diff --git a/shared/src/ledger/masp.rs b/shared/src/ledger/masp.rs index 3575695454..7ae4f142bb 100644 --- a/shared/src/ledger/masp.rs +++ b/shared/src/ledger/masp.rs @@ -14,6 +14,9 @@ use async_trait::async_trait; use borsh::{BorshDeserialize, BorshSerialize}; use itertools::Either; use masp_primitives::asset_type::AssetType; +#[cfg(feature = "mainnet")] +use masp_primitives::consensus::MainNetwork; +#[cfg(not(feature = "mainnet"))] use masp_primitives::consensus::TestNetwork; use masp_primitives::convert::AllowedConversion; use masp_primitives::ff::PrimeField; @@ -47,6 +50,7 @@ use masp_proofs::bellman::groth16::PreparedVerifyingKey; use masp_proofs::bls12_381::Bls12; use masp_proofs::prover::LocalTxProver; use masp_proofs::sapling::SaplingVerificationContext; +use namada_core::types::token::{Change, MaspDenom, TokenAddress}; use namada_core::types::transaction::AffineCurve; #[cfg(feature = "masp-tx-gen")] use rand_core::{CryptoRng, OsRng, RngCore}; @@ -54,8 +58,10 @@ use ripemd::Digest as RipemdDigest; #[cfg(feature = "masp-tx-gen")] use sha2::Digest; +use crate::ledger::args::InputAmount; use crate::ledger::queries::Client; -use crate::ledger::rpc::query_storage_value; +use crate::ledger::rpc::{query_conversion, query_storage_value}; +use crate::ledger::tx::decode_component; use crate::ledger::{args, rpc}; use crate::proto::Tx; use crate::tendermint_rpc::query::Query; @@ -381,24 +387,17 @@ pub fn find_valid_diversifier( /// Determine if using the current note would actually bring us closer to our /// target pub fn is_amount_required(src: Amount, dest: Amount, delta: Amount) -> bool { - if delta > Amount::zero() { - let gap = dest - src; - for (asset_type, value) in gap.components() { - if *value > 0 && delta[asset_type] > 0 { - return true; - } + let gap = dest - src; + for (asset_type, value) in gap.components() { + if *value >= 0 && delta[asset_type] >= 0 { + return true; } } false } -/// An extension of Option's cloned method for pair types -fn cloned_pair((a, b): (&T, &U)) -> (T, U) { - (a.clone(), b.clone()) -} - /// Errors that can occur when trying to retrieve pinned transaction -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Copy, Clone)] pub enum PinnedBalanceError { /// No transaction has yet been pinned to the given payment address NoTransactionPinned, @@ -406,15 +405,127 @@ pub enum PinnedBalanceError { InvalidViewingKey, } +// #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] +// pub struct MaspAmount { +// pub asset: TokenAddress, +// pub amount: token::Amount, +// } + +/// a masp change +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] +pub struct MaspChange { + /// the token address + pub asset: TokenAddress, + /// the change in the token + pub change: token::Change, +} + +/// a masp amount +#[derive( + BorshSerialize, BorshDeserialize, Debug, Clone, Default, PartialEq, Eq, +)] +pub struct MaspAmount(HashMap<(Epoch, TokenAddress), token::Change>); + +impl std::ops::Deref for MaspAmount { + type Target = HashMap<(Epoch, TokenAddress), token::Change>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for MaspAmount { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl std::ops::Add for MaspAmount { + type Output = MaspAmount; + + fn add(mut self, mut rhs: MaspAmount) -> Self::Output { + for (key, value) in rhs.drain() { + self.entry(key) + .and_modify(|val| *val += value) + .or_insert(value); + } + self.retain(|_, v| !v.is_zero()); + self + } +} + +impl std::ops::AddAssign for MaspAmount { + fn add_assign(&mut self, amount: MaspAmount) { + *self = self.clone() + amount + } +} + +// please stop copying and pasting make a function +impl std::ops::Sub for MaspAmount { + type Output = MaspAmount; + + fn sub(mut self, mut rhs: MaspAmount) -> Self::Output { + for (key, value) in rhs.drain() { + self.entry(key) + .and_modify(|val| *val -= value) + .or_insert(-value); + } + self.0.retain(|_, v| !v.is_zero()); + self + } +} + +impl std::ops::SubAssign for MaspAmount { + fn sub_assign(&mut self, amount: MaspAmount) { + *self = self.clone() - amount + } +} + +impl std::ops::Mul for MaspAmount { + type Output = Self; + + fn mul(mut self, rhs: Change) -> Self::Output { + for (_, value) in self.iter_mut() { + *value = *value * rhs + } + self + } +} + +impl<'a> From<&'a MaspAmount> for Amount { + fn from(masp_amount: &'a MaspAmount) -> Amount { + let mut res = Amount::zero(); + for ((epoch, key), val) in masp_amount.iter() { + for denom in MaspDenom::iter() { + let asset = make_asset_type( + Some(*epoch), + &key.address, + &key.sub_prefix, + denom, + ); + res += Amount::from_pair(asset, denom.denominate_i128(val)) + .unwrap(); + } + } + res + } +} + +impl From for Amount { + fn from(amt: MaspAmount) -> Self { + Self::from(&amt) + } +} + /// Represents the amount used of different conversions pub type Conversions = - HashMap, i64)>; + HashMap, i128)>; /// Represents the changes that were made to a list of transparent accounts -pub type TransferDelta = HashMap>; +pub type TransferDelta = HashMap; /// Represents the changes that were made to a list of shielded accounts -pub type TransactionDelta = HashMap; +pub type TransactionDelta = HashMap; /// Represents the current state of the shielded pool from the perspective of /// the chosen viewing keys. @@ -447,7 +558,8 @@ pub struct ShieldedContext { /// The set of note positions that have been spent pub spents: HashSet, /// Maps asset types to their decodings - pub asset_types: HashMap, + pub asset_types: + HashMap, MaspDenom, Epoch)>, /// Maps note positions to their corresponding viewing keys pub vk_map: HashMap, } @@ -560,7 +672,9 @@ impl ShieldedContext { while tx_ctx.last_txidx != self.last_txidx { if let Some(((height, idx), (epoch, tx, stx))) = tx_iter.next() { - tx_ctx.scan_tx(*height, *idx, *epoch, tx, stx); + tx_ctx + .scan_tx(client, *height, *idx, *epoch, tx, stx) + .await; } else { break; } @@ -576,7 +690,7 @@ impl ShieldedContext { // Now that we possess the unspent notes corresponding to both old and // new keys up until tx_pos, proceed to scan the new transactions. for ((height, idx), (epoch, tx, stx)) in &mut tx_iter { - self.scan_tx(*height, *idx, *epoch, tx, stx); + self.scan_tx(client, *height, *idx, *epoch, tx, stx).await; } } @@ -631,8 +745,9 @@ impl ShieldedContext { /// we have spent are updated. The witness map is maintained to make it /// easier to construct note merkle paths in other code. See /// https://zips.z.cash/protocol/protocol.pdf#scan - pub fn scan_tx( + pub async fn scan_tx( &mut self, + client: &U::C, height: BlockHeight, index: TxIndex, epoch: Epoch, @@ -663,7 +778,9 @@ impl ShieldedContext { self.witness_map.insert(note_pos, witness); // Let's try to see if any of our viewing keys can decrypt latest // note - for (vk, notes) in self.pos_map.iter_mut() { + let mut pos_map = HashMap::new(); + std::mem::swap(&mut pos_map, &mut self.pos_map); + for (vk, notes) in pos_map.iter_mut() { let decres = try_sapling_note_decryption::<_, OutputDescription<<::SaplingAuth as masp_primitives::transaction::components::sapling::Authorization>::Proof>>( &NETWORK, 1.into(), @@ -686,16 +803,25 @@ impl ShieldedContext { // Note the account changes let balance = transaction_delta .entry(*vk) - .or_insert_with(Amount::zero); - *balance += - Amount::from_nonnegative(note.asset_type, note.value) + .or_insert_with(MaspAmount::default); + *balance += self + .decode_all_amounts( + client, + Amount::from_nonnegative( + note.asset_type, + note.value, + ) .expect( "found note with invalid value or asset type", - ); + ), + ) + .await; + self.vk_map.insert(note_pos, *vk); break; } } + std::mem::swap(&mut pos_map, &mut self.pos_map); } // Cancel out those of our notes that have been spent for ss in shielded @@ -709,26 +835,38 @@ impl ShieldedContext { // Note the account changes let balance = transaction_delta .entry(self.vk_map[note_pos]) - .or_insert_with(Amount::zero); + .or_insert_with(MaspAmount::default); let note = self.note_map[note_pos]; - *balance -= - Amount::from_nonnegative(note.asset_type, note.value) - .expect("found note with invalid value or asset type"); + *balance -= self + .decode_all_amounts( + client, + Amount::from_nonnegative(note.asset_type, note.value) + .expect( + "found note with invalid value or asset type", + ), + ) + .await; } } // Record the changes to the transparent accounts - let transparent_delta = - Amount::from_nonnegative(tx.token.clone(), u64::from(tx.amount)) - .expect("invalid value for amount"); let mut transfer_delta = TransferDelta::new(); - transfer_delta - .insert(tx.source.clone(), Amount::zero() - &transparent_delta); - transfer_delta.insert(tx.target.clone(), transparent_delta); + let token_addr = TokenAddress { + address: tx.token.clone(), + sub_prefix: tx.sub_prefix.clone(), + }; + transfer_delta.insert( + tx.source.clone(), + MaspChange { + asset: token_addr, + change: -tx.amount.amount.change(), + }, + ); + self.last_txidx += 1; + self.delta_map.insert( (height, index), (epoch, transfer_delta, transaction_delta), ); - self.last_txidx += 1; } /// Summarize the effects on shielded and transparent accounts of each @@ -745,7 +883,11 @@ impl ShieldedContext { /// Compute the total unspent notes associated with the viewing key in the /// context. If the key is not in the context, then we do not know the /// balance and hence we return None. - pub fn compute_shielded_balance(&self, vk: &ViewingKey) -> Option { + pub async fn compute_shielded_balance( + &mut self, + client: &U::C, + vk: &ViewingKey, + ) -> Option { // Cannot query the balance of a key that's not in the map if !self.pos_map.contains_key(vk) { return None; @@ -766,7 +908,7 @@ impl ShieldedContext { .expect("found note with invalid value or asset type"); } } - Some(val_acc) + Some(self.decode_all_amounts(client, val_acc).await) } /// Query the ledger for the decoding of the given asset type and cache it @@ -775,16 +917,23 @@ impl ShieldedContext { &mut self, client: &U::C, asset_type: AssetType, - ) -> Option<(Address, Epoch)> { + ) -> Option<(Address, Option, MaspDenom, Epoch)> { // Try to find the decoding in the cache if let decoded @ Some(_) = self.asset_types.get(&asset_type) { return decoded.cloned(); } // Query for the ID of the last accepted transaction - let (addr, ep, _conv, _path): (Address, _, Amount, MerklePath) = - rpc::query_conversion(client, asset_type).await?; - self.asset_types.insert(asset_type, (addr.clone(), ep)); - Some((addr, ep)) + let (addr, sub_prefix, denom, ep, _conv, _path): ( + Address, + Option, + MaspDenom, + _, + Amount, + MerklePath, + ) = rpc::query_conversion(client, asset_type).await?; + self.asset_types + .insert(asset_type, (addr.clone(), sub_prefix.clone(), denom, ep)); + Some((addr, sub_prefix, denom, ep)) } /// Query the ledger for the conversion that is allowed for the given asset @@ -794,19 +943,17 @@ impl ShieldedContext { client: &U::C, asset_type: AssetType, conversions: &'a mut Conversions, - ) -> Option<&'a mut (AllowedConversion, MerklePath, i64)> { - match conversions.entry(asset_type) { - Entry::Occupied(conv_entry) => Some(conv_entry.into_mut()), - Entry::Vacant(conv_entry) => { - // Query for the ID of the last accepted transaction - let (addr, ep, conv, path): (Address, _, _, _) = - rpc::query_conversion(client, asset_type).await?; - self.asset_types.insert(asset_type, (addr, ep)); + ) { + if let Entry::Vacant(conv_entry) = conversions.entry(asset_type) { + // Query for the ID of the last accepted transaction + if let Some((addr, sub_prefix, denom, ep, conv, path)) = + query_conversion(client, asset_type).await + { + self.asset_types + .insert(asset_type, (addr, sub_prefix, denom, ep)); // If the conversion is 0, then we just have a pure decoding - if conv == Amount::zero() { - None - } else { - Some(conv_entry.insert((Amount::into(conv), path, 0))) + if conv != Amount::zero() { + conv_entry.insert((conv.into(), path, 0)); } } } @@ -821,20 +968,20 @@ impl ShieldedContext { client: &U::C, vk: &ViewingKey, target_epoch: Epoch, - ) -> Option { + ) -> Option { // First get the unexchanged balance - if let Some(balance) = self.compute_shielded_balance(vk) { - // And then exchange balance into current asset types - Some( - self.compute_exchanged_amount( + if let Some(balance) = self.compute_shielded_balance(client, vk).await { + let exchanged_amount = self + .compute_exchanged_amount( client, balance, target_epoch, HashMap::new(), ) .await - .0, - ) + .0; + // And then exchange balance into current asset types + Some(self.decode_all_amounts(client, exchanged_amount).await) } else { None } @@ -845,23 +992,36 @@ impl ShieldedContext { /// conversion used, the conversions are applied to the given input, and /// the trace amount that could not be converted is moved from input to /// output. - fn apply_conversion( + #[allow(clippy::too_many_arguments)] + async fn apply_conversion( + &mut self, + client: &U::C, conv: AllowedConversion, - asset_type: AssetType, - value: i64, - usage: &mut i64, - input: &mut Amount, - output: &mut Amount, + asset_type: (Epoch, TokenAddress, MaspDenom), + value: i128, + usage: &mut i128, + input: &mut MaspAmount, + output: &mut MaspAmount, ) { + // we do not need to convert negative values + if value <= 0 { + return; + } // If conversion if possible, accumulate the exchanged amount let conv: Amount = conv.into(); // The amount required of current asset to qualify for conversion - let threshold = -conv[&asset_type]; + let masp_asset = make_asset_type( + Some(asset_type.0), + &asset_type.1.address, + &asset_type.1.sub_prefix, + asset_type.2, + ); + let threshold = -conv[&masp_asset]; if threshold == 0 { eprintln!( "Asset threshold of selected conversion for asset type {} is \ 0, this is a bug, please report it.", - asset_type + masp_asset ); } // We should use an amount of the AllowedConversion that almost @@ -869,11 +1029,17 @@ impl ShieldedContext { let required = value / threshold; // Forget about the trace amount left over because we cannot // realize its value - let trace = Amount::from_pair(asset_type, value % threshold).unwrap(); + let trace = MaspAmount(HashMap::from([( + (asset_type.0, asset_type.1), + Change::from(value % threshold), + )])); // Record how much more of the given conversion has been used *usage += required; // Apply the conversions to input and move the trace amount to output - *input += conv * required - &trace; + *input += self + .decode_all_amounts(client, conv.clone() * required) + .await + - trace.clone(); *output += trace; } @@ -884,77 +1050,106 @@ impl ShieldedContext { pub async fn compute_exchanged_amount( &mut self, client: &U::C, - mut input: Amount, + mut input: MaspAmount, target_epoch: Epoch, mut conversions: Conversions, ) -> (Amount, Conversions) { // Where we will store our exchanged value - let mut output = Amount::zero(); + let mut output = MaspAmount::default(); // Repeatedly exchange assets until it is no longer possible - while let Some((asset_type, value)) = - input.components().next().map(cloned_pair) + while let Some(((asset_epoch, token_addr), value)) = input.iter().next() { - let target_asset_type = self - .decode_asset_type(client, asset_type) - .await - .map(|(addr, _epoch)| make_asset_type(target_epoch, &addr)) - .unwrap_or(asset_type); - let at_target_asset_type = asset_type == target_asset_type; - if let (Some((conv, _wit, usage)), false) = ( - self.query_allowed_conversion( - client, - asset_type, - &mut conversions, - ) - .await, - at_target_asset_type, - ) { - println!( - "converting current asset type to latest asset type..." + let value = *value; + let asset_epoch = *asset_epoch; + let token_addr = token_addr.clone(); + for denom in MaspDenom::iter() { + let target_asset_type = make_asset_type( + Some(target_epoch), + &token_addr.address, + &token_addr.sub_prefix, + denom, ); - // Not at the target asset type, not at the latest asset type. - // Apply conversion to get from current asset type to the latest - // asset type. - Self::apply_conversion( - conv.clone(), - asset_type, - value, - usage, - &mut input, - &mut output, + let asset_type = make_asset_type( + Some(asset_epoch), + &token_addr.address, + &token_addr.sub_prefix, + denom, ); - } else if let (Some((conv, _wit, usage)), false) = ( + let at_target_asset_type = target_epoch == asset_epoch; + + let denom_value = denom.denominate_i128(&value); self.query_allowed_conversion( client, target_asset_type, &mut conversions, ) - .await, - at_target_asset_type, - ) { - println!( - "converting latest asset type to target asset type..." - ); - // Not at the target asset type, yes at the latest asset type. - // Apply inverse conversion to get from latest asset type to - // the target asset type. - Self::apply_conversion( - conv.clone(), + .await; + self.query_allowed_conversion( + client, asset_type, - value, - usage, - &mut input, - &mut output, - ); - } else { - // At the target asset type. Then move component over to output. - let comp = input.project(asset_type); - output += ∁ - // Strike from input to avoid repeating computation - input -= comp; + &mut conversions, + ) + .await; + if let (Some((conv, _wit, usage)), false) = + (conversions.get_mut(&asset_type), at_target_asset_type) + { + println!( + "converting current asset type to latest asset type..." + ); + // Not at the target asset type, not at the latest asset + // type. Apply conversion to get from + // current asset type to the latest + // asset type. + self.apply_conversion( + client, + conv.clone(), + (asset_epoch, token_addr.clone(), denom), + denom_value, + usage, + &mut input, + &mut output, + ) + .await; + } else if let (Some((conv, _wit, usage)), false) = ( + conversions.get_mut(&target_asset_type), + at_target_asset_type, + ) { + println!( + "converting latest asset type to target asset type..." + ); + // Not at the target asset type, yet at the latest asset + // type. Apply inverse conversion to get + // from latest asset type to the target + // asset type. + self.apply_conversion( + client, + conv.clone(), + (asset_epoch, token_addr.clone(), denom), + denom_value, + usage, + &mut input, + &mut output, + ) + .await; + } else { + // At the target asset type. Then move component over to + // output. + let mut comp = MaspAmount::default(); + comp.insert( + (asset_epoch, token_addr.clone()), + denom_value.into(), + ); + for ((e, key), val) in input.iter() { + if *key == token_addr && *e == asset_epoch { + comp.insert((*e, key.clone()), *val); + } + } + output += comp.clone(); + input -= comp; + } } } - (output, conversions) + (output.into(), conversions) } /// Collect enough unspent notes in this context to exceed the given amount @@ -994,10 +1189,11 @@ impl ShieldedContext { // The amount contributed by this note before conversion let pre_contr = Amount::from_pair(note.asset_type, note.value) .expect("received note has invalid value or asset type"); + let input = self.decode_all_amounts(client, pre_contr).await; let (contr, proposed_convs) = self .compute_exchanged_amount( client, - pre_contr, + input, target_epoch, conversions.clone(), ) @@ -1089,7 +1285,6 @@ impl ShieldedContext { .expect( "found note with invalid value or asset type", ); - break; } _ => {} } @@ -1107,17 +1302,21 @@ impl ShieldedContext { client: &U::C, owner: PaymentAddress, viewing_key: &ViewingKey, - ) -> Result<(Amount, Epoch), PinnedBalanceError> { + ) -> Result<(MaspAmount, Epoch), PinnedBalanceError> { // Obtain the balance that will be exchanged let (amt, ep) = Self::compute_pinned_balance(client, owner, viewing_key).await?; + println!("Pinned balance: {:?}", amt); + // Establish connection with which to do exchange rate queries + let amount = self.decode_all_amounts(client, amt).await; + println!("Decoded pinned balance: {:?}", amount); // Finally, exchange the balance to the transaction's epoch - Ok(( - self.compute_exchanged_amount(client, amt, ep, HashMap::new()) - .await - .0, - ep, - )) + let computed_amount = self + .compute_exchanged_amount(client, amount, ep, HashMap::new()) + .await + .0; + println!("Exchanged amount: {:?}", computed_amount); + Ok((self.decode_all_amounts(client, computed_amount).await, ep)) } /// Convert an amount whose units are AssetTypes to one whose units are @@ -1128,15 +1327,25 @@ impl ShieldedContext { client: &U::C, amt: Amount, target_epoch: Epoch, - ) -> Amount
{ - let mut res = Amount::zero(); + ) -> HashMap { + let mut res = HashMap::new(); for (asset_type, val) in amt.components() { // Decode the asset type let decoded = self.decode_asset_type(client, *asset_type).await; // Only assets with the target timestamp count match decoded { - Some((addr, epoch)) if epoch == target_epoch => { - res += &Amount::from_pair(addr, *val).unwrap() + Some(asset_type @ (_, _, _, epoch)) + if epoch == target_epoch => + { + decode_component( + asset_type, + *val, + &mut res, + |address, sub_prefix, _| TokenAddress { + address, + sub_prefix, + }, + ); } _ => {} } @@ -1150,17 +1359,31 @@ impl ShieldedContext { &mut self, client: &U::C, amt: Amount, - ) -> Amount<(Address, Epoch)> { - let mut res = Amount::zero(); + ) -> MaspAmount { + let mut res: HashMap<(Epoch, TokenAddress), Change> = + HashMap::default(); for (asset_type, val) in amt.components() { // Decode the asset type - let decoded = self.decode_asset_type(client, *asset_type).await; - // Only assets with the target timestamp count - if let Some((addr, epoch)) = decoded { - res += &Amount::from_pair((addr, epoch), *val).unwrap() + if let Some(decoded) = + self.decode_asset_type(client, *asset_type).await + { + decode_component( + decoded, + *val, + &mut res, + |address, sub_prefix, epoch| { + ( + epoch, + TokenAddress { + address, + sub_prefix, + }, + ) + }, + ) } } - res + MaspAmount(res) } /// Make shielded components to embed within a Transfer object. If no @@ -1174,7 +1397,7 @@ impl ShieldedContext { pub async fn gen_shielded_transfer( &mut self, client: &U::C, - args: args::TxTransfer, + args: &args::TxTransfer, shielded_gas: bool, ) -> Result< Option<( @@ -1206,27 +1429,40 @@ impl ShieldedContext { let epoch = rpc::query_epoch(client).await; // Context required for storing which notes are in the source's // possesion - let amt: u64 = args.amount.into(); let memo = MemoBytes::empty(); // Now we build up the transaction within this object - let mut builder = Builder::<_, OsRng>::new(NETWORK, 1.into()); + let mut builder = Builder::::new(NETWORK, 1.into()); + + // break up a transfer into a number of transfers with suitable + // denominations + let InputAmount::Validated(amt) = args.amount else { + unreachable!("The function `gen_shielded_transfer` is only called by `submit_tx` which validates amounts.") + }; // Convert transaction amount into MASP types - let (asset_type, amount) = - convert_amount(epoch, &args.token, args.amount); - // The fee to be paid for the transaction - let tx_fee; + let (asset_types, amount) = convert_amount( + epoch, + &args.token, + &args.sub_prefix.as_ref(), + amt.amount, + ); + let tx_fee = // If there are shielded inputs if let Some(sk) = spending_key { + let InputAmount::Validated(fee) = args.tx.fee_amount else { + unreachable!("The function `gen_shielded_transfer` is only called by `submit_tx` which validates amounts.") + }; // Transaction fees need to match the amount in the wrapper Transfer // when MASP source is used - let (_, fee) = - convert_amount(epoch, &args.tx.fee_token, args.tx.fee_amount); - tx_fee = fee.clone(); - // If the gas is coming from the shielded pool, then our shielded - // inputs must also cover the gas fee - let required_amt = if shielded_gas { amount + fee } else { amount }; + let (_, shielded_fee) = + convert_amount(epoch, &args.tx.fee_token, &None, fee.amount); + let required_amt = if shielded_gas { + amount + shielded_fee.clone() + } else { + amount + }; + // Locate unspent notes that can help us meet the transaction amount let (_, unspent_notes, used_convs) = self .collect_unspent_notes( @@ -1244,20 +1480,17 @@ impl ShieldedContext { } // Commit the conversion notes used during summation for (conv, wit, value) in used_convs.values() { - if *value > 0 { - builder - .add_sapling_convert( - conv.clone(), - *value as u64, - wit.clone(), - ) - .map_err(builder::Error::SaplingBuild)?; + if value.is_positive() { + builder.add_sapling_convert( + conv.clone(), + *value as u64, + wit.clone(), + ) + .map_err(builder::Error::SaplingBuild)?; } } + shielded_fee } else { - // No transfer fees come from the shielded transaction for non-MASP - // sources - tx_fee = Amount::zero(); // We add a dummy UTXO to our transaction, but only the source of // the parent Transfer object is used to validate fund // availability @@ -1271,27 +1504,36 @@ impl ShieldedContext { source_enc.as_ref(), )); let script = TransparentAddress(hash.into()); - builder - .add_transparent_input(TxOut { - asset_type, - value: amt.try_into().expect("supplied amount too large"), - address: script, - }) - .map_err(builder::Error::TransparentBuild)?; - } + for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { + builder + .add_transparent_input(TxOut { + asset_type: *asset_type, + value: denom.denominate(&amt) as i128, + address: script, + }) + .map_err(builder::Error::TransparentBuild)?; + } + // No transfer fees come from the shielded transaction for non-MASP + // sources + Amount::zero() + }; + // Now handle the outputs of this transaction // If there is a shielded output if let Some(pa) = payment_address { let ovk_opt = spending_key.map(|x| x.expsk.ovk); - builder - .add_sapling_output( - ovk_opt, - pa.into(), - asset_type, - amt, - memo.clone(), - ) - .map_err(builder::Error::SaplingBuild)?; + for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) + { + builder + .add_sapling_output( + ovk_opt, + pa.into(), + *asset_type, + denom.denominate(&amt), + memo.clone(), + ) + .map_err(builder::Error::SaplingBuild)?; + } } else { // Embed the transparent target address into the shielded // transaction so that it can be signed @@ -1304,13 +1546,19 @@ impl ShieldedContext { let hash = ripemd::Ripemd160::digest(sha2::Sha256::digest( target_enc.as_ref(), )); - builder - .add_transparent_output( - &TransparentAddress(hash.into()), - asset_type, - amt.try_into().expect("supplied amount too large"), - ) - .map_err(builder::Error::TransparentBuild)?; + for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) + { + let vout = denom.denominate(&amt); + if vout != 0 { + builder + .add_transparent_output( + &TransparentAddress(hash.into()), + *asset_type, + vout as i128, + ) + .map_err(builder::Error::TransparentBuild)?; + } + } } // Now add outputs representing the change from this payment @@ -1449,17 +1697,18 @@ impl ShieldedContext { } // Describe how a Transfer simply subtracts from one // account and adds the same to another - let mut delta = TransferDelta::default(); - let tfer_delta = Amount::from_nonnegative( - transfer.token.clone(), - u64::from(transfer.amount), - ) - .expect("invalid value for amount"); - delta.insert( - transfer.source, - Amount::zero() - &tfer_delta, - ); - delta.insert(transfer.target, tfer_delta); + + let delta = TransferDelta::from([( + transfer.source.clone(), + MaspChange { + asset: TokenAddress { + address: transfer.token.clone(), + sub_prefix: transfer.sub_prefix.clone(), + }, + change: -transfer.amount.amount.change(), + }, + )]); + // No shielded accounts are affected by this // Transfer transfers.insert( @@ -1495,11 +1744,36 @@ fn extract_payload( } /// Make asset type corresponding to given address and epoch -pub fn make_asset_type(epoch: Epoch, token: &Address) -> AssetType { +pub fn make_asset_type( + epoch: Option, + token: &Address, + sub_prefix: &Option, + denom: MaspDenom, +) -> AssetType { // Typestamp the chosen token with the current epoch - let token_bytes = (token, epoch.0) - .try_to_vec() - .expect("token should serialize"); + let token_bytes = match epoch { + None => ( + token, + sub_prefix + .as_ref() + .map(|k| k.to_string()) + .unwrap_or_default(), + denom, + ) + .try_to_vec() + .expect("token should serialize"), + Some(epoch) => ( + token, + sub_prefix + .as_ref() + .map(|k| k.to_string()) + .unwrap_or_default(), + denom, + epoch.0, + ) + .try_to_vec() + .expect("token should serialize"), + }; // Generate the unique asset identifier from the unique token address AssetType::new(token_bytes.as_ref()).expect("unable to create asset type") } @@ -1508,13 +1782,24 @@ pub fn make_asset_type(epoch: Epoch, token: &Address) -> AssetType { fn convert_amount( epoch: Epoch, token: &Address, + sub_prefix: &Option<&String>, val: token::Amount, -) -> (AssetType, Amount) { - let asset_type = make_asset_type(epoch, token); - // Combine the value and unit into one amount - let amount = Amount::from_nonnegative(asset_type, u64::from(val)) - .expect("invalid value for amount"); - (asset_type, amount) +) -> ([AssetType; 4], Amount) { + let mut amount = Amount::zero(); + let asset_types: [AssetType; 4] = MaspDenom::iter() + .map(|denom| { + let asset_type = + make_asset_type(Some(epoch), token, sub_prefix, denom); + // Combine the value and unit into one amount + amount += + Amount::from_nonnegative(asset_type, denom.denominate(&val)) + .expect("invalid value for amount"); + asset_type + }) + .collect::>() + .try_into() + .expect("This can't fail"); + (asset_types, amount) } mod tests { diff --git a/shared/src/ledger/mod.rs b/shared/src/ledger/mod.rs index f933df26b3..399f75f800 100644 --- a/shared/src/ledger/mod.rs +++ b/shared/src/ledger/mod.rs @@ -14,6 +14,7 @@ pub mod queries; pub mod rpc; pub mod signing; pub mod storage; +#[allow(clippy::result_large_err)] pub mod tx; pub mod vp_host_fns; pub mod wallet; diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/native_vp/governance/mod.rs index 2c15a8d3a2..7d9323f256 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/native_vp/governance/mod.rs @@ -683,7 +683,7 @@ where } #[allow(clippy::upper_case_acronyms)] -#[derive(Debug)] +#[derive(Clone, Debug)] enum KeyType { #[allow(non_camel_case_types)] COUNTER, diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs index fe87319ff2..c00f57b5d9 100644 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ b/shared/src/ledger/native_vp/governance/utils.rs @@ -19,7 +19,6 @@ use crate::types::governance::{ ProposalVote, Tally, TallyResult, VotePower, VoteType, }; use crate::types::storage::Epoch; -use crate::types::token; /// Proposal structure holding votes information necessary to compute the /// outcome @@ -99,7 +98,7 @@ pub fn compute_tally( for (_, (amount, validator_vote)) in yay_validators.iter() { if let ProposalVote::Yay(vote_type) = validator_vote { if proposal_type == vote_type { - total_yay_staked_tokens += amount; + total_yay_staked_tokens += *amount; } else { // Log the error and continue tracing::error!( @@ -130,7 +129,7 @@ pub fn compute_tally( if !yay_validators.contains_key(validator_address) { // YAY: Add delegator amount whose validator // didn't vote / voted nay - total_yay_staked_tokens += vote_power; + total_yay_staked_tokens += *vote_power; } } ProposalVote::Nay => { @@ -138,7 +137,7 @@ pub fn compute_tally( // validator vote yay if yay_validators.contains_key(validator_address) { - total_yay_staked_tokens -= vote_power; + total_yay_staked_tokens -= *vote_power; } } @@ -176,14 +175,14 @@ pub fn compute_tally( result: tally_result, total_voting_power: total_stake, total_yay_power: total_yay_staked_tokens, - total_nay_power: 0, + total_nay_power: 0.into(), }) } else { Ok(ProposalResult { result: TallyResult::Rejected, total_voting_power: total_stake, total_yay_power: total_yay_staked_tokens, - total_nay_power: 0, + total_nay_power: 0.into(), }) } } @@ -194,8 +193,9 @@ pub fn compute_tally( validator_vote { for v in votes { - *total_yay_staked_tokens.entry(v).or_insert(0) += - amount; + *total_yay_staked_tokens + .entry(v) + .or_insert(VotePower::zero()) += *amount; } } else { // Log the error and continue @@ -236,7 +236,7 @@ pub fn compute_tally( total_yay_staked_tokens .get_mut(vote) { - *power -= vote_power; + *power -= *vote_power; } else { return Err(Error::Tally( format!( @@ -252,7 +252,9 @@ pub fn compute_tally( // this, add voting power *total_yay_staked_tokens .entry(vote) - .or_insert(0) += vote_power; + .or_insert( + VotePower::zero(), + ) += *vote_power; } } } else { @@ -272,7 +274,8 @@ pub fn compute_tally( for vote in delegator_votes { *total_yay_staked_tokens .entry(vote) - .or_insert(0) += vote_power; + .or_insert(VotePower::zero()) += + *vote_power; } } } @@ -295,7 +298,7 @@ pub fn compute_tally( total_yay_staked_tokens .get_mut(vote) { - *power -= vote_power; + *power -= *vote_power; } else { return Err(Error::Tally( format!( @@ -334,14 +337,16 @@ pub fn compute_tally( // At least 1/3 of the total voting power must vote Yay let total_yay_voted_power = total_yay_staked_tokens .iter() - .fold(0, |acc, (_, vote_power)| acc + vote_power); + .fold(VotePower::zero(), |acc, (_, vote_power)| { + acc + *vote_power + }); - match total_yay_voted_power.checked_mul(3) { + match total_yay_voted_power.checked_mul(3.into()) { Some(v) if v < total_stake => Ok(ProposalResult { result: TallyResult::Rejected, total_voting_power: total_stake, total_yay_power: total_yay_voted_power, - total_nay_power: 0, + total_nay_power: VotePower::zero(), }), _ => { // Select the winner council based on approval voting @@ -360,7 +365,7 @@ pub fn compute_tally( result: TallyResult::Passed(Tally::PGFCouncil(council)), total_voting_power: total_stake, total_yay_power: total_yay_voted_power, - total_nay_power: 0, + total_nay_power: VotePower::zero(), }) } } @@ -404,7 +409,8 @@ where epoch, )? .unwrap_or_default() - .into(); + .try_into() + .expect("Amount out of bounds"); yay_validators .insert(voter_address.clone(), (amount, vote)); @@ -420,13 +426,16 @@ where let amount = bond_amount(storage, &bond_id, epoch)?.1; - if amount != token::Amount::default() { + if !amount.is_zero() { let entry = delegators .entry(voter_address.to_owned()) .or_default(); entry.insert( validator.to_owned(), - (VotePower::from(amount), vote), + ( + VotePower::try_from(amount).unwrap(), + vote, + ), ); } } diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index 12d879e2f0..152112874b 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -5,13 +5,13 @@ pub mod vp; pub use namada_core::ledger::storage_api; use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::address; +pub use namada_core::types::dec::Dec; pub use namada_core::types::key::common; pub use namada_core::types::token; pub use namada_proof_of_stake; pub use namada_proof_of_stake::parameters::PosParams; pub use namada_proof_of_stake::storage::*; pub use namada_proof_of_stake::{staking_token_address, types}; -use rust_decimal::Decimal; pub use vp::PosVP; use crate::types::address::{Address, InternalAddress}; @@ -27,11 +27,13 @@ pub const SLASH_POOL_ADDRESS: Address = /// Calculate voting power in the tendermint context (which is stored as i64) /// from the number of tokens pub fn into_tm_voting_power( - votes_per_token: Decimal, - tokens: impl Into, + votes_per_token: Dec, + tokens: token::Amount, ) -> i64 { - let prod = decimal_mult_u64(votes_per_token, tokens.into()); - i64::try_from(prod).expect("Invalid validator voting power (i64)") + let tokens = tokens.change(); + let prod = votes_per_token * tokens; + let res = i128::try_from(prod).expect("Failed conversion to i128"); + i64::try_from(res).expect("Invalid validator voting power (i64)") } /// Initialize storage in the genesis block. diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index d65826ca68..799a34e5bd 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -878,10 +878,22 @@ mod test_rpc_handlers { b0i, b0ii, b1, - b2i(balance: token::Amount), - b3(a1: token::Amount, a2: token::Amount, a3: token::Amount), - b3i(a1: token::Amount, a2: token::Amount, a3: token::Amount), - b3ii(a1: token::Amount, a2: token::Amount, a3: token::Amount), + b2i(balance: token::DenominatedAmount), + b3( + a1: token::DenominatedAmount, + a2: token::DenominatedAmount, + a3: token::DenominatedAmount + ), + b3i( + a1: token::DenominatedAmount, + a2: token::DenominatedAmount, + a3: token::DenominatedAmount + ), + b3ii( + a1: token::DenominatedAmount, + a2: token::DenominatedAmount, + a3: token::DenominatedAmount + ), x, y(untyped_arg: &str), z(untyped_arg: &str), @@ -891,9 +903,9 @@ mod test_rpc_handlers { /// support optional args. pub fn b3iii( _ctx: RequestCtx<'_, D, H>, - a1: token::Amount, - a2: token::Amount, - a3: Option, + a1: token::DenominatedAmount, + a2: token::DenominatedAmount, + a3: Option, ) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, @@ -910,9 +922,9 @@ mod test_rpc_handlers { /// support optional args. pub fn b3iiii( _ctx: RequestCtx<'_, D, H>, - a1: token::Amount, - a2: token::Amount, - a3: Option, + a1: token::DenominatedAmount, + a2: token::DenominatedAmount, + a3: Option, a4: Option, ) -> storage_api::Result where @@ -966,14 +978,14 @@ mod test_rpc { }, ( "1" ) -> String = b1, ( "2" ) = { - ( "i" / [balance: token::Amount] ) -> String = b2i, + ( "i" / [balance: token::DenominatedAmount] ) -> String = b2i, }, - ( "3" / [a1: token::Amount] / [a2: token::Amount] ) = { - ( "i" / [a3: token:: Amount] ) -> String = b3i, - ( [a3: token:: Amount] ) -> String = b3, - ( [a3: token:: Amount] / "ii" ) -> String = b3ii, - ( [a3: opt token::Amount] / "iii" ) -> String = b3iii, - ( "iiii" / [a3: opt token::Amount] / "xyz" / [a4: opt Epoch] ) -> String = b3iiii, + ( "3" / [a1: token::DenominatedAmount] / [a2: token::DenominatedAmount] ) = { + ( "i" / [a3: token::DenominatedAmount] ) -> String = b3i, + ( [a3: token::DenominatedAmount] ) -> String = b3, + ( [a3: token::DenominatedAmount] / "ii" ) -> String = b3ii, + ( [a3: opt token::DenominatedAmount] / "iii" ) -> String = b3iii, + ( "iiii" / [a3: opt token::DenominatedAmount] / "xyz" / [a4: opt Epoch] ) -> String = b3iiii, }, }, ( "c" ) -> String = (with_options c), @@ -988,6 +1000,8 @@ mod test_rpc { #[cfg(test)] mod test { + use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; + use super::test_rpc::TEST_RPC; use crate::ledger::queries::testing::TestClient; use crate::ledger::queries::{RequestCtx, RequestQuery, Router}; @@ -1029,13 +1043,25 @@ mod test { let result = TEST_RPC.b1(&client).await.unwrap(); assert_eq!(result, "b1"); - let balance = token::Amount::from(123_000_000); + let balance = token::DenominatedAmount { + amount: token::Amount::native_whole(123_000_000), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; let result = TEST_RPC.b2i(&client, &balance).await.unwrap(); assert_eq!(result, format!("b2i/{balance}")); - let a1 = token::Amount::from(345); - let a2 = token::Amount::from(123_000); - let a3 = token::Amount::from(1_000_999); + let a1 = token::DenominatedAmount { + amount: token::Amount::native_whole(345), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + let a2 = token::DenominatedAmount { + amount: token::Amount::native_whole(123_000), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + let a3 = token::DenominatedAmount { + amount: token::Amount::native_whole(1_000_999), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; let result = TEST_RPC.b3(&client, &a1, &a2, &a3).await.unwrap(); assert_eq!(result, format!("b3/{a1}/{a2}/{a3}")); diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 32c6a16a20..889c6d7428 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -5,7 +5,8 @@ use masp_primitives::sapling::Node; use namada_core::ledger::storage::LastBlock; use namada_core::types::address::Address; use namada_core::types::hash::Hash; -use namada_core::types::storage::{BlockResults, KeySeg}; +use namada_core::types::storage::{BlockResults, Key, KeySeg}; +use namada_core::types::token::MaspDenom; use crate::ibc::core::ics04_channel::packet::Sequence; use crate::ibc::core::ics24_host::identifier::{ChannelId, ClientId, PortId}; @@ -23,6 +24,8 @@ use crate::types::transaction::TxResult; type Conversion = ( Address, + Option, + MaspDenom, Epoch, masp_primitives::transaction::components::Amount, MerklePath, @@ -151,7 +154,7 @@ where H: 'static + StorageHasher + Sync, { // Conversion values are constructed on request - if let Some((addr, epoch, conv, pos)) = ctx + if let Some(((addr, sub_prefix, denom), epoch, conv, pos)) = ctx .wl_storage .storage .conversion_state @@ -160,6 +163,8 @@ where { Ok(( addr.clone(), + sub_prefix.clone(), + *denom, *epoch, Into::::into( conv.clone(), @@ -470,7 +475,7 @@ mod test { // Request dry run tx let mut outer_tx = Tx::new(TxType::Decrypted(DecryptedTx::Decrypted { #[cfg(not(feature = "mainnet"))] - // To be able to dry-run testnet faucet withdrawal, pretend + // To be able to dry-run testnet faucet withdrawal, pretend // that we got a valid PoW has_valid_pow: true, })); @@ -515,7 +520,7 @@ mod test { assert!(!has_balance_key); // Then write some balance ... - let balance = token::Amount::from(1000); + let balance = token::Amount::native_whole(1000); StorageWrite::write(&mut client.wl_storage, &balance_key, balance)?; // It has to be committed to be visible in a query client.wl_storage.commit_tx(); diff --git a/shared/src/ledger/queries/vp/mod.rs b/shared/src/ledger/queries/vp/mod.rs index e575790bb4..ad05a2b88b 100644 --- a/shared/src/ledger/queries/vp/mod.rs +++ b/shared/src/ledger/queries/vp/mod.rs @@ -3,11 +3,16 @@ // Re-export to show in rustdoc! pub use pos::Pos; use pos::POS; +pub use token::Token; +use token::TOKEN; + pub mod pos; +mod token; // Validity predicate queries router! {VP, ( "pos" ) = (sub POS), + ( "token" ) = (sub TOKEN), } /// Client-only methods for the router type are composed from router functions. diff --git a/shared/src/ledger/queries/vp/pos.rs b/shared/src/ledger/queries/vp/pos.rs index 66d67a2f35..d872aa5002 100644 --- a/shared/src/ledger/queries/vp/pos.rs +++ b/shared/src/ledger/queries/vp/pos.rs @@ -207,7 +207,7 @@ where /// `None`. The total stake is a sum of validator's self-bonds and delegations /// to their address. /// Returns `None` when the given address is not a validator address. For a -/// validator with `0` stake, this returns `Ok(token::Amount::default())`. +/// validator with `0` stake, this returns `Ok(token::Amount::zero())`. fn validator_stake( ctx: RequestCtx<'_, D, H>, validator: Address, @@ -415,7 +415,7 @@ where let epoch = epoch.unwrap_or(ctx.wl_storage.storage.last_epoch); let handle = unbond_handle(&source, &validator); - let mut total = token::Amount::default(); + let mut total = token::Amount::zero(); for result in handle.iter(ctx.wl_storage)? { let ( lazy_map::NestedSubKey::Data { diff --git a/shared/src/ledger/queries/vp/token.rs b/shared/src/ledger/queries/vp/token.rs new file mode 100644 index 0000000000..cbad27005f --- /dev/null +++ b/shared/src/ledger/queries/vp/token.rs @@ -0,0 +1,43 @@ +use namada_core::ledger::storage::{DBIter, StorageHasher, DB}; +use namada_core::ledger::storage_api; +use namada_core::ledger::storage_api::token::read_denom; +use namada_core::types::address::Address; +use namada_core::types::storage::Key; +use namada_core::types::token; + +use crate::ledger::queries::RequestCtx; + +router! {TOKEN, + ( "denomination" / [addr: Address] / [sub_prefix: opt Key] ) -> Option = denomination, + ( "denomination" / [addr: Address] / "ibc" / [_ibc_junk: String] ) -> Option = denomination_ibc, +} + +/// Get the number of decimal places (in base 10) for a +/// token specified by `addr`. +fn denomination( + ctx: RequestCtx<'_, D, H>, + addr: Address, + sub_prefix: Option, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + read_denom(ctx.wl_storage, &addr, sub_prefix.as_ref()) +} + +// TODO Please fix this + +/// Get the number of decimal places (in base 10) for a +/// token specified by `addr`. +fn denomination_ibc( + ctx: RequestCtx<'_, D, H>, + addr: Address, + _ibc_junk: String, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + read_denom(ctx.wl_storage, &addr, None) +} diff --git a/shared/src/ledger/rpc.rs b/shared/src/ledger/rpc.rs index f8f0ea1e86..c6ec12c2ee 100644 --- a/shared/src/ledger/rpc.rs +++ b/shared/src/ledger/rpc.rs @@ -9,11 +9,14 @@ use namada_core::ledger::storage::LastBlock; use namada_core::ledger::testnet_pow; use namada_core::types::address::Address; use namada_core::types::storage::Key; -use namada_core::types::token::Amount; +use namada_core::types::token::{ + Amount, DenominatedAmount, Denomination, MaspDenom, TokenAddress, +}; use namada_proof_of_stake::types::{BondsAndUnbondsDetails, CommissionPair}; use serde::Serialize; use tokio::time::Duration; +use crate::ledger::args::InputAmount; use crate::ledger::events::Event; use crate::ledger::governance::parameters::GovParams; use crate::ledger::governance::storage as gov_storage; @@ -220,6 +223,8 @@ pub async fn query_conversion( asset_type: AssetType, ) -> Option<( Address, + Option, + MaspDenom, Epoch, masp_primitives::transaction::components::Amount, MerklePath, @@ -639,7 +644,8 @@ pub async fn get_proposal_votes( let amount: VotePower = get_validator_stake(client, epoch, &voter_address) .await - .into(); + .try_into() + .expect("Amount of bonds"); yay_validators.insert(voter_address, (amount, vote)); } else if !validators.contains(&voter_address) { let validator_address = @@ -774,15 +780,18 @@ pub async fn query_and_print_unbonds< } } if total_withdrawable != token::Amount::default() { - println!("Total withdrawable now: {total_withdrawable}."); + println!( + "Total withdrawable now: {}.", + total_withdrawable.to_string_native() + ); } if !not_yet_withdrawable.is_empty() { println!("Current epoch: {current_epoch}.") } for (withdraw_epoch, amount) in not_yet_withdrawable { println!( - "Amount {amount} withdrawable starting from epoch \ - {withdraw_epoch}." + "Amount {} withdrawable starting from epoch {withdraw_epoch}.", + amount.to_string_native() ); } } @@ -857,7 +866,8 @@ pub async fn get_governance_parameters< .expect("Parameter should be definied."); GovParams { - min_proposal_fund: u64::from(min_proposal_fund), + min_proposal_fund: u128::try_from(min_proposal_fund) + .expect("Amount out of bounds") as u64, max_proposal_code_size, min_proposal_period, max_proposal_period, @@ -920,3 +930,81 @@ pub async fn enriched_bonds_and_unbonds< .await, ) } + +/// Get the correct representation of the amount given the token type. +pub async fn validate_amount( + client: &C, + amount: InputAmount, + token: &Address, + sub_prefix: &Option, + force: bool, +) -> Option { + let input_amount = match amount { + InputAmount::Unvalidated(amt) => amt.canonical(), + InputAmount::Validated(amt) => return Some(amt), + }; + let denom = unwrap_client_response::>( + RPC.vp() + .token() + .denomination(client, token, sub_prefix) + .await, + ) + .or_else(|| { + if force { + println!( + "No denomination found for token: {token}, but --force was \ + passed. Defaulting to the provided denomination." + ); + Some(input_amount.denom) + } else { + println!( + "No denomination found for token: {token}, the input \ + arguments could not be parsed." + ); + None + } + })?; + if denom < input_amount.denom && !force { + println!( + "The input amount contained a higher precision than allowed by \ + {token}." + ); + None + } else { + match input_amount.increase_precision(denom) { + Ok(res) => Some(res), + Err(_) => { + println!( + "The amount provided requires more the 256 bits to \ + represent." + ); + None + } + } + } +} + +/// Look up the denomination of a token in order to format it +/// correctly as a string. +pub async fn format_denominated_amount< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + token: &TokenAddress, + amount: token::Amount, +) -> String { + let denom = unwrap_client_response::>( + RPC.vp() + .token() + .denomination(client, &token.address, &token.sub_prefix) + .await, + ) + .unwrap_or_else(|| { + println!( + "No denomination found for token: {token}, defaulting to zero \ + decimal places" + ); + 0.into() + }); + DenominatedAmount { amount, denom }.to_string() +} diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index 864ffcef2e..30bdb12d34 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -16,7 +16,10 @@ use masp_primitives::transaction::components::sapling::fees::{ InputView, OutputView, }; use namada_core::types::address::{masp, Address, ImplicitAddress}; -use namada_core::types::token::{self, Amount}; +use namada_core::types::storage::Key; +use namada_core::types::token::{ + self, Amount, DenominatedAmount, MaspDenom, TokenAddress, +}; use namada_core::types::transaction::{pos, MIN_FEE}; use prost::Message; use serde::{Deserialize, Serialize}; @@ -28,7 +31,9 @@ use crate::ibc::applications::transfer::msgs::transfer::{ use crate::ibc_proto::google::protobuf::Any; use crate::ledger::masp::make_asset_type; use crate::ledger::parameters::storage as parameter_storage; -use crate::ledger::rpc::{query_wasm_code_hash, TxBroadcastData}; +use crate::ledger::rpc::{ + format_denominated_amount, query_wasm_code_hash, TxBroadcastData, +}; use crate::ledger::tx::{ Error, TX_BOND_WASM, TX_CHANGE_COMMISSION_WASM, TX_IBC_WASM, TX_INIT_ACCOUNT_WASM, TX_INIT_PROPOSAL, TX_INIT_VALIDATOR_WASM, @@ -249,7 +254,7 @@ pub async fn sign_wrapper< #[cfg(not(feature = "mainnet"))] requires_pow: bool, ) -> TxBroadcastData { let fee_amount = if cfg!(feature = "mainnet") { - Amount::whole(MIN_FEE) + Amount::native_whole(MIN_FEE) } else { let wrapper_tx_fees_key = parameter_storage::get_wrapper_tx_fees_key(); rpc::query_storage_value::( @@ -268,15 +273,19 @@ pub async fn sign_wrapper< .unwrap_or_default(); let is_bal_sufficient = fee_amount <= balance; if !is_bal_sufficient { - eprintln!( + let token_addr = TokenAddress { + address: args.fee_token.clone(), + sub_prefix: None, + }; + let err_msg = format!( "The wrapper transaction source doesn't have enough balance to \ - pay fee {fee_amount}, got {balance}." + pay fee {}, got {}.", + format_denominated_amount(client, &token_addr, fee_amount).await, + format_denominated_amount(client, &token_addr, balance).await, ); + eprintln!("{}", err_msg); if !args.force && cfg!(feature = "mainnet") { - panic!( - "The wrapper transaction source doesn't have enough balance \ - to pay fee {fee_amount}, got {balance}." - ); + panic!("{}", err_msg); } } @@ -377,6 +386,7 @@ pub async fn sign_wrapper< } } +#[allow(clippy::result_large_err)] fn other_err(string: String) -> Result { Err(Error::Other(string)) } @@ -396,15 +406,25 @@ pub struct LedgerVector { fn make_ledger_amount_addr( tokens: &HashMap, output: &mut Vec, - amount: Amount, + amount: DenominatedAmount, token: &Address, + sub_prefix: &Option, prefix: &str, ) { + let token_address = TokenAddress { + address: token.clone(), + sub_prefix: sub_prefix.clone(), + }; if let Some(token) = tokens.get(token) { - output.push(format!("{}Amount: {} {}", prefix, token, amount)); + output.push(format!( + "{}Amount {}: {}", + prefix, + token_address.format_with_alias(token), + amount + )); } else { output.extend(vec![ - format!("{}Token: {}", prefix, token), + format!("{}Token: {}", prefix, token_address), format!("{}Amount: {}", prefix, amount), ]); } @@ -412,34 +432,41 @@ fn make_ledger_amount_addr( /// Adds a Ledger output line describing a given transaction amount and asset /// type -fn make_ledger_amount_asset( +async fn make_ledger_amount_asset( + client: &C, tokens: &HashMap, output: &mut Vec, amount: u64, token: &AssetType, - assets: &HashMap, + assets: &HashMap, MaspDenom, Epoch)>, prefix: &str, ) { - if let Some((token, _epoch)) = assets.get(token) { + if let Some((token, sub_prefix, _, _epoch)) = assets.get(token) { // If the AssetType can be decoded, then at least display Addressees + let token_addr = TokenAddress { + address: token.clone(), + sub_prefix: sub_prefix.clone(), + }; + let formatted_amt = + format_denominated_amount(client, &token_addr, amount.into()).await; if let Some(token) = tokens.get(token) { output.push(format!( "{}Amount: {} {}", prefix, - token, - Amount::from(amount) + token_addr.format_with_alias(token), + formatted_amt, )); } else { output.extend(vec![ - format!("{}Token: {}", prefix, token), - format!("{}Amount: {}", prefix, Amount::from(amount)), + format!("{}Token: {}", prefix, token_addr), + format!("{}Amount: {}", prefix, formatted_amt), ]); } } else { // Otherwise display the raw AssetTypes output.extend(vec![ format!("{}Token: {}", prefix, token), - format!("{}Amount: {}", prefix, Amount::from(amount)), + format!("{}Amount: {}", prefix, amount), ]); } } @@ -799,10 +826,16 @@ pub async fn to_ledger_vector< Section::MaspBuilder(builder) if builder.target == shielded_hash => { - for (addr, epoch) in &builder.asset_types { + for (addr, sub_prefix, denom, epoch) in &builder.asset_types + { asset_types.insert( - make_asset_type(*epoch, addr), - (addr.clone(), *epoch), + make_asset_type( + Some(*epoch), + addr, + sub_prefix, + *denom, + ), + (addr.clone(), sub_prefix.clone(), *denom, *epoch), ); } Some(builder) @@ -824,6 +857,7 @@ pub async fn to_ledger_vector< &mut tv.output, transfer.amount, &transfer.token, + &transfer.sub_prefix, "Sending ", ); } @@ -832,13 +866,15 @@ pub async fn to_ledger_vector< let vk = ExtendedViewingKey::from(*input.key()); tv.output.push(format!("Sender : {}", vk)); make_ledger_amount_asset( + client, &tokens, &mut tv.output, input.value(), &input.asset_type(), &asset_types, "Sending ", - ); + ) + .await; } } if transfer.target != masp() { @@ -849,6 +885,7 @@ pub async fn to_ledger_vector< &mut tv.output, transfer.amount, &transfer.token, + &transfer.sub_prefix, "Receiving ", ); } @@ -857,13 +894,15 @@ pub async fn to_ledger_vector< let pa = PaymentAddress::from(output.address()); tv.output.push(format!("Destination : {}", pa)); make_ledger_amount_asset( + client, &tokens, &mut tv.output, output.value(), &output.asset_type(), &asset_types, "Receiving ", - ); + ) + .await; } } if transfer.source != masp() && transfer.target != masp() { @@ -872,6 +911,7 @@ pub async fn to_ledger_vector< &mut tv.output, transfer.amount, &transfer.token, + &transfer.sub_prefix, "", ); } @@ -958,13 +998,13 @@ pub async fn to_ledger_vector< format!("Type : Bond"), format!("Source : {}", bond_source), format!("Validator : {}", bond.validator), - format!("Amount : {}", bond.amount), + format!("Amount : {}", bond.amount.to_string_native()), ]); tv.output_expert.extend(vec![ format!("Source : {}", bond_source), format!("Validator : {}", bond.validator), - format!("Amount : {}", bond.amount), + format!("Amount : {}", bond.amount.to_string_native()), ]); } else if code_hash == unbond_hash { let unbond = @@ -983,13 +1023,13 @@ pub async fn to_ledger_vector< format!("Code : Unbond"), format!("Source : {}", unbond_source), format!("Validator : {}", unbond.validator), - format!("Amount : {}", unbond.amount), + format!("Amount : {}", unbond.amount.to_string_native()), ]); tv.output_expert.extend(vec![ format!("Source : {}", unbond_source), format!("Validator : {}", unbond.validator), - format!("Amount : {}", unbond.amount), + format!("Amount : {}", unbond.amount.to_string_native()), ]); } else if code_hash == withdraw_hash { let withdraw = @@ -1035,19 +1075,32 @@ pub async fn to_ledger_vector< } if let Some(wrapper) = tx.header.wrapper() { + let gas_token = TokenAddress { + address: wrapper.fee.token.clone(), + sub_prefix: None, + }; + let gas_limit = format_denominated_amount( + client, + &gas_token, + Amount::from(wrapper.gas_limit), + ) + .await; + let gas_amount = + format_denominated_amount(client, &gas_token, wrapper.fee.amount) + .await; tv.output_expert.extend(vec![ format!("Timestamp : {}", tx.header.timestamp.0), format!("PK : {}", wrapper.pk), format!("Epoch : {}", wrapper.epoch), - format!("Gas limit : {}", Amount::from(wrapper.gas_limit)), - format!("Fee token : {}", wrapper.fee.token), + format!("Gas limit : {}", gas_limit), + format!("Fee token : {}", gas_token), ]); if let Some(token) = tokens.get(&wrapper.fee.token) { tv.output_expert - .push(format!("Fee amount : {} {}", token, wrapper.fee.amount)); + .push(format!("Fee amount : {} {}", token, gas_amount)); } else { tv.output_expert - .push(format!("Fee amount : {}", wrapper.fee.amount)); + .push(format!("Fee amount : {}", gas_amount)); } } diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index 628bcd8bfd..27245ecaed 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -1,6 +1,6 @@ //! SDK functions to construct different types of transactions use std::borrow::Cow; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::str::FromStr; use borsh::BorshSerialize; @@ -16,10 +16,12 @@ use masp_primitives::transaction::components::transparent::fees::{ }; use masp_primitives::transaction::components::Amount; use namada_core::types::address::{masp, masp_tx_key, Address}; +use namada_core::types::dec::Dec; +use namada_core::types::storage::Key; +use namada_core::types::token::MaspDenom; use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::types::CommissionPair; use prost::EncodeError; -use rust_decimal::Decimal; use sha2::{Digest as Sha2Digest, Sha256}; use thiserror::Error; use tokio::time::Duration; @@ -32,10 +34,10 @@ use crate::ibc::timestamp::Timestamp as IbcTimestamp; use crate::ibc::tx_msg::Msg; use crate::ibc::Height as IbcHeight; use crate::ibc_proto::cosmos::base::v1beta1::Coin; -use crate::ledger::args; +use crate::ledger::args::{self, InputAmount}; use crate::ledger::governance::storage as gov_storage; use crate::ledger::masp::{ShieldedContext, ShieldedUtils}; -use crate::ledger::rpc::{self, TxBroadcastData, TxResponse}; +use crate::ledger::rpc::{self, validate_amount, TxBroadcastData, TxResponse}; use crate::ledger::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey}; use crate::ledger::wallet::{Wallet, WalletUtils}; use crate::proto::{Code, Data, MaspBuilder, Section, Signature, Tx}; @@ -100,7 +102,7 @@ pub enum Error { TxBroadcast(RpcError), /// Invalid comission rate set #[error("Invalid new commission rate, received {0}")] - InvalidCommissionRate(Decimal), + InvalidCommissionRate(Dec), /// Invalid validator address #[error("The address {0} doesn't belong to any known validator account.")] InvalidValidatorAddress(Address), @@ -109,7 +111,7 @@ pub enum Error { "New rate, {0}, is too large of a change with respect to the \ predecessor epoch in which the rate will take effect." )] - TooLargeOfChange(Decimal), + TooLargeOfChange(Dec), /// Error retrieving from storage #[error("Error retrieving from storage")] Retrieval, @@ -130,13 +132,13 @@ pub enum Error { "The total bonds of the source {0} is lower than the amount to be \ unbonded. Amount to unbond is {1} and the total bonds is {2}." )] - LowerBondThanUnbond(Address, token::Amount, token::Amount), + LowerBondThanUnbond(Address, String, String), /// Balance is too low #[error( "The balance of the source {0} of token {1} is lower than the amount \ to be transferred. Amount to transfer is {2} and the balance is {3}." )] - BalanceTooLow(Address, Address, token::Amount, token::Amount), + BalanceTooLow(Address, Address, String, String), /// Token Address does not exist on chain #[error("The token address {0} doesn't exist on chain.")] TokenDoesNotExist(Address), @@ -158,13 +160,7 @@ pub enum Error { transferred and fees. Amount to transfer is {1} {2} and fees are {3} \ {4}." )] - NegativeBalanceAfterTransfer( - Address, - token::Amount, - Address, - token::Amount, - Address, - ), + NegativeBalanceAfterTransfer(Address, String, Address, String, Address), /// No Balance found for token #[error("{0}")] MaspError(builder::Error), @@ -527,6 +523,24 @@ pub async fn submit_tx( parsed } +/// decode components of a masp note +pub fn decode_component( + (addr, sub, denom, epoch): (Address, Option, MaspDenom, Epoch), + val: i128, + res: &mut HashMap, + mk_key: F, +) where + F: FnOnce(Address, Option, Epoch) -> K, + K: Eq + std::hash::Hash, +{ + let decoded_change = token::Change::from_masp_denominated(val, denom) + .expect("expected this to fit"); + + res.entry(mk_key(addr, sub, epoch)) + .and_modify(|val| *val += decoded_change) + .or_insert(decoded_change); +} + /// Save accounts initialized from a tx into the wallet, if any. pub async fn save_initialized_accounts( wallet: &mut Wallet, @@ -602,11 +616,9 @@ pub async fn submit_validator_commission_change< let validator = args.validator.clone(); if rpc::is_validator(client, &validator).await { - if args.rate < Decimal::ZERO || args.rate > Decimal::ONE { + if args.rate < Dec::zero() || args.rate > Dec::one() { eprintln!("Invalid new commission rate, received {}", args.rate); - if !args.tx.force { - return Err(Error::InvalidCommissionRate(args.rate)); - } + return Err(Error::InvalidCommissionRate(args.rate)); } let pipeline_epoch_minus_one = epoch + params.pipeline_len - 1; @@ -622,7 +634,7 @@ pub async fn submit_validator_commission_change< commission_rate, max_commission_change_per_epoch, }) => { - if (args.rate - commission_rate).abs() + if args.rate.abs_diff(&commission_rate) > max_commission_change_per_epoch { eprintln!( @@ -752,7 +764,7 @@ pub async fn submit_withdraw< Some(epoch), ) .await; - if tokens == 0.into() { + if tokens.is_zero() { eprintln!( "There are no unbonded bonds ready to withdraw in the current \ epoch {}.", @@ -763,7 +775,10 @@ pub async fn submit_withdraw< return Err(Error::NoUnbondReady(epoch)); } } else { - println!("Found {tokens} tokens that can be withdrawn."); + println!( + "Found {} tokens that can be withdrawn.", + tokens.to_string_native() + ); println!("Submitting transaction to withdraw them..."); } @@ -814,20 +829,25 @@ pub async fn submit_unbond< let bond_amount = rpc::query_bond(client, &bond_source, &args.validator, None).await; - println!("Bond amount available for unbonding: {} NAM", bond_amount); + println!( + "Bond amount available for unbonding: {} NAM", + bond_amount.to_string_native() + ); if args.amount > bond_amount { eprintln!( "The total bonds of the source {} is lower than the amount to \ be unbonded. Amount to unbond is {} and the total bonds is \ {}.", - bond_source, args.amount, bond_amount + bond_source, + args.amount.to_string_native(), + bond_amount.to_string_native() ); if !args.tx.force { return Err(Error::LowerBondThanUnbond( bond_source, - args.amount, - bond_amount, + args.amount.to_string_native(), + bond_amount.to_string_native(), )); } } @@ -898,21 +918,24 @@ pub async fn submit_unbond< std::cmp::Ordering::Equal => { println!( "Amount {} withdrawable starting from epoch {}", - latest_withdraw_amount_post - latest_withdraw_amount_pre, + (latest_withdraw_amount_post - latest_withdraw_amount_pre) + .to_string_native(), latest_withdraw_epoch_post ); } std::cmp::Ordering::Greater => { println!( "Amount {} withdrawable starting from epoch {}", - latest_withdraw_amount_post, latest_withdraw_epoch_post + latest_withdraw_amount_post.to_string_native(), + latest_withdraw_epoch_post, ); } } } else { println!( "Amount {} withdrawable starting from epoch {}", - latest_withdraw_amount_post, latest_withdraw_epoch_post + latest_withdraw_amount_post.to_string_native(), + latest_withdraw_epoch_post, ); } @@ -1068,10 +1091,14 @@ pub async fn submit_ibc_transfer< Some(sp) => sp.to_string().replace(RESERVED_ADDRESS_PREFIX, ""), None => token.to_string(), }; - let token = Coin { - denom, - amount: args.amount.to_string(), - }; + let amount = args + .amount + .to_string_native() + .split('.') + .next() + .expect("invalid amount") + .to_string(); + let token = Coin { denom, amount }; // this height should be that of the destination chain, not this chain let timeout_height = match args.timeout_height { @@ -1132,7 +1159,7 @@ async fn add_asset_type< C: crate::ledger::queries::Client + Sync, U: ShieldedUtils, >( - asset_types: &mut HashSet<(Address, Epoch)>, + asset_types: &mut HashSet<(Address, Option, MaspDenom, Epoch)>, shielded: &mut ShieldedContext, client: &C, asset_type: AssetType, @@ -1160,7 +1187,7 @@ async fn used_asset_types< shielded: &mut ShieldedContext, client: &C, builder: &Builder, -) -> Result, RpcError> { +) -> Result, MaspDenom, Epoch)>, RpcError> { let mut asset_types = HashSet::new(); // Collect all the asset types used in the Sapling inputs for input in builder.sapling_inputs() { @@ -1208,7 +1235,7 @@ pub async fn submit_transfer< client: &C, wallet: &mut Wallet, shielded: &mut ShieldedContext, - args: args::TxTransfer, + mut args: args::TxTransfer, ) -> Result<(), Error> { let source = args.source.effective_address(); let target = args.target.effective_address(); @@ -1222,7 +1249,7 @@ pub async fn submit_transfer< token_exists_or_err(token.clone(), args.tx.force, client).await?; // Check source balance let (sub_prefix, balance_key) = match &args.sub_prefix { - Some(sub_prefix) => { + Some(ref sub_prefix) => { let sub_prefix = storage::Key::parse(sub_prefix).unwrap(); let prefix = token::multitoken_balance_prefix(&token, &sub_prefix); ( @@ -1232,10 +1259,35 @@ pub async fn submit_transfer< } None => (None, token::balance_key(&token, &source)), }; + + // validate the amount given + let validated_amount = validate_amount( + client, + args.amount, + &token, + &sub_prefix, + args.tx.force, + ) + .await + .expect("expected to validate amount"); + let validate_fee = validate_amount( + client, + args.tx.fee_amount, + &args.tx.fee_token, + // TODO: Currently multi-tokens cannot be used to pay fees + &None, + args.tx.force, + ) + .await + .expect("expected to be able to validate fee"); + + args.amount = InputAmount::Validated(validated_amount); + args.tx.fee_amount = InputAmount::Validated(validate_fee); + check_balance_too_low_err::( &token, &source, - args.amount, + validated_amount.amount, balance_key, args.tx.force, client, @@ -1247,25 +1299,25 @@ pub async fn submit_transfer< // signer. Also, if the transaction is shielded, redact the amount and token // types by setting the transparent value to 0 and token type to a constant. // This has no side-effect because transaction is to self. - let (default_signer, amount, token) = + let (default_signer, _amount, token) = if source == masp_addr && target == masp_addr { // TODO Refactor me, we shouldn't rely on any specific token here. ( TxSigningKey::SecretKey(masp_tx_key()), - 0.into(), + token::Amount::default(), args.native_token.clone(), ) } else if source == masp_addr { ( TxSigningKey::SecretKey(masp_tx_key()), - args.amount, + validated_amount.amount, token.clone(), ) } else { ( TxSigningKey::WalletAddress(args.source.effective_address()), - args.amount, - token, + validated_amount.amount, + token.clone(), ) }; // If our chosen signer is the MASP sentinel key, then our shielded inputs @@ -1293,7 +1345,7 @@ pub async fn submit_transfer< for _ in 0..2 { // Construct the shielded part of the transaction, if any let stx_result = shielded - .gen_shielded_transfer(client, args.clone(), shielded_gas) + .gen_shielded_transfer(client, &args, shielded_gas) .await; let shielded_parts = match stx_result { @@ -1301,9 +1353,9 @@ pub async fn submit_transfer< Err(builder::Error::InsufficientFunds(_)) => { Err(Error::NegativeBalanceAfterTransfer( source.clone(), - args.amount, + validated_amount.amount.to_string_native(), token.clone(), - args.tx.fee_amount, + validate_fee.amount.to_string_native(), args.tx.fee_token.clone(), )) } @@ -1349,7 +1401,7 @@ pub async fn submit_transfer< target: target.clone(), token: token.clone(), sub_prefix: sub_prefix.clone(), - amount, + amount: validated_amount, key: key.clone(), // Link the Transfer to the MASP Transaction by hash code shielded: masp_hash, @@ -1652,7 +1704,7 @@ where /// Returns the given token if the given address exists on chain /// otherwise returns an error, force forces the address through even /// if it isn't on chain -async fn token_exists_or_err( +pub async fn token_exists_or_err( token: Address, force: bool, client: &C, @@ -1730,15 +1782,18 @@ async fn check_balance_too_low_err( "The balance of the source {} of token {} is lower \ than the amount to be transferred. Amount to \ transfer is {} and the balance is {}.", - source, token, amount, balance + source, + token, + amount.to_string_native(), + balance.to_string_native() ); Ok(()) } else { Err(Error::BalanceTooLow( source.clone(), token.clone(), - amount, - balance, + amount.to_string_native(), + balance.to_string_native(), )) } } else { diff --git a/shared/src/types/mod.rs b/shared/src/types/mod.rs index 1b73329efe..4d50ee4ac7 100644 --- a/shared/src/types/mod.rs +++ b/shared/src/types/mod.rs @@ -4,6 +4,6 @@ pub mod ibc; pub mod key; pub use namada_core::types::{ - address, chain, governance, hash, internal, masp, storage, time, token, - transaction, validity_predicate, + address, chain, dec, governance, hash, internal, masp, storage, time, + token, transaction, uint, validity_predicate, }; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 6f36d8f61f..64cd8a7e7c 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -20,6 +20,7 @@ mainnet = [ abciplus = [ "namada/abciplus", "namada/ibc-mocks", + "namada_apps/abciplus", "namada_vp_prelude/abciplus", "namada_tx_prelude/abciplus", ] @@ -38,8 +39,6 @@ ibc-relayer-types.workspace = true ibc-relayer.workspace = true prost.workspace = true regex.workspace = true -rust_decimal_macros.workspace = true -rust_decimal.workspace = true serde_json.workspace = true sha2.workspace = true tempfile.workspace = true diff --git a/tests/src/e2e/helpers.rs b/tests/src/e2e/helpers.rs index 60dc119051..6d9fee4298 100644 --- a/tests/src/e2e/helpers.rs +++ b/tests/src/e2e/helpers.rs @@ -17,6 +17,7 @@ use namada::types::token; use namada_apps::config::genesis::genesis_config; use namada_apps::config::utils::convert_tm_addr_to_socket_addr; use namada_apps::config::{Config, TendermintMode}; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use super::setup::{ self, sleep, NamadaBgCmd, NamadaCmd, Test, ENV_VAR_DEBUG, @@ -192,12 +193,13 @@ pub fn find_bonded_stake( .rsplit_once(' ') .unwrap() .1; - token::Amount::from_str(bonded_stake_str).map_err(|e| { - eyre!(format!( - "Bonded stake: {} parsed from {}, Error: {}\n\nOutput: {}", - bonded_stake_str, matched, e, unread - )) - }) + token::Amount::from_str(bonded_stake_str, NATIVE_MAX_DECIMAL_PLACES) + .map_err(|e| { + eyre!(format!( + "Bonded stake: {} parsed from {}, Error: {}\n\nOutput: {}", + bonded_stake_str, matched, e, unread + )) + }) } /// Get the last committed epoch. diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index 9e25f22d15..f217a59e6f 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -699,7 +699,7 @@ fn transfer_token( ALBERT, &receiver, NAM, - &Amount::whole(100000), + &Amount::native_whole(100000), port_channel_id_a, None, None, @@ -767,6 +767,7 @@ fn transfer_received_token( .to_string(); let rpc = get_actor_rpc(test, &Who::Validator(0)); + let amount = Amount::native_whole(50000).to_string_native(); let tx_args = [ "transfer", "--source", @@ -778,7 +779,7 @@ fn transfer_received_token( "--sub-prefix", &sub_prefix, "--amount", - "50000", + &amount, "--gas-amount", "0", "--gas-limit", @@ -824,7 +825,7 @@ fn transfer_back( BERTHA, &receiver, NAM, - &Amount::whole(50000), + &Amount::native_whole(50000), port_channel_id_b, Some(sub_prefix), None, @@ -883,7 +884,7 @@ fn transfer_timeout( ALBERT, &receiver, NAM, - &Amount::whole(100000), + &Amount::native_whole(100000), port_channel_id_a, None, Some(Duration::new(5, 0)), @@ -1025,7 +1026,7 @@ fn transfer( let rpc = get_actor_rpc(test, &Who::Validator(0)); let receiver = receiver.to_string(); - let amount = amount.to_string(); + let amount = amount.to_string_native(); let port_id = port_channel_id.port_id.to_string(); let channel_id = port_channel_id.channel_id.to_string(); let mut tx_args = vec![ diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 0b084e6f92..5273aa0842 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -29,6 +29,7 @@ use namada_apps::config::genesis::genesis_config::{ }; use namada_apps::config::utils::convert_tm_addr_to_socket_addr; use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; +use namada_core::types::token::{DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES}; use namada_test_utils::TestWasms; use serde_json::json; use setup::constants::*; @@ -372,7 +373,10 @@ fn ledger_txs_and_queries() -> Result<()> { target: find_address(&test, ALBERT).unwrap(), token: find_address(&test, NAM).unwrap(), sub_prefix: None, - amount: token::Amount::whole(10), + amount: token::DenominatedAmount { + amount: token::Amount::native_whole(10), + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }, key: None, shielded: None, } @@ -543,7 +547,7 @@ fn ledger_txs_and_queries() -> Result<()> { } let christel = find_address(&test, CHRISTEL)?; // as setup in `genesis/e2e-tests-single-node.toml` - let christel_balance = token::Amount::whole(1000000); + let christel_balance = token::Amount::native_whole(1000000); let nam = find_address(&test, NAM)?; let storage_key = token::balance_key(&nam, &christel).to_string(); let query_args_and_expected_response = vec![ @@ -824,9 +828,6 @@ fn masp_txs_and_queries() -> Result<()> { ), ]; - // Wait till epoch boundary - let _ep0 = epoch_sleep(&test, &validator_one_rpc, 720)?; - for (tx_args, tx_result) in &txs_args { for &dry_run in &[true, false] { let tx_args = if dry_run && tx_args[0] == "transfer" { @@ -834,7 +835,7 @@ fn masp_txs_and_queries() -> Result<()> { } else { tx_args.clone() }; - let mut client = run!(test, Bin::Client, tx_args, Some(300))?; + let mut client = run!(test, Bin::Client, tx_args, Some(720))?; if *tx_result == "Transaction is valid" && !dry_run { client.exp_string("Transaction accepted")?; @@ -864,7 +865,7 @@ fn masp_pinned_txs() -> Result<()> { let test = setup::network( |genesis| { let parameters = ParametersConfig { - epochs_per_year: epochs_per_year_from_min_duration(60), + epochs_per_year: epochs_per_year_from_min_duration(120), ..genesis.parameters }; GenesisConfig { @@ -926,6 +927,9 @@ fn masp_pinned_txs() -> Result<()> { client.exp_string("has not yet been consumed")?; client.assert_success(); + // Wait till epoch boundary + let _ep1 = epoch_sleep(&test, &validator_one_rpc, 720)?; + // Send 20 BTC from Albert to PPA(C) let mut client = run!( test, @@ -945,9 +949,15 @@ fn masp_pinned_txs() -> Result<()> { ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); + // Wait till epoch boundary + // This makes it more consistent for some reason? + let _ep2 = epoch_sleep(&test, &validator_one_rpc, 720)?; + // Assert PPA(C) has the 20 BTC transaction pinned to it let mut client = run!( test, @@ -1016,6 +1026,10 @@ fn masp_pinned_txs() -> Result<()> { #[test] fn masp_incentives() -> Result<()> { + // The number of decimal places used by BTC amounts. + const BTC_DENOMINATION: u8 = 8; + // The number of decimal places used by ETH amounts. + const ETH_DENOMINATION: u8 = 18; // Download the shielded pool parameters before starting node let _ = CLIShieldedUtils::new(PathBuf::new()); // Lengthen epoch to ensure that a transaction can be constructed and @@ -1049,7 +1063,7 @@ fn masp_incentives() -> Result<()> { let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); // Wait till epoch boundary - let ep0 = epoch_sleep(&test, &validator_one_rpc, 720)?; + let ep0 = get_epoch(&test, &validator_one_rpc)?; // Send 20 BTC from Albert to PA(A) let mut client = run!( @@ -1070,6 +1084,8 @@ fn masp_incentives() -> Result<()> { ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); @@ -1086,7 +1102,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("btc: 20")?; client.assert_success(); @@ -1104,7 +1120,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded nam balance found")?; client.assert_success(); @@ -1127,13 +1143,13 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("btc: 20")?; client.assert_success(); - let amt20 = token::Amount::from_str("20").unwrap(); - let amt30 = token::Amount::from_str("30").unwrap(); + let amt20 = token::Amount::from_uint(20, BTC_DENOMINATION).unwrap(); + let amt10 = token::Amount::from_uint(10, ETH_DENOMINATION).unwrap(); // Assert NAM balance at VK(A) is 20*BTC_reward*(epoch_1-epoch_0) let mut client = run!( @@ -1148,12 +1164,14 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep1.0 - ep0.0) - ))?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is 20*BTC_reward*(epoch_1-epoch_0) @@ -1169,12 +1187,14 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep1.0 - ep0.0) - ))?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep1.0 - ep0.0); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary @@ -1193,7 +1213,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("btc: 20")?; client.assert_success(); @@ -1211,12 +1231,14 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep2.0 - ep0.0) - ))?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is 20*BTC_reward*(epoch_2-epoch_0) @@ -1232,18 +1254,20 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep2.0 - ep0.0) - ))?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep2.0 - ep0.0); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary let ep3 = epoch_sleep(&test, &validator_one_rpc, 720)?; - // Send 30 ETH from Albert to PA(B) + // Send 10 ETH from Albert to PA(B) let mut client = run!( test, Bin::Client, @@ -1256,16 +1280,18 @@ fn masp_incentives() -> Result<()> { "--token", ETH, "--amount", - "30", + "10", "--node", &validator_one_rpc ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); - // Assert ETH balance at VK(B) is 30 + // Assert ETH balance at VK(B) is 10 let mut client = run!( test, Bin::Client, @@ -1278,9 +1304,9 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string("eth: 30")?; + client.exp_string("eth: 10")?; client.assert_success(); // Assert NAM balance at VK(B) is 0 @@ -1296,7 +1322,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded nam balance found")?; client.assert_success(); @@ -1304,7 +1330,7 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary let ep4 = epoch_sleep(&test, &validator_one_rpc, 720)?; - // Assert ETH balance at VK(B) is 30 + // Assert ETH balance at VK(B) is 10 let mut client = run!( test, Bin::Client, @@ -1317,12 +1343,12 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string("eth: 30")?; + client.exp_string("eth: 10")?; client.assert_success(); - // Assert NAM balance at VK(B) is 30*ETH_reward*(epoch_4-epoch_3) + // Assert NAM balance at VK(B) is 10*ETH_reward*(epoch_4-epoch_3) let mut client = run!( test, Bin::Client, @@ -1335,16 +1361,18 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - (amt30 * masp_rewards[ð()]).0 * (ep4.0 - ep3.0) - ))?; + let amt = (amt10 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is - // 20*BTC_reward*(epoch_4-epoch_0)+30*ETH_reward*(epoch_4-epoch_3) + // 20*BTC_reward*(epoch_4-epoch_0)+10*ETH_reward*(epoch_4-epoch_3) let mut client = run!( test, Bin::Client, @@ -1357,19 +1385,21 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - ((amt20 * masp_rewards[&btc()]).0 * (ep4.0 - ep0.0)) - + ((amt30 * masp_rewards[ð()]).0 * (ep4.0 - ep3.0)) - ))?; + let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep4.0 - ep0.0)) + + ((amt10 * masp_rewards[&(eth(), None)]).0 * (ep4.0 - ep3.0)); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated))?; client.assert_success(); // Wait till epoch boundary let ep5 = epoch_sleep(&test, &validator_one_rpc, 720)?; - // Send 30 ETH from SK(B) to Christel + // Send 10 ETH from SK(B) to Christel let mut client = run!( test, Bin::Client, @@ -1382,7 +1412,7 @@ fn masp_incentives() -> Result<()> { "--token", ETH, "--amount", - "30", + "10", "--signer", BERTHA, "--node", @@ -1390,6 +1420,8 @@ fn masp_incentives() -> Result<()> { ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); @@ -1406,14 +1438,14 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded eth balance found")?; client.assert_success(); let mut ep = get_epoch(&test, &validator_one_rpc)?; - // Assert NAM balance at VK(B) is 30*ETH_reward*(ep-epoch_3) + // Assert NAM balance at VK(B) is 10*ETH_reward*(ep-epoch_3) let mut client = run!( test, Bin::Client, @@ -1426,17 +1458,19 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - (amt30 * masp_rewards[ð()]).0 * (ep.0 - ep3.0) - ))?; + let amt = (amt10 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); ep = get_epoch(&test, &validator_one_rpc)?; // Assert NAM balance at MASP pool is - // 20*BTC_reward*(epoch_5-epoch_0)+30*ETH_reward*(epoch_5-epoch_3) + // 20*BTC_reward*(epoch_5-epoch_0)+10*ETH_reward*(epoch_5-epoch_3) let mut client = run!( test, Bin::Client, @@ -1449,13 +1483,15 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - ((amt20 * masp_rewards[&btc()]).0 * (ep.0 - ep0.0)) - + ((amt30 * masp_rewards[ð()]).0 * (ep.0 - ep3.0)) - ))?; + let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep.0 - ep0.0)) + + ((amt10 * masp_rewards[&(eth(), None)]).0 * (ep.0 - ep3.0)); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated))?; client.assert_success(); // Wait till epoch boundary @@ -1482,6 +1518,8 @@ fn masp_incentives() -> Result<()> { ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); @@ -1498,7 +1536,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded btc balance found")?; client.assert_success(); @@ -1516,12 +1554,14 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0) - ))?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is @@ -1538,13 +1578,15 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - ((amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[ð()]).0 * (ep5.0 - ep3.0)) - ))?; + let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) + + ((amt10 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary @@ -1563,15 +1605,17 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - (amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0) - ))?; + let amt = (amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated))?; client.assert_success(); - // Assert NAM balance at VK(B) is 30*ETH_reward*(epoch_5-epoch_3) + // Assert NAM balance at VK(B) is 10*ETH_reward*(epoch_5-epoch_3) let mut client = run!( test, Bin::Client, @@ -1584,16 +1628,18 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - (amt30 * masp_rewards[ð()]).0 * (ep5.0 - ep3.0) - ))?; + let amt = (amt10 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Assert NAM balance at MASP pool is - // 20*BTC_reward*(epoch_6-epoch_0)+30*ETH_reward*(epoch_5-epoch_3) + // 20*BTC_reward*(epoch_6-epoch_0)+10*ETH_reward*(epoch_5-epoch_3) let mut client = run!( test, Bin::Client, @@ -1606,20 +1652,22 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; - client.exp_string(&format!( - "nam: {}", - ((amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0)) - + ((amt30 * masp_rewards[ð()]).0 * (ep5.0 - ep3.0)) - ))?; + let amt = ((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) + + ((amt10 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)); + let denominated = DenominatedAmount { + amount: amt, + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }; + client.exp_string(&format!("nam: {}", denominated,))?; client.assert_success(); // Wait till epoch boundary to prevent conversion expiry during transaction // construction let _ep8 = epoch_sleep(&test, &validator_one_rpc, 720)?; - // Send 30*ETH_reward*(epoch_5-epoch_3) NAM from SK(B) to Christel + // Send 10*ETH_reward*(epoch_5-epoch_3) NAM from SK(B) to Christel let mut client = run!( test, Bin::Client, @@ -1632,7 +1680,8 @@ fn masp_incentives() -> Result<()> { "--token", NAM, "--amount", - &((amt30 * masp_rewards[ð()]).0 * (ep5.0 - ep3.0)).to_string(), + &((amt10 * masp_rewards[&(eth(), None)]).0 * (ep5.0 - ep3.0)) + .to_string_native(), "--signer", BERTHA, "--node", @@ -1640,6 +1689,8 @@ fn masp_incentives() -> Result<()> { ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); @@ -1659,7 +1710,8 @@ fn masp_incentives() -> Result<()> { "--token", NAM, "--amount", - &((amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0)).to_string(), + &((amt20 * masp_rewards[&(btc(), None)]).0 * (ep6.0 - ep0.0)) + .to_string_native(), "--signer", ALBERT, "--node", @@ -1667,6 +1719,8 @@ fn masp_incentives() -> Result<()> { ], Some(300) )?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid")?; client.assert_success(); @@ -1683,7 +1737,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded nam balance found")?; client.assert_success(); @@ -1701,7 +1755,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("No shielded nam balance found")?; client.assert_success(); @@ -1719,7 +1773,7 @@ fn masp_incentives() -> Result<()> { "--node", &validator_one_rpc ], - Some(300) + Some(60) )?; client.exp_string("nam: 0")?; client.assert_success(); @@ -1817,7 +1871,7 @@ fn invalid_transactions() -> Result<()> { "--token", BERTHA, "--amount", - "1_000_000.1", + "1000000.1", "--gas-amount", "0", "--gas-limit", @@ -1949,7 +2003,8 @@ fn pos_bonds() -> Result<()> { ]; let mut client = run_as!(test, Who::Validator(0), Bin::Client, tx_args, Some(40))?; - client.exp_string("Amount 5100 withdrawable starting from epoch ")?; + client + .exp_string("Amount 5100.000000 withdrawable starting from epoch ")?; client.assert_success(); // 5. Submit an unbond of the delegation @@ -1971,7 +2026,7 @@ fn pos_bonds() -> Result<()> { &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; - let expected = "Amount 3200 withdrawable starting from epoch "; + let expected = "Amount 3200.000000 withdrawable starting from epoch "; let (_unread, matched) = client.exp_regex(&format!("{expected}.*\n"))?; let epoch_raw = matched.trim().split_once(expected).unwrap().1; let delegation_withdrawable_epoch = Epoch::from_str(epoch_raw).unwrap(); @@ -2317,17 +2372,17 @@ fn test_bond_queries() -> Result<()> { ]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; client.exp_string("Transaction is valid.")?; + let (_, res) = client + .exp_regex(r"withdrawable starting from epoch [0-9]+") + .unwrap(); + let withdraw_epoch = + Epoch::from_str(res.split(' ').last().unwrap()).unwrap(); client.assert_success(); - // 6. Wait for epoch 7 - let start = Instant::now(); - let loop_timeout = Duration::new(20, 0); + // 6. Wait for withdraw_epoch loop { - if Instant::now().duration_since(start) > loop_timeout { - panic!("Timed out waiting for epoch: {}", 7); - } - let epoch = get_epoch(&test, &validator_one_rpc)?; - if epoch >= Epoch(7) { + let epoch = epoch_sleep(&test, &validator_one_rpc, 120)?; + if epoch >= withdraw_epoch { break; } } @@ -2336,11 +2391,11 @@ fn test_bond_queries() -> Result<()> { let tx_args = vec!["bonds", "--ledger-address", &validator_one_rpc]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; client.exp_string( - "All bonds total active: 200088\r -All bonds total: 200088\r -All unbonds total active: 412\r -All unbonds total: 412\r -All unbonds total withdrawable: 412\r", + "All bonds total active: 200088.000000\r +All bonds total: 200088.000000\r +All unbonds total active: 412.000000\r +All unbonds total: 412.000000\r +All unbonds total withdrawable: 412.000000\r", )?; client.assert_success(); @@ -2577,7 +2632,7 @@ fn pos_init_validator() -> Result<()> { find_bonded_stake(&test, new_validator, &non_validator_rpc)?; assert_eq!( bonded_stake, - token::Amount::whole(validator_stake + delegation) + token::Amount::native_whole(validator_stake + delegation) ); Ok(()) @@ -2899,7 +2954,7 @@ fn proposal_submission() -> Result<()> { // 9. Send a yay vote from a validator let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); while epoch.0 <= 13 { - sleep(1); + sleep(10); epoch = get_epoch(&test, &validator_one_rpc).unwrap(); } @@ -2964,7 +3019,7 @@ fn proposal_submission() -> Result<()> { // 11. Query the proposal and check the result let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); while epoch.0 <= 25 { - sleep(1); + sleep(10); epoch = get_epoch(&test, &validator_one_rpc).unwrap(); } @@ -2983,7 +3038,7 @@ fn proposal_submission() -> Result<()> { // 12. Wait proposal grace and check proposal author funds let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); while epoch.0 < 31 { - sleep(1); + sleep(10); epoch = get_epoch(&test, &validator_one_rpc).unwrap(); } diff --git a/tests/src/e2e/multitoken_tests.rs b/tests/src/e2e/multitoken_tests.rs index 460395cc59..0f2b15d877 100644 --- a/tests/src/e2e/multitoken_tests.rs +++ b/tests/src/e2e/multitoken_tests.rs @@ -26,7 +26,7 @@ fn test_multitoken_transfer_implicit_to_implicit() -> Result<()> { println!("Fake multitoken VP established at {}", multitoken_vp_addr); let albert_addr = e2e::helpers::find_address(&test, ALBERT)?; - let albert_starting_red_balance = token::Amount::from(100_000_000); + let albert_starting_red_balance = token::Amount::native_whole(100_000_000); helpers::mint_red_tokens( &test, &rpc_addr, @@ -35,7 +35,7 @@ fn test_multitoken_transfer_implicit_to_implicit() -> Result<()> { &albert_starting_red_balance, )?; - let transfer_amount = token::Amount::from(10_000_000); + let transfer_amount = token::Amount::native_whole(10_000_000); // make a transfer from Albert to Bertha, signed by Christel - this should // be rejected @@ -70,7 +70,7 @@ fn test_multitoken_transfer_implicit_to_implicit() -> Result<()> { ALBERT, BERTHA, ALBERT, - &token::Amount::from(10_000_000), + &token::Amount::native_whole(10_000_000), )?; authorized_transfer.exp_string("Transaction applied with result")?; authorized_transfer.exp_string("Transaction is valid")?; @@ -110,7 +110,8 @@ fn test_multitoken_transfer_established_to_implicit() -> Result<()> { established_alias, )?; - let established_starting_red_balance = token::Amount::from(100_000_000); + let established_starting_red_balance = + token::Amount::native_whole(100_000_000); // mint some red tokens for the established account let established_addr = e2e::helpers::find_address(&test, established_alias)?; @@ -122,7 +123,7 @@ fn test_multitoken_transfer_established_to_implicit() -> Result<()> { &established_starting_red_balance, )?; - let transfer_amount = token::Amount::from(10_000_000); + let transfer_amount = token::Amount::native_whole(10_000_000); // attempt an unauthorized transfer to Albert from the established account let mut unauthorized_transfer = helpers::attempt_red_tokens_transfer( &test, @@ -197,7 +198,7 @@ fn test_multitoken_transfer_implicit_to_established() -> Result<()> { )?; let albert_addr = e2e::helpers::find_address(&test, ALBERT)?; - let albert_starting_red_balance = token::Amount::from(100_000_000); + let albert_starting_red_balance = token::Amount::native_whole(100_000_000); helpers::mint_red_tokens( &test, &rpc_addr, @@ -206,7 +207,7 @@ fn test_multitoken_transfer_implicit_to_established() -> Result<()> { &albert_starting_red_balance, )?; - let transfer_amount = token::Amount::from(10_000_000); + let transfer_amount = token::Amount::native_whole(10_000_000); // attempt an unauthorized transfer from Albert to the established account let mut unauthorized_transfer = helpers::attempt_red_tokens_transfer( @@ -280,7 +281,8 @@ fn test_multitoken_transfer_established_to_established() -> Result<()> { established_alias, )?; - let established_starting_red_balance = token::Amount::from(100_000_000); + let established_starting_red_balance = + token::Amount::native_whole(100_000_000); // mint some red tokens for the established account let established_addr = e2e::helpers::find_address(&test, established_alias)?; @@ -302,7 +304,8 @@ fn test_multitoken_transfer_established_to_established() -> Result<()> { receiver_alias, )?; - let established_starting_red_balance = token::Amount::from(100_000_000); + let established_starting_red_balance = + token::Amount::native_whole(100_000_000); // mint some red tokens for the established account let established_addr = e2e::helpers::find_address(&test, established_alias)?; @@ -314,7 +317,7 @@ fn test_multitoken_transfer_established_to_established() -> Result<()> { &established_starting_red_balance, )?; - let transfer_amount = token::Amount::from(10_000_000); + let transfer_amount = token::Amount::native_whole(10_000_000); // attempt an unauthorized transfer let mut unauthorized_transfer = helpers::attempt_red_tokens_transfer( diff --git a/tests/src/e2e/multitoken_tests/helpers.rs b/tests/src/e2e/multitoken_tests/helpers.rs index 7008910b5e..27228cf266 100644 --- a/tests/src/e2e/multitoken_tests/helpers.rs +++ b/tests/src/e2e/multitoken_tests/helpers.rs @@ -1,11 +1,11 @@ //! Helpers for use in multitoken tests. use std::path::PathBuf; -use std::str::FromStr; use borsh::BorshSerialize; use color_eyre::eyre::Result; use eyre::Context; use namada_core::types::address::Address; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_core::types::{storage, token}; use namada_test_utils::tx_data::TxWriteData; use namada_test_utils::TestWasms; @@ -138,7 +138,7 @@ pub fn attempt_red_tokens_transfer( signer: &str, amount: &token::Amount, ) -> Result { - let amount = amount.to_string(); + let amount = amount.to_string_native(); let transfer_args = vec![ "transfer", "--token", @@ -184,6 +184,6 @@ pub fn fetch_red_token_balance( println!("Got balance for {}: {}", owner_alias, matched); let decimal = decimal_regex.find(&matched).unwrap().as_str(); client_balance.assert_success(); - token::Amount::from_str(decimal) + token::Amount::from_str(decimal, NATIVE_MAX_DECIMAL_PLACES) .wrap_err(format!("Failed to parse {}", matched)) } diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index 47cde7a28e..930b497480 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -21,6 +21,7 @@ use eyre::{eyre, Context}; use itertools::{Either, Itertools}; use namada::types::chain::ChainId; use namada_apps::client::utils; +use namada_apps::client::utils::REDUCED_CLI_PRINTING; use namada_apps::config::genesis::genesis_config::{self, GenesisConfig}; use namada_apps::{config, wallet}; use once_cell::sync::Lazy; @@ -118,7 +119,7 @@ pub fn network( eprintln!("Failed setting up colorful error reports {}", err); } }); - + env::set_var(REDUCED_CLI_PRINTING, "true"); let working_dir = working_dir(); let test_dir = TestDir::new(); diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index f4ba9de6f6..b76b4dd041 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -502,7 +502,7 @@ mod tests { validator: &Address, amount: token::Amount, ) -> bool { - let raw_amount: u64 = amount.into(); + let raw_amount: u128 = amount.try_into().unwrap(); let mut total_bonds: u64 = 0; for action in self.all_valid_actions().into_iter() { match action { @@ -513,8 +513,8 @@ mod tests { } => { if owner == &bond_owner && validator == &bond_validator { - let raw_amount: u64 = amount.into(); - total_bonds += raw_amount; + let raw_amount: u128 = amount.try_into().unwrap(); + total_bonds += raw_amount as u64; } } ValidPosAction::Unbond { @@ -524,15 +524,15 @@ mod tests { } => { if owner == &bond_owner && validator == &bond_validator { - let raw_amount: u64 = amount.into(); - total_bonds -= raw_amount; + let raw_amount: u128 = amount.try_into().unwrap(); + total_bonds -= raw_amount as u64; } } _ => {} } } - total_bonds >= raw_amount + total_bonds as u128 >= raw_amount } /// Find if the given owner and validator has unbonds that are ready to @@ -576,9 +576,10 @@ pub mod testing { use namada::types::key::RefTo; use namada::types::storage::Epoch; use namada::types::{address, key, token}; + use namada_core::types::dec::Dec; + use namada_core::types::token::{Amount, Change}; use namada_tx_prelude::{Address, StorageRead, StorageWrite}; use proptest::prelude::*; - use rust_decimal::Decimal; use crate::tx::{self, tx_host_env}; @@ -599,8 +600,8 @@ pub mod testing { InitValidator { address: Address, consensus_key: PublicKey, - commission_rate: Decimal, - max_commission_rate_change: Decimal, + commission_rate: Dec, + max_commission_rate_change: Dec, }, Bond { amount: token::Amount, @@ -640,14 +641,14 @@ pub mod testing { Bond { owner: Address, validator: Address, - delta: i128, + delta: Change, offset: DynEpochOffset, }, /// Add tokens unbonded from a bond at unbonding offset Unbond { owner: Address, validator: Address, - delta: i128, + delta: Change, }, /// Withdraw tokens from an unbond at the current epoch WithdrawUnbond { @@ -655,12 +656,12 @@ pub mod testing { validator: Address, }, TotalDeltas { - delta: i128, + delta: Change, offset: Either, }, ValidatorSet { validator: Address, - token_delta: i128, + token_delta: Change, offset: DynEpochOffset, }, ValidatorConsensusKey { @@ -670,7 +671,7 @@ pub mod testing { }, ValidatorDeltas { validator: Address, - delta: i128, + delta: Change, offset: DynEpochOffset, }, ValidatorState { @@ -678,7 +679,7 @@ pub mod testing { state: ValidatorState, }, StakingTokenPosBalance { - delta: i128, + delta: Change, }, ValidatorAddressRawHash { address: Address, @@ -687,11 +688,11 @@ pub mod testing { }, ValidatorCommissionRate { address: Address, - rate: Decimal, + rate: Dec, }, ValidatorMaxCommissionRateChange { address: Address, - change: Decimal, + change: Dec, }, } @@ -749,7 +750,7 @@ pub mod testing { arb_validator, ) .prop_map(|(amount, owner, validator)| ValidPosAction::Bond { - amount: amount.into(), + amount: Amount::from_uint(amount, 0).unwrap(), owner, validator, }); @@ -779,12 +780,15 @@ pub mod testing { // them let arb_unbond = arb_current_bond.prop_flat_map( |(bond_id, current_bond_amount)| { - let current_bond_amount: u64 = - current_bond_amount.into(); + let current_bond_amount = + >::try_into( + current_bond_amount, + ) + .unwrap() as u64; // Unbond an arbitrary amount up to what's available (0..current_bond_amount).prop_map(move |amount| { ValidPosAction::Unbond { - amount: amount.into(), + amount: Amount::from_uint(amount, 0).unwrap(), owner: bond_id.source.clone(), validator: bond_id.validator.clone(), } @@ -883,7 +887,7 @@ pub mod testing { }, PosStorageChange::ValidatorSet { validator: address.clone(), - token_delta: 0, + token_delta: 0.into(), offset, }, PosStorageChange::ValidatorConsensusKey { @@ -896,7 +900,7 @@ pub mod testing { }, PosStorageChange::ValidatorDeltas { validator: address.clone(), - delta: 0, + delta: 0.into(), offset, }, PosStorageChange::ValidatorCommissionRate { @@ -1302,13 +1306,11 @@ pub mod testing { ); let mut balance: token::Amount = tx::ctx().read(&balance_key).unwrap().unwrap_or_default(); - if delta < 0 { - let to_spend: u64 = (-delta).try_into().unwrap(); - let to_spend: token::Amount = to_spend.into(); + if !delta.non_negative() { + let to_spend = token::Amount::from_change(delta); balance.spend(&to_spend); } else { - let to_recv: u64 = delta.try_into().unwrap(); - let to_recv: token::Amount = to_recv.into(); + let to_recv = token::Amount::from_change(delta); balance.receive(&to_recv); } tx::ctx().write(&balance_key, balance).unwrap(); @@ -1363,7 +1365,7 @@ pub mod testing { pub fn apply_validator_set_change( _validator: Address, - _token_delta: i128, + _token_delta: Change, _offset: DynEpochOffset, _current_epoch: Epoch, _params: &PosParams, @@ -1542,7 +1544,7 @@ pub mod testing { PosStorageChange::Bond { owner, validator, - delta, + delta: delta.into(), offset, }, ] diff --git a/tests/src/storage_api/testnet_pow.rs b/tests/src/storage_api/testnet_pow.rs index 88d391796a..5e54188c1b 100644 --- a/tests/src/storage_api/testnet_pow.rs +++ b/tests/src/storage_api/testnet_pow.rs @@ -11,7 +11,7 @@ use crate::vp; fn test_challenge_and_solution() -> storage_api::Result<()> { let faucet_address = address::testing::established_address_1(); let difficulty = Difficulty::try_new(1).unwrap(); - let withdrawal_limit = token::Amount::whole(1_000); + let withdrawal_limit = token::Amount::native_whole(1_000); let mut tx_env = TestTxEnv::default(); diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index a442b5a6cd..240bc63e1f 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -238,7 +238,7 @@ pub fn init_storage() -> (Address, Address) { // initialize an account let account = tx::ctx().init_account(code_hash).unwrap(); let key = token::balance_key(&token, &account); - let init_bal = Amount::whole(100); + let init_bal = Amount::native_whole(100); let bytes = init_bal.try_to_vec().expect("encoding failed"); tx_host_env::with(|env| { env.wl_storage.storage.write(&key, &bytes).unwrap(); diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index f4344e33b2..9fa53dd3f2 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -1225,7 +1225,7 @@ mod tests { let balance: Option = tx_host_env::with(|env| { env.wl_storage.read(&balance_key).expect("read error") }); - assert_eq!(balance, Some(Amount::whole(0))); + assert_eq!(balance, Some(Amount::native_whole(0))); let escrow_key = token::balance_key( &token, &address::Address::Internal(address::InternalAddress::IbcEscrow), @@ -1233,7 +1233,7 @@ mod tests { let escrow: Option = tx_host_env::with(|env| { env.wl_storage.read(&escrow_key).expect("read error") }); - assert_eq!(escrow, Some(Amount::whole(100))); + assert_eq!(escrow, Some(Amount::native_whole(100))); } #[test] @@ -1253,7 +1253,7 @@ mod tests { let denom = format!("{}/{}/{}", port_id, channel_id, token); let key_prefix = ibc_storage::ibc_token_prefix(&denom).unwrap(); let balance_key = token::multitoken_balance_key(&key_prefix, &sender); - let init_bal = Amount::whole(100); + let init_bal = Amount::native_whole(100); writes.insert(balance_key.clone(), init_bal.try_to_vec().unwrap()); // original denom let hash = ibc_storage::calc_hash(&denom); @@ -1311,7 +1311,7 @@ mod tests { let balance: Option = tx_host_env::with(|env| { env.wl_storage.read(&balance_key).expect("read error") }); - assert_eq!(balance, Some(Amount::whole(0))); + assert_eq!(balance, Some(Amount::native_whole(0))); let burn_key = token::balance_key( &token, &address::Address::Internal(address::InternalAddress::IbcBurn), @@ -1319,7 +1319,7 @@ mod tests { let burn: Option = tx_host_env::with(|env| { env.wl_storage.read(&burn_key).expect("read error") }); - assert_eq!(burn, Some(Amount::whole(100))); + assert_eq!(burn, Some(Amount::native_whole(100))); } #[test] @@ -1393,7 +1393,7 @@ mod tests { let balance: Option = tx_host_env::with(|env| { env.wl_storage.read(&key).expect("read error") }); - assert_eq!(balance, Some(Amount::whole(100))); + assert_eq!(balance, Some(Amount::native_whole(100))); } #[test] @@ -1422,7 +1422,7 @@ mod tests { &token, &address::Address::Internal(address::InternalAddress::IbcEscrow), ); - let val = Amount::whole(100).try_to_vec().unwrap(); + let val = Amount::native_whole(100).try_to_vec().unwrap(); tx_host_env::with(|env| { env.wl_storage .storage @@ -1480,11 +1480,11 @@ mod tests { let balance: Option = tx_host_env::with(|env| { env.wl_storage.read(&key).expect("read error") }); - assert_eq!(balance, Some(Amount::whole(200))); + assert_eq!(balance, Some(Amount::native_whole(200))); let escrow: Option = tx_host_env::with(|env| { env.wl_storage.read(&escrow_key).expect("read error") }); - assert_eq!(escrow, Some(Amount::whole(0))); + assert_eq!(escrow, Some(Amount::native_whole(0))); } #[test] @@ -1513,7 +1513,7 @@ mod tests { &token, &address::Address::Internal(address::InternalAddress::IbcEscrow), ); - let val = Amount::whole(100).try_to_vec().unwrap(); + let val = Amount::native_whole(100).try_to_vec().unwrap(); tx_host_env::with(|env| { env.wl_storage .storage @@ -1579,11 +1579,11 @@ mod tests { let balance: Option = tx_host_env::with(|env| { env.wl_storage.read(&key).expect("read error") }); - assert_eq!(balance, Some(Amount::whole(100))); + assert_eq!(balance, Some(Amount::native_whole(100))); let escrow: Option = tx_host_env::with(|env| { env.wl_storage.read(&escrow_key).expect("read error") }); - assert_eq!(escrow, Some(Amount::whole(0))); + assert_eq!(escrow, Some(Amount::native_whole(0))); } #[test] diff --git a/tx_prelude/Cargo.toml b/tx_prelude/Cargo.toml index 4df5ee78a0..5fa5d24d68 100644 --- a/tx_prelude/Cargo.toml +++ b/tx_prelude/Cargo.toml @@ -27,6 +27,5 @@ namada_proof_of_stake = {path = "../proof_of_stake", default-features = false} namada_vm_env = {path = "../vm_env", default-features = false} borsh.workspace = true masp_primitives.workspace = true -rust_decimal.workspace = true sha2.workspace = true thiserror.workspace = true diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 25c4328272..27d2cd2962 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -1,5 +1,6 @@ //! Proof of Stake system integration with functions for transactions +use namada_core::types::dec::Dec; use namada_core::types::hash::Hash; use namada_core::types::transaction::InitValidator; use namada_core::types::{key, token}; @@ -9,7 +10,6 @@ use namada_proof_of_stake::{ read_pos_params, unbond_tokens, unjail_validator, withdraw_tokens, }; pub use namada_proof_of_stake::{parameters, types}; -use rust_decimal::Decimal; use super::*; @@ -56,7 +56,7 @@ impl Ctx { pub fn change_validator_commission_rate( &mut self, validator: &Address, - rate: &Decimal, + rate: &Dec, ) -> TxResult { let current_epoch = self.get_block_epoch()?; change_validator_commission_rate(self, validator, *rate, current_epoch) diff --git a/tx_prelude/src/token.rs b/tx_prelude/src/token.rs index 251d9a21f9..685a2e51a6 100644 --- a/tx_prelude/src/token.rs +++ b/tx_prelude/src/token.rs @@ -15,12 +15,12 @@ pub fn transfer( dest: &Address, token: &Address, sub_prefix: Option, - amount: Amount, + amount: DenominatedAmount, key: &Option, shielded_hash: &Option, shielded: &Option, ) -> TxResult { - if amount != Amount::default() { + if amount.amount != Amount::default() { let src_key = match &sub_prefix { Some(sub_prefix) => { let prefix = @@ -37,28 +37,31 @@ pub fn transfer( } None => token::balance_key(token, dest), }; - let src_bal: Option = match src { - Address::Internal(InternalAddress::IbcMint) => Some(Amount::max()), - Address::Internal(InternalAddress::IbcBurn) => { - log_string("invalid transfer from the burn address"); - unreachable!() - } - _ => ctx.read(&src_key)?, - }; - let mut src_bal = src_bal.unwrap_or_else(|| { - log_string(format!("src {} has no balance", src_key)); - unreachable!() - }); - src_bal.spend(&amount); - let mut dest_bal: Amount = match dest { - Address::Internal(InternalAddress::IbcMint) => { - log_string("invalid transfer to the mint address"); - unreachable!() - } - _ => ctx.read(&dest_key)?.unwrap_or_default(), - }; - dest_bal.receive(&amount); if src != dest { + let src_bal: Option = match src { + Address::Internal(InternalAddress::IbcMint) => { + Some(Amount::max_signed()) + } + Address::Internal(InternalAddress::IbcBurn) => { + log_string("invalid transfer from the burn address"); + unreachable!() + } + _ => ctx.read(&src_key)?, + }; + let mut src_bal = src_bal.unwrap_or_else(|| { + log_string(format!("src {} has no balance", src_key)); + unreachable!() + }); + src_bal.spend(&amount.amount); + let mut dest_bal: Amount = match dest { + Address::Internal(InternalAddress::IbcMint) => { + log_string("invalid transfer to the mint address"); + unreachable!() + } + _ => ctx.read(&dest_key)?.unwrap_or_default(), + }; + dest_bal.receive(&amount.amount); + match src { Address::Internal(InternalAddress::IbcMint) => { ctx.write_temp(&src_key, src_bal)?; @@ -139,12 +142,12 @@ pub fn transfer_with_keys( dest_key: &storage::Key, amount: Amount, ) -> TxResult { - let src_owner = is_any_token_balance_key(src_key); + let src_owner = is_any_token_or_multitoken_balance_key(src_key); let src_bal: Option = match src_owner { - Some(Address::Internal(InternalAddress::IbcMint)) => { - Some(Amount::max()) + Some([_, Address::Internal(InternalAddress::IbcMint)]) => { + Some(Amount::max_signed()) } - Some(Address::Internal(InternalAddress::IbcBurn)) => { + Some([_, Address::Internal(InternalAddress::IbcBurn)]) => { log_string("invalid transfer from the burn address"); unreachable!() } @@ -157,7 +160,7 @@ pub fn transfer_with_keys( src_bal.spend(&amount); let dest_owner = is_any_token_balance_key(dest_key); let mut dest_bal: Amount = match dest_owner { - Some(Address::Internal(InternalAddress::IbcMint)) => { + Some([_, Address::Internal(InternalAddress::IbcMint)]) => { log_string("invalid transfer to the mint address"); unreachable!() } @@ -165,13 +168,13 @@ pub fn transfer_with_keys( }; dest_bal.receive(&amount); match src_owner { - Some(Address::Internal(InternalAddress::IbcMint)) => { + Some([_, Address::Internal(InternalAddress::IbcMint)]) => { ctx.write_temp(src_key, src_bal)?; } _ => ctx.write(src_key, src_bal)?, } match dest_owner { - Some(Address::Internal(InternalAddress::IbcBurn)) => { + Some([_, Address::Internal(InternalAddress::IbcBurn)]) => { ctx.write_temp(dest_key, dest_bal)?; } _ => ctx.write(dest_key, dest_bal)?, diff --git a/vp_prelude/src/token.rs b/vp_prelude/src/token.rs new file mode 100644 index 0000000000..1e302204a7 --- /dev/null +++ b/vp_prelude/src/token.rs @@ -0,0 +1,69 @@ +//! A fungible token validity predicate. + +use std::collections::BTreeSet; + +use namada_core::types::address::{self, Address, InternalAddress}; +use namada_core::types::storage::Key; +/// Vp imports and functions. +use namada_core::types::storage::KeySeg; +use namada_core::types::token; +pub use namada_core::types::token::*; + +use super::*; + +/// A token validity predicate. +pub fn vp( + ctx: &Ctx, + token: &Address, + keys_changed: &BTreeSet, + verifiers: &BTreeSet
, +) -> VpResult { + let mut change: Change = Change::default(); + for key in keys_changed.iter() { + let owner: Option<&Address> = + match token::is_multitoken_balance_key(token, key) { + Some((_, o)) => Some(o), + None => token::is_balance_key(token, key), + }; + match owner { + None => { + // Unknown changes to this address space are disallowed, but + // unknown changes anywhere else are permitted + if key.segments.get(0) == Some(&token.to_db_key()) { + return reject(); + } + } + Some(owner) => { + // accumulate the change + let pre: Amount = match owner { + Address::Internal(InternalAddress::IbcMint) => { + Amount::max_signed() + } + Address::Internal(InternalAddress::IbcBurn) => { + Amount::default() + } + _ => ctx.read_pre(key)?.unwrap_or_default(), + }; + let post: Amount = match owner { + Address::Internal(InternalAddress::IbcMint) => { + ctx.read_temp(key)?.unwrap_or_else(Amount::max_signed) + } + Address::Internal(InternalAddress::IbcBurn) => { + ctx.read_temp(key)?.unwrap_or_default() + } + _ => ctx.read_post(key)?.unwrap_or_default(), + }; + let this_change = post.change() - pre.change(); + change += this_change; + // make sure that the spender approved the transaction + if !(this_change.non_negative() + || verifiers.contains(owner) + || *owner == address::masp()) + { + return reject(); + } + } + } + } + Ok(change.is_zero()) +} diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 91d9705943..3056f0af76 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2510,6 +2510,17 @@ dependencies = [ "parity-scale-codec", ] +[[package]] +name = "impl-num-traits" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951641f13f873bff03d4bf19ae8bec531935ac0ac2cc775f84d7edfdcfed3f17" +dependencies = [ + "integer-sqrt", + "num-traits", + "uint", +] + [[package]] name = "impl-serde" version = "0.4.0" @@ -2595,6 +2606,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + [[package]] name = "io-lifetimes" version = "1.0.10" @@ -2827,7 +2847,7 @@ dependencies = [ [[package]] name = "masp_note_encryption" version = "0.2.0" -source = "git+https://github.com/anoma/masp?rev=cfea8c95d3f73077ca3e25380fd27e5b46e828fd#cfea8c95d3f73077ca3e25380fd27e5b46e828fd" +source = "git+https://github.com/anoma/masp?rev=9320c6b69b5d2e97134866871e960f0a31703813#9320c6b69b5d2e97134866871e960f0a31703813" dependencies = [ "borsh", "chacha20 0.9.1", @@ -2840,7 +2860,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "0.9.0" -source = "git+https://github.com/anoma/masp?rev=cfea8c95d3f73077ca3e25380fd27e5b46e828fd#cfea8c95d3f73077ca3e25380fd27e5b46e828fd" +source = "git+https://github.com/anoma/masp?rev=9320c6b69b5d2e97134866871e960f0a31703813#9320c6b69b5d2e97134866871e960f0a31703813" dependencies = [ "aes", "bip0039", @@ -2870,7 +2890,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "0.9.0" -source = "git+https://github.com/anoma/masp?rev=cfea8c95d3f73077ca3e25380fd27e5b46e828fd#cfea8c95d3f73077ca3e25380fd27e5b46e828fd" +source = "git+https://github.com/anoma/masp?rev=9320c6b69b5d2e97134866871e960f0a31703813#9320c6b69b5d2e97134866871e960f0a31703813" dependencies = [ "bellman", "blake2b_simd", @@ -3092,8 +3112,6 @@ dependencies = [ "rand_core 0.6.4", "rayon", "ripemd", - "rust_decimal", - "rust_decimal_macros", "serde", "serde_json", "sha2 0.9.9", @@ -3129,25 +3147,26 @@ dependencies = [ "data-encoding", "derivative", "ed25519-consensus", + "eyre", "ferveo", "ferveo-common", "group-threshold-cryptography", "ibc", "ibc-proto", "ics23", + "impl-num-traits", "index-set", "itertools", "libsecp256k1 0.7.0", "masp_primitives", "namada_macros", + "num-traits", "proptest", "prost", "prost-types", "rand 0.8.5", "rand_core 0.6.4", "rayon", - "rust_decimal", - "rust_decimal_macros", "serde", "serde_json", "sha2 0.9.9", @@ -3157,6 +3176,7 @@ dependencies = [ "thiserror", "tonic-build", "tracing", + "uint", "zeroize", ] @@ -3179,8 +3199,6 @@ dependencies = [ "namada_core", "once_cell", "proptest", - "rust_decimal", - "rust_decimal_macros", "thiserror", "tracing", ] @@ -3210,8 +3228,6 @@ dependencies = [ "namada_vp_prelude", "prost", "regex", - "rust_decimal", - "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -3231,7 +3247,6 @@ dependencies = [ "namada_macros", "namada_proof_of_stake", "namada_vm_env", - "rust_decimal", "sha2 0.9.9", "thiserror", ] @@ -4207,21 +4222,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" dependencies = [ "arrayvec", - "borsh", "num-traits", "serde", ] -[[package]] -name = "rust_decimal_macros" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" -dependencies = [ - "quote", - "rust_decimal", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -4694,9 +4698,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" +checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" dependencies = [ "digest 0.9.0", "rand_core 0.6.4", @@ -5562,9 +5566,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "universal-hash" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", "subtle 2.4.1", @@ -5610,9 +5614,9 @@ dependencies = [ [[package]] name = "value-bag" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" [[package]] name = "version_check" diff --git a/wasm/checksums.json b/wasm/checksums.json index 3d4916ecf1..ba4de65e01 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,21 +1,21 @@ { - "tx_bond.wasm": "tx_bond.b4916d2177dd1f9ca69e5cc99b5703adaf69be109694d2a333c6ac37ab12edd9.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.469463cd762b36ee1c63a734d8e4b4091964eb97b1414f7d6e0cd6a374e97b69.wasm", - "tx_ibc.wasm": "tx_ibc.8db1e3c43e75ab4175cf1cbee07b18f8bf17da7bcd5da050e581f96cec5f3a78.wasm", - "tx_init_account.wasm": "tx_init_account.15093b244594a80d9901474a4e8c5524983ef57fbd0dc54bc939a30e7946c2fb.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.fb71c72d31b747e1057b66bb21535a9b1d855b00a02c88edd0f85bdab827375e.wasm", - "tx_init_validator.wasm": "tx_init_validator.bfb5935d535f7a5b73cf56041cef4c9c84ead3604412915250844695f96f48dc.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.c5a7f7ceeb2ef53584d009c0b6872c1b904e66cdba1f0489ba9f68cc6a66eaee.wasm", - "tx_transfer.wasm": "tx_transfer.e36b981d1cd2a341d5a246a33c5bc6a6105d1e5d1f515c4e3b7ead2d5c7165bf.wasm", - "tx_unbond.wasm": "tx_unbond.a0e7026d046587cdf83e3cb7a031a6f761e6d84ec21edfabec3e90ae60394cc9.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.d9e31886b86c1f14d877bb070b0210b75cf936ddb99276ac48eb09b041b2eba6.wasm", - "tx_update_vp.wasm": "tx_update_vp.1859826f598d1e19c07df13e2501c69da6657d3168a43e5c86a3a7087a0d4d34.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.d557b3e44a3a523d00d0b70df6de9974d7904bfeb1d3844c1a8b45a159b9404f.wasm", - "tx_withdraw.wasm": "tx_withdraw.de2968f1246eaefe2ea4a07ac01bf6691c88a8e2c63b13b7e8bfa7ed819dd297.wasm", - "vp_implicit.wasm": "vp_implicit.c9a519b03fee9074add41dd77637648116f779800dc8d54792774b62873ca9e4.wasm", - "vp_masp.wasm": "vp_masp.e01e9d15c877d8c47ef96520399c74dce80e840906337d048e628f544a092963.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.808fd709893c89fb112ffacf2fcffee8b387f59f3c0b4ac1710188d251608262.wasm", - "vp_token.wasm": "vp_token.51b63b1af3d3667cd337f32adfb6e811b707eb37cbfbb94dc0647e526f3a3123.wasm", - "vp_user.wasm": "vp_user.f24d32d8e14a168eedf1394aee7cdf9d5ebc928f030ee841fd9d4da80ef8b223.wasm", - "vp_validator.wasm": "vp_validator.e179dc2e2465a4b4ff2005b0a3dc400e988572574779d885b243460624097049.wasm" + "tx_bond.wasm": "tx_bond.fcdaed302f8734412a830b4993bff4619a4c559e014b71deaa5fed77fdef3680.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.26e4a245a07a07a9bdb90e87c09a19678489df09a1663b0a7d5672ff1bfa661b.wasm", + "tx_ibc.wasm": "tx_ibc.2327a35b2cf355e485e8f03e2c475b3c388167edc9ad15fbee630941320920b6.wasm", + "tx_init_account.wasm": "tx_init_account.9153e300b7198ce515693852c138c14b23838b6f85fa0a6db71f3294ca4b25ac.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.1630673aeef70942c7c786fef6944b96d47fef7e50df994b390b6500349d136e.wasm", + "tx_init_validator.wasm": "tx_init_validator.a2398d56e4002ac996069f0df93cbd7c61f4326438ed6e3cadac5a0460b547e9.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.d7268e61b97c3e6db9f0139b7db4b42b133b5e6ce42fe17ff4adc0988da520eb.wasm", + "tx_transfer.wasm": "tx_transfer.b2a7576aaa21bdca0ad0e810788b9c7cf3e58d7d0442a75a3290563d03e0412f.wasm", + "tx_unbond.wasm": "tx_unbond.6b9651b1ed2922d0c4982748ad20679298eb77929eaeefff6b2c792f9004c657.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.b2fab3af06dc591ef39ade0372bc6a1e850fb87d00dcdba3ab9452cebd278bea.wasm", + "tx_update_vp.wasm": "tx_update_vp.8e9a1a4827f6c899c2828493b213d21bdf32232eaf53fccb7a6d6535baa39f99.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.c16e405aedc46b59bb57d012d3e11e1ecbd28654c25557f376cabfb7bea483d9.wasm", + "tx_withdraw.wasm": "tx_withdraw.1751e4c9304349354f0b0afc3fe4214327c30f35bcffee2f805d4ff68d61b907.wasm", + "vp_implicit.wasm": "vp_implicit.d82b7b4525391f8777bcc4699bd973b0b6c3cdf82838791ca78ebd74392aa18e.wasm", + "vp_masp.wasm": "vp_masp.8b16fb8926a8fcda25796dd50c6e3ce70041b54692481102d8b5ba6f778853b2.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.a55fb86c460abfb44abf926a32db18c813722f97a87fc9dc3323c86dc4447f1c.wasm", + "vp_token.wasm": "vp_token.eb78c39b03703447b3f35926d2e67e11637f5c6eaa83fb6f9cfbec94f8732bb9.wasm", + "vp_user.wasm": "vp_user.6e831fc2fce1ae9b2b13549e3b4397c54ced19222efb697722c96c6fede0f143.wasm", + "vp_validator.wasm": "vp_validator.a3c3d2e361a530419601abcfad7880adfa8748655304c22014a48208a4d8ac92.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index 49a47c095c..ef3b7dfa5e 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -40,8 +40,8 @@ once_cell = {version = "1.8.0", optional = true} rust_decimal = {version = "=1.26.1", optional = true} wee_alloc = "0.4.5" getrandom = { version = "0.2", features = ["custom"] } -# branch = "murisi/namada-integration" -masp_primitives = { git = "https://github.com/anoma/masp", rev = "cfea8c95d3f73077ca3e25380fd27e5b46e828fd", optional = true } +#masp_proofs = { git = "https://github.com/anoma/masp", rev = "64caae74dc71dd20e6e7fbf3d87770bffe5c4789", optional = true } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "9320c6b69b5d2e97134866871e960f0a31703813", optional = true } ripemd = "0.1" [dev-dependencies] diff --git a/wasm/wasm_source/proptest-regressions/tx_bond.txt b/wasm/wasm_source/proptest-regressions/tx_bond.txt deleted file mode 100644 index 3a88756618..0000000000 --- a/wasm/wasm_source/proptest-regressions/tx_bond.txt +++ /dev/null @@ -1 +0,0 @@ -cc e54347c5114ef29538127ba9ad68d1572af839ec63c015318fc0827818853a22 diff --git a/wasm/wasm_source/proptest-regressions/tx_unbond.txt b/wasm/wasm_source/proptest-regressions/tx_unbond.txt deleted file mode 100644 index 8c589d1abd..0000000000 --- a/wasm/wasm_source/proptest-regressions/tx_unbond.txt +++ /dev/null @@ -1 +0,0 @@ -cc f22e874350910b197cb02a4a07ec5bef18e16c0d1a39eaabaee43d1fc05ce11d diff --git a/wasm/wasm_source/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index 35e8ba7fac..c161981abc 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -23,6 +23,7 @@ mod tests { read_total_stake, read_validator_stake, }; use namada::proto::{Code, Data, Signature, Tx}; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada::types::transaction::TxType; use namada_tests::log::test; @@ -39,7 +40,6 @@ mod tests { use namada_tx_prelude::token; use namada_vp_prelude::proof_of_stake::WeightedValidator; use proptest::prelude::*; - use rust_decimal; use super::*; @@ -71,8 +71,8 @@ mod tests { let is_delegation = matches!(&bond.source, Some(source) if *source != bond.validator); let consensus_key = key::testing::keypair_1().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).expect("Cannot fail"); + let max_commission_rate_change = Dec::new(1, 2).expect("Cannot fail"); let genesis_validators = [GenesisValidator { address: bond.validator.clone(), @@ -194,7 +194,7 @@ mod tests { // Check that the validator set and deltas are unchanged before pipeline // length and that they are updated between the pipeline and // unbonding lengths - if bond.amount == token::Amount::from(0) { + if bond.amount.is_zero() { // None of the optional storage fields should have been updated assert_eq!(epoched_validator_set_pre, epoched_validator_set_post); assert_eq!( @@ -225,7 +225,7 @@ mod tests { ..=pos_params.unbonding_len as usize { let expected_stake = - i128::from(initial_stake) + i128::from(bond.amount); + initial_stake.change() + bond.amount.change(); assert_eq!( epoched_validator_stake_post[epoch], token::Amount::from_change(expected_stake), @@ -349,7 +349,7 @@ mod tests { // Generate initial stake (initial_stake in token::testing::arb_amount_ceiled((i64::MAX/8) as u64)) // Use the initial stake to limit the bond amount - (bond in arb_bond(((i64::MAX/8) as u64) - u64::from(initial_stake)), + (bond in arb_bond(((i64::MAX/8) as u64) - u128::try_from(initial_stake).unwrap() as u64), // Use the generated initial stake too initial_stake in Just(initial_stake), ) -> (token::Amount, transaction::pos::Bond) { diff --git a/wasm/wasm_source/src/tx_change_validator_commission.rs b/wasm/wasm_source/src/tx_change_validator_commission.rs index 7668114c79..4c9410c748 100644 --- a/wasm/wasm_source/src/tx_change_validator_commission.rs +++ b/wasm/wasm_source/src/tx_change_validator_commission.rs @@ -11,7 +11,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { validator, new_rate, } = transaction::pos::CommissionChange::try_from_slice(&data[..]) - .wrap_err("failed to decode Decimal value")?; + .wrap_err("failed to decode Dec value")?; ctx.change_validator_commission_rate(&validator, &new_rate) } @@ -22,6 +22,7 @@ mod tests { use namada::ledger::pos::{PosParams, PosVP}; use namada::proof_of_stake::validator_commission_rate_handle; use namada::proto::{Code, Data, Signature, Tx}; + use namada::types::dec::{Dec, POS_DECIMAL_PRECISION}; use namada::types::storage::Epoch; use namada::types::transaction::TxType; use namada_tests::log::test; @@ -35,8 +36,6 @@ mod tests { use namada_tx_prelude::token; use namada_vp_prelude::proof_of_stake::GenesisValidator; use proptest::prelude::*; - use rust_decimal::prelude::ToPrimitive; - use rust_decimal::Decimal; use super::*; @@ -61,8 +60,8 @@ mod tests { } fn test_tx_change_validator_commission_aux( - initial_rate: Decimal, - max_change: Decimal, + initial_rate: Dec, + max_change: Dec, commission_change: transaction::pos::CommissionChange, key: key::common::SecretKey, pos_params: PosParams, @@ -70,7 +69,7 @@ mod tests { let consensus_key = key::testing::keypair_1().ref_to(); let genesis_validators = [GenesisValidator { address: commission_change.validator.clone(), - tokens: token::Amount::from(1_000_000), + tokens: token::Amount::from_uint(1_000_000, 0).unwrap(), consensus_key, commission_rate: initial_rate, max_commission_rate_change: max_change, @@ -97,7 +96,7 @@ mod tests { let commission_rate_handle = validator_commission_rate_handle(&commission_change.validator); - let mut commission_rates_pre = Vec::>::new(); + let mut commission_rates_pre = Vec::>::new(); for epoch in Epoch::default().iter_range(pos_params.unbonding_len + 1) { commission_rates_pre.push(commission_rate_handle.get( ctx(), @@ -156,35 +155,33 @@ mod tests { Ok(()) } - fn arb_rate(min: Decimal, max: Decimal) -> impl Strategy { - let int_min: u64 = (min * scale()).to_u64().unwrap_or_default(); - let int_max: u64 = (max * scale()).to_u64().unwrap(); - (int_min..=int_max).prop_map(|num| Decimal::from(num) / scale()) + fn arb_rate(min: Dec, max: Dec) -> impl Strategy { + let int_min: i128 = (min * scale()).try_into().unwrap(); + let int_max: i128 = (max * scale()).try_into().unwrap(); + (int_min..=int_max).prop_map(|num| { + Dec::new(num, POS_DECIMAL_PRECISION).unwrap() / scale() + }) } fn arb_new_rate( - rate_pre: Decimal, - max_change: Decimal, - ) -> impl Strategy { - assert!(max_change > Decimal::ZERO); - + rate_pre: Dec, + max_change: Dec, + ) -> impl Strategy { + assert!(max_change > Dec::zero()); // Arbitrary non-zero change - let arb_change = |ceil: Decimal| { + let arb_change = |ceil: Dec| { // Clamp the `ceil` to `max_change` and convert to an int - let ceil = (cmp::min(max_change, ceil) * scale()) - .abs() - .to_u64() - .unwrap(); - (0..ceil).prop_map(|c| + let ceil = (cmp::min(max_change, ceil) * scale()).abs().as_u128(); + (1..ceil).prop_map(|c| // Convert back from an int - Decimal::from(c) / scale()) + Dec::new(c as i128, POS_DECIMAL_PRECISION).unwrap() / scale()) }; // Addition let arb_add = || { arb_change( // Addition must not go over 1 - Decimal::ONE - rate_pre, + Dec::one() - rate_pre, ) .prop_map(move |c| rate_pre + c) }; @@ -198,9 +195,9 @@ mod tests { }; // Add or subtract from the previous rate - if rate_pre == Decimal::ZERO { + if rate_pre == Dec::zero() { arb_add().boxed() - } else if rate_pre == Decimal::ONE { + } else if rate_pre == Dec::one() { arb_sub().boxed() } else { prop_oneof![arb_add(), arb_sub()].boxed() @@ -208,13 +205,13 @@ mod tests { } fn arb_commission_change( - rate_pre: Decimal, - max_change: Decimal, + rate_pre: Dec, + max_change: Dec, ) -> impl Strategy { ( arb_established_address(), - if max_change == Decimal::ZERO { - Just(Decimal::ZERO).boxed() + if max_change.is_zero() { + Just(Dec::zero()).boxed() } else { arb_new_rate(rate_pre, max_change).boxed() }, @@ -228,11 +225,12 @@ mod tests { } fn arb_commission_info() - -> impl Strategy + -> impl Strategy { - let min = Decimal::ZERO; - let max = Decimal::ONE; - (arb_rate(min, max), arb_rate(min, max)).prop_flat_map( + let min = Dec::zero(); + let max = Dec::one(); + let non_zero_min = Dec::one() / scale(); + (arb_rate(min, max), arb_rate(non_zero_min, max)).prop_flat_map( |(rate, max_change)| { ( Just(rate), @@ -243,7 +241,7 @@ mod tests { ) } - fn scale() -> Decimal { - Decimal::from(100_000) + fn scale() -> Dec { + Dec::new(100_000, 0).unwrap() } } diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index 5b4c5f3859..b9a134b31b 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -24,6 +24,7 @@ mod tests { read_total_stake, read_validator_stake, unbond_handle, }; use namada::proto::{Code, Data, Signature, Tx}; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada::types::transaction::TxType; use namada_tests::log::test; @@ -69,8 +70,8 @@ mod tests { &unbond.source, Some(source) if *source != unbond.validator); let consensus_key = key::testing::keypair_1().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).expect("Cannot fail"); + let max_commission_rate_change = Dec::new(1, 2).expect("Cannot fail"); let genesis_validators = [GenesisValidator { address: unbond.validator.clone(), @@ -211,7 +212,7 @@ mod tests { let expected_amount_before_pipeline = if is_delegation { // When this is a delegation, there will be no bond until pipeline - 0.into() + token::Amount::default() } else { // Before pipeline offset, there can only be self-bond initial_stake @@ -285,7 +286,7 @@ mod tests { { let epoch = pos_params.unbonding_len + 1; let expected_stake = - i128::from(initial_stake) - i128::from(unbond.amount); + initial_stake.change() - unbond.amount.change(); assert_eq!( read_validator_stake( ctx(), @@ -418,7 +419,8 @@ mod tests { token::testing::arb_amount_ceiled((i64::MAX / 8) as u64).prop_flat_map( |initial_stake| { // Use the initial stake to limit the bond amount - let unbond = arb_unbond(u64::from(initial_stake)); + let unbond = + arb_unbond(u128::try_from(initial_stake).unwrap() as u64); // Use the generated initial stake too too (Just(initial_stake), unbond) }, diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index 267f180c94..a8fdc55526 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -13,7 +13,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let slashed = ctx.withdraw_tokens(withdraw.source.as_ref(), &withdraw.validator)?; if slashed != token::Amount::default() { - debug_log!("New withdrawal slashed for {}", slashed); + debug_log!("New withdrawal slashed for {}", slashed.to_string_native()); } Ok(()) } @@ -23,6 +23,7 @@ mod tests { use namada::ledger::pos::{GenesisValidator, PosParams, PosVP}; use namada::proof_of_stake::unbond_handle; use namada::proto::{Code, Data, Signature, Tx}; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada::types::transaction::TxType; use namada_tests::log::test; @@ -72,8 +73,8 @@ mod tests { let is_delegation = matches!( &withdraw.source, Some(source) if *source != withdraw.validator); let consensus_key = key::testing::keypair_1().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).expect("Cannot fail"); + let max_commission_rate_change = Dec::new(1, 2).expect("Cannot fail"); let genesis_validators = [GenesisValidator { address: withdraw.validator.clone(), @@ -236,7 +237,7 @@ mod tests { // stake let unbonded_amount = token::testing::arb_amount_non_zero_ceiled( - initial_stake.into(), + u128::try_from(initial_stake).unwrap() as u64, ); // Use the generated initial stake too too (Just(initial_stake), unbonded_amount) diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index fa5b7c8945..73e5bf54b3 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -18,7 +18,9 @@ use once_cell::unsync::Lazy; enum KeyType<'a> { /// Public key - written once revealed Pk(&'a Address), - Token(&'a Address), + Token { + owner: &'a Address, + }, PoS, GovernanceVote(&'a Address), Unknown, @@ -28,12 +30,12 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { fn from(key: &'a storage::Key) -> KeyType<'a> { if let Some(address) = key::is_pk_key(key) { Self::Pk(address) - } else if let Some(address) = token::is_any_token_balance_key(key) { - Self::Token(address) - } else if let Some((_, address)) = + } else if let Some([_, owner]) = token::is_any_token_balance_key(key) { + Self::Token { owner } + } else if let Some((_, [_, owner])) = token::is_any_multitoken_balance_key(key) { - Self::Token(address) + Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS } else if gov_storage::is_vote_key(key) { @@ -106,7 +108,7 @@ fn validate_tx( } true } - KeyType::Token(owner) => { + KeyType::Token { owner, .. } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); @@ -114,11 +116,13 @@ fn validate_tx( ctx.read_post(key)?.unwrap_or_default(); let change = post.change() - pre.change(); // debit has to signed, credit doesn't - let valid = change >= 0 || *valid_sig; + let valid = change.non_negative() || *valid_sig; + let sign = if change.non_negative() { "" } else { "-" }; debug_log!( - "token key: {}, change: {}, valid_sig: {}, valid \ + "token key: {}, change: {}{:?}, valid_sig: {}, valid \ modification: {}", key, + sign, change, *valid_sig, valid @@ -194,6 +198,7 @@ mod tests { // Use this as `#[test]` annotation to enable logging use namada::ledger::pos::{GenesisValidator, PosParams}; use namada::proto::{Code, Data, Signature}; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada::types::transaction::TxType; use namada_test_utils::TestWasms; @@ -202,7 +207,7 @@ mod tests { use namada_tests::tx::{self, tx_host_env, TestTxEnv}; use namada_tests::vp::vp_host_env::storage::Key; use namada_tests::vp::*; - use namada_tx_prelude::{StorageWrite, TxEnv}; + use namada_tx_prelude::{storage_api, StorageWrite, TxEnv}; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; use storage::testing::arb_account_storage_key_no_vp; @@ -333,15 +338,26 @@ mod tests { let vp_owner: Address = (&public_key).into(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Credit the tokens to the source before running the transaction to be // able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -379,10 +395,10 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), @@ -402,9 +418,9 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -412,7 +428,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { // Bond the tokens, then unbond some of them @@ -444,10 +467,10 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), @@ -467,9 +490,9 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -477,6 +500,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); tx_env.write_public_key(&vp_owner, &public_key); @@ -520,7 +551,7 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -528,6 +559,19 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); + + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -570,7 +614,7 @@ mod tests { let vp_owner: Address = (&public_key).into(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -578,9 +622,21 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); tx_env.write_public_key(&vp_owner, &public_key); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -628,7 +684,7 @@ mod tests { let source = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); @@ -636,6 +692,19 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); + + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { diff --git a/wasm/wasm_source/src/vp_masp.rs b/wasm/wasm_source/src/vp_masp.rs index b3ccda6286..99d756e237 100644 --- a/wasm/wasm_source/src/vp_masp.rs +++ b/wasm/wasm_source/src/vp_masp.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use masp_primitives::asset_type::AssetType; -use masp_primitives::transaction::components::{Amount, TxOut}; +use masp_primitives::transaction::components::Amount; /// Multi-asset shielded pool VP. use namada_vp_prelude::address::masp; use namada_vp_prelude::storage::Epoch; @@ -10,9 +10,14 @@ use ripemd::{Digest, Ripemd160}; /// Generates the current asset type given the current epoch and an /// unique token address -fn asset_type_from_epoched_address(epoch: Epoch, token: &Address) -> AssetType { +fn asset_type_from_epoched_address( + epoch: Epoch, + token: &Address, + sub_prefix: String, + denom: token::MaspDenom, +) -> AssetType { // Timestamp the chosen token with the current epoch - let token_bytes = (token, epoch.0) + let token_bytes = (token, sub_prefix, denom, epoch.0) .try_to_vec() .expect("token should serialize"); // Generate the unique asset identifier from the unique token address @@ -49,7 +54,7 @@ fn valid_transfer_amount( transparented value {}", unshielded_transfer_value, reporeted_transparent_value - ) + ); } res } @@ -58,11 +63,21 @@ fn valid_transfer_amount( fn convert_amount( epoch: Epoch, token: &Address, + sub_prefix: &Option, val: token::Amount, + denom: token::MaspDenom, ) -> (AssetType, Amount) { - let asset_type = asset_type_from_epoched_address(epoch, token); + let asset_type = asset_type_from_epoched_address( + epoch, + token, + sub_prefix + .as_ref() + .map(|k| k.to_string()) + .unwrap_or_default(), + denom, + ); // Combine the value and unit into one amount - let amount = Amount::from_nonnegative(asset_type, u64::from(val)) + let amount = Amount::from_nonnegative(asset_type, denom.denominate(&val)) .expect("invalid value or asset type for amount"); (asset_type, amount) } @@ -108,18 +123,22 @@ fn validate_tx( // Note that the asset type is timestamped so shields // where the shielded value has an incorrect timestamp // are automatically rejected - let (_transp_asset, transp_amt) = convert_amount( - ctx.get_block_epoch().unwrap(), - &transfer.token, - transfer.amount, - ); - - // Non-masp sources add to transparent tx pool - transparent_tx_pool += transp_amt; + for denom in token::MaspDenom::iter() { + let (_transp_asset, transp_amt) = convert_amount( + ctx.get_block_epoch().unwrap(), + &transfer.token, + &transfer.sub_prefix, + transfer.amount.into(), + denom, + ); + + // Non-masp sources add to transparent tx pool + transparent_tx_pool += transp_amt; + } } else { // Handle shielded input // The following boundary conditions must be satisfied - // 1. Zero transparent inupt + // 1. Zero transparent input // 2. the transparent transaction value pool's amount must equal the // containing wrapper transaction's fee amount // Satisfies 1. @@ -138,7 +157,7 @@ fn validate_tx( if transfer.target != masp() { // Handle transparent output // The following boundary conditions must be satisfied - // 1. One transparent output + // 1. One to 4 transparent outputs // 2. Asset type must be properly derived // 3. Value from the output must be the same as the containing // transfer @@ -155,55 +174,90 @@ fn validate_tx( be 1 but is {}", transp_bundle.vin.len() ); + return reject(); } - - let out: &TxOut = &transp_bundle.vout[0]; - - let expected_asset_type: AssetType = - asset_type_from_epoched_address( - ctx.get_block_epoch().unwrap(), - &transfer.token, + let out_length = transp_bundle.vout.len(); + if !(1..=4).contains(&out_length) { + debug_log!( + "Transparent output to a transaction to the masp must be \ + beteween 1 and 4 but is {}", + transp_bundle.vin.len() ); - // Satisfies 2. and 3. - if !(valid_asset_type(&expected_asset_type, &out.asset_type) - && valid_transfer_amount( - out.value as u64, - u64::from(transfer.amount), - )) - { return reject(); } + let mut outs = transp_bundle.vout.iter(); + let mut valid_count = 0; + for denom in token::MaspDenom::iter() { + let out = match outs.next() { + Some(out) => out, + None => continue, + }; - let (_transp_asset, transp_amt) = convert_amount( - ctx.get_block_epoch().unwrap(), - &transfer.token, - transfer.amount, - ); + let expected_asset_type: AssetType = + asset_type_from_epoched_address( + ctx.get_block_epoch().unwrap(), + &transfer.token, + transfer + .sub_prefix + .as_ref() + .map(|k| k.to_string()) + .unwrap_or_default(), + denom, + ); - // Non-masp destinations subtract from transparent tx pool - transparent_tx_pool -= transp_amt; + // Satisfies 2. and 3. + if !valid_asset_type(&expected_asset_type, &out.asset_type) { + // we don't know which masp denoms are necessary apriori. + // This is encoded via the asset types. + continue; + } + if !valid_transfer_amount( + out.value as u64, + denom.denominate(&transfer.amount.amount), + ) { + return reject(); + } - // Satisfies 4. - let target_enc = transfer - .target - .try_to_vec() - .expect("target address encoding"); + let (_transp_asset, transp_amt) = convert_amount( + ctx.get_block_epoch().unwrap(), + &transfer.token, + &transfer.sub_prefix, + transfer.amount.amount, + denom, + ); - let hash = Ripemd160::digest(sha256(&target_enc).as_slice()); + // Non-masp destinations subtract from transparent tx pool + transparent_tx_pool -= transp_amt; - if <[u8; 20]>::from(hash) != out.address.0 { - debug_log!( - "the public key of the output account does not match the \ - transfer target" - ); + // Satisfies 4. + let target_enc = transfer + .target + .try_to_vec() + .expect("target address encoding"); + + let hash = Ripemd160::digest(sha256(&target_enc).as_slice()); + + if <[u8; 20]>::from(hash) != out.address.0 { + debug_log!( + "the public key of the output account does not match \ + the transfer target" + ); + return reject(); + } + valid_count += 1; + } + // one or more of the denoms in the batch failed to verify + // the asset derivation. + if valid_count != out_length { return reject(); } } else { // Handle shielded output // The following boundary conditions must be satisfied // 1. Zero transparent output + // Satisfies 1. if let Some(transp_bundle) = shielded_tx.transparent_bundle() { if !transp_bundle.vout.is_empty() { diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index 430cb55b21..95077d3cc5 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -40,7 +40,8 @@ fn validate_tx( } for key in keys_changed.iter() { - let is_valid = if let Some(owner) = token::is_any_token_balance_key(key) + let is_valid = if let Some([_, owner]) = + token::is_any_token_balance_key(key) { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); @@ -48,7 +49,7 @@ fn validate_tx( ctx.read_post(key)?.unwrap_or_default(); let change = post.change() - pre.change(); - if change < 0 { + if !change.non_negative() { // Allow to withdraw without a sig if there's a valid PoW if ctx.has_valid_pow() { let max_free_debit = @@ -144,7 +145,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); @@ -153,6 +154,11 @@ mod tests { // able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; + // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -293,12 +299,12 @@ mod tests { // Init the VP let vp_owner = address::testing::established_address_1(); let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); - let withdrawal_limit = token::Amount::from(MAX_FREE_DEBIT as u64); + let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap(); testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(amount); + let amount = token::Amount::from_uint(amount, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -307,6 +313,10 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); tx_env.commit_genesis(); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into() + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -334,13 +344,13 @@ mod tests { // Init the VP let vp_owner = address::testing::established_address_1(); let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); - let withdrawal_limit = token::Amount::from(MAX_FREE_DEBIT as u64); + let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap(); testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); let target = address::testing::established_address_2(); let target_key = key::testing::keypair_1(); let token = address::nam(); - let amount = token::Amount::from(amount); + let amount = token::Amount::from_uint(amount, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -348,6 +358,8 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom(&mut tx_env.wl_storage, &token, None, token::NATIVE_MAX_DECIMAL_PLACES.into()).unwrap(); tx_env.commit_genesis(); // Construct a PoW solution like a client would @@ -355,6 +367,11 @@ mod tests { let solution = challenge.solve(); let solution_bytes = solution.try_to_vec().unwrap(); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; + // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Don't call `Solution::invalidate_if_valid` - this is done by the @@ -391,7 +408,7 @@ mod tests { // Init the VP let difficulty = testnet_pow::Difficulty::try_new(0).unwrap(); - let withdrawal_limit = token::Amount::from(MAX_FREE_DEBIT as u64); + let withdrawal_limit = token::Amount::from_uint(MAX_FREE_DEBIT as u64, 0).unwrap(); testnet_pow::init_faucet_storage(&mut tx_env.wl_storage, &vp_owner, difficulty, withdrawal_limit).unwrap(); let keypair = key::testing::keypair_1(); diff --git a/wasm/wasm_source/src/vp_token.rs b/wasm/wasm_source/src/vp_token.rs index 8ccc2c9053..29b639bd56 100644 --- a/wasm/wasm_source/src/vp_token.rs +++ b/wasm/wasm_source/src/vp_token.rs @@ -51,7 +51,7 @@ fn token_checks( keys_touched: &BTreeSet, verifiers: &BTreeSet
, ) -> VpResult { - let mut change: token::Change = 0; + let mut change = token::Change::default(); for key in keys_touched.iter() { let owner: Option<&Address> = token::is_balance_key(token, key) .or_else(|| { @@ -77,36 +77,45 @@ fn token_checks( } Some(owner) => { // accumulate the change - let pre: token::Amount = match owner { + let pre: token::Change = match owner { Address::Internal(InternalAddress::IbcMint) => { - token::Amount::max() + token::Change::maximum() } Address::Internal(InternalAddress::IbcBurn) => { - token::Amount::default() + token::Change::default() } - _ => ctx.read_pre(key)?.unwrap_or_default(), + _ => ctx + .read_pre::(key)? + .unwrap_or_default() + .change(), }; - let post: token::Amount = match owner { - Address::Internal(InternalAddress::IbcMint) => { - ctx.read_temp(key)?.unwrap_or_else(token::Amount::max) - } - Address::Internal(InternalAddress::IbcBurn) => { - ctx.read_temp(key)?.unwrap_or_default() - } - _ => ctx.read_post(key)?.unwrap_or_default(), + let post: token::Change = match owner { + Address::Internal(InternalAddress::IbcMint) => ctx + .read_temp::(key)? + .map(|x| x.change()) + .unwrap_or_else(token::Change::maximum), + Address::Internal(InternalAddress::IbcBurn) => ctx + .read_temp::(key)? + .unwrap_or_default() + .change(), + _ => ctx + .read_post::(key)? + .unwrap_or_default() + .change(), }; - let this_change = post.change() - pre.change(); + let this_change = post - pre; change += this_change; // make sure that the spender approved the transaction - if this_change < 0 - && !(verifiers.contains(owner) || *owner == address::masp()) + if !(this_change.non_negative() + || verifiers.contains(owner) + || *owner == address::masp()) { return reject(); } } } } - Ok(change == 0) + Ok(change.is_zero()) } #[cfg(test)] @@ -129,7 +138,8 @@ mod tests { let token = address::nam(); let src = address::testing::established_address_1(); let dest = address::testing::established_address_2(); - let total_supply = token::Amount::from(10_098_123); + let total_supply = + token::Amount::from_uint(10_098_123, 0).expect("Test failed"); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&token, &src, &dest]); @@ -147,7 +157,7 @@ mod tests { vp_host_env::init_from_tx(token.clone(), tx_env, |_address| { // Apply a transfer - let amount = token::Amount::from(100); + let amount = token::Amount::from_uint(100, 0).expect("Test failed"); token::transfer(tx::ctx(), &token, &src, &dest, amount).unwrap(); }); @@ -172,7 +182,8 @@ mod tests { let token = address::nam(); let src = address::testing::established_address_1(); let dest = address::testing::established_address_2(); - let total_supply = token::Amount::from(10_098_123); + let total_supply = + token::Amount::from_uint(10_098_123, 0).expect("Test failed"); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&token, &src, &dest]); @@ -190,8 +201,10 @@ mod tests { vp_host_env::init_from_tx(token.clone(), tx_env, |_address| { // Apply a transfer - let amount_in = token::Amount::from(100); - let amount_out = token::Amount::from(900); + let amount_in = + token::Amount::from_uint(100, 0).expect("Test failed"); + let amount_out = + token::Amount::from_uint(900, 0).expect("Test failed"); let src_key = token::balance_key(&token, &src); let src_balance = @@ -226,7 +239,8 @@ mod tests { let mut tx_env = TestTxEnv::default(); let token = address::nam(); let owner = address::testing::established_address_1(); - let total_supply = token::Amount::from(10_098_123); + let total_supply = + token::Amount::from_uint(10_098_123, 0).expect("Test failed"); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&token, &owner]); @@ -253,7 +267,8 @@ mod tests { tx::ctx() .write( &total_supply_key, - current_supply + token::Amount::from(1), + current_supply + + token::Amount::from_uint(1, 0).expect("Test failed"), ) .unwrap(); }); diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 79132efaf2..4c71180128 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -14,7 +14,7 @@ use namada_vp_prelude::*; use once_cell::unsync::Lazy; enum KeyType<'a> { - Token(&'a Address), + Token { owner: &'a Address }, PoS, Vp(&'a Address), Masp, @@ -24,12 +24,12 @@ enum KeyType<'a> { impl<'a> From<&'a storage::Key> for KeyType<'a> { fn from(key: &'a storage::Key) -> KeyType<'a> { - if let Some(address) = token::is_any_token_balance_key(key) { - Self::Token(address) - } else if let Some((_, address)) = + if let Some([_, owner]) = token::is_any_token_balance_key(key) { + Self::Token { owner } + } else if let Some((_, [_, owner])) = token::is_any_multitoken_balance_key(key) { - Self::Token(address) + Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS } else if gov_storage::is_vote_key(key) { @@ -81,7 +81,7 @@ fn validate_tx( for key in keys_changed.iter() { let key_type: KeyType = key.into(); let is_valid = match key_type { - KeyType::Token(owner) => { + KeyType::Token { owner, .. } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); @@ -89,9 +89,10 @@ fn validate_tx( ctx.read_post(key)?.unwrap_or_default(); let change = post.change() - pre.change(); // debit has to signed, credit doesn't - let valid = change >= 0 || addr == masp() || *valid_sig; + let valid = + change.non_negative() || addr == masp() || *valid_sig; debug_log!( - "token key: {}, change: {}, valid_sig: {}, valid \ + "token key: {}, change: {:?}, valid_sig: {}, valid \ modification: {}", key, change, @@ -185,6 +186,7 @@ mod tests { use address::testing::arb_non_internal_address; use namada::ledger::pos::{GenesisValidator, PosParams}; use namada::proto::{Code, Data, Signature}; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada::types::transaction::TxType; use namada_test_utils::TestWasms; @@ -227,7 +229,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); @@ -235,7 +237,19 @@ mod tests { // Credit the tokens to the source before running the transaction to be // able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -275,15 +289,27 @@ mod tests { let vp_owner = address::testing::established_address_1(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -325,7 +351,7 @@ mod tests { let public_key = keypair.ref_to(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -333,9 +359,22 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); tx_env.write_public_key(&vp_owner, &public_key); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; + // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -379,10 +418,10 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), @@ -402,12 +441,20 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -444,10 +491,10 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = rust_decimal::Decimal::new(5, 2); - let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), @@ -467,12 +514,20 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -520,7 +575,7 @@ mod tests { let source = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); @@ -529,6 +584,11 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; + // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { tx::ctx().insert_verifier(address).unwrap(); diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 05bc46aff9..cf76cbc98c 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -17,7 +17,7 @@ use namada_vp_prelude::*; use once_cell::unsync::Lazy; enum KeyType<'a> { - Token(&'a Address), + Token { owner: &'a Address }, PoS, Vp(&'a Address), GovernanceVote(&'a Address), @@ -26,12 +26,12 @@ enum KeyType<'a> { impl<'a> From<&'a storage::Key> for KeyType<'a> { fn from(key: &'a storage::Key) -> KeyType<'a> { - if let Some(address) = token::is_any_token_balance_key(key) { - Self::Token(address) - } else if let Some((_, address)) = + if let Some([_, owner]) = token::is_any_token_balance_key(key) { + Self::Token { owner } + } else if let Some((_, [_, owner])) = token::is_any_multitoken_balance_key(key) { - Self::Token(address) + Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS } else if gov_storage::is_vote_key(key) { @@ -81,7 +81,7 @@ fn validate_tx( for key in keys_changed.iter() { let key_type: KeyType = key.into(); let is_valid = match key_type { - KeyType::Token(owner) => { + KeyType::Token { owner, .. } => { if owner == &addr { let pre: token::Amount = ctx.read_pre(key)?.unwrap_or_default(); @@ -89,9 +89,9 @@ fn validate_tx( ctx.read_post(key)?.unwrap_or_default(); let change = post.change() - pre.change(); // debit has to signed, credit doesn't - let valid = change >= 0 || *valid_sig; + let valid = change.non_negative() || *valid_sig; debug_log!( - "token key: {}, change: {}, valid_sig: {}, valid \ + "token key: {}, change: {:?}, valid_sig: {}, valid \ modification: {}", key, change, @@ -193,6 +193,7 @@ mod tests { use address::testing::arb_non_internal_address; use namada::ledger::pos::{GenesisValidator, PosParams}; use namada::proto::{Code, Data, Signature}; + use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada::types::transaction::TxType; use namada_test_utils::TestWasms; @@ -205,7 +206,6 @@ mod tests { use namada_tx_prelude::{StorageWrite, TxEnv}; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; - use rust_decimal::Decimal; use storage::testing::arb_account_storage_key_no_vp; use super::*; @@ -236,7 +236,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let source = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &token]); @@ -244,7 +244,19 @@ mod tests { // Credit the tokens to the source before running the transaction to be // able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { // Apply transfer in a transaction @@ -284,7 +296,7 @@ mod tests { let vp_owner = address::testing::established_address_1(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -292,6 +304,18 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -334,7 +358,7 @@ mod tests { let public_key = keypair.ref_to(); let target = address::testing::established_address_2(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); @@ -342,8 +366,20 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); tx_env.write_public_key(&vp_owner, &public_key); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -388,10 +424,10 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = Decimal::new(5, 2); - let max_commission_rate_change = Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), @@ -411,9 +447,9 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -421,6 +457,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -434,7 +478,7 @@ mod tests { tx::ctx() .change_validator_commission_rate( &validator, - &Decimal::new(6, 2), + &Dec::new(6, 2).unwrap(), ) .unwrap(); }); @@ -459,10 +503,10 @@ mod tests { // Init PoS genesis let pos_params = PosParams::default(); let validator = address::testing::established_address_3(); - let initial_stake = token::Amount::from(10_098_123); + let initial_stake = token::Amount::from_uint(10_098_123, 0).unwrap(); let consensus_key = key::testing::keypair_2().ref_to(); - let commission_rate = Decimal::new(5, 2); - let max_commission_rate_change = Decimal::new(1, 2); + let commission_rate = Dec::new(5, 2).unwrap(); + let max_commission_rate_change = Dec::new(1, 2).unwrap(); let genesis_validators = [GenesisValidator { address: validator.clone(), @@ -482,9 +526,9 @@ mod tests { let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); - let bond_amount = token::Amount::from(5_098_123); - let unbond_amount = token::Amount::from(3_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + let bond_amount = token::Amount::from_uint(5_098_123, 0).unwrap(); + let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); @@ -492,6 +536,14 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); + // write the denomination of NAM into storage + storage_api::token::write_denom( + &mut tx_env.wl_storage, + &token, + None, + token::NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap(); tx_env.write_public_key(&vp_owner, &public_key); @@ -507,7 +559,7 @@ mod tests { tx::ctx() .change_validator_commission_rate( &validator, - &Decimal::new(6, 2), + &Dec::new(6, 2).unwrap(), ) .unwrap(); }); @@ -541,7 +593,7 @@ mod tests { let source = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); - let amount = token::Amount::from(10_098_123); + let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); @@ -549,6 +601,10 @@ mod tests { // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it tx_env.credit_tokens(&source, &token, None, amount); + let amount = token::DenominatedAmount { + amount, + denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), + }; // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { diff --git a/wasm_for_tests/tx_memory_limit.wasm b/wasm_for_tests/tx_memory_limit.wasm index d450b0de54..54a97073ea 100755 Binary files a/wasm_for_tests/tx_memory_limit.wasm and b/wasm_for_tests/tx_memory_limit.wasm differ diff --git a/wasm_for_tests/tx_mint_tokens.wasm b/wasm_for_tests/tx_mint_tokens.wasm index 86db03f4fa..cba4778bb0 100755 Binary files a/wasm_for_tests/tx_mint_tokens.wasm and b/wasm_for_tests/tx_mint_tokens.wasm differ diff --git a/wasm_for_tests/tx_no_op.wasm b/wasm_for_tests/tx_no_op.wasm index 23575cc268..cfe3f82655 100755 Binary files a/wasm_for_tests/tx_no_op.wasm and b/wasm_for_tests/tx_no_op.wasm differ diff --git a/wasm_for_tests/tx_proposal_code.wasm b/wasm_for_tests/tx_proposal_code.wasm index 7d6706c765..763b81954a 100755 Binary files a/wasm_for_tests/tx_proposal_code.wasm and b/wasm_for_tests/tx_proposal_code.wasm differ diff --git a/wasm_for_tests/tx_read_storage_key.wasm b/wasm_for_tests/tx_read_storage_key.wasm index 36ea6e223c..78ed02878d 100755 Binary files a/wasm_for_tests/tx_read_storage_key.wasm and b/wasm_for_tests/tx_read_storage_key.wasm differ diff --git a/wasm_for_tests/tx_write.wasm b/wasm_for_tests/tx_write.wasm index 256588b53c..7d08020360 100755 Binary files a/wasm_for_tests/tx_write.wasm and b/wasm_for_tests/tx_write.wasm differ diff --git a/wasm_for_tests/vp_always_false.wasm b/wasm_for_tests/vp_always_false.wasm index 6e29a627b2..3cbf4e7f65 100755 Binary files a/wasm_for_tests/vp_always_false.wasm and b/wasm_for_tests/vp_always_false.wasm differ diff --git a/wasm_for_tests/vp_always_true.wasm b/wasm_for_tests/vp_always_true.wasm index 8cdaf7eb21..4078697a27 100755 Binary files a/wasm_for_tests/vp_always_true.wasm and b/wasm_for_tests/vp_always_true.wasm differ diff --git a/wasm_for_tests/vp_eval.wasm b/wasm_for_tests/vp_eval.wasm index 189765f0ed..e07d50201b 100755 Binary files a/wasm_for_tests/vp_eval.wasm and b/wasm_for_tests/vp_eval.wasm differ diff --git a/wasm_for_tests/vp_memory_limit.wasm b/wasm_for_tests/vp_memory_limit.wasm index 5833a02f4a..7399b93789 100755 Binary files a/wasm_for_tests/vp_memory_limit.wasm and b/wasm_for_tests/vp_memory_limit.wasm differ diff --git a/wasm_for_tests/vp_read_storage_key.wasm b/wasm_for_tests/vp_read_storage_key.wasm index 0543a61536..d98d411016 100755 Binary files a/wasm_for_tests/vp_read_storage_key.wasm and b/wasm_for_tests/vp_read_storage_key.wasm differ diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index d76f374644..cc2ab500b1 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -2510,6 +2510,17 @@ dependencies = [ "parity-scale-codec", ] +[[package]] +name = "impl-num-traits" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951641f13f873bff03d4bf19ae8bec531935ac0ac2cc775f84d7edfdcfed3f17" +dependencies = [ + "integer-sqrt", + "num-traits", + "uint", +] + [[package]] name = "impl-serde" version = "0.4.0" @@ -2595,6 +2606,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + [[package]] name = "io-lifetimes" version = "1.0.10" @@ -2827,7 +2847,7 @@ dependencies = [ [[package]] name = "masp_note_encryption" version = "0.2.0" -source = "git+https://github.com/anoma/masp?rev=cfea8c95d3f73077ca3e25380fd27e5b46e828fd#cfea8c95d3f73077ca3e25380fd27e5b46e828fd" +source = "git+https://github.com/anoma/masp?rev=9320c6b69b5d2e97134866871e960f0a31703813#9320c6b69b5d2e97134866871e960f0a31703813" dependencies = [ "borsh", "chacha20 0.9.1", @@ -2840,7 +2860,7 @@ dependencies = [ [[package]] name = "masp_primitives" version = "0.9.0" -source = "git+https://github.com/anoma/masp?rev=cfea8c95d3f73077ca3e25380fd27e5b46e828fd#cfea8c95d3f73077ca3e25380fd27e5b46e828fd" +source = "git+https://github.com/anoma/masp?rev=9320c6b69b5d2e97134866871e960f0a31703813#9320c6b69b5d2e97134866871e960f0a31703813" dependencies = [ "aes", "bip0039", @@ -2870,7 +2890,7 @@ dependencies = [ [[package]] name = "masp_proofs" version = "0.9.0" -source = "git+https://github.com/anoma/masp?rev=cfea8c95d3f73077ca3e25380fd27e5b46e828fd#cfea8c95d3f73077ca3e25380fd27e5b46e828fd" +source = "git+https://github.com/anoma/masp?rev=9320c6b69b5d2e97134866871e960f0a31703813#9320c6b69b5d2e97134866871e960f0a31703813" dependencies = [ "bellman", "blake2b_simd", @@ -3092,8 +3112,6 @@ dependencies = [ "rand_core 0.6.4", "rayon", "ripemd", - "rust_decimal", - "rust_decimal_macros", "serde", "serde_json", "sha2 0.9.9", @@ -3129,25 +3147,26 @@ dependencies = [ "data-encoding", "derivative", "ed25519-consensus", + "eyre", "ferveo", "ferveo-common", "group-threshold-cryptography", "ibc", "ibc-proto", "ics23", + "impl-num-traits", "index-set", "itertools", "libsecp256k1 0.7.0", "masp_primitives", "namada_macros", + "num-traits", "proptest", "prost", "prost-types", "rand 0.8.5", "rand_core 0.6.4", "rayon", - "rust_decimal", - "rust_decimal_macros", "serde", "serde_json", "sha2 0.9.9", @@ -3157,6 +3176,7 @@ dependencies = [ "thiserror", "tonic-build", "tracing", + "uint", "zeroize", ] @@ -3179,8 +3199,6 @@ dependencies = [ "namada_core", "once_cell", "proptest", - "rust_decimal", - "rust_decimal_macros", "thiserror", "tracing", ] @@ -3210,8 +3228,6 @@ dependencies = [ "namada_vp_prelude", "prost", "regex", - "rust_decimal", - "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -3231,7 +3247,6 @@ dependencies = [ "namada_macros", "namada_proof_of_stake", "namada_vm_env", - "rust_decimal", "sha2 0.9.9", "thiserror", ] @@ -4192,28 +4207,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "rust_decimal" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" -dependencies = [ - "arrayvec", - "borsh", - "num-traits", - "serde", -] - -[[package]] -name = "rust_decimal_macros" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" -dependencies = [ - "quote", - "rust_decimal", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -4686,9 +4679,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" +checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" dependencies = [ "digest 0.9.0", "rand_core 0.6.4", @@ -5543,9 +5536,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "universal-hash" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", "subtle 2.4.1", @@ -5591,9 +5584,9 @@ dependencies = [ [[package]] name = "value-bag" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" [[package]] name = "version_check" diff --git a/wasm_for_tests/wasm_source/src/lib.rs b/wasm_for_tests/wasm_source/src/lib.rs index a37039e9c4..3822a8a01f 100644 --- a/wasm_for_tests/wasm_source/src/lib.rs +++ b/wasm_for_tests/wasm_source/src/lib.rs @@ -149,7 +149,7 @@ pub mod main { let target_key = token::balance_key(&token, &target); let mut target_bal: token::Amount = ctx.read(&target_key)?.unwrap_or_default(); - target_bal.receive(&amount); + target_bal.receive(&amount.amount); ctx.write(&target_key, target_bal)?; Ok(()) }