diff --git a/.github/workflows/ci-build-upload-binaries.yml b/.github/workflows/ci-build-upload-binaries.yml index 02803c4327..dbe39819ec 100644 --- a/.github/workflows/ci-build-upload-binaries.yml +++ b/.github/workflows/ci-build-upload-binaries.yml @@ -42,7 +42,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-20.04] + platform: [ ubuntu-20.04 ] runs-on: ${{ matrix.platform }} env: diff --git a/.github/workflows/ci-contracts-upload-binaries.yml b/.github/workflows/ci-contracts-upload-binaries.yml index 4347e5f333..14d0b63eae 100644 --- a/.github/workflows/ci-contracts-upload-binaries.yml +++ b/.github/workflows/ci-contracts-upload-binaries.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-20.04] + platform: [ ubuntu-20.04 ] runs-on: ${{ matrix.platform }} env: @@ -58,6 +58,7 @@ jobs: cp contracts/target/wasm32-unknown-unknown/release/nym_coconut_dkg.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR + cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR - name: Deploy branch to CI www continue-on-error: true diff --git a/Cargo.lock b/Cargo.lock index efa753841b..3b0583fa59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,18 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "accessory" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87537f9ae7cfa78d5b8ebd1a1db25959f5e737126be4d8eb44a5452fc4b63cde" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "addr" version = "0.15.6" @@ -20,9 +32,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -74,7 +86,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.15", + "getrandom", "once_cell", "version_check", ] @@ -177,9 +189,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] @@ -196,9 +208,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "argon2" @@ -254,7 +266,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -265,23 +277,24 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "async-tungstenite" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e9efbe14612da0a19fb983059a0b621e9cf6225d7018ecab4f9988215540dc" +checksum = "3609af4bbf701ddaf1f6bb4e6257dff4ff8932327d0e685d3f653724c258b1ac" dependencies = [ "futures-io", "futures-util", "log", "pin-project-lite", - "rustls-native-certs", + "rustls-native-certs 0.7.0", + "rustls-pki-types", "tokio", - "tokio-rustls 0.24.1", - "tungstenite", + "tokio-rustls 0.25.0", + "tungstenite 0.21.0", ] [[package]] @@ -347,7 +360,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.29", "itoa", "matchit", "memchr", @@ -459,9 +472,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -547,6 +560,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bit-vec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" + [[package]] name = "bitcoin_hashes" version = "0.11.0" @@ -639,16 +658,29 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "bloomfilter" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0bdbcf2078e0ba8a74e1fe0cf36f54054a04485759b61dfd60b174658e9607" +dependencies = [ + "bit-vec", + "getrandom", + "siphasher", +] + [[package]] name = "bls12_381" version = "0.8.0" -source = "git+https://github.com/jstuczyn/bls12_381?branch=feature/gt-serialization-0.8.0#c4543fde7d02efea6ecfcf22e14476ddb516b483" +source = "git+https://github.com/jstuczyn/bls12_381?branch=temp/experimental-serdect#22cd0a16b674af1629110a2dc8b6cf6c73ea4cd9" dependencies = [ "digest 0.9.0", "ff", "group", "pairing", "rand_core 0.6.4", + "serde", + "serdect 0.3.0-pre.0", "subtle 2.5.0", "zeroize", ] @@ -720,9 +752,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] @@ -764,9 +796,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.97" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "celes" @@ -791,9 +823,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha" @@ -896,9 +928,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -906,45 +938,45 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", - "clap_lex 0.7.0", + "clap_lex 0.7.1", "strsim 0.11.1", ] [[package]] name = "clap_complete" -version = "4.5.2" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e" +checksum = "d2020fa13af48afc65a9a87335bda648309ab3d154cd03c7ff95b378c7ed39c4" dependencies = [ - "clap 4.5.4", + "clap 4.5.7", ] [[package]] name = "clap_complete_fig" -version = "4.5.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b3e65f91fabdd23cac3d57d39d5d938b4daabd070c335c006dccb866a61110" +checksum = "fb4bc503cddc1cd320736fb555d6598309ad07c2ddeaa23891a10ffb759ee612" dependencies = [ - "clap 4.5.4", + "clap 4.5.7", "clap_complete", ] [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -958,9 +990,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "cloudabi" @@ -1009,6 +1041,18 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "comfy-table" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" +dependencies = [ + "crossterm 0.27.0", + "strum 0.26.3", + "strum_macros 0.26.4", + "unicode-width", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1018,6 +1062,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + [[package]] name = "console-api" version = "0.5.0" @@ -1135,19 +1192,19 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" dependencies = [ - "prost 0.12.4", - "prost-types 0.12.4", - "tendermint-proto", + "prost 0.12.6", + "prost-types 0.12.6", + "tendermint-proto 0.34.1", ] [[package]] name = "cosmos-sdk-proto" -version = "0.20.0" -source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#41ed4631e146268b0300033c8bbb993d79a49f58" +version = "0.22.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" dependencies = [ - "prost 0.12.4", - "prost-types 0.12.4", - "tendermint-proto", + "prost 0.12.6", + "prost-types 0.12.6", + "tendermint-proto 0.37.0", ] [[package]] @@ -1157,7 +1214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" dependencies = [ "bip32", - "cosmos-sdk-proto 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cosmos-sdk-proto 0.20.0", "ecdsa", "eyre", "k256", @@ -1166,17 +1223,17 @@ dependencies = [ "serde_json", "signature", "subtle-encoding", - "tendermint", + "tendermint 0.34.1", "thiserror", ] [[package]] name = "cosmrs" -version = "0.15.0" -source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#41ed4631e146268b0300033c8bbb993d79a49f58" +version = "0.17.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" dependencies = [ "bip32", - "cosmos-sdk-proto 0.20.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmos-sdk-proto 0.22.0-pre", "ecdsa", "eyre", "k256", @@ -1185,7 +1242,7 @@ dependencies = [ "serde_json", "signature", "subtle-encoding", - "tendermint", + "tendermint 0.37.0", "tendermint-rpc", "thiserror", ] @@ -1257,6 +1314,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cosmwasm-storage" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8935079712688b8d8d4c036378b38b49d13692621c6fcba96700fadfd5126a18" +dependencies = [ + "cosmwasm-std", + "serde", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1283,9 +1350,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -1316,6 +1383,32 @@ dependencies = [ "walkdir", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap 4.5.7", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + [[package]] name = "criterion-plot" version = "0.5.0" @@ -1328,9 +1421,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -1365,9 +1458,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -1379,7 +1472,7 @@ dependencies = [ "crossterm_winapi", "libc", "mio", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "signal-hook", "signal-hook-mio", "winapi", @@ -1395,12 +1488,25 @@ dependencies = [ "crossterm_winapi", "libc", "mio", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "signal-hook", "signal-hook-mio", "winapi", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "crossterm_winapi", + "libc", + "parking_lot 0.12.3", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -1558,7 +1664,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1683,12 +1789,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ - "darling_core 0.20.8", - "darling_macro 0.20.8", + "darling_core 0.20.9", + "darling_macro 0.20.9", ] [[package]] @@ -1707,16 +1813,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.63", + "strsim 0.11.1", + "syn 2.0.66", ] [[package]] @@ -1732,13 +1838,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ - "darling_core 0.20.8", + "darling_core 0.20.9", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1780,6 +1886,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "delegate-display" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "der" version = "0.7.9" @@ -1841,7 +1959,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1915,6 +2033,29 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "dkg-bypass-contract" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus", + "nym-coconut-dkg-common", + "nym-contracts-common", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -1943,7 +2084,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", - "serdect", + "serdect 0.2.0", "signature", "spki", ] @@ -2004,9 +2145,9 @@ dependencies = [ [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "elliptic-curve" @@ -2023,11 +2164,17 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sec1", - "serdect", + "serdect 0.2.0", "subtle 2.5.0", "zeroize", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -2096,11 +2243,11 @@ name = "explorer-api" version = "1.1.36" dependencies = [ "chrono", - "clap 4.5.4", + "clap 4.5.7", "dotenvy", "humantime-serde", "isocountry", - "itertools 0.12.1", + "itertools 0.13.0", "log", "maxminddb", "nym-bin-common", @@ -2181,6 +2328,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fancy_constructor" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71f317e4af73b2f8f608fac190c52eac4b1879d2145df1db2fe48881ca69435" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -2214,9 +2373,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "figment" -version = "0.10.18" +version = "0.10.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d032832d74006f99547004d49410a4b4218e4c33382d56ca3ff89df74f86b953" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ "atomic 0.6.0", "pear", @@ -2238,12 +2397,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "flate2" version = "1.0.30" @@ -2400,7 +2553,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -2473,19 +2626,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -2495,7 +2635,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -2523,9 +2663,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -2837,12 +2977,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http 1.1.0", "http-body 1.0.0", "pin-project-lite", @@ -2856,9 +2996,9 @@ checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" [[package]] name = "httpcodec" @@ -2903,9 +3043,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" dependencies = [ "bytes", "futures-channel", @@ -2953,7 +3093,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.28", + "hyper 0.14.29", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -2982,7 +3122,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.28", + "hyper 0.14.29", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2990,9 +3130,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-channel", @@ -3031,6 +3171,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -3049,12 +3307,14 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", ] [[package]] @@ -3065,13 +3325,15 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexed_db_futures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfbcff6ae46750b15cc594bfd277b188cbddcfdc1817848f97f03f26f8625b9e" +version = "0.4.2" +source = "git+https://github.com/TiemenSch/rust-indexed-db?branch=update-uuid#9745d015707008b0c410115d787014a6d1af2efb" dependencies = [ + "accessory", "cfg-if", + "delegate-display", + "fancy_constructor", "js-sys", - "uuid 1.6.1", + "uuid", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -3099,6 +3361,19 @@ dependencies = [ "serde", ] +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + [[package]] name = "inlinable_string" version = "0.1.15" @@ -3153,9 +3428,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -3281,6 +3556,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -3403,9 +3687,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" @@ -3436,9 +3720,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.16" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" dependencies = [ "cc", "libc", @@ -3448,9 +3732,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lioness" @@ -3464,6 +3748,12 @@ dependencies = [ "keystream", ] +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.12" @@ -3511,6 +3801,53 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d" +[[package]] +name = "macroific" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "macroific_core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "macroific_macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "matchers" version = "0.1.0" @@ -3577,9 +3914,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -3592,7 +3929,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.48.0", ] @@ -3738,9 +4075,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.5.0", "cfg-if", @@ -3847,16 +4184,24 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "nym-api" version = "1.1.40" dependencies = [ "anyhow", "async-trait", + "bincode", "bip39", + "bloomfilter", "bs58 0.5.1", "cfg-if", - "clap 4.5.4", + "clap 4.5.7", "console-subscriber", "cosmwasm-std", "cw-utils", @@ -3867,21 +4212,25 @@ dependencies = [ "futures", "getset", "humantime-serde", - "itertools 0.12.1", + "itertools 0.13.0", "k256", "log", "nym-api-requests", "nym-bandwidth-controller", "nym-bin-common", "nym-coconut", - "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-compact-ecash", "nym-config", "nym-contracts-common", "nym-credential-storage", "nym-credentials", + "nym-credentials-interface", "nym-crypto", "nym-dkg", + "nym-ecash-contract-common", + "nym-ecash-double-spending", + "nym-ecash-time", "nym-gateway-client", "nym-inclusion-probability", "nym-mixnet-contract-common", @@ -3923,19 +4272,24 @@ name = "nym-api-requests" version = "0.1.0" dependencies = [ "bs58 0.5.1", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "ecdsa", "getset", + "nym-compact-ecash", "nym-credentials-interface", "nym-crypto", + "nym-ecash-time", "nym-mixnet-contract-common", "nym-node-requests", "rocket", "schemars", "serde", + "serde-helpers", "serde_json", - "tendermint", + "sha2 0.10.8", + "tendermint 0.37.0", + "thiserror", "time", "ts-rs", ] @@ -4004,11 +4358,12 @@ version = "0.1.0" dependencies = [ "bip39", "log", - "nym-coconut", "nym-credential-storage", "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-time", "nym-network-defaults", "nym-validator-client", "rand 0.8.5", @@ -4021,7 +4376,7 @@ dependencies = [ name = "nym-bin-common" version = "0.6.0" dependencies = [ - "clap 4.5.4", + "clap 4.5.7", "clap_complete", "clap_complete_fig", "const-str", @@ -4045,7 +4400,7 @@ name = "nym-bity-integration" version = "0.1.0" dependencies = [ "anyhow", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "eyre", "k256", "nym-cli-commands", @@ -4063,7 +4418,7 @@ dependencies = [ "base64 0.13.1", "bip39", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "clap_complete", "clap_complete_fig", "dotenvy", @@ -4089,9 +4444,9 @@ dependencies = [ "bip39", "bs58 0.5.1", "cfg-if", - "clap 4.5.4", - "comfy-table", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "clap 4.5.7", + "comfy-table 6.2.0", + "cosmrs 0.17.0-pre", "cosmwasm-std", "csv", "cw-utils", @@ -4104,7 +4459,6 @@ dependencies = [ "nym-bandwidth-controller", "nym-bin-common", "nym-client-core", - "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", "nym-config", "nym-contracts-common", @@ -4113,6 +4467,7 @@ dependencies = [ "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", "nym-id", "nym-mixnet-contract-common", "nym-multisig-contract-common", @@ -4139,7 +4494,7 @@ name = "nym-client" version = "1.1.37" dependencies = [ "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "dirs 4.0.0", "futures", "log", @@ -4179,7 +4534,8 @@ dependencies = [ "base64 0.21.7", "bs58 0.5.1", "cfg-if", - "clap 4.5.4", + "clap 4.5.7", + "comfy-table 7.1.1", "futures", "gloo-timers", "http-body-util", @@ -4195,6 +4551,7 @@ dependencies = [ "nym-country-group", "nym-credential-storage", "nym-crypto", + "nym-ecash-time", "nym-explorer-client", "nym-gateway-client", "nym-gateway-requests", @@ -4219,7 +4576,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", - "tungstenite", + "tungstenite 0.20.1", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4248,7 +4605,7 @@ name = "nym-client-core-gateways-storage" version = "0.1.0" dependencies = [ "async-trait", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "log", "nym-crypto", "nym-gateway-requests", @@ -4315,13 +4672,13 @@ version = "0.5.0" dependencies = [ "bls12_381", "bs58 0.5.1", - "criterion", + "criterion 0.4.0", "digest 0.9.0", "doc-comment", "ff", - "getrandom 0.2.15", + "getrandom", "group", - "itertools 0.12.1", + "itertools 0.13.0", "nym-dkg", "nym-pemstore", "rand 0.8.5", @@ -4356,6 +4713,28 @@ dependencies = [ "nym-multisig-contract-common", ] +[[package]] +name = "nym-compact-ecash" +version = "0.1.0" +dependencies = [ + "bincode", + "bls12_381", + "bs58 0.5.1", + "cfg-if", + "criterion 0.5.1", + "digest 0.9.0", + "ff", + "group", + "itertools 0.12.1", + "nym-pemstore", + "rand 0.8.5", + "rayon", + "serde", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "nym-config" version = "0.1.0" @@ -4397,7 +4776,12 @@ name = "nym-credential-storage" version = "0.1.0" dependencies = [ "async-trait", + "bincode", "log", + "nym-compact-ecash", + "nym-credentials", + "nym-ecash-time", + "serde", "sqlx", "thiserror", "tokio", @@ -4411,12 +4795,14 @@ dependencies = [ "log", "nym-bandwidth-controller", "nym-client-core", - "nym-coconut", + "nym-compact-ecash", "nym-config", "nym-credential-storage", "nym-credentials", + "nym-ecash-time", "nym-validator-client", "thiserror", + "time", "tokio", ] @@ -4426,11 +4812,14 @@ version = "0.1.0" dependencies = [ "bincode", "bls12_381", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "log", "nym-api-requests", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-time", + "nym-network-defaults", "nym-validator-client", "rand 0.8.5", "serde", @@ -4444,9 +4833,12 @@ name = "nym-credentials-interface" version = "0.1.0" dependencies = [ "bls12_381", - "nym-coconut", + "nym-compact-ecash", + "nym-ecash-time", + "rand 0.8.5", "serde", "thiserror", + "time", ] [[package]] @@ -4482,7 +4874,7 @@ dependencies = [ "bitvec", "bls12_381", "bs58 0.5.1", - "criterion", + "criterion 0.4.0", "ff", "group", "lazy_static", @@ -4498,6 +4890,37 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-ecash-contract-common" +version = "0.1.0" +dependencies = [ + "bs58 0.5.1", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "cw2", + "nym-multisig-contract-common", + "thiserror", +] + +[[package]] +name = "nym-ecash-double-spending" +version = "0.1.0" +dependencies = [ + "bit-vec", + "bloomfilter", + "nym-network-defaults", +] + +[[package]] +name = "nym-ecash-time" +version = "0.1.0" +dependencies = [ + "nym-compact-ecash", + "time", +] + [[package]] name = "nym-execute" version = "0.1.0" @@ -4550,9 +4973,12 @@ dependencies = [ "anyhow", "async-trait", "bip39", + "bloomfilter", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "colored", + "cosmwasm-std", + "cw-utils", "dashmap", "defguard_wireguard_rs", "dirs 4.0.0", @@ -4560,7 +4986,6 @@ dependencies = [ "futures", "humantime-serde", "ipnetwork 0.16.0", - "log", "nym-api-requests", "nym-authenticator", "nym-bin-common", @@ -4568,6 +4993,8 @@ dependencies = [ "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-double-spending", "nym-gateway-requests", "nym-ip-packet-router", "nym-mixnet-client", @@ -4586,6 +5013,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", + "si-scale", "sqlx", "subtle-encoding", "thiserror", @@ -4594,6 +5022,7 @@ dependencies = [ "tokio-stream", "tokio-tungstenite", "tokio-util", + "tracing", "url", "zeroize", ] @@ -4603,7 +5032,7 @@ name = "nym-gateway-client" version = "0.1.0" dependencies = [ "futures", - "getrandom 0.2.15", + "getrandom", "gloo-utils 0.2.0", "log", "nym-bandwidth-controller", @@ -4624,7 +5053,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", - "tungstenite", + "tungstenite 0.20.1", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4639,7 +5068,7 @@ dependencies = [ "bs58 0.5.1", "futures", "generic-array 0.14.7", - "log", + "nym-compact-ecash", "nym-credentials", "nym-credentials-interface", "nym-crypto", @@ -4650,7 +5079,8 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tungstenite", + "tracing", + "tungstenite 0.20.1", "wasmtimer", "zeroize", ] @@ -4713,7 +5143,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "nym-bin-common", "nym-credential-storage", "nym-id", @@ -4755,7 +5185,7 @@ dependencies = [ "bincode", "bs58 0.5.1", "bytes", - "clap 4.5.4", + "clap 4.5.7", "etherparse", "futures", "log", @@ -4850,7 +5280,7 @@ dependencies = [ "anyhow", "axum 0.7.5", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "colored", "cupid", "dirs 4.0.0", @@ -4956,7 +5386,7 @@ dependencies = [ "anyhow", "async-trait", "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "dirs 4.0.0", "futures", "humantime-serde", @@ -5008,7 +5438,7 @@ dependencies = [ "bs58 0.5.1", "cargo_metadata", "celes", - "clap 4.5.4", + "clap 4.5.7", "colored", "cupid", "humantime-serde", @@ -5155,7 +5585,7 @@ name = "nym-nr-query" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.4", + "clap 4.5.7", "log", "nym-bin-common", "nym-network-defaults", @@ -5181,10 +5611,10 @@ dependencies = [ "blake3", "chacha20", "chacha20poly1305", - "criterion", + "criterion 0.4.0", "curve25519-dalek 4.1.2", "fastrand 2.1.0", - "getrandom 0.2.15", + "getrandom", "log", "rand 0.8.5", "rayon", @@ -5232,7 +5662,7 @@ dependencies = [ "nym-task", "nym-topology", "nym-validator-client", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pretty_env_logger", "rand 0.8.5", "reqwest 0.12.4", @@ -5243,6 +5673,7 @@ dependencies = [ "tokio-util", "toml 0.5.11", "url", + "zeroize", ] [[package]] @@ -5267,7 +5698,7 @@ name = "nym-socks5-client" version = "1.1.37" dependencies = [ "bs58 0.5.1", - "clap 4.5.4", + "clap 4.5.7", "log", "nym-bin-common", "nym-client-core", @@ -5536,7 +5967,7 @@ dependencies = [ "aes-gcm", "argon2", "generic-array 0.14.7", - "getrandom 0.2.15", + "getrandom", "rand 0.8.5", "serde", "serde_json", @@ -5599,11 +6030,11 @@ name = "nym-types" version = "1.0.0" dependencies = [ "base64 0.21.7", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "eyre", "hmac", - "itertools 0.12.1", + "itertools 0.13.0", "log", "nym-config", "nym-crypto", @@ -5632,7 +6063,7 @@ dependencies = [ "bip32", "bip39", "colored", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "cosmwasm-std", "cw-controllers", "cw-utils", @@ -5642,27 +6073,29 @@ dependencies = [ "eyre", "flate2", "futures", - "itertools 0.12.1", + "itertools 0.13.0", "log", "nym-api-requests", - "nym-coconut", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-compact-ecash", "nym-config", "nym-contracts-common", + "nym-ecash-contract-common", "nym-group-contract-common", "nym-http-api-client", "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", "nym-vesting-contract-common", - "prost 0.12.4", + "prost 0.12.6", "reqwest 0.12.4", "serde", "serde_json", "sha2 0.9.9", "tendermint-rpc", "thiserror", + "time", "tokio", "ts-rs", "url", @@ -5676,18 +6109,19 @@ version = "0.1.0" dependencies = [ "anyhow", "bip39", - "clap 4.5.4", + "clap 4.5.7", "cosmwasm-std", "futures", "humantime 2.1.0", "humantime-serde", "nym-bin-common", - "nym-coconut", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-compact-ecash", "nym-config", "nym-credentials", "nym-crypto", + "nym-ecash-time", "nym-network-defaults", "nym-task", "nym-validator-client", @@ -5722,7 +6156,7 @@ dependencies = [ name = "nym-wallet-types" version = "1.0.0" dependencies = [ - "cosmrs 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cosmrs 0.15.0", "cosmwasm-std", "hex-literal", "nym-config", @@ -5783,7 +6217,7 @@ version = "0.1.3" dependencies = [ "anyhow", "bytes", - "clap 4.5.4", + "clap 4.5.7", "dotenvy", "flate2", "futures", @@ -5813,14 +6247,14 @@ version = "0.1.0" dependencies = [ "async-trait", "const_format", - "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "cosmrs 0.17.0-pre", "eyre", "futures", "humantime 2.1.0", "serde", "sha2 0.10.8", "sqlx", - "tendermint", + "tendermint 0.37.0", "tendermint-rpc", "thiserror", "time", @@ -5833,9 +6267,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -6039,9 +6473,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core 0.9.10", @@ -6111,14 +6545,14 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "peg" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a" +checksum = "8a625d12ad770914cbf7eff6f9314c3ef803bfe364a1b20bc36ddf56673e71e5" dependencies = [ "peg-macros", "peg-runtime", @@ -6126,9 +6560,9 @@ dependencies = [ [[package]] name = "peg-macros" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" +checksum = "f241d42067ed3ab6a4fece1db720838e1418f36d868585a27931f95d6bc03582" dependencies = [ "peg-runtime", "proc-macro2", @@ -6137,9 +6571,9 @@ dependencies = [ [[package]] name = "peg-runtime" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" [[package]] name = "pem" @@ -6189,7 +6623,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -6220,7 +6654,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -6259,9 +6693,9 @@ checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -6272,15 +6706,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] @@ -6324,6 +6758,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "powerfmt" version = "0.2.0" @@ -6382,9 +6822,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -6397,7 +6837,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "version_check", "yansi", ] @@ -6412,7 +6852,7 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "protobuf", "thiserror", ] @@ -6429,12 +6869,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", - "prost-derive 0.12.5", + "prost-derive 0.12.6", ] [[package]] @@ -6452,15 +6892,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9554e3ab233f0a932403704f1a1d08c30d5ccd931adfdfa1e8b5a19b52c1d55a" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -6474,11 +6914,11 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ - "prost 0.12.4", + "prost 0.12.6", ] [[package]] @@ -6489,9 +6929,9 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "psl" -version = "2.1.39" +version = "2.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b320cda4ad7e8f4269fa415754418f83b38c666a5e2e99ea48825b274a373f3" +checksum = "ecec637c2e9d0c8c4bf78df069a53a103ebe3dbd0dc7eff1d60c1006a1c97254" dependencies = [ "psl-types", ] @@ -6549,7 +6989,7 @@ dependencies = [ "libc", "rand_chacha 0.1.1", "rand_core 0.4.2", - "rand_hc 0.1.0", + "rand_hc", "rand_isaac", "rand_jitter", "rand_os", @@ -6558,19 +6998,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc 0.2.0", -] - [[package]] name = "rand" version = "0.8.5" @@ -6592,16 +7019,6 @@ dependencies = [ "rand_core 0.3.1", ] -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -6632,9 +7049,6 @@ name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] [[package]] name = "rand_core" @@ -6642,7 +7056,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom", ] [[package]] @@ -6664,15 +7078,6 @@ dependencies = [ "rand_core 0.3.1", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "rand_isaac" version = "0.1.1" @@ -6806,7 +7211,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom 0.2.15", + "getrandom", "libredox", "thiserror", ] @@ -6828,19 +7233,19 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -6854,13 +7259,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -6871,9 +7276,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" @@ -6889,7 +7294,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.29", "hyper-rustls 0.24.2", "ipnet", "js-sys", @@ -6899,7 +7304,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.21.12", - "rustls-native-certs", + "rustls-native-certs 0.6.3", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -6956,7 +7361,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.1", + "webpki-roots 0.26.2", "winreg 0.52.0", ] @@ -6993,7 +7398,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", @@ -7028,7 +7433,7 @@ dependencies = [ "memchr", "multer", "num_cpus", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pin-project-lite", "rand 0.8.5", "ref-cast", @@ -7059,7 +7464,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.63", + "syn 2.0.66", "unicode-xid", "version_check", ] @@ -7091,7 +7496,7 @@ dependencies = [ "either", "futures", "http 0.2.12", - "hyper 0.14.28", + "hyper 0.14.29", "indexmap 2.2.6", "log", "memchr", @@ -7156,7 +7561,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.63", + "syn 2.0.66", "walkdir", ] @@ -7240,7 +7645,7 @@ dependencies = [ "log", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.3", + "rustls-webpki 0.102.4", "subtle 2.5.0", "zeroize", ] @@ -7257,6 +7662,19 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -7294,9 +7712,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.3" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -7305,9 +7723,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" @@ -7317,9 +7735,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "safer-ffi" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e0f9180bdf2f29dd5c731789ba0e68f65233c822ece645d288ec7d112e6333" +checksum = "44abae8773dc41fb96af52696b834b1f4c806006b456b22ee3602f7b061e3ad0" dependencies = [ "inventory", "libc", @@ -7334,9 +7752,9 @@ dependencies = [ [[package]] name = "safer_ffi-proc_macros" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209b37c0f75bc1a994ff8c8290b77005ea09951b59f6855f8c42667a57ed3285" +checksum = "6c9d4117a8a72f9b615169d4d720d79e74931f74003c73cc2f3927c700156ddf" dependencies = [ "macro_rules_attribute", "prettyplease", @@ -7365,9 +7783,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "indexmap 1.9.3", @@ -7378,14 +7796,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", - "serde_derive_internals 0.29.0", - "syn 2.0.63", + "serde_derive_internals 0.29.1", + "syn 2.0.66", ] [[package]] @@ -7420,7 +7838,7 @@ dependencies = [ "der", "generic-array 0.14.7", "pkcs8", - "serdect", + "serdect 0.2.0", "subtle 2.5.0", "zeroize", ] @@ -7492,13 +7910,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-helpers" +version = "0.1.0" +dependencies = [ + "base64 0.21.7", + "bs58 0.5.1", + "serde", +] + [[package]] name = "serde-json-wasm" version = "0.5.0" @@ -7541,13 +7968,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7558,18 +7985,18 @@ checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "serde_derive_internals" -version = "0.29.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7601,7 +8028,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7649,10 +8076,10 @@ version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ - "darling 0.20.8", + "darling 0.20.9", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -7678,6 +8105,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.3.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791ef964bfaba6be28a5c3f0c56836e17cb711ac009ca1074b9c735a3ebf240a" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -7768,6 +8205,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -7878,11 +8321,10 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" dependencies = [ - "itertools 0.12.1", "nom", "unicode_categories", ] @@ -7980,7 +8422,7 @@ name = "ssl-inject" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.4", + "clap 4.5.7", "hex", "tokio", ] @@ -7994,6 +8436,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "state" version = "0.6.0" @@ -8005,13 +8453,13 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", ] [[package]] @@ -8050,6 +8498,12 @@ dependencies = [ "strum_macros 0.25.3", ] +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + [[package]] name = "strum_macros" version = "0.23.1" @@ -8086,7 +8540,20 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.63", + "syn 2.0.66", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.66", ] [[package]] @@ -8129,9 +8596,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -8150,6 +8617,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "sysinfo" version = "0.27.8" @@ -8209,9 +8687,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" dependencies = [ "filetime", "libc", @@ -8245,8 +8723,8 @@ dependencies = [ "k256", "num-traits", "once_cell", - "prost 0.12.4", - "prost-types 0.12.4", + "prost 0.12.6", + "prost-types 0.12.6", "ripemd", "serde", "serde_bytes", @@ -8256,22 +8734,53 @@ dependencies = [ "signature", "subtle 2.5.0", "subtle-encoding", - "tendermint-proto", + "tendermint-proto 0.34.1", + "time", + "zeroize", +] + +[[package]] +name = "tendermint" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954496fbc9716eb4446cdd6d00c071a3e2f22578d62aa03b40c7e5b4fda3ed42" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost 0.12.6", + "prost-types 0.12.6", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.8", + "signature", + "subtle 2.5.0", + "subtle-encoding", + "tendermint-proto 0.37.0", "time", "zeroize", ] [[package]] name = "tendermint-config" -version = "0.34.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1a02da769166e2052cd537b1a97c78017632c2d9e19266367b27e73910434fc" +checksum = "f84b11b57d20ee4492a1452faff85f5c520adc36ca9fe5e701066935255bb89f" dependencies = [ "flex-error", "serde", "serde_json", - "tendermint", - "toml 0.5.11", + "tendermint 0.37.0", + "toml 0.8.14", "url", ] @@ -8285,8 +8794,24 @@ dependencies = [ "flex-error", "num-derive", "num-traits", - "prost 0.12.4", - "prost-types 0.12.4", + "prost 0.12.6", + "prost-types 0.12.6", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-proto" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc87024548c7f3da479885201e3da20ef29e85a3b13d04606b380ac4c7120d87" +dependencies = [ + "bytes", + "flex-error", + "prost 0.12.6", + "prost-types 0.12.6", "serde", "serde_bytes", "subtle-encoding", @@ -8295,18 +8820,19 @@ dependencies = [ [[package]] name = "tendermint-rpc" -version = "0.34.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbf0a4753b46a190f367337e0163d0b552a2674a6bac54e74f9f2cdcde2969b" +checksum = "dfdc2281e271277fda184d96d874a6fe59f569b130b634289257baacfc95aa85" dependencies = [ "async-trait", "async-tungstenite", "bytes", "flex-error", "futures", - "getrandom 0.2.15", + "getrandom", "peg", "pin-project", + "rand 0.8.5", "reqwest 0.11.27", "semver 1.0.23", "serde", @@ -8314,15 +8840,15 @@ dependencies = [ "serde_json", "subtle 2.5.0", "subtle-encoding", - "tendermint", + "tendermint 0.37.0", "tendermint-config", - "tendermint-proto", + "tendermint-proto 0.37.0", "thiserror", "time", "tokio", "tracing", "url", - "uuid 0.8.2", + "uuid", "walkdir", ] @@ -8335,6 +8861,45 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "testnet-manager" +version = "0.1.0" +dependencies = [ + "anyhow", + "bip39", + "bs58 0.5.1", + "clap 4.5.7", + "console", + "cw-utils", + "dkg-bypass-contract", + "indicatif", + "nym-bin-common", + "nym-coconut-dkg-common", + "nym-compact-ecash", + "nym-config", + "nym-contracts-common", + "nym-crypto", + "nym-ecash-contract-common", + "nym-group-contract-common", + "nym-mixnet-contract-common", + "nym-multisig-contract-common", + "nym-pemstore", + "nym-validator-client", + "nym-vesting-contract-common", + "rand 0.8.5", + "serde", + "serde_json", + "sqlx", + "tempfile", + "thiserror", + "time", + "tokio", + "toml 0.8.14", + "tracing", + "url", + "zeroize", +] + [[package]] name = "textwrap" version = "0.16.1" @@ -8343,22 +8908,22 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8427,6 +8992,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -8454,16 +9029,16 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", @@ -8484,13 +9059,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8564,12 +9139,12 @@ dependencies = [ [[package]] name = "tokio-tun" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65d79912ba514490b1f5a574b585e19082bd2a6b238970c87c57a66bd77206b5" +checksum = "68f5381752d5832fc811f89d54fc334951aa435022f494190ba7151661f206df" dependencies = [ "libc", - "nix 0.28.0", + "nix 0.29.0", "thiserror", "tokio", ] @@ -8585,7 +9160,7 @@ dependencies = [ "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", - "tungstenite", + "tungstenite 0.20.1", "webpki-roots 0.25.4", ] @@ -8670,7 +9245,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.8", + "winnow 0.6.13", ] [[package]] @@ -8688,7 +9263,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.29", "hyper-timeout", "percent-encoding", "pin-project", @@ -8778,7 +9353,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8936,7 +9511,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "termcolor", ] @@ -8963,7 +9538,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.28.0", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -8987,6 +9562,27 @@ dependencies = [ "webpki-roots 0.24.0", ] +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.22.4", + "rustls-pki-types", + "sha1", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -9058,6 +9654,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -9066,9 +9668,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-xid" @@ -9127,12 +9729,12 @@ checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3" [[package]] name = "url" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.0", "percent-encoding", "serde", ] @@ -9150,10 +9752,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "utf8parse" -version = "0.2.1" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" @@ -9177,7 +9791,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -9198,17 +9812,11 @@ dependencies = [ [[package]] name = "uuid" -version = "0.8.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" - -[[package]] -name = "uuid" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.15", + "getrandom", "serde", "wasm-bindgen", ] @@ -9271,12 +9879,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -9304,7 +9906,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -9338,7 +9940,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9371,7 +9973,7 @@ checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -9440,11 +10042,11 @@ name = "wasm-utils" version = "0.1.0" dependencies = [ "futures", - "getrandom 0.2.15", + "getrandom", "gloo-net", "gloo-utils 0.2.0", "js-sys", - "tungstenite", + "tungstenite 0.20.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -9458,7 +10060,7 @@ checksum = "5f656cd8858a5164932d8a90f936700860976ec21eb00e0fe2aa8cab13f6b4cf" dependencies = [ "futures", "js-sys", - "parking_lot 0.12.2", + "parking_lot 0.12.3", "pin-utils", "slab", "wasm-bindgen", @@ -9510,9 +10112,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +checksum = "3c452ad30530b54a4d8e71952716a212b08efd0f3562baa66c29a618b07da7c3" dependencies = [ "rustls-pki-types", ] @@ -9792,9 +10394,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] @@ -9839,6 +10441,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.5.1" @@ -9880,6 +10494,30 @@ dependencies = [ "is-terminal", ] +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.34" @@ -9897,7 +10535,28 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", ] [[package]] @@ -9917,7 +10576,29 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", +] + +[[package]] +name = "zerovec" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", ] [[package]] @@ -9939,20 +10620,21 @@ dependencies = [ "anyhow", "async-trait", "bs58 0.5.1", - "getrandom 0.2.15", + "getrandom", "js-sys", "nym-bin-common", "nym-coconut", + "nym-compact-ecash", "nym-credentials", "nym-crypto", "nym-http-api-client", - "rand 0.7.3", + "rand 0.8.5", "reqwest 0.12.4", "serde", "thiserror", "tokio", "tsify", - "uuid 1.6.1", + "uuid", "wasm-bindgen", "wasm-utils", "wasmtimer", diff --git a/Cargo.toml b/Cargo.toml index 18608553af..26fc8dbfaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "common/commands", "common/config", "common/cosmwasm-smart-contracts/coconut-bandwidth-contract", + "common/cosmwasm-smart-contracts/ecash-contract", "common/cosmwasm-smart-contracts/coconut-dkg", "common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/group-contract", @@ -47,6 +48,8 @@ members = [ "common/credentials-interface", "common/crypto", "common/dkg", + "common/ecash-double-spending", + "common/ecash-time", "common/execute", "common/exit-policy", "common/http-api-client", @@ -59,6 +62,7 @@ members = [ "common/node-tester-utils", "common/nonexhaustive-delayqueue", "common/nymcoconut", + "common/nym_offline_compact_ecash", "common/nym-id", "common/nym-metrics", "common/nymsphinx", @@ -74,6 +78,7 @@ members = [ "common/nymsphinx/types", "common/nyxd-scraper", "common/pemstore", + "common/serde-helpers", "common/socks5-client-core", "common/socks5/proxy-helpers", "common/socks5/requests", @@ -120,6 +125,8 @@ members = [ "wasm/mix-fetch", "wasm/node-tester", "wasm/zknym-lib", + "tools/internal/testnet-manager", + "tools/internal/testnet-manager/dkg-bypass-contract", ] default-members = [ @@ -139,6 +146,7 @@ exclude = [ "explorer", "contracts", "nym-wallet", + "nym-vpn/ui/src-tauri", "cpu-cycles", "sdk/ffi/cpp", ] @@ -163,8 +171,13 @@ axum-extra = "0.9.3" base64 = "0.21.4" bincode = "1.3.3" bip39 = { version = "2.0.0", features = ["zeroize"] } + +# can we unify those? +bit-vec = "0.7.0" bitvec = "1.0.0" + blake3 = "1.3.1" +bloomfilter = "1.0.14" bs58 = "0.5.1" bytecodec = "0.4.15" bytes = "1.5.0" @@ -216,11 +229,11 @@ httpcodec = "0.2.3" humantime = "2.1.0" humantime-serde = "1.1.1" hyper = "1.3.1" -indexed_db_futures = "0.3.0" inquire = "0.6.2" ip_network = "0.4.1" ipnetwork = "0.16" isocountry = "0.3.2" +itertools = "0.13.0" k256 = "0.13" lazy_static = "1.4.0" ledger-transport = "0.10.0" @@ -305,7 +318,8 @@ prometheus = { version = "0.13.0" } # coconut/DKG related # unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork # as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm -bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "feature/gt-serialization-0.8.0" } +# plus to make our live easier we need serde support from https://github.com/zkcrypto/bls12_381/pull/125 +bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "temp/experimental-serdect" } group = { version = "0.13.0", default-features = false } ff = { version = "0.13.0", default-features = false } @@ -328,16 +342,22 @@ cw-controllers = { version = "=1.1.0" } # cosmrs-related bip32 = { version = "0.5.1", default-features = false } -# temporarily using a fork again (yay.) because we need staking and slashing support -cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" } -#cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" } # unfortuntely we need a fork by yours truly to get the staking support -tendermint = "0.34" # same version as used by cosmrs -tendermint-rpc = "0.34" # same version as used by cosmrs +# temporarily using a fork again (yay.) because we need staking and slashing support (which are already on main but not released) +# plus response message parsing (which is, as of the time of writing this message, waiting to get merged) +#cosmrs = { path = "../cosmos-rust-fork/cosmos-rust/cosmrs" } +cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev = "4b1332e6d8258ac845cef71589c8d362a669675a" } # unfortuntely we need a fork by yours truly to get the staking support +tendermint = "0.37.0" # same version as used by cosmrs +tendermint-rpc = "0.37.0" # same version as used by cosmrs prost = { version = "0.12", default-features = false } # wasm-related dependencies gloo-utils = "0.2.0" gloo-net = "0.5.0" + +# use a separate branch due to feature unification failures +# this is blocked until the upstream removes outdates `wasm_bindgen` feature usage +# indexed_db_futures = "0.4.1" +indexed_db_futures = { git = "https://github.com/TiemenSch/rust-indexed-db", branch = "update-uuid" } js-sys = "0.3.69" serde-wasm-bindgen = "0.6.5" tsify = "0.4.5" @@ -345,7 +365,6 @@ wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4.39" wasmtimer = "0.2.0" web-sys = "0.3.69" -itertools = "0.12.0" # Profile settings for individual crates diff --git a/Makefile b/Makefile index ca56edb30b..f5c323db7f 100644 --- a/Makefile +++ b/Makefile @@ -133,7 +133,7 @@ clippy: sdk-wasm-lint # Build contracts ready for deploy # ----------------------------------------------------------------------------- -CONTRACTS=vesting_contract mixnet_contract +CONTRACTS=vesting_contract mixnet_contract nym_ecash CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS)) CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index c8f3bb0c0d..31aa4ae8ec 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -26,6 +26,7 @@ pub(crate) mod import_credential; pub(crate) mod init; mod list_gateways; pub(crate) mod run; +mod show_ticketbooks; mod switch_gateway; pub(crate) struct CliNativeClient; @@ -84,6 +85,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -116,6 +120,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), diff --git a/clients/native/src/commands/show_ticketbooks.rs b/clients/native/src/commands/show_ticketbooks.rs new file mode 100644 index 0000000000..2518a50ff2 --- /dev/null +++ b/clients/native/src/commands/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliNativeClient; +use crate::error::ClientError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), ClientError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 59ff639e1c..34f0738a12 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -30,6 +30,7 @@ mod import_credential; pub mod init; mod list_gateways; pub(crate) mod run; +mod show_ticketbooks; mod switch_gateway; pub(crate) struct CliSocks5Client; @@ -88,6 +89,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -123,6 +127,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), diff --git a/clients/socks5/src/commands/show_ticketbooks.rs b/clients/socks5/src/commands/show_ticketbooks.rs new file mode 100644 index 0000000000..41a37ef6eb --- /dev/null +++ b/clients/socks5/src/commands/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliSocks5Client; +use crate::error::Socks5ClientError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), Socks5ClientError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/common/bandwidth-controller/Cargo.toml b/common/bandwidth-controller/Cargo.toml index b122849639..e56476afa6 100644 --- a/common/bandwidth-controller/Cargo.toml +++ b/common/bandwidth-controller/Cargo.toml @@ -14,13 +14,14 @@ thiserror = { workspace = true } url = { workspace = true } zeroize = { workspace = true } -nym-coconut = { path = "../nymcoconut" } +nym-ecash-time = { path = "../ecash-time" } nym-credential-storage = { path = "../credential-storage" } nym-credentials = { path = "../credentials" } nym-credentials-interface = { path = "../credentials-interface" } nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] } nym-network-defaults = { path = "../network-defaults" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } +nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies.nym-validator-client] path = "../client-libs/validator-client" diff --git a/common/bandwidth-controller/src/acquire/mod.rs b/common/bandwidth-controller/src/acquire/mod.rs index 4f2431b45b..fadb2581bf 100644 --- a/common/bandwidth-controller/src/acquire/mod.rs +++ b/common/bandwidth-controller/src/acquire/mod.rs @@ -1,87 +1,117 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::error::BandwidthControllerError; -use nym_credential_storage::models::StorableIssuedCredential; +use crate::utils::{get_coin_index_signatures, get_expiration_date_signatures}; +use log::info; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::{CredentialType, IssuanceBandwidthCredential}; -use nym_credentials::coconut::utils::obtain_aggregate_signature; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nyxd::contract_traits::CoconutBandwidthSigningClient; +use nym_credentials::ecash::bandwidth::IssuanceTicketBook; +use nym_credentials::ecash::utils::obtain_aggregate_wallet; +use nym_credentials::IssuedTicketBook; +use nym_crypto::asymmetric::identity; +use nym_ecash_time::{ecash_default_expiration_date, Date}; +use nym_validator_client::coconut::all_ecash_api_clients; +use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; -use nym_validator_client::nyxd::Coin; +use nym_validator_client::nyxd::contract_traits::EcashSigningClient; +use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::EcashApiClient; use rand::rngs::OsRng; -use state::State; -use zeroize::Zeroizing; -pub mod state; - -pub async fn deposit(client: &C, amount: Coin) -> Result +pub async fn make_deposit( + client: &C, + client_id: &[u8], + expiration: Option, +) -> Result where - C: CoconutBandwidthSigningClient + Sync, + C: EcashSigningClient + Sync, { let mut rng = OsRng; let signing_key = identity::PrivateKey::new(&mut rng); - let encryption_key = encryption::PrivateKey::new(&mut rng); + let expiration = expiration.unwrap_or_else(ecash_default_expiration_date); - let tx_hash = client - .deposit( - amount.clone(), - CredentialType::Voucher.to_string(), - signing_key.public_key().to_base58_string(), - encryption_key.public_key().to_base58_string(), - None, - ) - .await? - .transaction_hash; + let result = client + .make_ticketbook_deposit(signing_key.public_key().to_base58_string(), None) + .await?; - let voucher = - IssuanceBandwidthCredential::new_voucher(amount, tx_hash, signing_key, encryption_key); + let deposit_id = result.parse_singleton_u32_contract_data()?; - let state = State { voucher }; + info!("our ticketbook deposit has been stored under id {deposit_id}"); - Ok(state) + Ok(IssuanceTicketBook::new_with_expiration( + deposit_id, + client_id, + signing_key, + expiration, + )) } -pub async fn get_bandwidth_voucher( - state: &State, +pub async fn query_and_persist_required_global_signatures( + storage: &S, + epoch_id: EpochId, + expiration_date: Date, + apis: Vec, +) -> Result<(), BandwidthControllerError> +where + S: Storage, + ::StorageError: Send + Sync + 'static, +{ + log::info!("Getting expiration date signatures"); + // this will also persist the signatures in the storage if they were not there already + get_expiration_date_signatures(storage, epoch_id, expiration_date, apis.clone()).await?; + + log::info!("Getting coin indices signatures"); + // this will also persist the signatures in the storage if they were not there already + get_coin_index_signatures(storage, epoch_id, apis).await?; + Ok(()) +} + +pub async fn get_ticket_book( + issuance_data: &IssuanceTicketBook, client: &C, storage: &St, -) -> Result<(), BandwidthControllerError> + apis: Option>, +) -> Result where C: DkgQueryClient + Send + Sync, St: Storage, ::StorageError: Send + Sync + 'static, { - // temporary - assert!(state.voucher.typ().is_voucher()); - let epoch_id = client.get_current_epoch().await?.epoch_id; let threshold = client .get_current_epoch_threshold() .await? .ok_or(BandwidthControllerError::NoThreshold)?; - let coconut_api_clients = all_coconut_api_clients(client, epoch_id).await?; - - let signature = - obtain_aggregate_signature(&state.voucher, &coconut_api_clients, threshold).await?; - let issued = state.voucher.to_issued_credential(signature, epoch_id); - - // make sure the data gets zeroized after persisting it - let credential_data = Zeroizing::new(issued.pack_v1()); - let storable = StorableIssuedCredential { - serialization_revision: issued.current_serialization_revision(), - credential_data: credential_data.as_ref(), - credential_type: issued.typ().to_string(), - epoch_id: epoch_id - .try_into() - .expect("our epoch is has run over u32::MAX!"), + let apis = match apis { + Some(apis) => apis, + None => all_ecash_api_clients(client, epoch_id).await?, }; + log::info!("Querying wallet signatures"); + let wallet = obtain_aggregate_wallet(issuance_data, &apis, threshold).await?; + info!("managed to obtain sufficient number of partial signatures!"); + + log::info!("Getting expiration date signatures"); + // this will also persist the signatures in the storage if they were not there already + get_expiration_date_signatures( + storage, + epoch_id, + issuance_data.expiration_date(), + apis.clone(), + ) + .await?; + + log::info!("Getting coin indices signatures"); + // this will also persist the signatures in the storage if they were not there already + get_coin_index_signatures(storage, epoch_id, apis).await?; + + let issued = issuance_data.to_issued_ticketbook(wallet, epoch_id); + + info!("persisting the ticketbook into the storage..."); storage - .insert_issued_credential(storable) + .insert_issued_ticketbook(&issued) .await - .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err))) + .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?; + Ok(issued) } diff --git a/common/bandwidth-controller/src/acquire/state.rs b/common/bandwidth-controller/src/acquire/state.rs deleted file mode 100644 index 68945289b7..0000000000 --- a/common/bandwidth-controller/src/acquire/state.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2022-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; - -pub struct State { - pub voucher: IssuanceBandwidthCredential, -} - -impl State { - pub fn new(voucher: IssuanceBandwidthCredential) -> Self { - State { voucher } - } -} diff --git a/common/bandwidth-controller/src/error.rs b/common/bandwidth-controller/src/error.rs index 44832aaab4..01619b53ed 100644 --- a/common/bandwidth-controller/src/error.rs +++ b/common/bandwidth-controller/src/error.rs @@ -1,12 +1,12 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_coconut::CoconutError; use nym_credential_storage::error::StorageError; use nym_credentials::error::Error as CredentialsError; +use nym_credentials_interface::CompactEcashError; use nym_crypto::asymmetric::encryption::KeyRecoveryError; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; -use nym_validator_client::coconut::CoconutApiError; +use nym_validator_client::coconut::EcashApiError; use nym_validator_client::error::ValidatorClientError; use thiserror::Error; @@ -16,7 +16,7 @@ pub enum BandwidthControllerError { Nyxd(#[from] nym_validator_client::nyxd::error::NyxdError), #[error("coconut api query failure: {0}")] - CoconutApiError(#[from] CoconutApiError), + CoconutApiError(#[from] EcashApiError), #[error("There was a credential storage error - {0}")] CredentialStorageError(Box), @@ -28,8 +28,8 @@ pub enum BandwidthControllerError { #[error(transparent)] StorageError(#[from] StorageError), - #[error("Coconut error - {0}")] - CoconutError(#[from] CoconutError), + #[error("Ecash error - {0}")] + EcashError(#[from] CompactEcashError), #[error("Validator client error - {0}")] ValidatorError(#[from] ValidatorClientError), @@ -51,4 +51,15 @@ pub enum BandwidthControllerError { #[error("can't handle recovering storage with revision {stored}. {expected} was expected")] UnsupportedCredentialStorageRevision { stored: u8, expected: u8 }, + + #[error("did not receive a valid response for aggregated data ({typ}) from ANY nym-api")] + ExhaustedApiQueries { typ: String }, +} + +impl BandwidthControllerError { + pub fn credential_storage_error( + source: impl std::error::Error + Send + Sync + 'static, + ) -> Self { + BandwidthControllerError::CredentialStorageError(Box::new(source)) + } } diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index 91201b9a92..4485009b68 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -1,16 +1,24 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + use crate::error::BandwidthControllerError; -use crate::utils::stored_credential_to_issued_bandwidth; -use log::{debug, error, warn}; +use crate::utils::{ + get_aggregate_verification_key, get_coin_index_signatures, get_expiration_date_signatures, + ApiClientsWrapper, +}; +use log::error; +use nym_credential_storage::models::RetrievedTicketbook; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::coconut::bandwidth::CredentialSpendingData; -use nym_credentials::coconut::utils::obtain_aggregate_verification_key; -use nym_credentials::IssuedBandwidthCredential; -use nym_credentials_interface::VerificationKey; -use nym_validator_client::coconut::all_coconut_api_clients; +use nym_credentials::ecash::bandwidth::CredentialSpendingData; +use nym_credentials_interface::{ + AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, NymPayInfo, VerificationKeyAuth, +}; +use nym_ecash_time::Date; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; @@ -35,13 +43,20 @@ pub struct PreparedCredential { /// could use correct verification key for validation. pub epoch_id: EpochId, - /// The database id of the stored credential. - pub credential_id: i64, + /// Auxiliary metadata associated with the withdrawn credential + pub metadata: PreparedCredentialMetadata, } -pub struct RetrievedCredential { - pub credential: IssuedBandwidthCredential, - pub credential_id: i64, +#[derive(Copy, Clone)] +pub struct PreparedCredentialMetadata { + /// The database id of the stored credential. + pub ticketbook_id: i64, + + /// The number of tickets withdrawn in this credential + pub tickets_withdrawn: u32, + + /// The amount of tickets used INCLUDING those tickets that JUST got withdrawn + pub used_tickets: u32, } impl BandwidthController { @@ -50,111 +65,155 @@ impl BandwidthController { } /// Tries to retrieve one of the stored, unused credentials that hasn't yet expired. - /// It marks any retrieved intermediate credentials as expired. - pub async fn get_next_usable_credential( + pub async fn get_next_usable_ticketbook( &self, - gateway_id: &str, - ) -> Result + tickets: u32, + ) -> Result where ::StorageError: Send + Sync + 'static, { - loop { - let Some(maybe_next) = self - .storage - .get_next_unspent_credential(gateway_id) - .await - .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))? - else { - return Err(BandwidthControllerError::NoCredentialsAvailable); - }; - let id = maybe_next.id; + let Some(ticketbook) = self + .storage + .get_next_unspent_usable_ticketbook(tickets) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + else { + return Err(BandwidthControllerError::NoCredentialsAvailable); + }; - // try to deserialize it - let valid_credential = match stored_credential_to_issued_bandwidth(maybe_next) { - // check if it has already expired - Ok(credential) => match credential.variant_data() { - BandwidthCredentialIssuedDataVariant::Voucher(_) => { - debug!("credential {id} is a bandwidth voucher"); - credential - } - BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => { - debug!("credential {id} is a free pass"); - if freepass_info.expired() { - warn!("the free pass (id: {id}) has already expired! The expiration was set to {}", freepass_info.expiry_date()); - self.storage.mark_expired(id).await.map_err(|err| { - BandwidthControllerError::CredentialStorageError(Box::new(err)) - })?; - continue; - } - credential - } - }, - Err(err) => { - error!("failed to deserialize credential with id {id}: {err}. it may need to be manually removed from the storage"); - return Err(err); - } - }; - return Ok(RetrievedCredential { - credential: valid_credential, - credential_id: id, - }); - } + Ok(ticketbook) } - pub fn storage(&self) -> &St { - &self.storage + pub async fn attempt_revert_ticket_usage( + &self, + info: PreparedCredentialMetadata, + ) -> Result + where + ::StorageError: Send + Sync + 'static, + { + self.storage + .attempt_revert_ticketbook_withdrawal( + info.ticketbook_id, + info.used_tickets, + info.tickets_withdrawn, + ) + .await + .map_err(BandwidthControllerError::credential_storage_error) } async fn get_aggregate_verification_key( &self, epoch_id: EpochId, - ) -> Result + apis: &mut ApiClientsWrapper, + ) -> Result where C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let coconut_api_clients = all_coconut_api_clients(&self.client, epoch_id).await?; - Ok(obtain_aggregate_verification_key(&coconut_api_clients)?) + let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; + get_aggregate_verification_key(&self.storage, epoch_id, ecash_apis).await } - pub async fn prepare_bandwidth_credential( + async fn get_coin_index_signatures( &self, - gateway_id: &str, + epoch_id: EpochId, + apis: &mut ApiClientsWrapper, + ) -> Result, BandwidthControllerError> + where + C: DkgQueryClient + Sync + Send, + ::StorageError: Send + Sync + 'static, + { + let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; + get_coin_index_signatures(&self.storage, epoch_id, ecash_apis).await + } + + async fn get_expiration_date_signatures( + &self, + epoch_id: EpochId, + expiration_date: Date, + apis: &mut ApiClientsWrapper, + ) -> Result, BandwidthControllerError> + where + C: DkgQueryClient + Sync + Send, + ::StorageError: Send + Sync + 'static, + { + let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; + get_expiration_date_signatures(&self.storage, epoch_id, expiration_date, ecash_apis).await + } + + async fn prepare_ecash_ticket_inner( + &self, + provider_pk: [u8; 32], + tickets_to_spend: u32, + mut retrieved_ticketbook: RetrievedTicketbook, + ) -> Result + where + C: DkgQueryClient + Sync + Send, + ::StorageError: Send + Sync + 'static, + { + let epoch_id = retrieved_ticketbook.ticketbook.epoch_id(); + let expiration_date = retrieved_ticketbook.ticketbook.expiration_date(); + let mut api_clients = Default::default(); + + let verification_key = self + .get_aggregate_verification_key(epoch_id, &mut api_clients) + .await?; + let expiration_signatures = self + .get_expiration_date_signatures(epoch_id, expiration_date, &mut api_clients) + .await?; + let coin_indices_signatures = self + .get_coin_index_signatures(epoch_id, &mut api_clients) + .await?; + + let pay_info = NymPayInfo::generate(provider_pk); + + let spend_request = retrieved_ticketbook.ticketbook.prepare_for_spending( + &verification_key, + pay_info.into(), + &coin_indices_signatures, + &expiration_signatures, + tickets_to_spend as u64, + )?; + Ok(spend_request) + } + + pub async fn prepare_ecash_ticket( + &self, + provider_pk: [u8; 32], + tickets_to_spend: u32, ) -> Result where C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let retrieved_credential = self.get_next_usable_credential(gateway_id).await?; + let retrieved_ticketbook = self.get_next_usable_ticketbook(tickets_to_spend).await?; - let epoch_id = retrieved_credential.credential.epoch_id(); - let credential_id = retrieved_credential.credential_id; + let ticketbook_id = retrieved_ticketbook.ticketbook_id; + let epoch_id = retrieved_ticketbook.ticketbook.epoch_id(); - let verification_key = self.get_aggregate_verification_key(epoch_id).await?; + let used_tickets = + retrieved_ticketbook.ticketbook.spent_tickets() as u32 + tickets_to_spend; + let metadata = PreparedCredentialMetadata { + ticketbook_id, + tickets_withdrawn: tickets_to_spend, + used_tickets, + }; - let spend_request = retrieved_credential - .credential - .prepare_for_spending(&verification_key)?; - - Ok(PreparedCredential { - data: spend_request, - epoch_id, - credential_id, - }) - } - - pub async fn consume_credential( - &self, - id: i64, - gateway_id: &str, - ) -> Result<(), BandwidthControllerError> - where - ::StorageError: Send + Sync + 'static, - { - self.storage - .consume_coconut_credential(id, gateway_id) + match self + .prepare_ecash_ticket_inner(provider_pk, tickets_to_spend, retrieved_ticketbook) .await - .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err))) + { + Ok(data) => Ok(PreparedCredential { + data, + epoch_id, + metadata, + }), + Err(err) => { + error!("failed to prepare credential spending request. attempting to revert withdrawal..."); + self.attempt_revert_ticket_usage(metadata).await?; + Err(err) + } + } } } diff --git a/common/bandwidth-controller/src/utils.rs b/common/bandwidth-controller/src/utils.rs index 89891dba48..0d2427b2f8 100644 --- a/common/bandwidth-controller/src/utils.rs +++ b/common/bandwidth-controller/src/utils.rs @@ -2,21 +2,180 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::BandwidthControllerError; -use nym_credential_storage::models::StoredIssuedCredential; -use nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; -use nym_credentials::coconut::bandwidth::IssuedBandwidthCredential; +use log::warn; +use nym_credential_storage::storage::Storage; +use nym_credentials_interface::{ + AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, VerificationKeyAuth, +}; +use nym_ecash_time::Date; +use nym_validator_client::coconut::all_ecash_api_clients; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::DkgQueryClient; +use nym_validator_client::EcashApiClient; +use rand::prelude::SliceRandom; +use rand::thread_rng; +use std::fmt::Display; +use std::future::Future; -pub fn stored_credential_to_issued_bandwidth( - cred: StoredIssuedCredential, -) -> Result { - if cred.serialization_revision != CURRENT_SERIALIZATION_REVISION { - return Err( - BandwidthControllerError::UnsupportedCredentialStorageRevision { - stored: cred.serialization_revision, - expected: CURRENT_SERIALIZATION_REVISION, - }, - ); +// it really doesn't need the RwLock because it's never moved across tasks, +// but we need all the Send/Sync action +#[derive(Default)] +pub(crate) struct ApiClientsWrapper(Option>); + +impl ApiClientsWrapper { + pub(crate) async fn get_or_init( + &mut self, + epoch_id: EpochId, + dkg_client: &C, + ) -> Result, BandwidthControllerError> + where + C: DkgQueryClient + Sync + Send, + { + if let Some(cached) = &self.0 { + return Ok(cached.clone()); + } + + let clients = all_ecash_api_clients(dkg_client, epoch_id).await?; + + // technically we don't have to be cloning all the clients here, but it's way simpler than + // dealing with locking and whatnot given the performance penalty is negligible + self.0 = Some(clients.clone()); + Ok(clients) } - - Ok(IssuedBandwidthCredential::unpack_v1(&cred.credential_data)?) +} + +pub(crate) async fn query_random_apis_until_success( + mut apis: Vec, + f: F, + typ: impl Into, +) -> Result +where + F: Fn(EcashApiClient) -> U, + U: Future>, + E: Display, +{ + // try apis in pseudorandom way to remove any bias towards the first registered dealer + apis.shuffle(&mut thread_rng()); + + for api in apis { + let disp = api.to_string(); + match f(api).await { + Ok(res) => return Ok(res), + Err(err) => { + warn!("failed to obtain valid response from API {disp}: {err}") + } + } + } + Err(BandwidthControllerError::ExhaustedApiQueries { typ: typ.into() }) +} + +pub(crate) async fn get_aggregate_verification_key( + storage: &St, + epoch_id: EpochId, + ecash_apis: Vec, +) -> Result +where + St: Storage, + ::StorageError: Send + Sync + 'static, +{ + if let Some(stored) = storage + .get_master_verification_key(epoch_id) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + { + return Ok(stored); + }; + + let master_vk = query_random_apis_until_success( + ecash_apis, + |api| async move { api.api_client.master_verification_key(Some(epoch_id)).await }, + format!("aggregated verification key for epoch {epoch_id}"), + ) + .await? + .key; + + // store the retrieved key + storage + .insert_master_verification_key(epoch_id, &master_vk) + .await + .map_err(BandwidthControllerError::credential_storage_error)?; + + Ok(master_vk) +} + +pub(crate) async fn get_coin_index_signatures( + storage: &St, + epoch_id: EpochId, + ecash_apis: Vec, +) -> Result, BandwidthControllerError> +where + St: Storage, + ::StorageError: Send + Sync + 'static, +{ + if let Some(stored) = storage + .get_coin_index_signatures(epoch_id) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + { + return Ok(stored); + }; + + let index_sigs = query_random_apis_until_success( + ecash_apis, + |api| async move { + api.api_client + .global_coin_indices_signatures(Some(epoch_id)) + .await + }, + format!("aggregated coin index signatures for epoch {epoch_id}"), + ) + .await? + .signatures; + + // store the retrieved key + storage + .insert_coin_index_signatures(epoch_id, &index_sigs) + .await + .map_err(BandwidthControllerError::credential_storage_error)?; + + Ok(index_sigs) +} + +pub(crate) async fn get_expiration_date_signatures( + storage: &St, + epoch_id: EpochId, + expiration_date: Date, + ecash_apis: Vec, +) -> Result, BandwidthControllerError> +where + St: Storage, + ::StorageError: Send + Sync + 'static, +{ + if let Some(stored) = storage + .get_expiration_date_signatures(expiration_date) + .await + .map_err(BandwidthControllerError::credential_storage_error)? + { + return Ok(stored); + }; + + let expiration_sigs = query_random_apis_until_success( + ecash_apis, + |api| async move { + api.api_client + .global_expiration_date_signatures(Some(expiration_date)) + .await + }, + format!("aggregated coin index signatures for date {expiration_date}"), + ) + .await? + .signatures; + + // store the retrieved key + storage + .insert_expiration_date_signatures(epoch_id, expiration_date, &expiration_sigs) + .await + .map_err(BandwidthControllerError::credential_storage_error)?; + + Ok(expiration_sigs) } diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index 1d7c34d51c..92250213c8 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -14,6 +14,7 @@ base64 = { workspace = true } bs58 = { workspace = true } cfg-if = { workspace = true } clap = { workspace = true, optional = true } +comfy-table = { version = "7.1.1", optional = true } futures = { workspace = true } humantime-serde = { workspace = true } log = { workspace = true } @@ -50,6 +51,7 @@ nym-network-defaults = { path = "../network-defaults" } nym-client-core-config-types = { path = "./config-types", features = ["disk-persistence"] } nym-client-core-surb-storage = { path = "./surb-storage" } nym-client-core-gateways-storage = { path = "./gateways-storage" } +nym-ecash-time = { path = "../ecash-time" } ### For serving prometheus metrics [target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper] @@ -112,7 +114,7 @@ tempfile = { workspace = true } [features] default = [] -cli = ["clap"] +cli = ["clap", "comfy-table"] fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"] fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"] wasm = ["nym-gateway-client/wasm"] diff --git a/common/client-core/src/cli_helpers/client_show_ticketbooks.rs b/common/client-core/src/cli_helpers/client_show_ticketbooks.rs new file mode 100644 index 0000000000..9c6e2e71ef --- /dev/null +++ b/common/client-core/src/cli_helpers/client_show_ticketbooks.rs @@ -0,0 +1,127 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli_helpers::{CliClient, CliClientConfig}; +use crate::error::ClientCoreError; +use nym_credential_storage::models::BasicTicketbookInformation; +use nym_credential_storage::storage::Storage; +use nym_ecash_time::ecash_today; +use nym_network_defaults::TICKET_BANDWIDTH_VALUE; +use serde::{Deserialize, Serialize}; +use time::Date; + +#[derive(Serialize, Deserialize)] +pub struct AvailableTicketbook { + pub id: i64, + pub expiration: Date, + pub issued_tickets: u32, + pub claimed_tickets: u32, + pub ticket_size: u64, +} + +impl AvailableTicketbook { + #[cfg(feature = "cli")] + fn table_row(&self) -> comfy_table::Row { + let ecash_today = ecash_today().date(); + + let issued = self.issued_tickets; + let si_issued = si_scale::helpers::bibytes2((issued as u64 * self.ticket_size) as f64); + + let claimed = self.claimed_tickets; + let si_claimed = si_scale::helpers::bibytes2((claimed as u64 * self.ticket_size) as f64); + + let remaining = issued - claimed; + let si_remaining = + si_scale::helpers::bibytes2((remaining as u64 * self.ticket_size) as f64); + let si_size = si_scale::helpers::bibytes2(self.ticket_size as f64); + + let expiration = if self.expiration <= ecash_today { + comfy_table::Cell::new(format!("EXPIRED ON {}", self.expiration)) + .fg(comfy_table::Color::Red) + .add_attribute(comfy_table::Attribute::Bold) + } else { + comfy_table::Cell::new(self.expiration.to_string()) + }; + + vec![ + comfy_table::Cell::new(self.id.to_string()), + expiration, + comfy_table::Cell::new(format!("{issued} ({si_issued})")), + comfy_table::Cell::new(format!("{claimed} ({si_claimed})")), + comfy_table::Cell::new(format!("{remaining} ({si_remaining})")), + comfy_table::Cell::new(si_size), + ] + .into() + } +} + +impl From for AvailableTicketbook { + fn from(value: BasicTicketbookInformation) -> Self { + AvailableTicketbook { + id: value.id, + expiration: value.expiration_date, + issued_tickets: value.total_tickets, + claimed_tickets: value.used_tickets, + ticket_size: TICKET_BANDWIDTH_VALUE, + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct AvailableTicketbooks(Vec); + +#[cfg(feature = "cli")] +impl std::fmt::Display for AvailableTicketbooks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut table = comfy_table::Table::new(); + table.set_header(vec![ + "id", + "expiration", + "issued tickets (bandwidth)", + "claimed tickets (bandwidth)", + "remaining tickets (bandwidth)", + "ticket size", + ]); + + for ticketbook in &self.0 { + table.add_row(ticketbook.table_row()); + } + + writeln!(f, "{table}")?; + Ok(()) + } +} + +#[cfg_attr(feature = "cli", derive(clap::Args))] +#[derive(Debug, Clone)] +pub struct CommonShowTicketbooksArgs { + /// Id of client that is going to display the ticketbook information + #[cfg_attr(feature = "cli", clap(long))] + pub id: String, +} + +pub async fn show_ticketbooks(args: A) -> Result +where + A: AsRef, + C: CliClient, +{ + let common_args = args.as_ref(); + let id = &common_args.id; + + let config = C::try_load_current_config(id).await?; + let paths = config.common_paths(); + + let credentials_store = + nym_credential_storage::initialise_persistent_storage(&paths.credentials_database).await; + let ticketbooks = credentials_store + .get_ticketbooks_info() + .await + .map_err(|err| ClientCoreError::CredentialStoreError { + source: Box::new(err), + })?; + + Ok(AvailableTicketbooks( + ticketbooks.into_iter().map(Into::into).collect(), + )) +} diff --git a/common/client-core/src/cli_helpers/mod.rs b/common/client-core/src/cli_helpers/mod.rs index 5ab51b8b4c..a3660a60e6 100644 --- a/common/client-core/src/cli_helpers/mod.rs +++ b/common/client-core/src/cli_helpers/mod.rs @@ -6,6 +6,7 @@ pub mod client_import_credential; pub mod client_init; pub mod client_list_gateways; pub mod client_run; +pub mod client_show_ticketbooks; pub mod client_switch_gateway; pub mod traits; mod types; diff --git a/common/client-core/src/client/mix_traffic/transceiver.rs b/common/client-core/src/client/mix_traffic/transceiver.rs index b6197fb490..0862911d93 100644 --- a/common/client-core/src/client/mix_traffic/transceiver.rs +++ b/common/client-core/src/client/mix_traffic/transceiver.rs @@ -3,10 +3,12 @@ use async_trait::async_trait; use log::{debug, error}; +use nym_credential_storage::storage::Storage as CredentialStorage; use nym_crypto::asymmetric::identity; use nym_gateway_client::GatewayClient; pub use nym_gateway_client::{GatewayPacketRouter, PacketRouter}; use nym_sphinx::forwarding::packet::MixPacket; +use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use std::fmt::Debug; use std::os::raw::c_int as RawFd; use thiserror::Error; @@ -111,8 +113,9 @@ impl RemoteGateway { impl GatewayTransceiver for RemoteGateway where - C: Send, - St: Send, + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, { fn gateway_identity(&self) -> identity::PublicKey { self.gateway_client.gateway_identity() @@ -126,8 +129,9 @@ where #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl GatewaySender for RemoteGateway where - C: Send, - St: Send, + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, { async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> { self.gateway_client diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index bdfecfda5e..bcaca3763c 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -63,6 +63,11 @@ pub enum ClientCoreError { source: Box, }, + #[error("experienced a failure with our credentials storage: {source}")] + CredentialStoreError { + source: Box, + }, + #[error("the gateway id is invalid - {0}")] UnableToCreatePublicKeyFromGatewayId(Ed25519RecoveryError), diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index 91c320bb46..4ba1341bb9 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -23,12 +23,12 @@ use nym_gateway_requests::{ BinaryRequest, ClientControlRequest, ServerResponse, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, }; -use nym_network_defaults::{REMAINING_BANDWIDTH_THRESHOLD, TOKENS_TO_BURN}; +use nym_network_defaults::REMAINING_BANDWIDTH_THRESHOLD; use nym_sphinx::forwarding::packet::MixPacket; use nym_task::TaskClient; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use rand::rngs::OsRng; - +use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::Arc; use std::time::Duration; use tungstenite::protocol::Message; @@ -83,7 +83,7 @@ impl GatewayConfig { pub struct GatewayClient { authenticated: bool, disabled_credentials_mode: bool, - bandwidth_remaining: i64, + bandwidth_remaining: Arc, gateway_address: String, gateway_identity: identity::PublicKey, local_identity: Arc, @@ -122,7 +122,7 @@ impl GatewayClient { GatewayClient { authenticated: false, disabled_credentials_mode: true, - bandwidth_remaining: 0, + bandwidth_remaining: Arc::new(AtomicI64::new(0)), gateway_address: config.gateway_listener, gateway_identity: config.gateway_identity, local_identity, @@ -182,7 +182,7 @@ impl GatewayClient { } pub fn remaining_bandwidth(&self) -> i64 { - self.bandwidth_remaining + self.bandwidth_remaining.load(Ordering::Acquire) } #[cfg(not(target_arch = "wasm32"))] @@ -259,7 +259,7 @@ impl GatewayClient { self.authenticated = false; for i in 1..self.reconnection_attempts { - info!("attempt {}...", i); + info!("reconnection attempt {}...", i); if self.try_reconnect().await.is_ok() { info!("managed to reconnect!"); return Ok(()); @@ -269,7 +269,7 @@ impl GatewayClient { } // final attempt (done separately to be able to return a proper error) - info!("attempt {}", self.reconnection_attempts); + info!("reconnection attempt {}", self.reconnection_attempts); match self.try_reconnect().await { Ok(_) => { info!("managed to reconnect!"); @@ -537,7 +537,8 @@ impl GatewayClient { } => { self.check_gateway_protocol(protocol_version)?; self.authenticated = status; - self.bandwidth_remaining = bandwidth_remaining; + self.bandwidth_remaining + .store(bandwidth_remaining, Ordering::Release); self.negotiated_protocol = protocol_version; log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}"); self.task_client.send_status_msg(Box::new( @@ -576,44 +577,54 @@ impl GatewayClient { } } - async fn claim_coconut_bandwidth( + async fn claim_ecash_bandwidth( &mut self, credential: CredentialSpendingData, ) -> Result<(), GatewayClientError> { let mut rng = OsRng; let iv = IV::new_random(&mut rng); - let msg = ClientControlRequest::new_enc_coconut_bandwidth_credential_v2( + let msg = ClientControlRequest::new_enc_ecash_credential( credential, self.shared_key.as_ref().unwrap(), iv, ) .into(); - self.bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self.send_websocket_message(msg).await? { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), _ => Err(GatewayClientError::UnexpectedResponse), }?; + self.bandwidth_remaining + .store(bandwidth_remaining, Ordering::Relaxed); Ok(()) } async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> { let msg = ClientControlRequest::ClaimFreeTestnetBandwidth.into(); - self.bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self.send_websocket_message(msg).await? { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), _ => Err(GatewayClientError::UnexpectedResponse), }?; - + self.bandwidth_remaining + .store(bandwidth_remaining, Ordering::Release); Ok(()) } + fn unchecked_bandwidth_controller(&self) -> &BandwidthController { + self.bandwidth_controller.as_ref().unwrap() + } + pub async fn claim_bandwidth(&mut self) -> Result<(), GatewayClientError> where C: DkgQueryClient + Send + Sync, St: CredentialStorage, ::StorageError: Send + Sync + 'static, { + // TODO: make it configurable + const TICKETS_TO_SPEND: u32 = 1; + if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } @@ -641,49 +652,42 @@ impl GatewayClient { negotiated_protocol: Some(gateway_protocol), }); } - - let gateway_id = self.gateway_identity().to_base58_string(); - let prepared_credential = self - .bandwidth_controller - .as_ref() - .unwrap() - .prepare_bandwidth_credential(&gateway_id) + .unchecked_bandwidth_controller() + .prepare_ecash_ticket(self.gateway_identity.to_bytes(), TICKETS_TO_SPEND) .await?; - self.claim_coconut_bandwidth(prepared_credential.data) - .await?; - self.bandwidth_controller - .as_ref() - .unwrap() - .consume_credential(prepared_credential.credential_id, &gateway_id) - .await?; - - Ok(()) - } - - fn estimate_required_bandwidth(&self, packets: &[MixPacket]) -> i64 { - packets - .iter() - .map(|packet| packet.packet().len()) - .sum::() as i64 + match self.claim_ecash_bandwidth(prepared_credential.data).await { + Ok(_) => Ok(()), + Err(err) => { + error!("failed to claim ecash bandwidth with the gateway... attempting to revert storage withdrawal"); + self.unchecked_bandwidth_controller() + .attempt_revert_ticket_usage(prepared_credential.metadata) + .await?; + Err(err) + } + } } pub async fn batch_send_mix_packets( &mut self, packets: Vec, - ) -> Result<(), GatewayClientError> { + ) -> Result<(), GatewayClientError> + where + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, + { debug!("Sending {} mix packets", packets.len()); if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } - if self.estimate_required_bandwidth(&packets) > self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth( - self.estimate_required_bandwidth(&packets), - self.bandwidth_remaining, - )); + let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire); + if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD { + self.claim_bandwidth().await?; } + if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } @@ -742,19 +746,20 @@ impl GatewayClient { } // TODO: possibly make responses optional - pub async fn send_mix_packet( - &mut self, - mix_packet: MixPacket, - ) -> Result<(), GatewayClientError> { + pub async fn send_mix_packet(&mut self, mix_packet: MixPacket) -> Result<(), GatewayClientError> + where + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, + { if !self.authenticated { return Err(GatewayClientError::NotAuthenticated); } - if (mix_packet.packet().len() as i64) > self.bandwidth_remaining { - return Err(GatewayClientError::NotEnoughBandwidth( - mix_packet.packet().len() as i64, - self.bandwidth_remaining, - )); + let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire); + if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD { + self.claim_bandwidth().await?; } + if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } @@ -808,6 +813,7 @@ impl GatewayClient { .as_ref() .expect("no shared key present even though we're authenticated!"), ), + self.bandwidth_remaining.clone(), self.task_client.clone(), ) } @@ -848,10 +854,9 @@ impl GatewayClient { self.establish_connection().await?; } let shared_key = self.perform_initial_authentication().await?; - - if self.bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD { - info!("Claiming more bandwidth for your tokens. This will use {} token(s) from your wallet. \ - Stop the process now if you don't want that to happen.", TOKENS_TO_BURN); + let bandwidth_remaining = self.bandwidth_remaining.load(Ordering::Acquire); + if bandwidth_remaining < REMAINING_BANDWIDTH_THRESHOLD { + info!("Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen."); self.claim_bandwidth().await?; } @@ -888,7 +893,7 @@ impl GatewayClient { GatewayClient { authenticated: false, disabled_credentials_mode: true, - bandwidth_remaining: 0, + bandwidth_remaining: Arc::new(AtomicI64::new(0)), gateway_address: gateway_listener.to_string(), gateway_identity, local_identity, diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 386ad5b147..128f6919e6 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -79,6 +79,7 @@ impl PartiallyDelegated { fn recover_received_plaintexts( ws_msgs: Vec, shared_key: &SharedKeys, + bandwidth_remaining: Arc, ) -> Result>, GatewayClientError> { let mut plaintexts = Vec::with_capacity(ws_msgs.len()); for ws_msg in ws_msgs { @@ -104,7 +105,11 @@ impl PartiallyDelegated { { ServerResponse::Send { remaining_bandwidth, - } => maybe_log_bandwidth(remaining_bandwidth), + } => { + maybe_log_bandwidth(remaining_bandwidth); + bandwidth_remaining + .store(remaining_bandwidth, std::sync::atomic::Ordering::Release) + } ServerResponse::Error { message } => { error!("gateway failure: {message}"); return Err(GatewayClientError::GatewayError(message)); @@ -129,8 +134,10 @@ impl PartiallyDelegated { ws_msgs: Vec, packet_router: &PacketRouter, shared_key: &SharedKeys, + bandwidth_remaining: Arc, ) -> Result<(), GatewayClientError> { - let plaintexts = Self::recover_received_plaintexts(ws_msgs, shared_key)?; + let plaintexts = + Self::recover_received_plaintexts(ws_msgs, shared_key, bandwidth_remaining)?; packet_router.route_received(plaintexts) } @@ -138,6 +145,7 @@ impl PartiallyDelegated { conn: WsConn, mut packet_router: PacketRouter, shared_key: Arc, + bandwidth_remaining: Arc, mut shutdown: TaskClient, ) -> Self { // when called for, it NEEDS TO yield back the stream so that we could merge it and @@ -169,7 +177,7 @@ impl PartiallyDelegated { Ok(msgs) => msgs }; - if let Err(err) = Self::route_socket_messages(ws_msgs, &packet_router, shared_key.as_ref()) { + if let Err(err) = Self::route_socket_messages(ws_msgs, &packet_router, shared_key.as_ref(), bandwidth_remaining.clone()) { log::error!("Route socket messages failed: {err}"); break Err(err) } diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 32f371eb23..a9a348a28e 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -17,6 +17,7 @@ nym-contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common nym-mixnet-contract-common = { path = "../../cosmwasm-smart-contracts/mixnet-contract" } nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-contract" } nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" } +nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" } nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" } serde = { workspace = true, features = ["derive"] } @@ -26,9 +27,10 @@ thiserror = { workspace = true } log = { workspace = true } url = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["sync", "time"] } +time = { workspace = true, features = ["formatting"] } futures = { workspace = true } -nym-coconut = { path = "../../nymcoconut" } +nym-compact-ecash = { path = "../../nym_offline_compact_ecash" } nym-network-defaults = { path = "../../network-defaults" } nym-api-requests = { path = "../../../nym-api/nym-api-requests" } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index b7b1e62c23..32cfd9ae2f 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -8,10 +8,14 @@ use crate::{ nym_api, DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ReqwestRpcClient, ValidatorClientError, }; -use nym_api_requests::coconut::models::FreePassNonceResponse; -use nym_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, FreePassRequest, VerifyCredentialBody, - VerifyCredentialResponse, +use nym_api_requests::ecash::models::{ + AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, + BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse, + SpentCredentialsResponse, VerifyEcashTicketBody, +}; +use nym_api_requests::ecash::{ + BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, + PartialExpirationDateSignatureResponse, VerificationKeyResponse, }; use nym_api_requests::models::{DescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::models::{ @@ -19,8 +23,10 @@ use nym_api_requests::models::{ RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::nym_nodes::SkimmedNode; +use nym_coconut_dkg_common::types::EpochId; use nym_http_api_client::UserAgent; use nym_network_defaults::NymNetworkDetails; +use time::Date; use url::Url; pub use crate::nym_api::NymApiClientExt; @@ -29,7 +35,7 @@ pub use nym_mixnet_contract_common::{ }; // re-export the type to not break existing imports -pub use crate::coconut::CoconutApiClient; +pub use crate::coconut::EcashApiClient; #[cfg(feature = "http-client")] use crate::rpc::http_client; @@ -375,24 +381,73 @@ impl NymApiClient { Ok(self.nym_api.blind_sign(request_body).await?) } - pub async fn verify_bandwidth_credential( + pub async fn verify_ecash_ticket( &self, - request_body: &VerifyCredentialBody, - ) -> Result { + request_body: &VerifyEcashTicketBody, + ) -> Result { + Ok(self.nym_api.verify_ecash_ticket(request_body).await?) + } + + pub async fn batch_redeem_ecash_tickets( + &self, + request_body: &BatchRedeemTicketsBody, + ) -> Result { Ok(self .nym_api - .verify_bandwidth_credential(request_body) + .batch_redeem_ecash_tickets(request_body) .await?) } - pub async fn free_pass_nonce(&self) -> Result { - Ok(self.nym_api.free_pass_nonce().await?) + pub async fn spent_credentials_filter( + &self, + ) -> Result { + Ok(self.nym_api.double_spending_filter_v1().await?) } - pub async fn issue_free_pass_credential( + pub async fn partial_expiration_date_signatures( &self, - request: &FreePassRequest, - ) -> Result { - Ok(self.nym_api.free_pass(request).await?) + expiration_date: Option, + ) -> Result { + Ok(self + .nym_api + .partial_expiration_date_signatures(expiration_date) + .await?) + } + + pub async fn partial_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + Ok(self + .nym_api + .partial_coin_indices_signatures(epoch_id) + .await?) + } + + pub async fn global_expiration_date_signatures( + &self, + expiration_date: Option, + ) -> Result { + Ok(self + .nym_api + .global_expiration_date_signatures(expiration_date) + .await?) + } + + pub async fn global_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + Ok(self + .nym_api + .global_coin_indices_signatures(epoch_id) + .await?) + } + + pub async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result { + Ok(self.nym_api.master_verification_key(epoch_id).await?) } } diff --git a/common/client-libs/validator-client/src/coconut/mod.rs b/common/client-libs/validator-client/src/coconut/mod.rs index 66f5d0e05a..09ce560b1c 100644 --- a/common/client-libs/validator-client/src/coconut/mod.rs +++ b/common/client-libs/validator-client/src/coconut/mod.rs @@ -4,26 +4,40 @@ use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; use crate::nyxd::error::NyxdError; use crate::NymApiClient; -use nym_coconut::{Base58, CoconutError, VerificationKey}; use nym_coconut_dkg_common::types::{EpochId, NodeIndex}; use nym_coconut_dkg_common::verification_key::ContractVKShare; +use nym_compact_ecash::error::CompactEcashError; +use nym_compact_ecash::{Base58, VerificationKeyAuth}; +use std::fmt::{Display, Formatter}; use thiserror::Error; use url::Url; // TODO: it really doesn't feel like this should live in this crate. #[derive(Clone)] -pub struct CoconutApiClient { +pub struct EcashApiClient { pub api_client: NymApiClient, - pub verification_key: VerificationKey, + pub verification_key: VerificationKeyAuth, pub node_id: NodeIndex, pub cosmos_address: cosmrs::AccountId, } +impl Display for EcashApiClient { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[id: {}] {} @ {}", + self.node_id, + self.cosmos_address, + self.api_client.api_url() + ) + } +} + // TODO: this should be using the coconut error // (which is in different crate; perhaps this client should be moved there?) #[derive(Debug, Error)] -pub enum CoconutApiError { +pub enum EcashApiError { // TODO: ask @BN whether this is a correct error message #[error("the provided key share hasn't been verified")] UnverifiedShare, @@ -43,7 +57,7 @@ pub enum CoconutApiError { #[error("the provided verification key is malformed: {source}")] MalformedVerificationKey { #[from] - source: CoconutError, + source: CompactEcashError, }, #[error("the provided account address is malformed: {source}")] @@ -53,29 +67,29 @@ pub enum CoconutApiError { }, } -impl TryFrom for CoconutApiClient { - type Error = CoconutApiError; +impl TryFrom for EcashApiClient { + type Error = EcashApiError; fn try_from(share: ContractVKShare) -> Result { if !share.verified { - return Err(CoconutApiError::UnverifiedShare); + return Err(EcashApiError::UnverifiedShare); } let url_address = Url::parse(&share.announce_address)?; - Ok(CoconutApiClient { + Ok(EcashApiClient { api_client: NymApiClient::new(url_address), - verification_key: VerificationKey::try_from_bs58(&share.share)?, + verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?, node_id: share.node_index, cosmos_address: share.owner.as_str().parse()?, }) } } -pub async fn all_coconut_api_clients( +pub async fn all_ecash_api_clients( client: &C, epoch_id: EpochId, -) -> Result, CoconutApiError> +) -> Result, EcashApiError> where C: DkgQueryClient + Sync + Send, { diff --git a/common/client-libs/validator-client/src/error.rs b/common/client-libs/validator-client/src/error.rs index d439a86848..11bdb3d745 100644 --- a/common/client-libs/validator-client/src/error.rs +++ b/common/client-libs/validator-client/src/error.rs @@ -7,7 +7,7 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum ValidatorClientError { - #[error("nym api request failed - {source}")] + #[error("nym api request failed: {source}")] NymAPIError { #[from] source: nym_api::error::NymAPIError, @@ -19,7 +19,7 @@ pub enum ValidatorClientError { #[error("One of the provided URLs was malformed - {0}")] MalformedUrlProvided(#[from] url::ParseError), - #[error("nyxd request failed - {0}")] + #[error("nyxd request failed: {0}")] NyxdError(#[from] crate::nyxd::error::NyxdError), #[error("No validator API url has been provided")] diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index 5af96ffe23..725b86450b 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -15,7 +15,7 @@ pub use crate::error::ValidatorClientError; pub use crate::rpc::reqwest::ReqwestRpcClient; pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet; pub use client::NymApiClient; -pub use client::{Client, CoconutApiClient, Config}; +pub use client::{Client, Config, EcashApiClient}; pub use nym_api_requests::*; pub use nym_http_api_client::UserAgent; diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 8c2f46d1d1..b9b3d91cd2 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -2,16 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nym_api::error::NymAPIError; -use crate::nym_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; +use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG}; use async_trait::async_trait; +use nym_api_requests::ecash::models::{ + AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, + BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse, + VerifyEcashTicketBody, +}; +use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; +use nym_http_api_client::{ApiClient, NO_PARAMS}; +use nym_mixnet_contract_common::mixnode::MixNodeDetails; +use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; +use time::format_description::BorrowedFormatItem; +use time::Date; + +pub mod error; +pub mod routes; + +use nym_api_requests::ecash::VerificationKeyResponse; pub use nym_api_requests::{ - coconut::{ + ecash::{ models::{ EpochCredentialsResponse, IssuedCredential, IssuedCredentialBody, - IssuedCredentialResponse, IssuedCredentialsResponse, + IssuedCredentialResponse, IssuedCredentialsResponse, SpentCredentialsResponse, }, BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, - VerifyCredentialBody, VerifyCredentialResponse, + PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, + VerifyEcashCredentialBody, }, models::{ ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse, @@ -22,18 +39,12 @@ pub use nym_api_requests::{ }, }; pub use nym_coconut_dkg_common::types::EpochId; -use nym_http_api_client::{ApiClient, NO_PARAMS}; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; - -pub mod error; -pub mod routes; - -use nym_api_requests::coconut::models::FreePassNonceResponse; -use nym_api_requests::coconut::FreePassRequest; -use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; pub use nym_http_api_client::Client; +pub fn rfc_3339_date() -> Vec> { + time::format_description::parse("[year]-[month]-[day]").unwrap() +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NymApiClientExt: ApiClient { @@ -420,36 +431,6 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn free_pass_nonce(&self) -> Result { - self.get_json( - &[ - routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_FREE_PASS_NONCE, - ], - NO_PARAMS, - ) - .await - } - - async fn free_pass( - &self, - request: &FreePassRequest, - ) -> Result { - self.post_json( - &[ - routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_FREE_PASS, - ], - NO_PARAMS, - request, - ) - .await - } - async fn blind_sign( &self, request_body: &BlindSignRequestBody, @@ -457,9 +438,8 @@ pub trait NymApiClientExt: ApiClient { self.post_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_BLIND_SIGN, + routes::ECASH_ROUTES, + routes::ECASH_BLIND_SIGN, ], NO_PARAMS, request_body, @@ -467,16 +447,15 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn verify_bandwidth_credential( + async fn verify_ecash_ticket( &self, - request_body: &VerifyCredentialBody, - ) -> Result { + request_body: &VerifyEcashTicketBody, + ) -> Result { self.post_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_VERIFY_BANDWIDTH_CREDENTIAL, + routes::ECASH_ROUTES, + routes::VERIFY_ECASH_TICKET, ], NO_PARAMS, request_body, @@ -484,6 +463,139 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn batch_redeem_ecash_tickets( + &self, + request_body: &BatchRedeemTicketsBody, + ) -> Result { + self.post_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::BATCH_REDEEM_ECASH_TICKETS, + ], + NO_PARAMS, + request_body, + ) + .await + } + + async fn double_spending_filter_v1(&self) -> Result { + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::DOUBLE_SPENDING_FILTER_V1, + ], + NO_PARAMS, + ) + .await + } + + async fn partial_expiration_date_signatures( + &self, + expiration_date: Option, + ) -> Result { + let params = match expiration_date { + None => Vec::new(), + Some(exp) => vec![( + ecash::EXPIRATION_DATE_PARAM, + exp.format(&rfc_3339_date()).unwrap(), + )], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::PARTIAL_EXPIRATION_DATE_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn partial_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + let params = match epoch_id { + None => Vec::new(), + Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::PARTIAL_COIN_INDICES_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn global_expiration_date_signatures( + &self, + expiration_date: Option, + ) -> Result { + let params = match expiration_date { + None => Vec::new(), + Some(exp) => vec![( + ecash::EXPIRATION_DATE_PARAM, + exp.format(&rfc_3339_date()).unwrap(), + )], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::GLOBAL_EXPIRATION_DATE_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn global_coin_indices_signatures( + &self, + epoch_id: Option, + ) -> Result { + let params = match epoch_id { + None => Vec::new(), + Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], + }; + + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::GLOBAL_COIN_INDICES_SIGNATURES, + ], + ¶ms, + ) + .await + } + + async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result { + let params = match epoch_id { + None => Vec::new(), + Some(epoch_id) => vec![(ecash::EPOCH_ID_PARAM, epoch_id.to_string())], + }; + self.get_json( + &[ + routes::API_VERSION, + routes::ECASH_ROUTES, + routes::ecash::MASTER_VERIFICATION_KEY, + ], + ¶ms, + ) + .await + } + async fn epoch_credentials( &self, dkg_epoch: EpochId, @@ -491,9 +603,8 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_EPOCH_CREDENTIALS, + routes::ECASH_ROUTES, + routes::ECASH_EPOCH_CREDENTIALS, &dkg_epoch.to_string(), ], NO_PARAMS, @@ -508,9 +619,8 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_ISSUED_CREDENTIAL, + routes::ECASH_ROUTES, + routes::ECASH_ISSUED_CREDENTIAL, &credential_id.to_string(), ], NO_PARAMS, @@ -525,9 +635,8 @@ pub trait NymApiClientExt: ApiClient { self.post_json( &[ routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_ISSUED_CREDENTIALS, + routes::ECASH_ROUTES, + routes::ECASH_ISSUED_CREDENTIALS, ], NO_PARAMS, &CredentialsRequestBody { diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index cc2a17d018..c6a50db513 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -12,16 +12,27 @@ pub const DETAILED: &str = "detailed"; pub const DETAILED_UNFILTERED: &str = "detailed-unfiltered"; pub const ACTIVE: &str = "active"; pub const REWARDED: &str = "rewarded"; -pub const COCONUT_ROUTES: &str = "coconut"; -pub const BANDWIDTH: &str = "bandwidth"; +pub const DOUBLE_SPENDING_FILTER_V1: &str = "double-spending-filter-v1"; -pub const COCONUT_FREE_PASS: &str = "free-pass"; -pub const COCONUT_FREE_PASS_NONCE: &str = "free-pass-nonce"; -pub const COCONUT_BLIND_SIGN: &str = "blind-sign"; -pub const COCONUT_VERIFY_BANDWIDTH_CREDENTIAL: &str = "verify-bandwidth-credential"; -pub const COCONUT_EPOCH_CREDENTIALS: &str = "epoch-credentials"; -pub const COCONUT_ISSUED_CREDENTIAL: &str = "issued-credential"; -pub const COCONUT_ISSUED_CREDENTIALS: &str = "issued-credentials"; +pub const ECASH_ROUTES: &str = "ecash"; + +pub use ecash::*; +pub mod ecash { + pub const ECASH_BLIND_SIGN: &str = "blind-sign"; + pub const VERIFY_ECASH_TICKET: &str = "verify-ecash-ticket"; + pub const BATCH_REDEEM_ECASH_TICKETS: &str = "batch-redeem-ecash-tickets"; + pub const PARTIAL_EXPIRATION_DATE_SIGNATURES: &str = "partial-expiration-date-signatures"; + pub const GLOBAL_EXPIRATION_DATE_SIGNATURES: &str = "aggregated-expiration-date-signatures"; + pub const PARTIAL_COIN_INDICES_SIGNATURES: &str = "partial-coin-indices-signatures"; + pub const GLOBAL_COIN_INDICES_SIGNATURES: &str = "aggregated-coin-indices-signatures"; + pub const MASTER_VERIFICATION_KEY: &str = "master-verification-key"; + pub const ECASH_EPOCH_CREDENTIALS: &str = "epoch-credentials"; + pub const ECASH_ISSUED_CREDENTIAL: &str = "issued-credential"; + pub const ECASH_ISSUED_CREDENTIALS: &str = "issued-credentials"; + + pub const EXPIRATION_DATE_PARAM: &str = "expiration_date"; + pub const EPOCH_ID_PARAM: &str = "epoch_id"; +} pub const STATUS_ROUTES: &str = "status"; pub const MIXNODE: &str = "mixnode"; diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_query_client.rs deleted file mode 100644 index 1737edf613..0000000000 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_query_client.rs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::collect_paged; -use crate::nyxd::contract_traits::NymContractsProvider; -use crate::nyxd::error::NyxdError; -use crate::nyxd::CosmWasmClient; -use async_trait::async_trait; -use nym_coconut_bandwidth_contract_common::msg::QueryMsg as CoconutBandwidthQueryMsg; -use nym_coconut_bandwidth_contract_common::spend_credential::{ - PagedSpendCredentialResponse, SpendCredential, SpendCredentialResponse, -}; -use serde::Deserialize; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait CoconutBandwidthQueryClient { - async fn query_coconut_bandwidth_contract( - &self, - query: CoconutBandwidthQueryMsg, - ) -> Result - where - for<'a> T: Deserialize<'a>; - - async fn get_spent_credential( - &self, - blinded_serial_number: String, - ) -> Result { - self.query_coconut_bandwidth_contract(CoconutBandwidthQueryMsg::GetSpentCredential { - blinded_serial_number, - }) - .await - } - - async fn get_all_spent_credential_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_coconut_bandwidth_contract(CoconutBandwidthQueryMsg::GetAllSpentCredentials { - limit, - start_after, - }) - .await - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait PagedCoconutBandwidthQueryClient: CoconutBandwidthQueryClient { - async fn get_all_spent_credentials(&self) -> Result, NyxdError> { - collect_paged!(self, get_all_spent_credential_paged, spend_credentials) - } -} - -#[async_trait] -impl PagedCoconutBandwidthQueryClient for T where T: CoconutBandwidthQueryClient {} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl CoconutBandwidthQueryClient for C -where - C: CosmWasmClient + NymContractsProvider + Send + Sync, -{ - async fn query_coconut_bandwidth_contract( - &self, - query: CoconutBandwidthQueryMsg, - ) -> Result - where - for<'a> T: Deserialize<'a>, - { - let coconut_bandwidth_contract_address = self - .coconut_bandwidth_contract_address() - .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; - self.query_contract_smart(coconut_bandwidth_contract_address, &query) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::nyxd::contract_traits::tests::IgnoreValue; - - // it's enough that this compiles and clippy is happy about it - #[allow(dead_code)] - fn all_query_variants_are_covered( - client: C, - msg: CoconutBandwidthQueryMsg, - ) { - match msg { - CoconutBandwidthQueryMsg::GetSpentCredential { - blinded_serial_number, - } => client.get_spent_credential(blinded_serial_number).ignore(), - CoconutBandwidthQueryMsg::GetAllSpentCredentials { limit, start_after } => client - .get_all_spent_credential_paged(start_after, limit) - .ignore(), - }; - } -} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs deleted file mode 100644 index 2402dc078f..0000000000 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/coconut_bandwidth_signing_client.rs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::nyxd::contract_traits::NymContractsProvider; -use crate::nyxd::cosmwasm_client::types::ExecuteResult; -use crate::nyxd::error::NyxdError; -use crate::nyxd::{Coin, Fee, SigningCosmWasmClient}; -use crate::signing::signer::OfflineSigner; -use async_trait::async_trait; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialData; -use nym_coconut_bandwidth_contract_common::{ - deposit::DepositData, msg::ExecuteMsg as CoconutBandwidthExecuteMsg, -}; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait CoconutBandwidthSigningClient { - async fn execute_coconut_bandwidth_contract( - &self, - fee: Option, - msg: CoconutBandwidthExecuteMsg, - memo: String, - funds: Vec, - ) -> Result; - - async fn deposit( - &self, - amount: Coin, - info: String, - verification_key: String, - encryption_key: String, - fee: Option, - ) -> Result { - let req = CoconutBandwidthExecuteMsg::DepositFunds { - data: DepositData::new(info, verification_key, encryption_key), - }; - self.execute_coconut_bandwidth_contract( - fee, - req, - "CoconutBandwidth::Deposit".to_string(), - vec![amount], - ) - .await - } - - async fn spend_credential( - &self, - funds: Coin, - blinded_serial_number: String, - gateway_cosmos_address: String, - fee: Option, - ) -> Result { - let req = CoconutBandwidthExecuteMsg::SpendCredential { - data: SpendCredentialData::new( - funds.into(), - blinded_serial_number, - gateway_cosmos_address, - ), - }; - self.execute_coconut_bandwidth_contract( - fee, - req, - "CoconutBandwidth::SpendCredential".to_string(), - vec![], - ) - .await - } - - async fn release_funds( - &self, - amount: Coin, - fee: Option, - ) -> Result { - self.execute_coconut_bandwidth_contract( - fee, - CoconutBandwidthExecuteMsg::ReleaseFunds { - funds: amount.into(), - }, - "CoconutBandwidth::ReleaseFunds".to_string(), - vec![], - ) - .await - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl CoconutBandwidthSigningClient for C -where - C: SigningCosmWasmClient + NymContractsProvider + Sync, - NyxdError: From<::Error>, -{ - async fn execute_coconut_bandwidth_contract( - &self, - fee: Option, - msg: CoconutBandwidthExecuteMsg, - memo: String, - funds: Vec, - ) -> Result { - let coconut_bandwidth_contract_address = self - .coconut_bandwidth_contract_address() - .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; - - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; - - self.execute( - signer_address, - coconut_bandwidth_contract_address, - &msg, - fee, - memo, - funds, - ) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::nyxd::contract_traits::tests::{mock_coin, IgnoreValue}; - - // it's enough that this compiles and clippy is happy about it - #[allow(dead_code)] - fn all_execute_variants_are_covered( - client: C, - msg: CoconutBandwidthExecuteMsg, - ) { - match msg { - CoconutBandwidthExecuteMsg::DepositFunds { data } => client - .deposit( - mock_coin(), - data.deposit_info().to_string(), - data.identity_key().to_string(), - data.encryption_key().to_string(), - None, - ) - .ignore(), - CoconutBandwidthExecuteMsg::SpendCredential { data } => client - .spend_credential( - mock_coin(), - data.blinded_serial_number().to_string(), - data.gateway_cosmos_address().to_string(), - None, - ) - .ignore(), - CoconutBandwidthExecuteMsg::ReleaseFunds { funds } => { - client.release_funds(funds.into(), None).ignore() - } - }; - } -} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs index c85f85c399..4d8fd3237c 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs @@ -51,6 +51,11 @@ pub trait DkgQueryClient { self.query_dkg_contract(request).await } + async fn get_epoch_threshold(&self, epoch_id: EpochId) -> Result, NyxdError> { + let request = DkgQueryMsg::GetEpochThreshold { epoch_id }; + self.query_dkg_contract(request).await + } + async fn get_registered_dealer_details( &self, address: &AccountId, @@ -256,6 +261,9 @@ mod tests { DkgQueryMsg::GetCurrentEpochThreshold {} => { client.get_current_epoch_threshold().ignore() } + DkgQueryMsg::GetEpochThreshold { epoch_id } => { + client.get_epoch_threshold(epoch_id).ignore() + } DkgQueryMsg::GetRegisteredDealer { dealer_address, epoch_id, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_query_client.rs new file mode 100644 index 0000000000..7c4c6c6b4e --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_query_client.rs @@ -0,0 +1,123 @@ +// Copyright 2022-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::collect_paged; +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::error::NyxdError; +use crate::nyxd::CosmWasmClient; +use async_trait::async_trait; +use cosmwasm_std::Coin; +use nym_ecash_contract_common::msg::QueryMsg as EcashQueryMsg; +use serde::Deserialize; + +pub use nym_ecash_contract_common::blacklist::{ + BlacklistedAccount, BlacklistedAccountResponse, PagedBlacklistedAccountResponse, +}; +pub use nym_ecash_contract_common::deposit::{ + Deposit, DepositData, DepositId, DepositResponse, PagedDepositsResponse, +}; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait EcashQueryClient { + async fn query_ecash_contract(&self, query: EcashQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>; + + async fn get_blacklisted_account( + &self, + public_key: String, + ) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetBlacklistedAccount { public_key }) + .await + } + + async fn get_blacklist_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetBlacklistPaged { start_after, limit }) + .await + } + + async fn get_required_deposit_amount(&self) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetRequiredDepositAmount {}) + .await + } + + async fn get_deposit(&self, deposit_id: u32) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetDeposit { deposit_id }) + .await + } + + async fn get_deposits_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_ecash_contract(EcashQueryMsg::GetDepositsPaged { start_after, limit }) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PagedEcashQueryClient: EcashQueryClient { + async fn get_all_blacklisted_accounts(&self) -> Result, NyxdError> { + collect_paged!(self, get_blacklist_paged, accounts) + } + + async fn get_all_deposits(&self) -> Result, NyxdError> { + collect_paged!(self, get_deposits_paged, deposits) + } +} + +#[async_trait] +impl PagedEcashQueryClient for T where T: EcashQueryClient {} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl EcashQueryClient for C +where + C: CosmWasmClient + NymContractsProvider + Send + Sync, +{ + async fn query_ecash_contract(&self, query: EcashQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>, + { + let ecash_contract_address = self + .ecash_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; + self.query_contract_smart(ecash_contract_address, &query) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_ecash_contract_common::msg::QueryMsg; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_query_variants_are_covered( + client: C, + msg: EcashQueryMsg, + ) { + match msg { + EcashQueryMsg::GetBlacklistedAccount { public_key } => { + client.get_blacklisted_account(public_key).ignore() + } + QueryMsg::GetBlacklistPaged { limit, start_after } => { + client.get_blacklist_paged(start_after, limit).ignore() + } + QueryMsg::GetDeposit { deposit_id } => client.get_deposit(deposit_id).ignore(), + QueryMsg::GetDepositsPaged { limit, start_after } => { + client.get_deposits_paged(start_after, limit).ignore() + } + QueryMsg::GetRequiredDepositAmount {} => client.get_required_deposit_amount().ignore(), + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs new file mode 100644 index 0000000000..98a3e2675b --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs @@ -0,0 +1,124 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::cosmwasm_client::types::ExecuteResult; +use crate::nyxd::error::NyxdError; +use crate::nyxd::{Coin, Fee, SigningCosmWasmClient}; +use crate::signing::signer::OfflineSigner; +use async_trait::async_trait; +use nym_ecash_contract_common::events::TICKET_BOOK_VALUE; +use nym_ecash_contract_common::msg::ExecuteMsg as EcashExecuteMsg; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait EcashSigningClient { + async fn execute_ecash_contract( + &self, + fee: Option, + msg: EcashExecuteMsg, + memo: String, + funds: Vec, + ) -> Result; + + async fn make_ticketbook_deposit( + &self, + public_key: String, + fee: Option, + ) -> Result { + let req = EcashExecuteMsg::DepositTicketBookFunds { + identity_key: public_key, + }; + let amount = Coin::new(TICKET_BOOK_VALUE, "unym"); + self.execute_ecash_contract(fee, req, "Ecash::Deposit".to_string(), vec![amount]) + .await + } + + async fn request_ticket_redemption( + &self, + commitment_bs58: String, + number_of_tickets: u16, + fee: Option, + ) -> Result { + let req = EcashExecuteMsg::RequestRedemption { + commitment_bs58, + number_of_tickets, + }; + self.execute_ecash_contract(fee, req, Default::default(), vec![]) + .await + } + + async fn propose_for_blacklist( + &self, + public_key: String, + fee: Option, + ) -> Result { + let req = EcashExecuteMsg::ProposeToBlacklist { public_key }; + self.execute_ecash_contract(fee, req, "Ecash::ProposeToBlacklist".to_string(), vec![]) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl EcashSigningClient for C +where + C: SigningCosmWasmClient + NymContractsProvider + Sync, + NyxdError: From<::Error>, +{ + async fn execute_ecash_contract( + &self, + fee: Option, + msg: EcashExecuteMsg, + memo: String, + funds: Vec, + ) -> Result { + let ecash_contract_address = self + .ecash_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; + + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); + let signer_address = &self.signer_addresses()?[0]; + + self.execute( + signer_address, + ecash_contract_address, + &msg, + fee, + memo, + funds, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_ecash_contract_common::msg::ExecuteMsg; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_execute_variants_are_covered( + client: C, + msg: EcashExecuteMsg, + ) { + match msg { + EcashExecuteMsg::DepositTicketBookFunds { identity_key } => client + .make_ticketbook_deposit(identity_key.to_string(), None) + .ignore(), + EcashExecuteMsg::AddToBlacklist { public_key: _ } => unimplemented!(), //no add to blacklist method on client + EcashExecuteMsg::ProposeToBlacklist { public_key } => { + client.propose_for_blacklist(public_key, None).ignore() + } + ExecuteMsg::RequestRedemption { + commitment_bs58, + number_of_tickets, + } => client + .request_ticket_redemption(commitment_bs58, number_of_tickets, None) + .ignore(), + ExecuteMsg::RedeemTickets { .. } => unimplemented!(), // no redeem tickets method for the client + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs index d58da6fbd3..8210c5b476 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs @@ -8,34 +8,32 @@ use std::str::FromStr; // TODO: all of those could/should be derived via a macro // query clients -pub mod coconut_bandwidth_query_client; pub mod dkg_query_client; +pub mod ecash_query_client; pub mod group_query_client; pub mod mixnet_query_client; pub mod multisig_query_client; pub mod vesting_query_client; // signing clients -pub mod coconut_bandwidth_signing_client; pub mod dkg_signing_client; +pub mod ecash_signing_client; pub mod group_signing_client; pub mod mixnet_signing_client; pub mod multisig_signing_client; pub mod vesting_signing_client; // re-export query traits -pub use coconut_bandwidth_query_client::{ - CoconutBandwidthQueryClient, PagedCoconutBandwidthQueryClient, -}; pub use dkg_query_client::{DkgQueryClient, PagedDkgQueryClient}; +pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient}; pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient}; pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient}; pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient}; pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient}; // re-export signing traits -pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient; pub use dkg_signing_client::DkgSigningClient; +pub use ecash_signing_client::EcashSigningClient; pub use group_signing_client::GroupSigningClient; pub use mixnet_signing_client::MixnetSigningClient; pub use multisig_signing_client::MultisigSigningClient; @@ -48,7 +46,7 @@ pub trait NymContractsProvider { fn vesting_contract_address(&self) -> Option<&AccountId>; // coconut-related - fn coconut_bandwidth_contract_address(&self) -> Option<&AccountId>; + fn ecash_contract_address(&self) -> Option<&AccountId>; fn dkg_contract_address(&self) -> Option<&AccountId>; fn group_contract_address(&self) -> Option<&AccountId>; fn multisig_contract_address(&self) -> Option<&AccountId>; @@ -59,7 +57,7 @@ pub struct TypedNymContracts { pub mixnet_contract_address: Option, pub vesting_contract_address: Option, - pub coconut_bandwidth_contract_address: Option, + pub ecash_contract_address: Option, pub group_contract_address: Option, pub multisig_contract_address: Option, pub coconut_dkg_contract_address: Option, @@ -78,8 +76,8 @@ impl TryFrom for TypedNymContracts { .vesting_contract_address .map(|addr| addr.parse()) .transpose()?, - coconut_bandwidth_contract_address: value - .coconut_bandwidth_contract_address + ecash_contract_address: value + .ecash_contract_address .map(|addr| addr.parse()) .transpose()?, group_contract_address: value diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs index 57c6ce2bf7..6094219401 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_query_client.rs @@ -6,7 +6,7 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cw3::{ - ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterDetail, + ProposalListResponse, ProposalResponse, VoteInfo, VoteListResponse, VoteResponse, VoterDetail, VoterListResponse, VoterResponse, }; use cw_utils::ThresholdResponse; @@ -134,6 +134,28 @@ pub trait PagedMultisigQueryClient: MultisigQueryClient { Ok(voters) } + + async fn get_all_votes(&self, proposal_id: u64) -> Result, NyxdError> { + let mut votes = Vec::new(); + let mut start_after = None; + + loop { + let mut paged_response = self + .list_votes(proposal_id, start_after.take(), None) + .await?; + + let last_voter = paged_response.votes.last().map(|vote| vote.voter.clone()); + votes.append(&mut paged_response.votes); + + if let Some(start_after_res) = last_voter { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(votes) + } } #[async_trait] diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs index f7249f02ea..ae28cf0ef3 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs @@ -31,15 +31,15 @@ pub trait MultisigSigningClient: NymContractsProvider { voucher_value: Coin, fee: Option, ) -> Result { - let coconut_bandwidth_contract_address = self - .coconut_bandwidth_contract_address() + let ecash_contract_address = self + .ecash_contract_address() .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; let release_funds_req = CoconutBandwidthExecuteMsg::ReleaseFunds { funds: voucher_value.into(), }; let release_funds_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: coconut_bandwidth_contract_address.to_string(), + contract_addr: ecash_contract_address.to_string(), msg: to_binary(&release_funds_req)?, funds: vec![], }); diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs index 2a26c97155..8d0bb0fb4f 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs @@ -2,31 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nyxd::cosmwasm_client::client_traits::CosmWasmClient; -use crate::nyxd::cosmwasm_client::helpers::{compress_wasm_code, CheckResponse}; +use crate::nyxd::cosmwasm_client::helpers::{ + compress_wasm_code, parse_msg_responses, CheckResponse, +}; use crate::nyxd::cosmwasm_client::logs::parse_raw_logs; use crate::nyxd::cosmwasm_client::types::*; use crate::nyxd::error::NyxdError; use crate::nyxd::fee::{Fee, DEFAULT_SIMULATED_GAS_MULTIPLIER}; +use crate::nyxd::helpers::find_tx_attribute; use crate::nyxd::{Coin, GasAdjustable, GasPrice, TxResponse}; use crate::signing::signer::OfflineSigner; use crate::signing::tx_signer::TxSigner; use crate::signing::SignerData; use async_trait::async_trait; use cosmrs::bank::MsgSend; +use cosmrs::cosmwasm::{MsgClearAdmin, MsgUpdateAdmin}; use cosmrs::distribution::MsgWithdrawDelegatorReward; use cosmrs::feegrant::{ AllowedMsgAllowance, BasicAllowance, MsgGrantAllowance, MsgRevokeAllowance, }; use cosmrs::proto::cosmos::tx::signing::v1beta1::SignMode; use cosmrs::staking::{MsgDelegate, MsgUndelegate}; -use cosmrs::tendermint::abci::{Event, EventAttribute}; use cosmrs::tx::{self, Msg}; use cosmrs::{cosmwasm, AccountId, Any, Tx}; use log::debug; use serde::Serialize; use sha2::Digest; use sha2::Sha256; - use std::time::SystemTime; use tendermint_rpc::endpoint::broadcast; @@ -52,20 +54,6 @@ fn single_unspecified_signer_auth( } .auth_info(empty_fee()) } -// Searches in events for an event of the given event type which contains an -// attribute for with the given key. -fn find_attribute<'a>( - events: &'a [Event], - event_type: &str, - attr_key: &str, -) -> Option<&'a EventAttribute> { - events - .iter() - .find(|attr| attr.kind == event_type)? - .attributes - .iter() - .find(|attr| attr.key == attr_key) -} #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -132,8 +120,7 @@ where .await? .check_response()?; - let logs = parse_raw_logs(tx_res.tx_result.log)?; - let events = tx_res.tx_result.events; + let logs = parse_raw_logs(&tx_res.tx_result.log)?; let gas_info = GasInfo { gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(), gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(), @@ -143,9 +130,8 @@ where // the reason I think unwrap here is fine is that if the transaction succeeded and those // fields do not exist or code_id is not a number, there's no way we can recover, we're probably connected // to wrong validator or something - let code_id = find_attribute(&events, "store_code", "code_id") + let code_id = find_tx_attribute(&tx_res, "store_code", "code_id") .unwrap() - .value .parse() .unwrap(); @@ -156,7 +142,7 @@ where compressed_checksum, code_id, logs, - events, + events: tx_res.tx_result.events, transaction_hash: tx_res.hash, gas_info, }) @@ -198,8 +184,7 @@ where .await? .check_response()?; - let logs = parse_raw_logs(tx_res.tx_result.log)?; - let events = tx_res.tx_result.events; + let logs = parse_raw_logs(&tx_res.tx_result.log)?; let gas_info = GasInfo { gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(), gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(), @@ -208,16 +193,15 @@ where // the reason I think unwrap here is fine is that if the transaction succeeded and those // fields do not exist or address is malformed, there's no way we can recover, we're probably connected // to wrong validator or something - let contract_address = find_attribute(&events, "instantiate", "_contract_address") + let contract_address = find_tx_attribute(&tx_res, "instantiate", "_contract_address") .unwrap() - .value .parse() .unwrap(); Ok(InstantiateResult { contract_address, logs, - events, + events: tx_res.tx_result.events, transaction_hash: tx_res.hash, gas_info, }) @@ -231,7 +215,7 @@ where fee: Fee, memo: impl Into + Send + 'static, ) -> Result { - let change_admin_msg = sealed::cosmwasm::MsgUpdateAdmin { + let change_admin_msg = MsgUpdateAdmin { sender: sender_address.clone(), new_admin: new_admin.clone(), contract: contract_address.clone(), @@ -263,7 +247,7 @@ where fee: Fee, memo: impl Into + Send + 'static, ) -> Result { - let change_admin_msg = sealed::cosmwasm::MsgClearAdmin { + let change_admin_msg = MsgClearAdmin { sender: sender_address.clone(), contract: contract_address.clone(), } @@ -355,10 +339,11 @@ where gas_wanted: tx_res.tx_result.gas_wanted.try_into().unwrap_or_default(), gas_used: tx_res.tx_result.gas_used.try_into().unwrap_or_default(), }; + Ok(ExecuteResult { logs: parse_raw_logs(tx_res.tx_result.log)?, + msg_responses: parse_msg_responses(tx_res.tx_result.data), events: tx_res.tx_result.events, - data: tx_res.tx_result.data.into(), transaction_hash: tx_res.hash, gas_info, }) @@ -401,8 +386,8 @@ where }; Ok(ExecuteResult { logs: parse_raw_logs(tx_res.tx_result.log)?, + msg_responses: parse_msg_responses(tx_res.tx_result.data), events: tx_res.tx_result.events, - data: tx_res.tx_result.data.into(), transaction_hash: tx_res.hash, gas_info, }) @@ -731,167 +716,3 @@ where )?) } } - -// a temporary bypass until https://github.com/cosmos/cosmos-rust/pull/419 is merged -mod sealed { - pub mod cosmwasm { - use cosmrs::{proto, tx::Msg, AccountId, ErrorReport, Result}; - - /// MsgUpdateAdmin sets a new admin for a smart contract - #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgUpdateAdmin { - /// Sender is the that actor that signed the messages - pub sender: AccountId, - - /// NewAdmin address to be set - pub new_admin: AccountId, - - /// Contract is the address of the smart contract - pub contract: AccountId, - } - - impl Msg for MsgUpdateAdmin { - type Proto = proto::cosmwasm::wasm::v1::MsgUpdateAdmin; - } - - impl TryFrom for MsgUpdateAdmin { - type Error = ErrorReport; - - fn try_from( - proto: proto::cosmwasm::wasm::v1::MsgUpdateAdmin, - ) -> Result { - MsgUpdateAdmin::try_from(&proto) - } - } - - impl TryFrom<&proto::cosmwasm::wasm::v1::MsgUpdateAdmin> for MsgUpdateAdmin { - type Error = ErrorReport; - - fn try_from( - proto: &proto::cosmwasm::wasm::v1::MsgUpdateAdmin, - ) -> Result { - Ok(MsgUpdateAdmin { - sender: proto.sender.parse()?, - new_admin: proto.new_admin.parse()?, - contract: proto.contract.parse()?, - }) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - fn from(msg: MsgUpdateAdmin) -> proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - proto::cosmwasm::wasm::v1::MsgUpdateAdmin::from(&msg) - } - } - - impl From<&MsgUpdateAdmin> for proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - fn from(msg: &MsgUpdateAdmin) -> proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - proto::cosmwasm::wasm::v1::MsgUpdateAdmin { - sender: msg.sender.to_string(), - new_admin: msg.new_admin.to_string(), - contract: msg.contract.to_string(), - } - } - } - - /// MsgUpdateAdminResponse returns empty data - #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgUpdateAdminResponse {} - - impl Msg for MsgUpdateAdminResponse { - type Proto = proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse; - } - - impl TryFrom for MsgUpdateAdminResponse { - type Error = ErrorReport; - - fn try_from( - _proto: proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse, - ) -> Result { - Ok(MsgUpdateAdminResponse {}) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse { - fn from( - _msg: MsgUpdateAdminResponse, - ) -> proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse { - proto::cosmwasm::wasm::v1::MsgUpdateAdminResponse {} - } - } - - /// MsgClearAdmin removes any admin stored for a smart contract - #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgClearAdmin { - /// Sender is the that actor that signed the messages - pub sender: AccountId, - - /// Contract is the address of the smart contract - pub contract: AccountId, - } - - impl Msg for MsgClearAdmin { - type Proto = proto::cosmwasm::wasm::v1::MsgClearAdmin; - } - - impl TryFrom for MsgClearAdmin { - type Error = ErrorReport; - - fn try_from(proto: proto::cosmwasm::wasm::v1::MsgClearAdmin) -> Result { - MsgClearAdmin::try_from(&proto) - } - } - - impl TryFrom<&proto::cosmwasm::wasm::v1::MsgClearAdmin> for MsgClearAdmin { - type Error = ErrorReport; - - fn try_from(proto: &proto::cosmwasm::wasm::v1::MsgClearAdmin) -> Result { - Ok(MsgClearAdmin { - sender: proto.sender.parse()?, - contract: proto.contract.parse()?, - }) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgClearAdmin { - fn from(msg: MsgClearAdmin) -> proto::cosmwasm::wasm::v1::MsgClearAdmin { - proto::cosmwasm::wasm::v1::MsgClearAdmin::from(&msg) - } - } - - impl From<&MsgClearAdmin> for proto::cosmwasm::wasm::v1::MsgClearAdmin { - fn from(msg: &MsgClearAdmin) -> proto::cosmwasm::wasm::v1::MsgClearAdmin { - proto::cosmwasm::wasm::v1::MsgClearAdmin { - sender: msg.sender.to_string(), - contract: msg.contract.to_string(), - } - } - } - - /// MsgClearAdminResponse returns empty data - #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] - pub struct MsgClearAdminResponse {} - - impl Msg for MsgClearAdminResponse { - type Proto = proto::cosmwasm::wasm::v1::MsgClearAdminResponse; - } - - impl TryFrom for MsgClearAdminResponse { - type Error = ErrorReport; - - fn try_from( - _proto: proto::cosmwasm::wasm::v1::MsgClearAdminResponse, - ) -> Result { - Ok(MsgClearAdminResponse {}) - } - } - - impl From for proto::cosmwasm::wasm::v1::MsgClearAdminResponse { - fn from( - _msg: MsgClearAdminResponse, - ) -> proto::cosmwasm::wasm::v1::MsgClearAdminResponse { - proto::cosmwasm::wasm::v1::MsgClearAdminResponse {} - } - } - } -} diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs index 1718ab2f02..c437125b16 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs @@ -2,9 +2,87 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nyxd::error::NyxdError; +use cosmrs::abci::TxMsgData; +use cosmrs::cosmwasm::MsgExecuteContractResponse; use cosmrs::proto::cosmos::base::query::v1beta1::{PageRequest, PageResponse}; +use log::error; +use prost::bytes::Bytes; use tendermint_rpc::endpoint::broadcast; +use crate::nyxd::cosmwasm_client::types::ExecuteResult; +pub use cosmrs::abci::MsgResponse; + +pub fn parse_msg_responses(data: Bytes) -> Vec { + // it seems that currently, on wasmd 0.43 + tendermint-rs 0.37 + cosmrs 0.17.0-pre + // the data is left in undecoded base64 form, but I'd imagine this might change so if the decoding fails, + // use the bytes directly instead + let data = if let Ok(decoded) = base64::decode(&data) { + decoded + } else { + error!("failed to base64-decode the 'data' field of the TxResponse - has the chain been upgraded and introduced some breaking changes?"); + data.into() + }; + + match TxMsgData::try_from(data) { + Ok(tx_msg_data) => tx_msg_data.msg_responses, + Err(err) => { + error!("failed to parse tx responses - has the chain been upgraded and introduced some breaking changes? the error was {err}"); + Vec::new() + } + } +} + +// requires there's a single response message +pub trait ToSingletonContractData: Sized { + fn parse_singleton_u32_contract_data(&self) -> Result { + let b = self.to_singleton_contract_data()?; + if b.len() != 4 { + return Err(NyxdError::MalformedResponseData { + got: b.len(), + expected: 4, + }); + } + Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]])) + } + + fn parse_singleton_u64_contract_data(&self) -> Result { + let b = self.to_singleton_contract_data()?; + if b.len() != 8 { + return Err(NyxdError::MalformedResponseData { + got: b.len(), + expected: 8, + }); + } + Ok(u64::from_be_bytes([ + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + ])) + } + + fn to_singleton_contract_data(&self) -> Result, NyxdError>; +} + +impl ToSingletonContractData for ExecuteResult { + fn to_singleton_contract_data(&self) -> Result, NyxdError> { + if self.msg_responses.len() != 1 { + return Err(NyxdError::UnexpectedNumberOfMsgResponses { + got: self.msg_responses.len(), + }); + } + + self.msg_responses[0].to_contract_response_data() + } +} + +pub trait ToContractResponseData: Sized { + fn to_contract_response_data(&self) -> Result, NyxdError>; +} + +impl ToContractResponseData for MsgResponse { + fn to_contract_response_data(&self) -> Result, NyxdError> { + Ok(self.try_decode_as::()?.data) + } +} + pub(crate) trait CheckResponse: Sized { fn check_response(self) -> Result; } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs index 24180d8466..4a31a1fc7e 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/logs.rs @@ -3,10 +3,11 @@ use crate::nyxd::error::NyxdError; use itertools::Itertools; +use nym_ecash_contract_common::events::PROPOSAL_ID_ATTRIBUTE_NAME; use serde::{Deserialize, Serialize}; -pub use nym_coconut_bandwidth_contract_common::event_attributes::*; pub use nym_coconut_dkg_common::event_attributes::*; +pub use nym_ecash_contract_common::event_attributes::*; // it seems that currently validators just emit stringified events (which are also returned as part of deliverTx response) // as their logs @@ -33,6 +34,25 @@ pub fn find_attribute<'a>( .find(|attr| attr.key == attribute_key) } +/// Search for the proposal id in the given log. It'll be in the LAST wasm event, with attribute key "proposal_id" +pub fn find_proposal_id(logs: &[Log]) -> Result { + let maybe_attributes = logs + .iter() + .rev() + .flat_map(|log| log.events.iter()) + .find(|event| event.ty == "wasm") + .ok_or(NyxdError::ComswasmEventNotFound)? + .attributes + .iter() + .find(|attr| attr.key == PROPOSAL_ID_ATTRIBUTE_NAME); + let attribute = maybe_attributes.ok_or(NyxdError::ComswasmAttributeNotFound)?; + + attribute + .value + .parse::() + .map_err(|_| NyxdError::DeserializationError("proposal_id".into())) +} + // these two functions were separated so that the internal logic could actually be tested fn parse_raw_str_logs(raw: &str) -> Result, NyxdError> { // From Cosmos SDK > 0.50 onwards, log field is not populated @@ -49,7 +69,7 @@ fn parse_raw_str_logs(raw: &str) -> Result, NyxdError> { Ok(logs) } -pub fn parse_raw_logs(raw: String) -> Result, NyxdError> { +pub fn parse_raw_logs>(raw: S) -> Result, NyxdError> { parse_raw_str_logs(raw.as_ref()) } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs index 98f2f8d07c..a779343311 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs @@ -18,6 +18,8 @@ use tendermint_rpc::endpoint::*; use tendermint_rpc::query::Query; use tendermint_rpc::{Error as TendermintRpcError, Order, Paging, SimpleRequest}; +pub use helpers::{ToContractResponseData, ToSingletonContractData}; + #[cfg(feature = "http-client")] use crate::http_client; #[cfg(feature = "http-client")] diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs index b10f9b65ec..564a17441e 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs @@ -30,6 +30,7 @@ use prost::Message; use serde::Serialize; pub use cosmrs::abci::GasInfo; +pub use cosmrs::abci::MsgResponse; pub type ContractCodeId = u64; @@ -240,7 +241,7 @@ pub struct UploadResult { pub gas_info: GasInfo, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct InstantiateOptions { /// The funds that are transferred from the sender to the newly created contract. /// The funds are transferred as part of the message execution after the contract address is @@ -262,6 +263,11 @@ impl InstantiateOptions { admin, } } + + pub fn with_admin(mut self, admin: AccountId) -> Self { + self.admin = Some(admin); + self + } } #[derive(Debug, Serialize)] @@ -307,7 +313,7 @@ pub struct MigrateResult { pub struct ExecuteResult { pub logs: Vec, - pub data: Vec, + pub msg_responses: Vec, pub events: Vec, diff --git a/common/client-libs/validator-client/src/nyxd/error.rs b/common/client-libs/validator-client/src/nyxd/error.rs index 905d484b72..c71ed596ad 100644 --- a/common/client-libs/validator-client/src/nyxd/error.rs +++ b/common/client-libs/validator-client/src/nyxd/error.rs @@ -32,6 +32,12 @@ pub enum NyxdError { #[error("There was an issue on the cosmrs side: {0}")] CosmrsErrorReport(#[from] cosmrs::ErrorReport), + #[error("cosmwasm event not found")] + ComswasmEventNotFound, + + #[error("cosmwasm attribute not found")] + ComswasmAttributeNotFound, + #[error("Failed to derive account address")] AccountDerivationError, @@ -142,6 +148,12 @@ pub enum NyxdError { #[error("Account had an unexpected bech32 prefix. Expected: {expected}, got: {got}")] UnexpectedBech32Prefix { got: String, expected: String }, + + #[error("the transaction returned unexpected, {got}, number of MsgResponse. Expected to receive a single one")] + UnexpectedNumberOfMsgResponses { got: usize }, + + #[error("the response data has invalid size. got {got} bytes, but expected {expected} bytes instead")] + MalformedResponseData { got: usize, expected: usize }, } // The purpose of parsing the abci query result is that we want to generate the `pretty_log` if diff --git a/common/client-libs/validator-client/src/nyxd/helpers.rs b/common/client-libs/validator-client/src/nyxd/helpers.rs index 85999eac6f..cec865252a 100644 --- a/common/client-libs/validator-client/src/nyxd/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/helpers.rs @@ -3,11 +3,16 @@ use crate::nyxd::TxResponse; +// Searches in events for an event of the given event type which contains an +// attribute for with the given key. pub fn find_tx_attribute(tx: &TxResponse, event_type: &str, attribute_key: &str) -> Option { let event = tx.tx_result.events.iter().find(|e| e.kind == event_type)?; - let attribute = event - .attributes - .iter() - .find(|attr| attr.key == attribute_key)?; - Some(attribute.value.clone()) + let attribute = event.attributes.iter().find(|&attr| { + if let Ok(key_str) = attr.key_str() { + key_str == attribute_key + } else { + false + } + })?; + Some(attribute.value_str().ok().map(|str| str.to_string())).flatten() } diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index e396af4053..5265b499f9 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -240,8 +240,8 @@ impl NyxdClient { self.config.contracts.vesting_contract_address = Some(address); } - pub fn set_coconut_bandwidth_contract_address(&mut self, address: AccountId) { - self.config.contracts.coconut_bandwidth_contract_address = Some(address); + pub fn set_ecash_contract_address(&mut self, address: AccountId) { + self.config.contracts.ecash_contract_address = Some(address); } pub fn set_multisig_contract_address(&mut self, address: AccountId) { @@ -262,11 +262,8 @@ impl NymContractsProvider for NyxdClient { self.config.contracts.vesting_contract_address.as_ref() } - fn coconut_bandwidth_contract_address(&self) -> Option<&AccountId> { - self.config - .contracts - .coconut_bandwidth_contract_address - .as_ref() + fn ecash_contract_address(&self) -> Option<&AccountId> { + self.config.contracts.ecash_contract_address.as_ref() } fn dkg_contract_address(&self) -> Option<&AccountId> { @@ -379,6 +376,14 @@ where } } + pub fn mix_coin(&self, amount: u128) -> Coin { + Coin::new(amount, &self.config.chain_details.mix_denom.base) + } + + pub fn mix_coins(&self, amount: u128) -> Vec { + vec![self.mix_coin(amount)] + } + pub fn cw_address(&self) -> Addr { // the call to unchecked is fine here as we're converting directly from `AccountId` // which must have been a valid bech32 address diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index 0649e4e876..2045cf0d2a 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -43,9 +43,9 @@ nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common" } nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } nym-vesting-contract-common = { path = "../cosmwasm-smart-contracts/vesting-contract" } -nym-coconut-bandwidth-contract-common = { path = "../cosmwasm-smart-contracts/coconut-bandwidth-contract" } nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" } nym-multisig-contract-common = { path = "../cosmwasm-smart-contracts/multisig-contract" } +nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } nym-sphinx = { path = "../../common/nymsphinx" } nym-client-core = { path = "../../common/client-core" } nym-config = { path = "../../common/config" } diff --git a/common/commands/src/coconut/generate_freepass.rs b/common/commands/src/coconut/generate_freepass.rs deleted file mode 100644 index 6ea535ebfe..0000000000 --- a/common/commands/src/coconut/generate_freepass.rs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use anyhow::{anyhow, bail}; -use clap::ArgGroup; -use clap::Parser; -use futures::StreamExt; -use log::{error, info}; -use nym_coconut_dkg_common::types::EpochId; -use nym_credential_utils::utils::block_until_coconut_is_available; -use nym_credentials::coconut::bandwidth::freepass::MAX_FREE_PASS_VALIDITY; -use nym_credentials::{ - obtain_aggregate_verification_key, IssuanceBandwidthCredential, IssuedBandwidthCredential, -}; -use nym_credentials_interface::VerificationKey; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, NymContractsProvider}; -use nym_validator_client::nyxd::CosmWasmClient; -use nym_validator_client::signing::AccountData; -use nym_validator_client::CoconutApiClient; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; -use std::sync::Arc; -use time::format_description::well_known::Rfc3339; -use time::OffsetDateTime; -use zeroize::Zeroizing; - -fn parse_rfc3339_expiration_date(raw: &str) -> Result { - OffsetDateTime::parse(raw, &Rfc3339) -} - -#[derive(Debug, Parser)] -#[clap(group(ArgGroup::new("expiration").required(true)))] -pub struct Args { - /// Specifies the expiration date of the free pass(es) - /// Can't be set to more than a week into the future. - #[clap(long, group = "expiration", value_parser = parse_rfc3339_expiration_date)] - pub(crate) expiration_date: Option, - - /// The expiration of the free pass(es) expresses as unix timestamp. - /// Can't be set to more than a week into the future. - #[clap(long, group = "expiration")] - pub(crate) expiration_timestamp: Option, - - /// The number of free passes to issue - #[clap(long, default_value = "1")] - pub(crate) amount: u64, - - /// Path to the output directory for generated free passes. - #[clap(long)] - pub(crate) output_dir: PathBuf, -} - -async fn get_freepass( - api_clients: Vec, - aggregate_vk: &VerificationKey, - threshold: u64, - epoch_id: EpochId, - signing_account: &AccountData, - expiration_date: OffsetDateTime, -) -> anyhow::Result { - let issuance_pass = IssuanceBandwidthCredential::new_freepass(Some(expiration_date)); - let signing_data = issuance_pass.prepare_for_signing(); - - let credential_shares = Arc::new(tokio::sync::Mutex::new(Vec::new())); - - futures::stream::iter(api_clients) - .for_each_concurrent(None, |client| async { - // move the client into the block - let client = client; - let api_url = client.api_client.api_url(); - - info!("contacting {api_url} for blinded free pass"); - - match issuance_pass - .obtain_partial_freepass_credential( - &client.api_client, - signing_account, - &client.verification_key, - signing_data.clone(), - ) - .await - { - Ok(partial_credential) => { - credential_shares - .lock() - .await - .push((partial_credential, client.node_id).into()); - } - Err(err) => { - error!("failed to obtain partial free pass from {api_url}: {err}") - } - } - }) - .await; - - // SAFETY: the futures have completed, so we MUST have the only arc reference - #[allow(clippy::unwrap_used)] - let credential_shares = Arc::into_inner(credential_shares).unwrap().into_inner(); - - if credential_shares.len() < threshold as usize { - bail!("we managed to obtain only {} partial credentials while the minimum threshold is {threshold}", credential_shares.len()); - } - - let signature = issuance_pass.aggregate_signature_shares(aggregate_vk, &credential_shares)?; - Ok(issuance_pass.into_issued_credential(signature, epoch_id)) -} - -pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { - let address = client.address(); - - if !args.output_dir.is_dir() { - bail!("the provided output directory is not a directory!"); - } - - if args.output_dir.read_dir()?.next().is_some() { - bail!("the provided output directory is not empty!"); - } - - let Some(bandwidth_contract) = client.coconut_bandwidth_contract_address() else { - bail!("the bandwidth contract address is not set") - }; - - let Some(bandwidth_admin) = client - .get_contract(bandwidth_contract) - .await - .map(|c| c.contract_info.admin)? - else { - bail!("the bandwidth contract doesn't have any admin set") - }; - - // sanity checks since nym-apis will reject invalid requests anyway - if address != bandwidth_admin { - bail!("the provided mnemonic does not correspond to the current admin of the bandwidth contract") - } - - let expiration_date = match args.expiration_date { - Some(date) => date, - // SAFETY: one of those arguments must have been set - None => OffsetDateTime::from_unix_timestamp(args.expiration_timestamp.unwrap())?, - }; - - let now = OffsetDateTime::now_utc(); - - if expiration_date > now + MAX_FREE_PASS_VALIDITY { - bail!("the provided free pass request has too long expiry (expiry is set to on {expiration_date})") - } - - if expiration_date < now { - bail!("the provided free pass expiry is set in the past!") - } - - // issuance start - block_until_coconut_is_available(&client).await?; - - let signing_account = client.signing_account()?; - - let epoch_id = client.get_current_epoch().await?.epoch_id; - let threshold = client - .get_current_epoch_threshold() - .await? - .ok_or(anyhow!("no threshold available"))?; - let api_clients = all_coconut_api_clients(&client, epoch_id).await?; - - if api_clients.len() < threshold as usize { - bail!( - "we have only {} api clients available while the minimum threshold is {threshold}", - api_clients.len() - ) - } - let aggregate_vk = obtain_aggregate_verification_key(&api_clients)?; - - for i in 0..args.amount { - let human_index = i + 1; - info!("trying to obtain free pass {human_index}/{}", args.amount); - let free_pass = get_freepass( - api_clients.clone(), - &aggregate_vk, - threshold, - epoch_id, - &signing_account, - expiration_date, - ) - .await?; - let credential_data = Zeroizing::new(free_pass.pack_v1()); - let output = args.output_dir.join(format!("freepass_{i}.nym")); - info!("saving the freepass to '{}'", output.display()); - File::create(output)?.write_all(&credential_data)?; - } - - Ok(()) -} diff --git a/common/commands/src/coconut/import_credential.rs b/common/commands/src/coconut/import_ticket_book.rs similarity index 100% rename from common/commands/src/coconut/import_credential.rs rename to common/commands/src/coconut/import_ticket_book.rs diff --git a/common/commands/src/coconut/issue_credentials.rs b/common/commands/src/coconut/issue_ticket_book.rs similarity index 65% rename from common/commands/src/coconut/issue_credentials.rs rename to common/commands/src/coconut/issue_ticket_book.rs index 9533af614e..a1d2f0f8ef 100644 --- a/common/commands/src/coconut/issue_credentials.rs +++ b/common/commands/src/coconut/issue_ticket_book.rs @@ -7,7 +7,7 @@ use anyhow::bail; use clap::Parser; use nym_credential_storage::initialise_persistent_storage; use nym_credential_utils::utils; -use nym_validator_client::nyxd::Coin; +use nym_crypto::asymmetric::identity; use std::path::PathBuf; #[derive(Debug, Parser)] @@ -15,21 +15,9 @@ pub struct Args { /// Config file of the client that is supposed to use the credential. #[clap(long)] pub(crate) client_config: PathBuf, - - /// The amount of utokens the credential will hold. - #[clap(long, default_value = "0")] - pub(crate) amount: u64, - - /// Path to a directory used to store recovery files for unconsumed deposits - #[clap(long)] - pub(crate) recovery_dir: PathBuf, } pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { - if args.amount == 0 { - bail!("did not specify credential amount") - } - let loaded = CommonConfigsWrapper::try_load(args.client_config)?; if let Ok(id) = loaded.try_get_id() { @@ -40,16 +28,18 @@ pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { bail!("the loaded config does not have a credentials store information") }; + let Ok(private_id_key) = loaded.try_get_private_id_key() else { + bail!("the loaded config does not have a public id key information") + }; + println!( "using credentials store at '{}'", credentials_store.display() ); - let denom = &client.current_chain_details().mix_denom.base; - let coin = Coin::new(args.amount as u128, denom); - let persistent_storage = initialise_persistent_storage(credentials_store).await; - utils::issue_credential(&client, coin, &persistent_storage, args.recovery_dir).await?; + let private_id_key: identity::PrivateKey = nym_pemstore::load_key(private_id_key)?; + utils::issue_credential(&client, &persistent_storage, &private_id_key.to_bytes()).await?; Ok(()) } diff --git a/common/commands/src/coconut/mod.rs b/common/commands/src/coconut/mod.rs index 700c7d521f..74421dd42b 100644 --- a/common/commands/src/coconut/mod.rs +++ b/common/commands/src/coconut/mod.rs @@ -3,22 +3,20 @@ use clap::{Args, Subcommand}; -pub mod generate_freepass; -pub mod import_credential; -pub mod issue_credentials; -pub mod recover_credentials; +pub mod import_ticket_book; +pub mod issue_ticket_book; +pub mod recover_ticket_book; #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] -pub struct Coconut { +pub struct Ecash { #[clap(subcommand)] - pub command: CoconutCommands, + pub command: EcashCommands, } #[derive(Debug, Subcommand)] -pub enum CoconutCommands { - GenerateFreepass(generate_freepass::Args), - IssueCredentials(issue_credentials::Args), - RecoverCredentials(recover_credentials::Args), - ImportCredential(import_credential::Args), +pub enum EcashCommands { + IssueTicketBook(issue_ticket_book::Args), + RecoverTicketBook(recover_ticket_book::Args), + ImportTicketBook(import_ticket_book::Args), } diff --git a/common/commands/src/coconut/recover_credentials.rs b/common/commands/src/coconut/recover_ticket_book.rs similarity index 69% rename from common/commands/src/coconut/recover_credentials.rs rename to common/commands/src/coconut/recover_ticket_book.rs index 025ea68c2c..8bd5c8c960 100644 --- a/common/commands/src/coconut/recover_credentials.rs +++ b/common/commands/src/coconut/recover_ticket_book.rs @@ -6,7 +6,7 @@ use crate::utils::CommonConfigsWrapper; use anyhow::bail; use clap::Parser; use nym_credential_storage::initialise_persistent_storage; -use nym_credential_utils::{recovery_storage, utils}; +use nym_credential_utils::utils; use std::path::PathBuf; #[derive(Debug, Parser)] @@ -14,10 +14,6 @@ pub struct Args { /// Config file of the client that is supposed to use the credential. #[clap(long)] pub(crate) client_config: PathBuf, - - /// Path to a directory used to store recovery files for unconsumed deposits - #[clap(long)] - pub(crate) recovery_dir: PathBuf, } pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> { @@ -37,12 +33,9 @@ pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> { ); let persistent_storage = initialise_persistent_storage(credentials_store).await; - let recovery_storage = recovery_storage::RecoveryStorage::new(args.recovery_dir)?; - let recovered = - utils::recover_credentials(&client, &recovery_storage, &persistent_storage).await?; + let recovered = utils::recover_deposits(&client, &persistent_storage).await?; - // TODO: denom? - println!("recovered {recovered} worth of credentials"); + println!("recovered {recovered} ticketbooks"); Ok(()) } diff --git a/common/commands/src/utils.rs b/common/commands/src/utils.rs index 25797c71e9..0d6e40ffbc 100644 --- a/common/commands/src/utils.rs +++ b/common/commands/src/utils.rs @@ -123,6 +123,21 @@ impl CommonConfigsWrapper { } } + pub(crate) fn try_get_private_id_key(&self) -> anyhow::Result { + match self { + CommonConfigsWrapper::NymClients(cfg) => Ok(cfg + .storage_paths + .inner + .keys + .private_identity_key_file + .clone()), + CommonConfigsWrapper::NymApi(_cfg) => { + todo!() //SW this will depend on the new network monitor structure. Ping @Drazen + } + CommonConfigsWrapper::Unknown(cfg) => cfg.try_get_private_id_key(), + } + } + pub(crate) fn try_get_credentials_store(&self) -> anyhow::Result { match self { CommonConfigsWrapper::NymClients(cfg) => { @@ -225,4 +240,17 @@ impl UnknownConfigWrapper { bail!("no 'credentials_database_path' field present in the config") } } + + pub(crate) fn try_get_private_id_key(&self) -> anyhow::Result { + let id_val = self + .find_value("keys.private_identity_key_file") + .ok_or_else(|| { + anyhow!("no 'keys.private_identity_key_file' field present in the config") + })?; + if let toml::Value::String(pub_id_key) = id_val { + Ok(pub_id_key.parse()?) + } else { + bail!("no 'keys.private_identity_key_file' field present in the config") + } + } } diff --git a/common/commands/src/validator/cosmwasm/generators/coconut_bandwidth.rs b/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs similarity index 68% rename from common/commands/src/validator/cosmwasm/generators/coconut_bandwidth.rs rename to common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs index 3deb1efe01..c3632b1130 100644 --- a/common/commands/src/validator/cosmwasm/generators/coconut_bandwidth.rs +++ b/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs @@ -6,17 +6,20 @@ use std::str::FromStr; use clap::Parser; use log::{debug, info}; -use nym_coconut_bandwidth_contract_common::msg::InstantiateMsg; +use nym_ecash_contract_common::msg::InstantiateMsg; use nym_validator_client::nyxd::AccountId; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub pool_addr: String, + pub group_addr: Option, #[clap(long)] pub multisig_addr: Option, + #[clap(long)] + pub holding_account: AccountId, + #[clap(long)] pub mix_denom: Option, } @@ -26,8 +29,15 @@ pub async fn generate(args: Args) { debug!("Received arguments: {:?}", args); + let group_addr = args.group_addr.unwrap_or_else(|| { + let address = std::env::var(nym_network_defaults::var_names::GROUP_CONTRACT_ADDRESS) + .expect("Multisig address has to be set"); + AccountId::from_str(address.as_str()) + .expect("Failed converting multisig address to AccountId") + }); + let multisig_addr = args.multisig_addr.unwrap_or_else(|| { - let address = std::env::var(nym_network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS) + let address = std::env::var(nym_network_defaults::var_names::MULTISIG_CONTRACT_ADDRESS) .expect("Multisig address has to be set"); AccountId::from_str(address.as_str()) .expect("Failed converting multisig address to AccountId") @@ -38,7 +48,8 @@ pub async fn generate(args: Args) { }); let instantiate_msg = InstantiateMsg { - pool_addr: args.pool_addr, + holding_account: args.holding_account.to_string(), + group_addr: group_addr.to_string(), multisig_addr: multisig_addr.to_string(), mix_denom, }; diff --git a/common/commands/src/validator/cosmwasm/generators/mod.rs b/common/commands/src/validator/cosmwasm/generators/mod.rs index 85bd5a93e2..8829ec60fe 100644 --- a/common/commands/src/validator/cosmwasm/generators/mod.rs +++ b/common/commands/src/validator/cosmwasm/generators/mod.rs @@ -3,8 +3,8 @@ use clap::{Args, Subcommand}; -pub mod coconut_bandwidth; pub mod coconut_dkg; +pub mod ecash_bandwidth; pub mod mixnet; pub mod multisig; pub mod vesting; @@ -18,7 +18,7 @@ pub struct GenerateMessage { #[derive(Debug, Subcommand)] pub enum GenerateMessageCommands { - CoconutBandwidth(coconut_bandwidth::Args), + EcashBandwidth(ecash_bandwidth::Args), CoconutDKG(coconut_dkg::Args), Mixnet(mixnet::Args), Multisig(multisig::Args), diff --git a/common/commands/src/validator/cosmwasm/generators/multisig.rs b/common/commands/src/validator/cosmwasm/generators/multisig.rs index 37f05a5e71..90abae9e28 100644 --- a/common/commands/src/validator/cosmwasm/generators/multisig.rs +++ b/common/commands/src/validator/cosmwasm/generators/multisig.rs @@ -22,7 +22,7 @@ pub struct Args { pub max_voting_period: u64, #[clap(long)] - pub coconut_bandwidth_contract_address: Option, + pub ecash_contract_address: Option, #[clap(long)] pub coconut_dkg_contract_address: Option, @@ -33,14 +33,12 @@ pub async fn generate(args: Args) { debug!("Received arguments: {:?}", args); - let coconut_bandwidth_contract_address = - args.coconut_bandwidth_contract_address.unwrap_or_else(|| { - let address = - std::env::var(nym_network_defaults::var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS) - .expect("Coconut bandwidth contract address has to be set"); - AccountId::from_str(address.as_str()) - .expect("Failed converting bandwidth contract address to AccountId") - }); + let ecash_contract_address = args.ecash_contract_address.unwrap_or_else(|| { + let address = std::env::var(nym_network_defaults::var_names::ECASH_CONTRACT_ADDRESS) + .expect("Coconut bandwidth contract address has to be set"); + AccountId::from_str(address.as_str()) + .expect("Failed converting bandwidth contract address to AccountId") + }); let coconut_dkg_contract_address = args.coconut_dkg_contract_address.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::COCONUT_DKG_CONTRACT_ADDRESS) @@ -58,7 +56,7 @@ pub async fn generate(args: Args) { max_voting_period: Duration::Time(args.max_voting_period), executor: None, proposal_deposit: None, - coconut_bandwidth_contract_address: coconut_bandwidth_contract_address.to_string(), + coconut_bandwidth_contract_address: ecash_contract_address.to_string(), coconut_dkg_contract_address: coconut_dkg_contract_address.to_string(), }; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index 0b3da8a552..f9d1dc0bb0 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -87,6 +87,9 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(u64))] GetCurrentEpochThreshold {}, + #[cfg_attr(feature = "schema", returns(u64))] + GetEpochThreshold { epoch_id: EpochId }, + #[cfg_attr(feature = "schema", returns(StateAdvanceResponse))] CanAdvanceState {}, diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/events.rs b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs index 39d5c0007a..a8fdf9b816 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/events.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use cosmwasm_std::Event; +use std::str::FromStr; /// Looks up value of particular attribute in the provided event. If it fails to find it, /// the function panics. @@ -31,6 +32,23 @@ pub fn may_find_attribute(event: &Event, key: &str) -> Option { None } +pub fn try_find_attribute( + events: &[Event], + event_name: &str, + key: &str, +) -> Option> +where + T: FromStr, +{ + for event in events { + if event.ty == event_name { + let value = may_find_attribute(event, key)?; + return Some(value.parse()); + } + } + None +} + pub trait OptionallyAddAttribute { fn add_optional_attribute( self, diff --git a/common/cosmwasm-smart-contracts/ecash-contract/Cargo.toml b/common/cosmwasm-smart-contracts/ecash-contract/Cargo.toml new file mode 100644 index 0000000000..54bc53fe18 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nym-ecash-contract-common" +version = "0.1.0" +edition = "2021" +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bs58.workspace = true +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw2 = { workspace = true, optional = true } +nym-multisig-contract-common = { path = "../multisig-contract" } +thiserror.workspace = true +cw-utils = { workspace = true } +cw-controllers = { workspace = true } + + +[features] +schema = ["cw2"] diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/blacklist.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/blacklist.rs new file mode 100644 index 0000000000..00b6dfc6ae --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/blacklist.rs @@ -0,0 +1,71 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct BlacklistedAccount { + pub public_key: String, + pub info: Blacklisting, +} + +impl From<(String, Blacklisting)> for BlacklistedAccount { + fn from((public_key, info): (String, Blacklisting)) -> Self { + BlacklistedAccount { public_key, info } + } +} + +#[cw_serde] +pub struct Blacklisting { + pub proposal_id: u64, + pub finalized_at_height: Option, +} + +impl Blacklisting { + pub fn new(proposal_id: u64) -> Self { + Blacklisting { + proposal_id, + finalized_at_height: None, + } + } +} + +impl BlacklistedAccount { + pub fn public_key(&self) -> &str { + &self.public_key + } +} + +#[cw_serde] +pub struct PagedBlacklistedAccountResponse { + pub accounts: Vec, + pub per_page: usize, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +impl PagedBlacklistedAccountResponse { + pub fn new( + accounts: Vec, + per_page: usize, + start_next_after: Option, + ) -> Self { + PagedBlacklistedAccountResponse { + accounts, + per_page, + start_next_after, + } + } +} + +#[cw_serde] +pub struct BlacklistedAccountResponse { + pub account: Option, +} + +impl BlacklistedAccountResponse { + pub fn new(account: Option) -> Self { + BlacklistedAccountResponse { account } + } +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/deposit.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/deposit.rs new file mode 100644 index 0000000000..86efdbd5fa --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/deposit.rs @@ -0,0 +1,76 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::EcashContractError; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{StdError, StdResult}; + +pub type DepositId = u32; + +#[cw_serde] +pub struct Deposit { + pub bs58_encoded_ed25519_pubkey: String, +} + +impl Deposit { + pub fn new(bs58_encoded_ed25519_pubkey: String) -> Self { + Deposit { + bs58_encoded_ed25519_pubkey, + } + } + + pub fn get_ed25519_pubkey_bytes(raw: &str) -> Result<[u8; 32], EcashContractError> { + let mut ed25519_pubkey_bytes = [0u8; 32]; + bs58::decode(raw) + .onto(&mut ed25519_pubkey_bytes) + .map_err(|_| EcashContractError::MalformedEd25519Identity)?; + + Ok(ed25519_pubkey_bytes) + } + + pub fn encode_pubkey_bytes(raw: &[u8]) -> String { + bs58::encode(raw).into_string() + } + + pub fn to_bytes(&self) -> Result<[u8; 32], EcashContractError> { + Self::get_ed25519_pubkey_bytes(&self.bs58_encoded_ed25519_pubkey) + } + + pub fn try_from_bytes(bytes: &[u8]) -> StdResult { + if bytes.len() != 32 { + return Err(StdError::generic_err("malformed deposit data")); + } + + Ok(Deposit { + bs58_encoded_ed25519_pubkey: Self::encode_pubkey_bytes(bytes), + }) + } +} + +#[cw_serde] +pub struct DepositResponse { + pub id: DepositId, + + pub deposit: Option, +} + +#[cw_serde] +pub struct DepositData { + pub id: DepositId, + + pub deposit: Deposit, +} + +impl From<(DepositId, Deposit)> for DepositData { + fn from((id, deposit): (DepositId, Deposit)) -> Self { + DepositData { id, deposit } + } +} + +#[cw_serde] +pub struct PagedDepositsResponse { + pub deposits: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/error.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/error.rs new file mode 100644 index 0000000000..3c7778d553 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/error.rs @@ -0,0 +1,68 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Coin, StdError}; +use cw_controllers::AdminError; +use cw_utils::PaymentError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum EcashContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error("Invalid deposit")] + InvalidDeposit(#[from] PaymentError), + + #[error("received wrong amount for deposit. got: {received}. required: {amount}")] + WrongAmount { received: u128, amount: u128 }, + + #[error("There aren't enough funds in the contract")] + NotEnoughFunds, + + #[error(transparent)] + Admin(#[from] AdminError), + + #[error("could not find proposal id inside the multisig reply SubMsg")] + MissingProposalId, + + // realistically this should NEVER be thrown + #[error("the proposal id returned by the multisig contract could not be parsed into an u64")] + MalformedProposalId, + + #[error("Group contract invalid address '{addr}'")] + InvalidGroup { addr: String }, + + #[error("Unauthorized")] + Unauthorized, + + #[error("Failed to parse {value} into a valid SemVer version: {error_message}")] + SemVerFailure { + value: String, + error_message: String, + }, + + #[error("received an invalid reply id: {id}. it does not correspond to any sent SubMsg")] + InvalidReplyId { id: u64 }, + + #[error("reached the maximum of 255 different deposit types")] + MaximumDepositTypesReached, + + #[error("compressed deposit info {typ} does not corresponds to any known type")] + UnknownCompressedDepositInfoType { typ: u8 }, + + #[error("deposit info {typ} does not corresponds to any previously seen type")] + UnknownDepositInfoType { typ: String }, + + #[error("the provided ed25519 identity was malformed")] + MalformedEd25519Identity, + + #[error("the required deposit amount has changed since the contract was created! This was not expected! It used to be {at_init} but it's {current} now! Please let the developers know ASAP!")] + DepositAmountChanged { at_init: Coin, current: Coin }, + + #[error("the e-cash ticket value has changed since the contract was created! This was not expected! It used to be {at_init} but it's {current} now! Please let the developers know ASAP!")] + TicketValueChanged { at_init: Coin, current: Coin }, + + #[error("the provided tickets redemption commitment is malformed")] + MalformedRedemptionCommitment, +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/event_attributes.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/event_attributes.rs new file mode 100644 index 0000000000..80f5daf68a --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/event_attributes.rs @@ -0,0 +1,4 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const BANDWIDTH_PROPOSAL_ID: &str = "proposal_id"; diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/events.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/events.rs new file mode 100644 index 0000000000..79892aa2b0 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/events.rs @@ -0,0 +1,18 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// event types +pub const DEPOSITED_FUNDS_EVENT_TYPE: &str = "deposited-funds"; + +// a 'wasm-' prefix is added to all cosmwasm events +pub const COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE: &str = "wasm-deposited-funds"; + +pub const DEPOSIT_ID: &str = "deposit-id"; + +pub const TICKET_BOOK_VALUE: u128 = 50_000_000; +pub const TICKET_VALUE: u128 = 50_000; + +pub const WASM_EVENT_NAME: &str = "wasm"; +pub const PROPOSAL_ID_ATTRIBUTE_NAME: &str = "proposal_id"; +pub const BLACKLIST_PROPOSAL_REPLY_ID: u64 = 7759; +pub const REDEMPTION_PROPOSAL_REPLY_ID: u64 = 2137; diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/lib.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/lib.rs new file mode 100644 index 0000000000..10e07c1009 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/lib.rs @@ -0,0 +1,12 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod blacklist; +pub mod deposit; +pub mod error; +pub mod event_attributes; +pub mod events; +pub mod msg; +pub mod redeem_credential; + +pub use error::EcashContractError; diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/msg.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/msg.rs new file mode 100644 index 0000000000..4b5c965978 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/msg.rs @@ -0,0 +1,76 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; + +#[cfg(feature = "schema")] +use crate::blacklist::{BlacklistedAccountResponse, PagedBlacklistedAccountResponse}; +#[cfg(feature = "schema")] +use crate::deposit::{DepositResponse, PagedDepositsResponse}; +#[cfg(feature = "schema")] +use cosmwasm_schema::QueryResponses; +#[cfg(feature = "schema")] +use cosmwasm_std::Coin; + +#[cw_serde] +pub struct InstantiateMsg { + pub holding_account: String, + pub multisig_addr: String, + pub group_addr: String, + pub mix_denom: String, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Used by clients to request ticket books from the signers + DepositTicketBookFunds { + identity_key: String, + }, + + /// Used by gateways to batch redeem tokens from the spent tickets + RequestRedemption { + commitment_bs58: String, + number_of_tickets: u16, + }, + + /// The actual message that gets executed, after multisig votes, that transfers the ticket tokens into gateway's (and the holding) account + RedeemTickets { + n: u16, + gw: String, + }, + // SpendCredential { + // serial_number: String, + // gateway_cosmos_address: String, + // }, + ProposeToBlacklist { + public_key: String, + }, + AddToBlacklist { + public_key: String, + }, +} + +#[cw_serde] +#[cfg_attr(feature = "schema", derive(QueryResponses))] +pub enum QueryMsg { + #[cfg_attr(feature = "schema", returns(BlacklistedAccountResponse))] + GetBlacklistedAccount { public_key: String }, + + #[cfg_attr(feature = "schema", returns(PagedBlacklistedAccountResponse))] + GetBlacklistPaged { + limit: Option, + start_after: Option, + }, + + #[cfg_attr(feature = "schema", returns(Coin))] + GetRequiredDepositAmount {}, + + #[cfg_attr(feature = "schema", returns(DepositResponse))] + GetDeposit { deposit_id: u32 }, + + #[cfg_attr(feature = "schema", returns(PagedDepositsResponse))] + GetDepositsPaged { + limit: Option, + start_after: Option, + }, +} diff --git a/common/cosmwasm-smart-contracts/ecash-contract/src/redeem_credential.rs b/common/cosmwasm-smart-contracts/ecash-contract/src/redeem_credential.rs new file mode 100644 index 0000000000..76bf86f5a8 --- /dev/null +++ b/common/cosmwasm-smart-contracts/ecash-contract/src/redeem_credential.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// TODO: to be moved to multisig +pub const BATCH_REDEMPTION_PROPOSAL_TITLE: &str = "ecash-redemption"; diff --git a/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs b/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs index ead941cad0..39ba25b38c 100644 --- a/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/multisig-contract/src/error.rs @@ -47,4 +47,10 @@ pub enum ContractError { #[error("{0}")] Deposit(#[from] DepositError), + + #[error("the provided redemption digest does not have valid base58 encoding or is not 32 bytes long")] + MalformedRedemptionDigest, + + #[error("the provided redemption proposal data is malformed and can't be decoded")] + MalformedRedemptionProposalData, } diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index 056d94b12b..b822d49db6 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -8,22 +8,31 @@ license.workspace = true [dependencies] async-trait = { workspace = true } +bincode = { workspace = true, optional = true } log = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true, features = ["sync"]} +serde = { workspace = true, features = ["derive"], optional = true } +tokio = { workspace = true, features = ["sync"] } zeroize = { workspace = true, features = ["zeroize_derive"] } +nym-credentials = { path = "../credentials" } +nym-compact-ecash = { path = "../nym_offline_compact_ecash" } +nym-ecash-time = { path = "../ecash-time" } + [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true -features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] +features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] workspace = true -features = [ "rt-multi-thread", "net", "signal", "fs" ] +features = ["rt-multi-thread", "net", "signal", "fs"] [build-dependencies] sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +[features] +persistent-storage = ["bincode", "serde"] \ No newline at end of file diff --git a/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql b/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql new file mode 100644 index 0000000000..10b585d096 --- /dev/null +++ b/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql @@ -0,0 +1,66 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +DROP TABLE coconut_credentials; + +CREATE TABLE master_verification_key ( + epoch_id INTEGER PRIMARY KEY NOT NULL, + + serialised_key BLOB NOT NULL +); + +CREATE TABLE coin_indices_signatures +( + epoch_id INTEGER PRIMARY KEY NOT NULL, + + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE expiration_date_signatures ( + expiration_date DATE NOT NULL UNIQUE PRIMARY KEY, + + epoch_id INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL +); + + +CREATE TABLE ecash_ticketbook +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + + -- introduce a way for us to introduce breaking changes in serialization of data + serialization_revision INTEGER NOT NULL, + + -- the actual crypto data of the ticketbook (wallet, keys, etc.) + ticketbook_data BLOB NOT NULL UNIQUE, + + -- for each ticketbook we MUST have corresponding expiration date signatures + expiration_date DATE NOT NULL REFERENCES expiration_date_signatures(expiration_date), + + -- for each ticketbook we MUST have corresponding coin index signatures + epoch_id INTEGER NOT NULL REFERENCES coin_indices_signatures(epoch_id), + + -- the initial number of tickets the wallet has been created for + total_tickets INTEGER NOT NULL, + + -- how many tickets have been used so far (the `l` value of the wallet) + used_tickets INTEGER NOT NULL +); + +-- data for ticketbooks that have an associated deposit, but failed to get issued +CREATE TABLE pending_issuance +( + deposit_id INTEGER NOT NULL PRIMARY KEY, + + -- introduce a way for us to introduce breaking changes in serialization of data + serialization_revision INTEGER NOT NULL, + + pending_ticketbook_data BLOB NOT NULL UNIQUE, + + -- for each ticketbook we MUST have corresponding expiration date signatures + expiration_date DATE NOT NULL REFERENCES expiration_date_signatures(expiration_date) +); \ No newline at end of file diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index 78d18f3b88..998c25cdeb 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -1,23 +1,34 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{CredentialUsage, StoredIssuedCredential}; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; +use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; +use zeroize::Zeroizing; #[derive(Clone)] -pub struct CoconutCredentialManager { - inner: Arc>, +pub struct MemoryEcachTicketbookManager { + inner: Arc>, } #[derive(Default)] -struct CoconutCredentialManagerInner { - credentials: Vec, - credential_usage: Vec, +struct EcashCredentialManagerInner { + ticketbooks: HashMap, + pending: HashMap, + master_vk: HashMap, + coin_indices_sigs: HashMap>, + expiration_date_sigs: HashMap>, _next_id: i64, } -impl CoconutCredentialManagerInner { +impl EcashCredentialManagerInner { fn next_id(&mut self) -> i64 { let next = self._next_id; self._next_id += 1; @@ -25,108 +36,209 @@ impl CoconutCredentialManagerInner { } } -impl CoconutCredentialManager { +// hehe, that's hacky AF, but it works as a **TEMPORARY** workaround +fn hack_clone_ticketbook(book: &IssuedTicketBook) -> IssuedTicketBook { + let ser = book.pack(); + let data = Zeroizing::new(ser.data); + IssuedTicketBook::try_unpack(&data, None).unwrap() +} + +impl MemoryEcachTicketbookManager { /// Creates new empty instance of the `CoconutCredentialManager`. pub fn new() -> Self { - CoconutCredentialManager { + MemoryEcachTicketbookManager { inner: Default::default(), } } - pub async fn insert_issued_credential( - &self, - credential_type: String, - serialization_revision: u8, - credential_data: &[u8], - epoch_id: u32, - ) { - let mut inner = self.inner.write().await; - let id = inner.next_id(); - inner.credentials.push(StoredIssuedCredential { - id, - serialization_revision, - credential_data: credential_data.to_vec(), - credential_type, - epoch_id, - expired: false, - }) - } - - async fn bandwidth_voucher_spent(&self, id: i64) -> bool { - self.inner - .read() - .await - .credential_usage - .iter() - .any(|c| c.credential_id == id) - } - - async fn freepass_spent(&self, id: i64, gateway_id: &str) -> bool { - self.inner - .read() - .await - .credential_usage - .iter() - .any(|c| c.credential_id == id && c.gateway_id_bs58 == gateway_id) - } - - /// Tries to retrieve one of the stored, unused credentials. - pub async fn get_next_unspect_bandwidth_voucher(&self) -> Option { - let guard = self.inner.read().await; - for credential in guard - .credentials - .iter() - .filter(|c| c.credential_type == "BandwidthVoucher") - { - if !self.bandwidth_voucher_spent(credential.id).await { - return Some(credential.clone()); - } - } - None - } - - pub async fn get_next_unspect_freepass( - &self, - gateway_id: &str, - ) -> Option { - let guard = self.inner.read().await; - for credential in guard - .credentials - .iter() - .filter(|c| c.credential_type == "FreeBandwidthPass") - { - if credential.expired { - continue; - } - if !self.freepass_spent(credential.id, gateway_id).await { - return Some(credential.clone()); - } - } - None - } - - /// Consumes in the database the specified credential. - /// - /// # Arguments - /// - /// * `id`: Database id. - pub async fn consume_coconut_credential(&self, id: i64, gateway_id: &str) { + pub(crate) async fn cleanup_expired(&self) { let mut guard = self.inner.write().await; - guard.credential_usage.push(CredentialUsage { - credential_id: id, - gateway_id_bs58: gateway_id.to_string(), - }); + + let mut to_remove = Vec::new(); + + for t in guard.ticketbooks.values() { + if t.ticketbook.expired() { + to_remove.push(t.ticketbook_id); + } + } + + for id in to_remove { + guard.ticketbooks.remove(&id); + } } - /// Marks the specified credential as expired - /// - /// # Arguments - /// - /// * `id`: Id of the credential to mark as expired. - pub async fn mark_expired(&self, id: i64) { - let mut creds = self.inner.write().await; - if let Some(cred) = creds.credentials.get_mut(id as usize) { - cred.expired = true; + pub async fn get_next_unspent_ticketbook_and_update( + &self, + tickets: u32, + ) -> Option { + let mut guard = self.inner.write().await; + + for t in guard.ticketbooks.values_mut() { + if !t.ticketbook.expired() + && t.ticketbook.spent_tickets() + tickets as u64 + <= t.ticketbook.params_total_tickets() + { + t.ticketbook + .update_spent_tickets(t.ticketbook.spent_tickets() + tickets as u64); + return Some(RetrievedTicketbook { + ticketbook_id: t.ticketbook_id, + ticketbook: hack_clone_ticketbook(&t.ticketbook), + }); + } } + + None + } + + pub(crate) async fn revert_ticketbook_withdrawal( + &self, + ticketbook_id: i64, + withdrawn: u32, + expected_current_total_spent: u32, + ) -> bool { + let mut guard = self.inner.write().await; + + let Some(book) = guard.ticketbooks.get_mut(&ticketbook_id) else { + return false; + }; + + if book.ticketbook.spent_tickets() == expected_current_total_spent as u64 { + book.ticketbook + .update_spent_tickets(book.ticketbook.spent_tickets() - withdrawn as u64); + true + } else { + false + } + } + + pub(crate) async fn insert_pending_ticketbook(&self, ticketbook: &IssuanceTicketBook) { + let mut guard = self.inner.write().await; + + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + let id = ticketbook.deposit_id() as i64; + guard.pending.insert( + id, + RetrievedPendingTicketbook { + pending_id: ticketbook.deposit_id() as i64, + pending_ticketbook: IssuanceTicketBook::try_unpack(&data, None).unwrap(), + }, + ); + } + + pub(crate) async fn get_pending_ticketbooks(&self) -> Vec { + let guard = self.inner.read().await; + + let mut pending = Vec::new(); + + for p in guard.pending.values() { + // 🫠 + let ser = p.pending_ticketbook.pack(); + let data = Zeroizing::new(ser.data); + pending.push(RetrievedPendingTicketbook { + pending_id: p.pending_id, + pending_ticketbook: IssuanceTicketBook::try_unpack(&data, None).unwrap(), + }) + } + + pending + } + + pub(crate) async fn remove_pending_ticketbook(&self, pending_id: i64) { + let mut guard = self.inner.write().await; + + guard.pending.remove(&pending_id); + } + + pub(crate) async fn insert_new_ticketbook(&self, ticketbook: &IssuedTicketBook) { + let mut guard = self.inner.write().await; + let id = guard.next_id(); + + // hehe, that's hacky AF, but it works as a **TEMPORARY** workaround + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + guard.ticketbooks.insert( + id, + RetrievedTicketbook { + ticketbook_id: id, + ticketbook: IssuedTicketBook::try_unpack(&data, None).unwrap(), + }, + ); + } + + pub(crate) async fn get_ticketbooks_info(&self) -> Vec { + let guard = self.inner.read().await; + + guard + .ticketbooks + .values() + .map(|t| BasicTicketbookInformation { + id: t.ticketbook_id, + expiration_date: t.ticketbook.expiration_date(), + epoch_id: t.ticketbook.epoch_id() as u32, + total_tickets: t.ticketbook.spent_tickets() as u32, + used_tickets: t.ticketbook.params_total_tickets() as u32, + }) + .collect() + } + + pub(crate) async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Option { + let guard = self.inner.read().await; + + guard.master_vk.get(&epoch_id).cloned() + } + + pub(crate) async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) { + let mut guard = self.inner.write().await; + + guard.master_vk.insert(epoch_id, key.clone()); + } + + pub(crate) async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Option> { + let guard = self.inner.read().await; + + guard.coin_indices_sigs.get(&epoch_id).cloned() + } + + pub(crate) async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + sigs: &[AnnotatedCoinIndexSignature], + ) { + let mut guard = self.inner.write().await; + + guard.coin_indices_sigs.insert(epoch_id, sigs.to_vec()); + } + + pub(crate) async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Option> { + let guard = self.inner.read().await; + + guard.expiration_date_sigs.get(&expiration_date).cloned() + } + + pub(crate) async fn insert_expiration_date_signatures( + &self, + _epoch_id: u64, + expiration_date: Date, + sigs: &[AnnotatedExpirationDateSignature], + ) { + let mut guard = self.inner.write().await; + + guard + .expiration_date_sigs + .insert(expiration_date, sigs.to_vec()); } } diff --git a/common/credential-storage/src/backends/mod.rs b/common/credential-storage/src/backends/mod.rs index 2cd1334af3..bdb8eb6d9c 100644 --- a/common/credential-storage/src/backends/mod.rs +++ b/common/credential-storage/src/backends/mod.rs @@ -2,5 +2,5 @@ // SPDX-License-Identifier: Apache-2.0 pub mod memory; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))] pub mod sqlite; diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index 4d4face955..a3b8a00349 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -1,116 +1,279 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::StoredIssuedCredential; +use crate::models::{ + BasicTicketbookInformation, RawExpirationDateSignatures, StoredIssuedTicketbook, + StoredPendingTicketbook, +}; +use nym_ecash_time::Date; +use sqlx::{Executor, Sqlite, Transaction}; #[derive(Clone)] -pub struct CoconutCredentialManager { +pub struct SqliteEcashTicketbookManager { connection_pool: sqlx::SqlitePool, } -impl CoconutCredentialManager { - /// Creates new instance of the `CoconutCredentialManager` with the provided sqlite connection pool. +impl SqliteEcashTicketbookManager { + /// Creates new instance of the `EcashTicketbookManager` with the provided sqlite connection pool. /// /// # Arguments /// /// * `connection_pool`: database connection pool to use. pub fn new(connection_pool: sqlx::SqlitePool) -> Self { - CoconutCredentialManager { connection_pool } + SqliteEcashTicketbookManager { connection_pool } } - pub async fn insert_issued_credential( + pub(crate) async fn cleanup_expired(&self, deadline: Date) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM ecash_ticketbook WHERE expiration_date <= ?", + deadline + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn begin_storage_tx(&self) -> Result, sqlx::Error> { + self.connection_pool.begin().await + } + + pub(crate) async fn insert_pending_ticketbook( &self, - credential_type: String, - serialization_revision: u8, - credential_data: &[u8], + serialisation_revision: u8, + deposit_id: u32, + data: &[u8], + expiration_date: Date, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO pending_issuance + (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date) + VALUES (?, ?, ?, ?) + "#, + deposit_id, + serialisation_revision, + data, + expiration_date, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_new_ticketbook( + &self, + serialisation_revision: u8, + data: &[u8], + expiration_date: Date, epoch_id: u32, + total_tickets: u32, + used_tickets: u32, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO coconut_credentials(serialization_revision, credential_type, credential_data, epoch_id, expired) - VALUES (?, ?, ?, ?, false) + INSERT INTO ecash_ticketbook + (serialization_revision, ticketbook_data, expiration_date, epoch_id, total_tickets, used_tickets) + VALUES (?, ?, ?, ?, ?, ?) "#, - serialization_revision, credential_type, credential_data, epoch_id + serialisation_revision, + data, + expiration_date, + epoch_id, + total_tickets, + used_tickets, ).execute(&self.connection_pool).await?; + Ok(()) } - pub async fn get_next_unspect_freepass( + pub(crate) async fn get_ticketbooks_info( &self, - gateway_id: &str, - ) -> Result, sqlx::Error> { - // get a credential of freepass type that doesn't appear in `credential_usage` for the provided gateway_id + ) -> Result, sqlx::Error> { sqlx::query_as( r#" - SELECT * - FROM coconut_credentials - WHERE coconut_credentials.credential_type == "FreeBandwidthPass" AND coconut_credentials.expired = false - AND NOT EXISTS (SELECT 1 - FROM credential_usage - WHERE credential_usage.credential_id = coconut_credentials.id - AND credential_usage.gateway_id_bs58 == ?) - ORDER BY coconut_credentials.id - LIMIT 1 - "#, + SELECT id, expiration_date, epoch_id, total_tickets, used_tickets + FROM ecash_ticketbook + "#, ) - .bind(gateway_id) - .fetch_optional(&self.connection_pool) + .fetch_all(&self.connection_pool) .await } - pub async fn get_next_unspect_bandwidth_voucher( + pub(crate) async fn decrease_used_ticketbook_tickets( &self, - ) -> Result, sqlx::Error> { - // get a credential of bandwidth voucher type that doesn't appear in `credential_usage` for any gateway_id - sqlx::query_as( + ticketbook_id: i64, + reverted_spent: u32, + expected_current_total_spent: u32, + ) -> Result { + // the 'AND' clause will ensure this will only be executed if nobody else interacted with the row + let affected = sqlx::query!( r#" - SELECT * - FROM coconut_credentials - WHERE coconut_credentials.credential_type == "BandwidthVoucher" - AND NOT EXISTS (SELECT 1 - FROM credential_usage - WHERE credential_usage.credential_id = coconut_credentials.id) - ORDER BY coconut_credentials.id - LIMIT 1 + UPDATE ecash_ticketbook + SET used_tickets = used_tickets - ? + WHERE id = ? + AND used_tickets = ? "#, + reverted_spent, + ticketbook_id, + expected_current_total_spent ) - .fetch_optional(&self.connection_pool) - .await + .execute(&self.connection_pool) + .await? + .rows_affected(); + Ok(affected > 0) } - /// Consumes in the database the specified credential. - /// - /// # Arguments - /// - /// * `id`: Database id. - /// * `gateway_id`: id of the gateway that received the credential - pub async fn consume_coconut_credential( + pub(crate) async fn get_pending_ticketbooks( &self, - id: i64, - gateway_id: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM pending_issuance") + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn remove_pending_ticketbook( + &self, + pending_id: i64, ) -> Result<(), sqlx::Error> { sqlx::query!( - "INSERT INTO credential_usage (credential_id, gateway_id_bs58) VALUES (?, ?)", - id, - gateway_id + "DELETE FROM pending_issuance WHERE deposit_id = ?", + pending_id ) .execute(&self.connection_pool) .await?; Ok(()) } - /// Marks the specified credential as expired - /// - /// # Arguments - /// - /// * `id`: Id of the credential to mark as expired. - pub async fn mark_expired(&self, id: i64) -> Result<(), sqlx::Error> { + pub(crate) async fn get_master_verification_key( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { sqlx::query!( - "UPDATE coconut_credentials SET expired = TRUE WHERE id = ?", - id + "SELECT serialised_key FROM master_verification_key WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_key)) + } + + pub(crate) async fn insert_master_verification_key( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO master_verification_key(epoch_id, serialised_key) VALUES (?, ?)", + epoch_id, + data ) .execute(&self.connection_pool) .await?; Ok(()) } + + pub(crate) async fn get_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_signatures FROM coin_indices_signatures WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_signatures)) + } + + pub(crate) async fn insert_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO coin_indices_signatures(epoch_id, serialised_signatures) VALUES (?, ?)", + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawExpirationDateSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures + FROM expiration_date_signatures + WHERE expiration_date = ? + "#, + expiration_date + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn insert_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)", + expiration_date, + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } +} + +pub(crate) async fn get_next_unspent_ticketbook<'a, E>( + executor: E, + deadline: Date, + tickets: u32, +) -> Result, sqlx::Error> +where + E: Executor<'a, Database = Sqlite>, +{ + sqlx::query_as( + r#" + SELECT * + FROM ecash_ticketbook + WHERE used_tickets + ? <= total_tickets + AND expiration_date >= ? + ORDER BY expiration_date ASC + LIMIT 1 + "#, + ) + .bind(tickets) + .bind(deadline) + .fetch_optional(executor) + .await +} + +pub(crate) async fn increase_used_ticketbook_tickets<'a, E>( + executor: E, + ticketbook_id: i64, + extra_spent: u32, +) -> Result<(), sqlx::Error> +where + E: Executor<'a, Database = Sqlite>, +{ + sqlx::query!( + "UPDATE ecash_ticketbook SET used_tickets = used_tickets + ? WHERE id = ?", + extra_spent, + ticketbook_id + ) + .execute(executor) + .await?; + Ok(()) } diff --git a/common/credential-storage/src/ephemeral_storage.rs b/common/credential-storage/src/ephemeral_storage.rs index ead11edcf8..e1a18942c2 100644 --- a/common/credential-storage/src/ephemeral_storage.rs +++ b/common/credential-storage/src/ephemeral_storage.rs @@ -1,26 +1,30 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::fmt::{self, Debug, Formatter}; - -use crate::backends::memory::CoconutCredentialManager; +use crate::backends::memory::MemoryEcachTicketbookManager; use crate::error::StorageError; -use crate::models::{StorableIssuedCredential, StoredIssuedCredential}; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; use crate::storage::Storage; use async_trait::async_trait; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; +use std::fmt::{self, Debug, Formatter}; pub type EphemeralCredentialStorage = EphemeralStorage; // note that clone here is fine as upon cloning the same underlying pool will be used #[derive(Clone)] pub struct EphemeralStorage { - coconut_credential_manager: CoconutCredentialManager, + storage_manager: MemoryEcachTicketbookManager, } impl Default for EphemeralStorage { fn default() -> Self { EphemeralStorage { - coconut_credential_manager: CoconutCredentialManager::new(), + storage_manager: MemoryEcachTicketbookManager::new(), } } } @@ -35,55 +39,135 @@ impl Debug for EphemeralStorage { impl Storage for EphemeralStorage { type StorageError = StorageError; - async fn insert_issued_credential<'a>( + async fn cleanup_expired(&self) -> Result<(), Self::StorageError> { + self.storage_manager.cleanup_expired().await; + Ok(()) + } + + async fn insert_pending_ticketbook( &self, - bandwidth_credential: StorableIssuedCredential<'a>, - ) -> Result<(), StorageError> { - self.coconut_credential_manager - .insert_issued_credential( - bandwidth_credential.credential_type, - bandwidth_credential.serialization_revision, - bandwidth_credential.credential_data, - bandwidth_credential.epoch_id, - ) + ticketbook: &IssuanceTicketBook, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_pending_ticketbook(ticketbook) .await; Ok(()) } - async fn get_next_unspent_credential( + async fn insert_issued_ticketbook( &self, - gateway_id: &str, - ) -> Result, Self::StorageError> { - // first try to get a free pass if available, otherwise fallback to bandwidth voucher - let maybe_freepass = self - .coconut_credential_manager - .get_next_unspect_freepass(gateway_id) - .await; - if maybe_freepass.is_some() { - return Ok(maybe_freepass); - } + ticketbook: &IssuedTicketBook, + ) -> Result<(), StorageError> { + self.storage_manager.insert_new_ticketbook(ticketbook).await; + Ok(()) + } + async fn get_ticketbooks_info( + &self, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_ticketbooks_info().await) + } + + async fn get_pending_ticketbooks( + &self, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_pending_ticketbooks().await) + } + + async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_pending_ticketbook(pending_id) + .await; + Ok(()) + } + + /// Tries to retrieve one of the stored ticketbook, + /// that has not yet expired and has required number of unspent tickets. + /// it immediately updated the on-disk number of used tickets so that another task + /// could obtain their own tickets at the same time + async fn get_next_unspent_usable_ticketbook( + &self, + tickets: u32, + ) -> Result, Self::StorageError> { Ok(self - .coconut_credential_manager - .get_next_unspect_bandwidth_voucher() + .storage_manager + .get_next_unspent_ticketbook_and_update(tickets) .await) } - async fn consume_coconut_credential( + async fn attempt_revert_ticketbook_withdrawal( &self, - id: i64, - gateway_id: &str, - ) -> Result<(), StorageError> { - self.coconut_credential_manager - .consume_coconut_credential(id, gateway_id) - .await; + ticketbook_id: i64, + previous_total_spent: u32, + withdrawn: u32, + ) -> Result { + Ok(self + .storage_manager + .revert_ticketbook_withdrawal(ticketbook_id, previous_total_spent, withdrawn) + .await) + } + async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Result, Self::StorageError> { + Ok(self + .storage_manager + .get_master_verification_key(epoch_id) + .await) + } + + async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_master_verification_key(epoch_id, key) + .await; Ok(()) } - async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError> { - self.coconut_credential_manager.mark_expired(id).await; + async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Result>, Self::StorageError> { + Ok(self + .storage_manager + .get_coin_index_signatures(epoch_id) + .await) + } + async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + data: &[AnnotatedCoinIndexSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_coin_index_signatures(epoch_id, data) + .await; + Ok(()) + } + + async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result>, Self::StorageError> { + Ok(self + .storage_manager + .get_expiration_date_signatures(expiration_date) + .await) + } + + async fn insert_expiration_date_signatures( + &self, + epoch_id: u64, + expiration_date: Date, + data: &[AnnotatedExpirationDateSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_expiration_date_signatures(epoch_id, expiration_date, data) + .await; Ok(()) } } diff --git a/common/credential-storage/src/error.rs b/common/credential-storage/src/error.rs index 42b94c2fc4..665e4db638 100644 --- a/common/credential-storage/src/error.rs +++ b/common/credential-storage/src/error.rs @@ -9,6 +9,9 @@ pub enum StorageError { #[error("Database experienced an internal error - {0}")] InternalDatabaseError(#[from] sqlx::Error), + #[error("experienced internal storage error due to database inconsistency: {reason}")] + DatabaseInconsistency { reason: String }, + #[cfg(not(target_arch = "wasm32"))] #[error("Failed to perform database migration - {0}")] MigrationError(#[from] sqlx::migrate::MigrateError), @@ -19,6 +22,17 @@ pub enum StorageError { #[error("No unused credential in database. You need to buy at least one")] NoCredential, + #[error("No signatures for epoch {epoch_id} in the database")] + NoSignatures { epoch_id: i64 }, + #[error("Database unique constraint violation. Is the credential already imported?")] ConstraintUnique, } + +impl StorageError { + pub fn database_inconsistency>(reason: S) -> StorageError { + StorageError::DatabaseInconsistency { + reason: reason.into(), + } + } +} diff --git a/common/credential-storage/src/lib.rs b/common/credential-storage/src/lib.rs index c27e904192..e320926175 100644 --- a/common/credential-storage/src/lib.rs +++ b/common/credential-storage/src/lib.rs @@ -5,21 +5,20 @@ use crate::ephemeral_storage::EphemeralStorage; -#[cfg(not(target_arch = "wasm32"))] -use crate::persistent_storage::PersistentStorage; -#[cfg(not(target_arch = "wasm32"))] -use std::path::Path; - mod backends; pub mod ephemeral_storage; pub mod error; pub mod models; -#[cfg(not(target_arch = "wasm32"))] + +#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))] pub mod persistent_storage; + pub mod storage; -#[cfg(not(target_arch = "wasm32"))] -pub async fn initialise_persistent_storage>(path: P) -> PersistentStorage { +#[cfg(all(not(target_arch = "wasm32"), feature = "persistent-storage"))] +pub async fn initialise_persistent_storage>( + path: P, +) -> crate::persistent_storage::PersistentStorage { match persistent_storage::PersistentStorage::init(path).await { Err(err) => panic!("failed to initialise credential storage - {err}"), Ok(storage) => storage, diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index 49c004ece0..c400631645 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -1,44 +1,62 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; use zeroize::{Zeroize, ZeroizeOnDrop}; -// #[derive(Clone)] -// pub struct CoconutCredential { -// #[allow(dead_code)] -// pub id: i64, -// pub voucher_value: String, -// pub voucher_info: String, -// pub serial_number: String, -// pub binding_number: String, -// pub signature: String, -// pub epoch_id: String, -// pub consumed: bool, -// } +pub struct RetrievedTicketbook { + pub ticketbook_id: i64, + pub ticketbook: IssuedTicketBook, +} + +pub struct RetrievedPendingTicketbook { + pub pending_id: i64, + pub pending_ticketbook: IssuanceTicketBook, +} + +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct BasicTicketbookInformation { + pub id: i64, + pub expiration_date: Date, + pub epoch_id: u32, + pub total_tickets: u32, + pub used_tickets: u32, +} #[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] #[derive(Zeroize, ZeroizeOnDrop, Clone)] -pub struct StoredIssuedCredential { +pub struct StoredIssuedTicketbook { pub id: i64, pub serialization_revision: u8, - pub credential_data: Vec, - pub credential_type: String, + + pub ticketbook_data: Vec, + + #[zeroize(skip)] + pub expiration_date: Date, pub epoch_id: u32, - pub expired: bool, -} -pub struct StorableIssuedCredential<'a> { - pub serialization_revision: u8, - pub credential_data: &'a [u8], - pub credential_type: String, - - pub epoch_id: u32, + pub total_tickets: u32, + pub used_tickets: u32, } #[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] -pub struct CredentialUsage { - pub credential_id: i64, - pub gateway_id_bs58: String, +#[derive(Zeroize, ZeroizeOnDrop, Clone)] +pub struct StoredPendingTicketbook { + pub deposit_id: i64, + + pub serialization_revision: u8, + + pub pending_ticketbook_data: Vec, + + #[zeroize(skip)] + pub expiration_date: Date, +} + +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct RawExpirationDateSignatures { + pub epoch_id: u32, + pub serialised_signatures: Vec, } diff --git a/common/credential-storage/src/persistent_storage.rs b/common/credential-storage/src/persistent_storage.rs deleted file mode 100644 index cd2bd0170f..0000000000 --- a/common/credential-storage/src/persistent_storage.rs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::backends::sqlite::CoconutCredentialManager; -use crate::error::StorageError; -use crate::storage::Storage; - -use crate::models::{StorableIssuedCredential, StoredIssuedCredential}; -use async_trait::async_trait; -use log::{debug, error}; -use sqlx::ConnectOptions; -use std::path::Path; - -// note that clone here is fine as upon cloning the same underlying pool will be used -#[derive(Clone)] -pub struct PersistentStorage { - coconut_credential_manager: CoconutCredentialManager, -} - -impl PersistentStorage { - /// Initialises `PersistentStorage` using the provided path. - /// - /// # Arguments - /// - /// * `database_path`: path to the database. - pub async fn init>(database_path: P) -> Result { - debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() - ); - - let mut opts = sqlx::sqlite::SqliteConnectOptions::new() - .filename(database_path) - .create_if_missing(true); - - opts.disable_statement_logging(); - - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { - Ok(db) => db, - Err(err) => { - error!("Failed to connect to SQLx database: {err}"); - return Err(err.into()); - } - }; - - if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { - error!("Failed to perform migration on the SQLx database: {err}"); - return Err(err.into()); - } - - Ok(PersistentStorage { - coconut_credential_manager: CoconutCredentialManager::new(connection_pool.clone()), - }) - } -} - -#[async_trait] -impl Storage for PersistentStorage { - type StorageError = StorageError; - - async fn insert_issued_credential<'a>( - &self, - bandwidth_credential: StorableIssuedCredential<'a>, - ) -> Result<(), Self::StorageError> { - self.coconut_credential_manager - .insert_issued_credential( - bandwidth_credential.credential_type, - bandwidth_credential.serialization_revision, - bandwidth_credential.credential_data, - bandwidth_credential.epoch_id, - ) - .await - .map_err(|err| { - // There is one error we want to handle specifically. - // Check if database_error is `SqliteError` with code 2067 which - // means UNIQUE constraint violation - if let Some(db_error) = err.as_database_error() { - if db_error.code().map_or(false, |code| code == "2067") { - StorageError::ConstraintUnique - } else { - err.into() - } - } else { - err.into() - } - }) - } - - async fn get_next_unspent_credential( - &self, - gateway_id: &str, - ) -> Result, Self::StorageError> { - // first try to get a free pass if available, otherwise fallback to bandwidth voucher - let maybe_freepass = self - .coconut_credential_manager - .get_next_unspect_freepass(gateway_id) - .await?; - if maybe_freepass.is_some() { - return Ok(maybe_freepass); - } - - Ok(self - .coconut_credential_manager - .get_next_unspect_bandwidth_voucher() - .await?) - } - - async fn consume_coconut_credential( - &self, - id: i64, - gateway_id: &str, - ) -> Result<(), StorageError> { - self.coconut_credential_manager - .consume_coconut_credential(id, gateway_id) - .await?; - - Ok(()) - } - - async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError> { - self.coconut_credential_manager.mark_expired(id).await?; - - Ok(()) - } -} diff --git a/common/credential-storage/src/persistent_storage/helpers.rs b/common/credential-storage/src/persistent_storage/helpers.rs new file mode 100644 index 0000000000..614800d609 --- /dev/null +++ b/common/credential-storage/src/persistent_storage/helpers.rs @@ -0,0 +1,54 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::StorageError; +use bincode::Options; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +struct StorageBorrowedSerdeWrapper<'a, T>(&'a T); + +#[derive(Serialize, Deserialize)] +struct StorageSerdeWrapper(T); + +pub(crate) fn serialise_coin_index_signatures(sigs: &[AnnotatedCoinIndexSignature]) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_coin_index_signatures( + raw: &[u8], +) -> Result, StorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + StorageError::database_inconsistency("malformed stored coin index signatures") + })?; + Ok(de.0) +} + +pub(crate) fn serialise_expiration_date_signatures( + sigs: &[AnnotatedExpirationDateSignature], +) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_expiration_date_signatures( + raw: &[u8], +) -> Result, StorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + StorageError::database_inconsistency("malformed expiration date signatures") + })?; + Ok(de.0) +} + +// storage serialiser used for non-critical data, such as global expiration signatures or master verification keys, +// i.e. data that could always be queried for again if malformed +fn storage_serialiser() -> impl bincode::Options { + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs new file mode 100644 index 0000000000..4ad915db15 --- /dev/null +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -0,0 +1,307 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::backends::sqlite::{ + get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager, +}; +use crate::error::StorageError; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::persistent_storage::helpers::{ + deserialise_coin_index_signatures, deserialise_expiration_date_signatures, + serialise_coin_index_signatures, serialise_expiration_date_signatures, +}; +use crate::storage::Storage; +use async_trait::async_trait; +use log::{debug, error}; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::{ecash_today, Date, EcashTime}; +use sqlx::ConnectOptions; +use std::path::Path; +use zeroize::Zeroizing; + +mod helpers; + +// note that clone here is fine as upon cloning the same underlying pool will be used +#[derive(Clone)] +pub struct PersistentStorage { + storage_manager: SqliteEcashTicketbookManager, +} + +impl PersistentStorage { + /// Initialises `PersistentStorage` using the provided path. + /// + /// # Arguments + /// + /// * `database_path`: path to the database. + pub async fn init>(database_path: P) -> Result { + debug!( + "Attempting to connect to database {:?}", + database_path.as_ref().as_os_str() + ); + + let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(true); + + opts.disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(db) => db, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(err.into()); + } + }; + + if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + error!("Failed to perform migration on the SQLx database: {err}"); + return Err(err.into()); + } + + Ok(PersistentStorage { + storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()), + }) + } +} + +#[async_trait] +impl Storage for PersistentStorage { + type StorageError = StorageError; + + /// remove all expired ticketbooks and expiration date signatures + async fn cleanup_expired(&self) -> Result<(), Self::StorageError> { + let ecash_yesterday = ecash_today().date().previous_day().unwrap(); + self.storage_manager + .cleanup_expired(ecash_yesterday) + .await?; + Ok(()) + } + + async fn insert_pending_ticketbook( + &self, + ticketbook: &IssuanceTicketBook, + ) -> Result<(), Self::StorageError> { + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + let serialisation_revision = ser.revision; + + self.storage_manager + .insert_pending_ticketbook( + serialisation_revision, + ticketbook.deposit_id(), + &data, + ticketbook.expiration_date(), + ) + .await?; + + Ok(()) + } + + async fn insert_issued_ticketbook( + &self, + ticketbook: &IssuedTicketBook, + ) -> Result<(), Self::StorageError> { + let ser = ticketbook.pack(); + let data = Zeroizing::new(ser.data); + let serialisation_revision = ser.revision; + + self.storage_manager + .insert_new_ticketbook( + serialisation_revision, + &data, + ticketbook.expiration_date(), + ticketbook.epoch_id() as u32, + ticketbook.params_total_tickets() as u32, + ticketbook.spent_tickets() as u32, + ) + .await?; + + Ok(()) + } + + async fn get_ticketbooks_info( + &self, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_ticketbooks_info().await?) + } + + async fn get_pending_ticketbooks( + &self, + ) -> Result, Self::StorageError> { + let pending = self + .storage_manager + .get_pending_ticketbooks() + .await? + .into_iter() + .map(|p| { + IssuanceTicketBook::try_unpack(&p.pending_ticketbook_data, p.serialization_revision) + .map_err(|err| { + StorageError::database_inconsistency(format!( + "failed to deserialise stored pending ticketbook: {err}" + )) + }) + .map(|pending_ticketbook| RetrievedPendingTicketbook { + pending_id: p.deposit_id, + pending_ticketbook, + }) + }) + .collect::>()?; + Ok(pending) + } + + async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_pending_ticketbook(pending_id) + .await?; + Ok(()) + } + + /// Tries to retrieve one of the stored ticketbook, + /// that has not yet expired and has required number of unspent tickets. + /// it immediately updated the on-disk number of used tickets so that another task + /// could obtain their own tickets at the same time + async fn get_next_unspent_usable_ticketbook( + &self, + tickets: u32, + ) -> Result, Self::StorageError> { + let deadline = ecash_today().ecash_date(); + let mut tx = self.storage_manager.begin_storage_tx().await?; + + // we don't want ticketbooks with expiration in the past + let Some(raw) = get_next_unspent_ticketbook(&mut tx, deadline, tickets).await? else { + // make sure to finish our tx + tx.commit().await?; + return Ok(None); + }; + + let mut deserialised = + IssuedTicketBook::try_unpack(&raw.ticketbook_data, raw.serialization_revision) + .map_err(|err| { + StorageError::database_inconsistency(format!( + "failed to deserialise stored ticketbook: {err}" + )) + })?; + + increase_used_ticketbook_tickets(&mut tx, raw.id, tickets).await?; + tx.commit().await?; + + // set the number of spent tickets on the crypto object + // TODO: I don't like how that's required and can be easily missed, + // perhaps we shouldn't be storing the `IssuedTicketBook` data in the db, + // but all of its fields instead? + deserialised.update_spent_tickets(raw.used_tickets as u64); + Ok(Some(RetrievedTicketbook { + ticketbook_id: raw.id, + ticketbook: deserialised, + })) + } + + async fn attempt_revert_ticketbook_withdrawal( + &self, + ticketbook_id: i64, + withdrawn: u32, + expected_current_total_spent: u32, + ) -> Result { + Ok(self + .storage_manager + .decrease_used_ticketbook_tickets( + ticketbook_id, + withdrawn, + expected_current_total_spent, + ) + .await?) + } + + async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Result, Self::StorageError> { + let Some(raw) = self + .storage_manager + .get_master_verification_key(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + let master_vk = VerificationKeyAuth::from_bytes(&raw).map_err(|_| { + StorageError::database_inconsistency("malformed stored master verification key") + })?; + + Ok(Some(master_vk)) + } + + async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) -> Result<(), Self::StorageError> { + Ok(self + .storage_manager + .insert_master_verification_key(epoch_id as i64, &key.to_bytes()) + .await?) + } + + async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Result>, Self::StorageError> { + let Some(raw) = self + .storage_manager + .get_coin_index_signatures(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_coin_index_signatures(&raw)?)) + } + + async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_coin_index_signatures(epoch_id as i64, &serialise_coin_index_signatures(sigs)) + .await?; + Ok(()) + } + + async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result>, Self::StorageError> { + let Some(raw) = self + .storage_manager + .get_expiration_date_signatures(expiration_date) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_expiration_date_signatures( + &raw.serialised_signatures, + )?)) + } + + async fn insert_expiration_date_signatures( + &self, + epoch_id: u64, + expiration_date: Date, + sigs: &[AnnotatedExpirationDateSignature], + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_expiration_date_signatures( + epoch_id as i64, + expiration_date, + &serialise_expiration_date_signatures(sigs), + ) + .await?; + Ok(()) + } +} diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index 7880a3722e..8b3ebc74ec 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -1,42 +1,93 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{StorableIssuedCredential, StoredIssuedCredential}; +use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; use async_trait::async_trait; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; +use nym_ecash_time::Date; use std::error::Error; +// for future reference, if you want to make a query for "how much bandwidth do we have left" +// do something along the lines of +// `SELECT total_tickets, used_tickets FROM ecash_ticketbook WHERE expiration_date >= ?`, today_date +// then for each calculate the diff total_tickets - used_tickets and multiply the result by the size of the ticket #[async_trait] pub trait Storage: Send + Sync { type StorageError: Error; - async fn insert_issued_credential<'a>( + /// remove all expired ticketbooks and expiration date signatures + async fn cleanup_expired(&self) -> Result<(), Self::StorageError>; + + async fn insert_pending_ticketbook( &self, - bandwidth_credential: StorableIssuedCredential<'a>, + ticketbook: &IssuanceTicketBook, ) -> Result<(), Self::StorageError>; - /// Tries to retrieve one of the stored, unused credentials, - /// that is also not marked as expired - async fn get_next_unspent_credential( + async fn insert_issued_ticketbook( &self, - gateway_id: &str, - ) -> Result, Self::StorageError>; - - /// Marks as consumed in the database the specified credential. - /// - /// # Arguments - /// - /// * `id`: Id of the credential to be consumed. - /// * `gateway_id`: id of the gateway that received the credential. - async fn consume_coconut_credential( - &self, - id: i64, - gateway_id: &str, + ticketbook: &IssuedTicketBook, ) -> Result<(), Self::StorageError>; - /// Marks the specified credential as expired - /// - /// # Arguments - /// - /// * `id`: Id of the credential to mark as expired. - async fn mark_expired(&self, id: i64) -> Result<(), Self::StorageError>; + async fn get_ticketbooks_info( + &self, + ) -> Result, Self::StorageError>; + + async fn get_pending_ticketbooks( + &self, + ) -> Result, Self::StorageError>; + + async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError>; + + /// Tries to retrieve one of the stored ticketbook, + /// that has not yet expired and has required number of unspent tickets. + /// it immediately updated the on-disk number of used tickets so that another task + /// could obtain their own tickets at the same time + async fn get_next_unspent_usable_ticketbook( + &self, + tickets: u32, + ) -> Result, Self::StorageError>; + + async fn attempt_revert_ticketbook_withdrawal( + &self, + ticketbook_id: i64, + withdrawn: u32, + expected_current_total_spent: u32, + ) -> Result; + + async fn get_master_verification_key( + &self, + epoch_id: u64, + ) -> Result, Self::StorageError>; + + async fn insert_master_verification_key( + &self, + epoch_id: u64, + key: &VerificationKeyAuth, + ) -> Result<(), Self::StorageError>; + + async fn get_coin_index_signatures( + &self, + epoch_id: u64, + ) -> Result>, Self::StorageError>; + + async fn insert_coin_index_signatures( + &self, + epoch_id: u64, + data: &[AnnotatedCoinIndexSignature], + ) -> Result<(), Self::StorageError>; + + async fn get_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result>, Self::StorageError>; + + async fn insert_expiration_date_signatures( + &self, + epoch_id: u64, + expiration_date: Date, + data: &[AnnotatedExpirationDateSignature], + ) -> Result<(), Self::StorageError>; } diff --git a/common/credential-utils/Cargo.toml b/common/credential-utils/Cargo.toml index 232c9cdb1f..3e2d534fa2 100644 --- a/common/credential-utils/Cargo.toml +++ b/common/credential-utils/Cargo.toml @@ -10,11 +10,13 @@ license.workspace = true log = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } +time.workspace = true nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } -nym-coconut = { path = "../nymcoconut" } nym-credentials = { path = "../../common/credentials" } -nym-credential-storage = { path = "../../common/credential-storage" } +nym-credential-storage = { path = "../../common/credential-storage", features = ["persistent-storage"] } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-config = { path = "../../common/config" } nym-client-core = { path = "../../common/client-core" } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } +nym-ecash-time = { path = "../../common/ecash-time" } \ No newline at end of file diff --git a/common/credential-utils/src/errors.rs b/common/credential-utils/src/errors.rs index 6407929a99..456f542180 100644 --- a/common/credential-utils/src/errors.rs +++ b/common/credential-utils/src/errors.rs @@ -3,6 +3,7 @@ use nym_credential_storage::error::StorageError; use nym_credentials::error::Error as CredentialError; +use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::error::NyxdError; use std::num::ParseIntError; use thiserror::Error; @@ -17,15 +18,30 @@ pub enum Error { #[error(transparent)] BandwidthControllerError(#[from] nym_bandwidth_controller::error::BandwidthControllerError), + #[error(transparent)] + EcashApiError(#[from] EcashApiError), + #[error(transparent)] Nyxd(#[from] NyxdError), #[error(transparent)] Credential(#[from] CredentialError), - #[error("Could not use shared storage: {0}")] - SharedStorageError(#[from] StorageError), + #[error("could not use shared storage: {0}")] + SharedStorageError(Box), #[error("failed to parse credential value: {0}")] MalformedCredentialValue(#[from] ParseIntError), } + +impl Error { + pub fn storage_error(source: impl std::error::Error + Send + Sync + 'static) -> Self { + Error::SharedStorageError(Box::new(source)) + } +} + +impl From for Error { + fn from(value: StorageError) -> Self { + Self::storage_error(value) + } +} diff --git a/common/credential-utils/src/lib.rs b/common/credential-utils/src/lib.rs index 6e5fbc3158..60bd6d2bcf 100644 --- a/common/credential-utils/src/lib.rs +++ b/common/credential-utils/src/lib.rs @@ -1,5 +1,4 @@ pub mod errors; -pub mod recovery_storage; pub mod utils; pub use errors::{Error, Result}; diff --git a/common/credential-utils/src/recovery_storage.rs b/common/credential-utils/src/recovery_storage.rs deleted file mode 100644 index 0344a3ad86..0000000000 --- a/common/credential-utils/src/recovery_storage.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::errors::Result; -use log::error; -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; -use std::fs::{create_dir_all, read_dir, File}; -use std::io::{Read, Write}; -use std::path::PathBuf; - -pub const DUMPED_VOUCHER_EXTENSION: &str = "credentialrecovery"; - -pub struct RecoveryStorage { - recovery_dir: PathBuf, -} - -impl RecoveryStorage { - pub fn new(recovery_dir: PathBuf) -> Result { - create_dir_all(&recovery_dir)?; - Ok(Self { recovery_dir }) - } - - pub fn unconsumed_vouchers(&self) -> Result> { - let entries = read_dir(&self.recovery_dir)?; - - let mut paths = vec![]; - for entry in entries.flatten() { - let path = entry.path(); - if let Some(extension) = path.extension() { - if extension == DUMPED_VOUCHER_EXTENSION { - paths.push(path) - } - } - } - - let mut vouchers = vec![]; - for path in paths { - if let Ok(mut file) = File::open(&path) { - let mut buff = Vec::new(); - if file.read_to_end(&mut buff).is_ok() { - match IssuanceBandwidthCredential::try_from_recovered_bytes(&buff) { - Ok(voucher) => vouchers.push(voucher), - Err(err) => { - error!("failed to parse the voucher at {}: {err}", path.display()) - } - } - } - } - } - - Ok(vouchers) - } - - pub fn voucher_filename(voucher: &IssuanceBandwidthCredential) -> String { - let prefix = voucher.typ().to_string(); - let suffix = voucher.blinded_serial_number_bs58(); - format!("{prefix}-{suffix}.{DUMPED_VOUCHER_EXTENSION}") - } - - pub fn insert_voucher(&self, voucher: &IssuanceBandwidthCredential) -> Result { - let file_name = Self::voucher_filename(voucher); - let file_path = self.recovery_dir.join(file_name); - let mut file = File::create(&file_path)?; - let buff = voucher.to_recovery_bytes(); - file.write_all(&buff)?; - - Ok(file_path) - } - - pub fn remove_voucher(&self, file_name: String) -> Result<()> { - let file_path = self.recovery_dir.join(file_name); - Ok(std::fs::remove_file(file_path)?) - } -} diff --git a/common/credential-utils/src/utils.rs b/common/credential-utils/src/utils.rs index 33a0b20541..7c77b077b5 100644 --- a/common/credential-utils/src/utils.rs +++ b/common/credential-utils/src/utils.rs @@ -1,74 +1,75 @@ use crate::errors::{Error, Result}; -use crate::recovery_storage::RecoveryStorage; use log::*; -use nym_bandwidth_controller::acquire::state::State; +use nym_bandwidth_controller::acquire::{ + get_ticket_book, query_and_persist_required_global_signatures, +}; use nym_client_core::config::disk_persistence::CommonClientPaths; use nym_config::DEFAULT_DATA_DIR; use nym_credential_storage::persistent_storage::PersistentStorage; -use nym_credentials::coconut::bandwidth::CredentialType; +use nym_credential_storage::storage::Storage; +use nym_ecash_time::ecash_default_expiration_date; +use nym_validator_client::coconut::all_ecash_api_clients; use nym_validator_client::nyxd::contract_traits::{ - dkg_query_client::EpochState, CoconutBandwidthSigningClient, DkgQueryClient, + dkg_query_client::EpochState, DkgQueryClient, EcashSigningClient, }; -use nym_validator_client::nyxd::Coin; use std::path::PathBuf; -use std::time::{Duration, SystemTime}; +use std::time::Duration; +use time::OffsetDateTime; -pub async fn issue_credential( - client: &C, - amount: Coin, - persistent_storage: &PersistentStorage, - recovery_storage_path: PathBuf, -) -> Result<()> +pub async fn issue_credential(client: &C, storage: &S, client_id: &[u8]) -> Result<()> where - C: DkgQueryClient + CoconutBandwidthSigningClient + Send + Sync, + C: DkgQueryClient + EcashSigningClient + Send + Sync, + S: Storage, + ::StorageError: Send + Sync + 'static, { - let recovery_storage = setup_recovery_storage(recovery_storage_path).await; - - block_until_coconut_is_available(client).await?; + block_until_ecash_is_available(client).await?; info!("Starting to deposit funds, don't kill the process"); - if let Ok(recovered_amount) = - recover_credentials(client, &recovery_storage, persistent_storage).await - { - if recovered_amount != 0 { - info!( - "Recovered credentials in the amount of {}", - recovered_amount - ); + if let Ok(recovered_ticketbooks) = recover_deposits(client, storage).await { + if recovered_ticketbooks != 0 { + info!("managed to recover {recovered_ticketbooks} ticket books. no need to make fresh deposit"); return Ok(()); } }; - let state = nym_bandwidth_controller::acquire::deposit(client, amount.clone()).await?; + let epoch_id = client.get_current_epoch().await?.epoch_id; + let apis = all_ecash_api_clients(client, epoch_id).await?; + let ticketbook_expiration = ecash_default_expiration_date(); - if nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, client, persistent_storage) + // make sure we have all required coin indices and expiration date signatures before attempting the deposit + query_and_persist_required_global_signatures( + storage, + epoch_id, + ticketbook_expiration, + apis.clone(), + ) + .await?; + + let issuance_data = nym_bandwidth_controller::acquire::make_deposit( + client, + client_id, + Some(ticketbook_expiration), + ) + .await?; + info!("Deposit done"); + + if get_ticket_book(&issuance_data, client, storage, Some(apis)) .await .is_err() { - warn!("Failed to obtain credential. Dumping recovery data.",); - match recovery_storage.insert_voucher(&state.voucher) { - Ok(file_path) => { - warn!("Dumped recovery data to {}. Try using recovery mode to convert it to a credential", file_path.to_str().unwrap()); - } - Err(e) => { - error!("Could not dump recovery data to file system due to {:?}, the deposit will be lost!", e) - } - } + error!("failed to obtain credential. saving recovery data..."); - return Err(Error::Credential( - nym_credentials::error::Error::BandwidthCredentialError, - )); + storage.insert_pending_ticketbook(&issuance_data).await.inspect_err(|err| { + let deposit = issuance_data.deposit_id(); + error!("could not save the recovery data for deposit {deposit}: {err}. the data will unfortunately get lost") + }).map_err(Error::storage_error)? } - info!("Succeeded adding a credential with amount {amount}"); + info!("Succeeded adding a ticketbook"); Ok(()) } -pub async fn setup_recovery_storage(recovery_dir: PathBuf) -> RecoveryStorage { - RecoveryStorage::new(recovery_dir).expect("") -} - pub async fn setup_persistent_storage(client_home_directory: PathBuf) -> PersistentStorage { let data_dir = client_home_directory.join(DEFAULT_DATA_DIR); let paths = CommonClientPaths::new_base(data_dir); @@ -77,16 +78,13 @@ pub async fn setup_persistent_storage(client_home_directory: PathBuf) -> Persist nym_credential_storage::initialise_persistent_storage(db_path).await } -pub async fn block_until_coconut_is_available(client: &C) -> Result<()> +pub async fn block_until_ecash_is_available(client: &C) -> Result<()> where C: DkgQueryClient + Send + Sync, { loop { let epoch = client.get_current_epoch().await?; - let current_timestamp_secs = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("the system clock is set to 01/01/1970 (or earlier)") - .as_secs(); + let current_timestamp_secs = OffsetDateTime::now_utc().unix_timestamp() as u64; if epoch.state.is_final() { break; @@ -101,7 +99,7 @@ where } else { // this should never be the case since the only case where final timestamp is unknown is when it's waiting for initialisation, // but let's guard ourselves against future changes - info!("it is unknown when coconut will be come available. Going to check again later"); + info!("it is unknown when ecash will be come available. Going to check again later"); tokio::time::sleep(Duration::from_secs(60 * 5)).await; } } @@ -109,42 +107,47 @@ where Ok(()) } -pub async fn recover_credentials( - client: &C, - recovery_storage: &RecoveryStorage, - shared_storage: &PersistentStorage, -) -> Result +pub async fn recover_deposits(client: &C, storage: &S) -> Result where C: DkgQueryClient + Send + Sync, + S: Storage, + ::StorageError: Send + Sync + 'static, { - let mut recovered_amount: u128 = 0; - for voucher in recovery_storage.unconsumed_vouchers()? { - let voucher_value = match voucher.typ() { - CredentialType::Voucher => voucher.get_bandwidth_attribute(), - CredentialType::FreePass => { - error!("unimplemented recovery of free pass credentials"); - continue; - } - }; - recovered_amount += voucher_value.parse::()?; + info!("checking for any incomplete previous issuance attempts..."); - let voucher_name = RecoveryStorage::voucher_filename(&voucher); - let state = State::new(voucher); + let incomplete = storage + .get_pending_ticketbooks() + .await + .map_err(Error::storage_error)?; + info!( + "we recovered {} incomplete ticketbook issuances", + incomplete.len() + ); - if let Err(e) = - nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, client, shared_storage) - .await - { - error!("Could not recover deposit {voucher_name} due to {e}, try again later",) - } else { - info!( - "Converted deposit {voucher_name} to a credential, removing recovery data for it", - ); - if let Err(err) = recovery_storage.remove_voucher(voucher_name) { - warn!("Could not remove recovery data: {err}"); + let mut recovered_books = 0; + for issuance in incomplete { + let deposit = issuance.pending_ticketbook.deposit_id(); + if issuance.pending_ticketbook.expired() { + warn!("ticketbook data associated with deposit {deposit} has expired. if you haven't contacted more than 1/3 of signers. it could still be recoverable (but out of scope of this library)"); + continue; + } + + if issuance.pending_ticketbook.check_expiration_date() { + warn!("deposit {deposit} was made with a different expiration date, it's validity will be shorter than the max one"); + } + + match get_ticket_book(&issuance.pending_ticketbook, client, storage, None).await { + Err(err) => error!("could not recover deposit {deposit} due to: {err}"), + Ok(_) => { + info!("managed to recover deposit {deposit}! the ticketbook has been added to the storage"); + storage + .remove_pending_ticketbook(issuance.pending_id) + .await + .map_err(Error::storage_error)?; + recovered_books += 1; } } } - Ok(recovered_amount) + Ok(recovered_books) } diff --git a/common/credentials-interface/Cargo.toml b/common/credentials-interface/Cargo.toml index 5393ea37fd..7d2365683f 100644 --- a/common/credentials-interface/Cargo.toml +++ b/common/credentials-interface/Cargo.toml @@ -14,5 +14,8 @@ license.workspace = true bls12_381 = { workspace = true, default-features = false } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } +time = { workspace = true, features = ["serde"] } +rand = { workspace = true } -nym-coconut = { path = "../nymcoconut" } +nym-compact-ecash = { path = "../nym_offline_compact_ecash" } +nym-ecash-time = { path = "../ecash-time" } \ No newline at end of file diff --git a/common/credentials-interface/src/lib.rs b/common/credentials-interface/src/lib.rs index 1337e633d5..765c479644 100644 --- a/common/credentials-interface/src/lib.rs +++ b/common/credentials-interface/src/lib.rs @@ -1,136 +1,218 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bls12_381::Scalar; +use rand::Rng; use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; -use thiserror::Error; +use time::{Date, OffsetDateTime}; -pub use nym_coconut::{ - aggregate_signature_shares, aggregate_signature_shares_and_verify, aggregate_verification_keys, - blind_sign, hash_to_scalar, keygen, prepare_blind_sign, prove_bandwidth_credential, - verify_credential, Attribute, Base58, BlindSignRequest, BlindedSerialNumber, BlindedSignature, - Bytable, CoconutError, KeyPair, Parameters, PrivateAttribute, PublicAttribute, SecretKey, - Signature, SignatureShare, VerificationKey, VerifyCredentialRequest, +pub use nym_compact_ecash::{ + aggregate_verification_keys, aggregate_wallets, constants, ecash_parameters, + error::CompactEcashError, + generate_keypair_user, generate_keypair_user_from_seed, issue_verify, + scheme::coin_indices_signatures::aggregate_indices_signatures, + scheme::coin_indices_signatures::{ + AnnotatedCoinIndexSignature, CoinIndexSignature, CoinIndexSignatureShare, + PartialCoinIndexSignature, + }, + scheme::expiration_date_signatures::aggregate_expiration_signatures, + scheme::expiration_date_signatures::date_scalar, + scheme::expiration_date_signatures::{ + AnnotatedExpirationDateSignature, ExpirationDateSignature, ExpirationDateSignatureShare, + PartialExpirationDateSignature, + }, + scheme::keygen::KeyPairUser, + scheme::withdrawal::RequestInfo, + scheme::Payment, + scheme::{Wallet, WalletSignatures}, + withdrawal_request, Base58, BlindedSignature, Bytable, PartialWallet, PayInfo, PublicKeyUser, + SecretKeyUser, VerificationKeyAuth, WithdrawalRequest, }; - -pub const VOUCHER_INFO_TYPE: &str = "BandwidthVoucher"; -pub const FREE_PASS_INFO_TYPE: &str = "FreeBandwidthPass"; - -// pub trait NymCredential { -// fn prove_credential(&self) -> Result<(), ()>; -// } - -#[derive(Debug, Error)] -#[error("{0} is not a valid credential type")] -pub struct UnknownCredentialType(String); - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub enum CredentialType { - Voucher, - FreePass, -} - -impl FromStr for CredentialType { - type Err = UnknownCredentialType; - - fn from_str(s: &str) -> Result { - if s == VOUCHER_INFO_TYPE { - Ok(CredentialType::Voucher) - } else if s == FREE_PASS_INFO_TYPE { - Ok(CredentialType::FreePass) - } else { - Err(UnknownCredentialType(s.to_string())) - } - } -} - -impl CredentialType { - pub fn validate(&self, type_plain: &str) -> bool { - match self { - CredentialType::Voucher => type_plain == VOUCHER_INFO_TYPE, - CredentialType::FreePass => type_plain == FREE_PASS_INFO_TYPE, - } - } - - pub fn is_free_pass(&self) -> bool { - matches!(self, CredentialType::FreePass) - } - - pub fn is_voucher(&self) -> bool { - matches!(self, CredentialType::Voucher) - } -} - -impl Display for CredentialType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - CredentialType::Voucher => VOUCHER_INFO_TYPE.fmt(f), - CredentialType::FreePass => FREE_PASS_INFO_TYPE.fmt(f), - } - } -} +use nym_ecash_time::EcashTime; #[derive(Debug, Clone)] pub struct CredentialSigningData { - pub pedersen_commitments_openings: Vec, + pub withdrawal_request: WithdrawalRequest, - pub blind_sign_request: BlindSignRequest, + pub request_info: RequestInfo, - pub public_attributes_plain: Vec, + pub ecash_pub_key: PublicKeyUser, - pub typ: CredentialType, + pub expiration_date: Date, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct CredentialSpendingData { - pub embedded_private_attributes: usize, + pub payment: Payment, - pub verify_credential_request: VerifyCredentialRequest, + pub pay_info: PayInfo, - pub public_attributes_plain: Vec, - - pub typ: CredentialType, + pub spend_date: Date, + // pub value: u64, /// The (DKG) epoch id under which the credential has been issued so that the verifier could use correct verification key for validation. pub epoch_id: u64, } impl CredentialSpendingData { - pub fn verify(&self, params: &Parameters, verification_key: &VerificationKey) -> bool { - let hashed_public_attributes = self - .public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect::>(); - - // get references to the attributes - let public_attributes = hashed_public_attributes.iter().collect::>(); - - verify_credential( - params, + pub fn verify(&self, verification_key: &VerificationKeyAuth) -> Result<(), CompactEcashError> { + self.payment.spend_verify( verification_key, - &self.verify_credential_request, - &public_attributes, + &self.pay_info, + date_scalar(self.spend_date.ecash_unix_timestamp()), ) } - pub fn validate_type_attribute(&self) -> bool { - // the first attribute is variant specific bandwidth encoding, the second one should be the type - let Some(type_plain) = self.public_attributes_plain.get(1) else { - return false; + pub fn encoded_serial_number(&self) -> Vec { + self.payment.encoded_serial_number() + } + + pub fn serial_number_b58(&self) -> String { + self.payment.serial_number_bs58() + } + + pub fn to_bytes(&self) -> Vec { + // simple length prefixed serialization + // TODO: change it to a standard format instead + let mut bytes = Vec::new(); + let payment_bytes = self.payment.to_bytes(); + + bytes.extend_from_slice(&(payment_bytes.len() as u32).to_be_bytes()); + bytes.extend_from_slice(&payment_bytes); + bytes.extend_from_slice(&self.pay_info.pay_info_bytes); //this is 72 bytes long + bytes.extend_from_slice(&self.spend_date.to_julian_day().to_be_bytes()); + bytes.extend_from_slice(&self.epoch_id.to_be_bytes()); + + bytes + } + + pub fn try_from_bytes(raw: &[u8]) -> Result { + // minimum length: 72 (pay_info) + 8 (epoch_id) + 4 (spend date) + 4 (payment length prefix) + if raw.len() < 72 + 8 + 4 + 4 { + return Err(CompactEcashError::DeserializationFailure { + object: "EcashCredential".into(), + }); + } + let mut index = 0; + //SAFETY : casting a slice of length 4 into an array of size 4 + let payment_len = u32::from_be_bytes(raw[index..index + 4].try_into().unwrap()) as usize; + index += 4; + + if raw[index..].len() != payment_len + 84 { + return Err(CompactEcashError::DeserializationFailure { + object: "EcashCredential".into(), + }); + } + let payment = Payment::try_from(&raw[index..index + payment_len])?; + index += payment_len; + + let pay_info = PayInfo { + //SAFETY : casting a slice of length 72 into an array of size 72 + pay_info_bytes: raw[index..index + 72].try_into().unwrap(), }; + index += 72; - self.typ.validate(type_plain) - } + //SAFETY : casting a slice of length 4 into an array of size 4 + let spend_date_julian = i32::from_be_bytes(raw[index..index + 4].try_into().unwrap()); + let spend_date = Date::from_julian_day(spend_date_julian).map_err(|_| { + CompactEcashError::DeserializationFailure { + object: "CredentialSpendingData".into(), + } + })?; + index += 4; - pub fn get_bandwidth_attribute(&self) -> Option<&String> { - // the first attribute is variant specific bandwidth encoding, the second one should be the type - self.public_attributes_plain.first() - } + if raw[index..].len() != 8 { + return Err(CompactEcashError::DeserializationFailure { + object: "EcashCredential".into(), + }); + } - pub fn blinded_serial_number(&self) -> BlindedSerialNumber { - self.verify_credential_request.blinded_serial_number() + //SAFETY : casting a slice of length 8 into an array of size 8 + let epoch_id = u64::from_be_bytes(raw[index..].try_into().unwrap()); + + Ok(CredentialSpendingData { + payment, + pay_info, + spend_date, + epoch_id, + }) + } +} + +impl Bytable for CredentialSpendingData { + fn to_byte_vec(&self) -> Vec { + self.to_bytes() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + Self::try_from_bytes(slice) + } +} + +impl Base58 for CredentialSpendingData {} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub struct NymPayInfo { + randomness: [u8; 32], + timestamp: i64, + provider_public_key: [u8; 32], +} + +impl NymPayInfo { + /// Generates a new `NymPayInfo` instance with random bytes, a timestamp, and a provider public key. + /// + /// # Arguments + /// + /// * `provider_pk` - The public key of the payment provider. + /// + /// # Returns + /// + /// A new `NymPayInfo` instance. + /// + pub fn generate(provider_pk: [u8; 32]) -> Self { + let mut randomness = [0u8; 32]; + rand::thread_rng().fill(&mut randomness[..32]); + + let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + + NymPayInfo { + randomness, + timestamp, + provider_public_key: provider_pk, + } + } + + pub fn timestamp(&self) -> i64 { + self.timestamp + } + + pub fn pk(&self) -> [u8; 32] { + self.provider_public_key + } +} + +impl From for PayInfo { + fn from(value: NymPayInfo) -> Self { + let mut pay_info_bytes = [0u8; 72]; + + pay_info_bytes[..32].copy_from_slice(&value.randomness); + pay_info_bytes[32..40].copy_from_slice(&value.timestamp.to_be_bytes()); + pay_info_bytes[40..].copy_from_slice(&value.provider_public_key); + + PayInfo { pay_info_bytes } + } +} + +impl From for NymPayInfo { + fn from(value: PayInfo) -> Self { + //SAFETY : slice to array of same length + let randomness = value.pay_info_bytes[..32].try_into().unwrap(); + let timestamp = i64::from_be_bytes(value.pay_info_bytes[32..40].try_into().unwrap()); + let provider_public_key = value.pay_info_bytes[40..].try_into().unwrap(); + + NymPayInfo { + randomness, + timestamp, + provider_public_key, + } } } diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index a51604a142..48159258fc 100644 --- a/common/credentials/Cargo.toml +++ b/common/credentials/Cargo.toml @@ -16,11 +16,15 @@ time = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } zeroize = { workspace = true } +nym-ecash-time = { path = "../ecash-time", features = ["expiration"] } + # I guess temporarily until we get serde support in coconut up and running nym-credentials-interface = { path = "../credentials-interface" } -nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "serde"] } +nym-crypto = { path = "../crypto" } nym-api-requests = { path = "../../nym-api/nym-api-requests" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } +nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } +nym-network-defaults = { path = "../network-defaults" } [dev-dependencies] rand = "0.8.5" diff --git a/common/credentials/src/coconut/bandwidth/freepass.rs b/common/credentials/src/coconut/bandwidth/freepass.rs deleted file mode 100644 index 9329af2a6e..0000000000 --- a/common/credentials/src/coconut/bandwidth/freepass.rs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_api_requests::coconut::FreePassRequest; -use nym_credentials_interface::{ - hash_to_scalar, Attribute, BlindedSignature, CredentialSigningData, PublicAttribute, -}; -use nym_validator_client::signing::AccountData; -use serde::{Deserialize, Serialize}; -use time::{Duration, OffsetDateTime, Time}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -pub const DEFAULT_FREE_PASS_VALIDITY: Duration = Duration::WEEK; // 1 week -pub const MAX_FREE_PASS_VALIDITY: Duration = Duration::weeks(12); // 12 weeks - -#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct FreePassIssuedData { - /// the plain validity value of this credential expressed as unix timestamp - #[zeroize(skip)] - expiry_date: OffsetDateTime, -} - -impl<'a> From<&'a FreePassIssuanceData> for FreePassIssuedData { - fn from(value: &'a FreePassIssuanceData) -> Self { - FreePassIssuedData { - expiry_date: value.expiry_date, - } - } -} - -impl FreePassIssuedData { - pub fn expired(&self) -> bool { - self.expiry_date <= OffsetDateTime::now_utc() - } - - pub fn expiry_date(&self) -> OffsetDateTime { - self.expiry_date - } - - pub fn expiry_date_plain(&self) -> String { - self.expiry_date.unix_timestamp().to_string() - } -} - -#[derive(Zeroize, Serialize, Deserialize)] -pub struct FreePassIssuanceData { - /// the plain validity value of this credential expressed as unix timestamp - #[zeroize(skip)] - expiry_date: OffsetDateTime, - - // the expiry date, as unix timestamp, hashed into a scalar - #[serde(with = "scalar_serde_helper")] - expiry_date_prehashed: PublicAttribute, -} - -impl FreePassIssuanceData { - pub fn new(expiry_date: Option) -> Self { - // ideally we should have implemented a proper error handling here, sure. - // but given it's meant to only be used by nym, imo it's fine to just panic here in case of invalid arguments - let expiry_date = if let Some(provided) = expiry_date { - if provided - OffsetDateTime::now_utc() > MAX_FREE_PASS_VALIDITY { - panic!("the provided expiry date is bigger than the maximum value of {MAX_FREE_PASS_VALIDITY}"); - } - - provided - } else { - Self::default_expiry_date() - }; - - let expiry_date_prehashed = hash_to_scalar(expiry_date.unix_timestamp().to_string()); - - FreePassIssuanceData { - expiry_date, - expiry_date_prehashed, - } - } - - pub fn default_expiry_date() -> OffsetDateTime { - // set it to the furthest midnight in the future such as it's no more than a week away, - // i.e. if it's currently for example 9:43 on 2nd March 2024, it will set it to 0:00 on 9th March 2024 - (OffsetDateTime::now_utc() + DEFAULT_FREE_PASS_VALIDITY).replace_time(Time::MIDNIGHT) - } - - pub fn expiry_date_attribute(&self) -> &Attribute { - &self.expiry_date_prehashed - } - - pub fn expiry_date_plain(&self) -> String { - self.expiry_date.unix_timestamp().to_string() - } - - pub async fn obtain_free_pass_nonce( - &self, - client: &nym_validator_client::client::NymApiClient, - ) -> Result<[u8; 16], Error> { - let server_response = client.free_pass_nonce().await?; - Ok(server_response.current_nonce) - } - - pub fn create_free_pass_request( - &self, - signing_request: &CredentialSigningData, - account_data: &AccountData, - issuer_nonce: [u8; 16], - ) -> Result { - let nonce_signature = account_data - .private_key() - .sign(&issuer_nonce) - .map_err(|_| Error::Secp256k1SignFailure)?; - - Ok(FreePassRequest { - cosmos_pubkey: account_data.public_key(), - inner_sign_request: signing_request.blind_sign_request.clone(), - used_nonce: issuer_nonce, - nonce_signature, - public_attributes_plain: signing_request.public_attributes_plain.clone(), - }) - } - - pub async fn obtain_blinded_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - request: &FreePassRequest, - ) -> Result { - let server_response = client.issue_free_pass_credential(request).await?; - Ok(server_response.blinded_signature) - } - - pub async fn request_blinded_credential( - &self, - signing_request: &CredentialSigningData, - account_data: &AccountData, - client: &nym_validator_client::client::NymApiClient, - ) -> Result { - let signing_nonce = self.obtain_free_pass_nonce(client).await?; - let request = - self.create_free_pass_request(signing_request, account_data, signing_nonce)?; - self.obtain_blinded_credential(client, &request).await - } -} diff --git a/common/credentials/src/coconut/bandwidth/issuance.rs b/common/credentials/src/coconut/bandwidth/issuance.rs deleted file mode 100644 index a845b6ac7b..0000000000 --- a/common/credentials/src/coconut/bandwidth/issuance.rs +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::freepass::FreePassIssuanceData; -use crate::coconut::bandwidth::issued::IssuedBandwidthCredential; -use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData; -use crate::coconut::bandwidth::{ - bandwidth_credential_params, CredentialSigningData, CredentialType, -}; -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_credentials_interface::{ - aggregate_signature_shares, aggregate_signature_shares_and_verify, hash_to_scalar, - prepare_blind_sign, Attribute, BlindedSerialNumber, BlindedSignature, Parameters, - PrivateAttribute, PublicAttribute, Signature, SignatureShare, VerificationKey, -}; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_validator_client::nym_api::EpochId; -use nym_validator_client::signing::AccountData; -use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -pub use nym_validator_client::nyxd::{Coin, Hash}; - -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub enum BandwidthCredentialIssuanceDataVariant { - Voucher(BandwidthVoucherIssuanceData), - FreePass(FreePassIssuanceData), -} - -impl From for BandwidthCredentialIssuanceDataVariant { - fn from(value: FreePassIssuanceData) -> Self { - BandwidthCredentialIssuanceDataVariant::FreePass(value) - } -} - -impl From for BandwidthCredentialIssuanceDataVariant { - fn from(value: BandwidthVoucherIssuanceData) -> Self { - BandwidthCredentialIssuanceDataVariant::Voucher(value) - } -} - -impl BandwidthCredentialIssuanceDataVariant { - pub fn info(&self) -> CredentialType { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(..) => CredentialType::Voucher, - BandwidthCredentialIssuanceDataVariant::FreePass(..) => CredentialType::FreePass, - } - } - - // currently this works under the assumption of there being a single unique public attribute for given variant - pub fn public_value(&self) -> &Attribute { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_attribute(), - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - freepass.expiry_date_attribute() - } - } - } - - // currently this works under the assumption of there being a single unique public attribute for given variant - pub fn public_value_plain(&self) -> String { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => voucher.value_plain(), - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - freepass.expiry_date_plain() - } - } - } - - pub fn voucher_data(&self) -> Option<&BandwidthVoucherIssuanceData> { - match self { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => Some(voucher), - _ => None, - } - } -} - -// all types of bandwidth credentials contain serial number and binding number -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct IssuanceBandwidthCredential { - // private attributes - /// a random secret value generated by the client used for double-spending detection - #[serde(with = "scalar_serde_helper")] - serial_number: PrivateAttribute, - - /// a random secret value generated by the client used to bind multiple credentials together - #[serde(with = "scalar_serde_helper")] - binding_number: PrivateAttribute, - - /// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass - variant_data: BandwidthCredentialIssuanceDataVariant, - - /// type of the bandwdith credential hashed onto a scalar - #[serde(with = "scalar_serde_helper")] - type_prehashed: PublicAttribute, -} - -impl IssuanceBandwidthCredential { - pub const PUBLIC_ATTRIBUTES: u32 = 2; - pub const PRIVATE_ATTRIBUTES: u32 = 2; - pub const ENCODED_ATTRIBUTES: u32 = Self::PUBLIC_ATTRIBUTES + Self::PRIVATE_ATTRIBUTES; - - pub fn default_parameters() -> Parameters { - // safety: the unwrap is fine here as Self::ENCODED_ATTRIBUTES is non-zero - Parameters::new(Self::ENCODED_ATTRIBUTES).unwrap() - } - - pub fn new>(variant_data: B) -> Self { - let variant_data = variant_data.into(); - let type_prehashed = hash_to_scalar(variant_data.info().to_string()); - - let params = bandwidth_credential_params(); - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - - IssuanceBandwidthCredential { - serial_number, - binding_number, - variant_data, - type_prehashed, - } - } - - pub fn new_voucher( - value: impl Into, - deposit_tx_hash: Hash, - signing_key: identity::PrivateKey, - unused_ed25519: encryption::PrivateKey, - ) -> Self { - Self::new(BandwidthVoucherIssuanceData::new( - value, - deposit_tx_hash, - signing_key, - unused_ed25519, - )) - } - - pub fn new_freepass(expiry_date: Option) -> Self { - Self::new(FreePassIssuanceData::new(expiry_date)) - } - - pub fn blind_serial_number(&self) -> BlindedSerialNumber { - (bandwidth_credential_params().gen2() * self.serial_number).into() - } - - pub fn blinded_serial_number_bs58(&self) -> String { - use nym_credentials_interface::Base58; - - self.blind_serial_number().to_bs58() - } - - pub fn typ(&self) -> CredentialType { - self.variant_data.info() - } - - pub fn get_private_attributes(&self) -> Vec<&PrivateAttribute> { - vec![&self.serial_number, &self.binding_number] - } - - pub fn get_public_attributes(&self) -> Vec<&PublicAttribute> { - vec![self.variant_data.public_value(), &self.type_prehashed] - } - - pub fn get_plain_public_attributes(&self) -> Vec { - vec![ - self.variant_data.public_value_plain(), - self.typ().to_string(), - ] - } - - pub fn get_variant_data(&self) -> &BandwidthCredentialIssuanceDataVariant { - &self.variant_data - } - - pub fn get_bandwidth_attribute(&self) -> String { - self.variant_data.public_value_plain() - } - - pub fn prepare_for_signing(&self) -> CredentialSigningData { - let params = bandwidth_credential_params(); - - // safety: the creation of the request can only fail if one provided invalid parameters - // and we created then specific to this type of the credential so the unwrap is fine - let (pedersen_commitments_openings, blind_sign_request) = prepare_blind_sign( - params, - &[&self.serial_number, &self.binding_number], - &self.get_public_attributes(), - ) - .unwrap(); - - CredentialSigningData { - pedersen_commitments_openings, - blind_sign_request, - public_attributes_plain: self.get_plain_public_attributes(), - typ: self.typ(), - } - } - - pub fn unblind_signature( - &self, - validator_vk: &VerificationKey, - signing_data: &CredentialSigningData, - blinded_signature: BlindedSignature, - ) -> Result { - let public_attributes = self.get_public_attributes(); - let private_attributes = self.get_private_attributes(); - - let params = bandwidth_credential_params(); - let unblinded_signature = blinded_signature.unblind_and_verify( - params, - validator_vk, - &private_attributes, - &public_attributes, - &signing_data.blind_sign_request.get_commitment_hash(), - &signing_data.pedersen_commitments_openings, - )?; - - Ok(unblinded_signature) - } - - pub async fn obtain_partial_freepass_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - account_data: &AccountData, - validator_vk: &VerificationKey, - signing_data: impl Into>, - ) -> Result { - // if we provided signing data, do use them, otherwise generate fresh data - let signing_data = signing_data - .into() - .unwrap_or_else(|| self.prepare_for_signing()); - - let blinded_signature = match &self.variant_data { - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - freepass - .request_blinded_credential(&signing_data, account_data, client) - .await? - } - _ => return Err(Error::NotAFreePass), - }; - self.unblind_signature(validator_vk, &signing_data, blinded_signature) - } - - // ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers - pub async fn obtain_partial_bandwidth_voucher_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - validator_vk: &VerificationKey, - signing_data: impl Into>, - ) -> Result { - // if we provided signing data, do use them, otherwise generate fresh data - let signing_data = signing_data - .into() - .unwrap_or_else(|| self.prepare_for_signing()); - - let blinded_signature = match &self.variant_data { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => { - // TODO: the request can be re-used between different apis - let request = voucher.create_blind_sign_request_body(&signing_data); - voucher.obtain_blinded_credential(client, &request).await? - } - _ => return Err(Error::NotABandwdithVoucher), - }; - self.unblind_signature(validator_vk, &signing_data, blinded_signature) - } - - pub fn unchecked_aggregate_signature_shares( - &self, - shares: &[SignatureShare], - ) -> Result { - aggregate_signature_shares(shares).map_err(Error::SignatureAggregationError) - } - - pub fn aggregate_signature_shares( - &self, - verification_key: &VerificationKey, - shares: &[SignatureShare], - ) -> Result { - let public_attributes = self.get_public_attributes(); - let private_attributes = self.get_private_attributes(); - - let params = bandwidth_credential_params(); - - let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len()); - attributes.extend_from_slice(&private_attributes); - attributes.extend_from_slice(&public_attributes); - - aggregate_signature_shares_and_verify(params, verification_key, &attributes, shares) - .map_err(Error::SignatureAggregationError) - } - - // also drops self after the conversion - pub fn into_issued_credential( - self, - aggregate_signature: Signature, - epoch_id: EpochId, - ) -> IssuedBandwidthCredential { - self.to_issued_credential(aggregate_signature, epoch_id) - } - - pub fn to_issued_credential( - &self, - aggregate_signature: Signature, - epoch_id: EpochId, - ) -> IssuedBandwidthCredential { - IssuedBandwidthCredential::new( - self.serial_number, - self.binding_number, - aggregate_signature, - (&self.variant_data).into(), - self.type_prehashed, - epoch_id, - ) - } - - // TODO: is that actually needed? - pub fn to_recovery_bytes(&self) -> Vec { - use bincode::Options; - // safety: our data format is stable and thus the serialization should not fail - make_recovery_bincode_serializer().serialize(self).unwrap() - } - - // TODO: is that actually needed? - // idea: make it consistent with the issued credential and its vX serde - pub fn try_from_recovered_bytes(bytes: &[u8]) -> Result { - use bincode::Options; - make_recovery_bincode_serializer() - .deserialize(bytes) - .map_err(|source| Error::RecoveryCredentialDeserializationFailure { source }) - } -} - -fn make_recovery_bincode_serializer() -> impl bincode::Options { - use bincode::Options; - bincode::DefaultOptions::new() - .with_big_endian() - .with_varint_encoding() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn assert_zeroize_on_drop() {} - - fn assert_zeroize() {} - - #[test] - fn credential_is_zeroized() { - assert_zeroize::(); - assert_zeroize_on_drop::(); - } -} diff --git a/common/credentials/src/coconut/bandwidth/issued.rs b/common/credentials/src/coconut/bandwidth/issued.rs deleted file mode 100644 index 00e0ab341b..0000000000 --- a/common/credentials/src/coconut/bandwidth/issued.rs +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::bandwidth_credential_params; -use crate::coconut::bandwidth::freepass::FreePassIssuedData; -use crate::coconut::bandwidth::issuance::{ - BandwidthCredentialIssuanceDataVariant, IssuanceBandwidthCredential, -}; -use crate::coconut::bandwidth::voucher::BandwidthVoucherIssuedData; -use crate::coconut::bandwidth::{CredentialSpendingData, CredentialType}; -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_credentials_interface::prove_bandwidth_credential; -use nym_credentials_interface::{ - Parameters, PrivateAttribute, PublicAttribute, Signature, VerificationKey, -}; -use nym_validator_client::nym_api::EpochId; -use serde::{Deserialize, Serialize}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -pub const CURRENT_SERIALIZATION_REVISION: u8 = 1; - -#[derive(Debug, Zeroize, Serialize, Deserialize)] -pub enum BandwidthCredentialIssuedDataVariant { - Voucher(BandwidthVoucherIssuedData), - FreePass(FreePassIssuedData), -} - -impl<'a> From<&'a BandwidthCredentialIssuanceDataVariant> for BandwidthCredentialIssuedDataVariant { - fn from(value: &'a BandwidthCredentialIssuanceDataVariant) -> Self { - match value { - BandwidthCredentialIssuanceDataVariant::Voucher(voucher) => { - BandwidthCredentialIssuedDataVariant::Voucher(voucher.into()) - } - BandwidthCredentialIssuanceDataVariant::FreePass(freepass) => { - BandwidthCredentialIssuedDataVariant::FreePass(freepass.into()) - } - } - } -} - -impl From for BandwidthCredentialIssuedDataVariant { - fn from(value: FreePassIssuedData) -> Self { - BandwidthCredentialIssuedDataVariant::FreePass(value) - } -} - -impl From for BandwidthCredentialIssuedDataVariant { - fn from(value: BandwidthVoucherIssuedData) -> Self { - BandwidthCredentialIssuedDataVariant::Voucher(value) - } -} - -impl BandwidthCredentialIssuedDataVariant { - pub fn info(&self) -> CredentialType { - match self { - BandwidthCredentialIssuedDataVariant::Voucher(..) => CredentialType::Voucher, - BandwidthCredentialIssuedDataVariant::FreePass(..) => CredentialType::FreePass, - } - } - - // currently this works under the assumption of there being a single unique public attribute for given variant - pub fn public_value_plain(&self) -> String { - match self { - BandwidthCredentialIssuedDataVariant::Voucher(voucher) => voucher.value_plain(), - BandwidthCredentialIssuedDataVariant::FreePass(freepass) => { - freepass.expiry_date_plain() - } - } - } -} - -// the only important thing to zeroize here are the private attributes, the rest can be made fully public for what we're concerned -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct IssuedBandwidthCredential { - // private attributes - /// a random secret value generated by the client used for double-spending detection - #[serde(with = "scalar_serde_helper")] - serial_number: PrivateAttribute, - - /// a random secret value generated by the client used to bind multiple credentials together - #[serde(with = "scalar_serde_helper")] - binding_number: PrivateAttribute, - - /// the underlying aggregated signature on the attributes - #[zeroize(skip)] - signature: Signature, - - /// data specific to given bandwidth credential, for example a value for bandwidth voucher and expiry date for the free pass - variant_data: BandwidthCredentialIssuedDataVariant, - - /// type of the bandwdith credential hashed onto a scalar - #[serde(with = "scalar_serde_helper")] - type_prehashed: PublicAttribute, - - /// Specifies the (DKG) epoch id when this credential has been issued - epoch_id: EpochId, -} - -impl IssuedBandwidthCredential { - pub fn new( - serial_number: PrivateAttribute, - binding_number: PrivateAttribute, - signature: Signature, - variant_data: BandwidthCredentialIssuedDataVariant, - type_prehashed: PublicAttribute, - epoch_id: EpochId, - ) -> Self { - IssuedBandwidthCredential { - serial_number, - binding_number, - signature, - variant_data, - type_prehashed, - epoch_id, - } - } - - pub fn try_unpack(bytes: &[u8], revision: impl Into>) -> Result { - let revision = revision.into().unwrap_or(CURRENT_SERIALIZATION_REVISION); - - match revision { - 1 => Self::unpack_v1(bytes), - _ => Err(Error::UnknownSerializationRevision { revision }), - } - } - - pub fn epoch_id(&self) -> EpochId { - self.epoch_id - } - - pub fn variant_data(&self) -> &BandwidthCredentialIssuedDataVariant { - &self.variant_data - } - - pub fn current_serialization_revision(&self) -> u8 { - CURRENT_SERIALIZATION_REVISION - } - - /// Pack (serialize) this credential data into a stream of bytes using v1 serializer. - pub fn pack_v1(&self) -> Vec { - use bincode::Options; - // safety: our data format is stable and thus the serialization should not fail - make_storable_bincode_serializer().serialize(self).unwrap() - } - - /// Unpack (deserialize) the credential data from the given bytes using v1 serializer. - pub fn unpack_v1(bytes: &[u8]) -> Result { - use bincode::Options; - make_storable_bincode_serializer() - .deserialize(bytes) - .map_err(|source| Error::SerializationFailure { - source, - revision: 1, - }) - } - - pub fn default_parameters() -> Parameters { - IssuanceBandwidthCredential::default_parameters() - } - - pub fn typ(&self) -> CredentialType { - self.variant_data.info() - } - - pub fn get_plain_public_attributes(&self) -> Vec { - vec![ - self.variant_data.public_value_plain(), - self.typ().to_string(), - ] - } - - pub fn prepare_for_spending( - &self, - verification_key: &VerificationKey, - ) -> Result { - let params = bandwidth_credential_params(); - - let verify_credential_request = prove_bandwidth_credential( - params, - verification_key, - &self.signature, - &self.serial_number, - &self.binding_number, - )?; - - Ok(CredentialSpendingData { - embedded_private_attributes: IssuanceBandwidthCredential::PRIVATE_ATTRIBUTES as usize, - verify_credential_request, - public_attributes_plain: self.get_plain_public_attributes(), - typ: self.typ(), - epoch_id: self.epoch_id, - }) - } -} - -fn make_storable_bincode_serializer() -> impl bincode::Options { - use bincode::Options; - bincode::DefaultOptions::new() - .with_big_endian() - .with_varint_encoding() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn assert_zeroize_on_drop() {} - - fn assert_zeroize() {} - - #[test] - fn credential_is_zeroized() { - assert_zeroize::(); - assert_zeroize_on_drop::(); - } -} diff --git a/common/credentials/src/coconut/bandwidth/mod.rs b/common/credentials/src/coconut/bandwidth/mod.rs deleted file mode 100644 index de49c9d4b4..0000000000 --- a/common/credentials/src/coconut/bandwidth/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use std::sync::OnceLock; - -pub use issuance::IssuanceBandwidthCredential; -pub use issued::IssuedBandwidthCredential; -pub use nym_credentials_interface::{ - CredentialSigningData, CredentialSpendingData, CredentialType, Parameters, - UnknownCredentialType, -}; - -pub mod freepass; -pub mod issuance; -pub mod issued; -pub mod voucher; - -// works under the assumption of having 4 attributes in the underlying credential(s) -pub fn bandwidth_credential_params() -> &'static Parameters { - static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock = OnceLock::new(); - BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(IssuanceBandwidthCredential::default_parameters) -} diff --git a/common/credentials/src/coconut/bandwidth/voucher.rs b/common/credentials/src/coconut/bandwidth/voucher.rs deleted file mode 100644 index 58c93edf7b..0000000000 --- a/common/credentials/src/coconut/bandwidth/voucher.rs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::CredentialSigningData; -use crate::coconut::utils::scalar_serde_helper; -use crate::error::Error; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_credentials_interface::{ - hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, CredentialType, PublicAttribute, -}; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_validator_client::nyxd::{Coin, Hash}; -use serde::{Deserialize, Serialize}; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct BandwidthVoucherIssuedData { - /// the plain value (e.g., bandwidth) encoded in this voucher - // note: for legacy reasons we're only using the value of the coin and ignoring the denom - #[zeroize(skip)] - value: Coin, -} - -impl<'a> From<&'a BandwidthVoucherIssuanceData> for BandwidthVoucherIssuedData { - fn from(value: &'a BandwidthVoucherIssuanceData) -> Self { - BandwidthVoucherIssuedData { - value: value.value.clone(), - } - } -} - -impl BandwidthVoucherIssuedData { - pub fn new(value: Coin) -> Self { - BandwidthVoucherIssuedData { value } - } - - pub fn value(&self) -> &Coin { - &self.value - } - - pub fn value_plain(&self) -> String { - self.value.amount.to_string() - } -} - -#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -pub struct BandwidthVoucherIssuanceData { - /// the plain value (e.g., bandwidth) encoded in this voucher - // note: for legacy reasons we're only using the value of the coin and ignoring the denom - #[zeroize(skip)] - value: Coin, - - // note: as mentioned above, we're only hashing the value of the coin! - #[serde(with = "scalar_serde_helper")] - value_prehashed: PublicAttribute, - - /// the hash of the deposit transaction - #[zeroize(skip)] - deposit_tx_hash: Hash, - - /// base58 encoded private key ensuring the depositer requested these attributes - signing_key: identity::PrivateKey, - - /// base58 encoded private key ensuring only this client receives the signature share - unused_ed25519: encryption::PrivateKey, -} - -impl BandwidthVoucherIssuanceData { - pub fn new( - value: impl Into, - deposit_tx_hash: Hash, - signing_key: identity::PrivateKey, - unused_ed25519: encryption::PrivateKey, - ) -> Self { - let value = value.into(); - let value_prehashed = hash_to_scalar(value.amount.to_string()); - - BandwidthVoucherIssuanceData { - value, - value_prehashed, - deposit_tx_hash, - signing_key, - unused_ed25519, - } - } - - pub fn request_plaintext(request: &BlindSignRequest, tx_hash: Hash) -> Vec { - let mut message = request.to_bytes(); - message.extend_from_slice(tx_hash.as_bytes()); - message - } - - fn request_signature(&self, signing_request: &CredentialSigningData) -> identity::Signature { - let message = - Self::request_plaintext(&signing_request.blind_sign_request, self.deposit_tx_hash); - self.signing_key.sign(message) - } - - pub fn create_blind_sign_request_body( - &self, - signing_request: &CredentialSigningData, - ) -> BlindSignRequestBody { - let request_signature = self.request_signature(signing_request); - - BlindSignRequestBody::new( - signing_request.blind_sign_request.clone(), - self.deposit_tx_hash, - request_signature, - signing_request.public_attributes_plain.clone(), - ) - } - - pub async fn obtain_blinded_credential( - &self, - client: &nym_validator_client::client::NymApiClient, - request_body: &BlindSignRequestBody, - ) -> Result { - let server_response = client.blind_sign(request_body).await?; - Ok(server_response.blinded_signature) - } - - pub fn value_plain(&self) -> String { - self.value.amount.to_string() - } - - pub fn value_attribute(&self) -> &Attribute { - &self.value_prehashed - } - - pub fn typ() -> CredentialType { - CredentialType::Voucher - } - - pub fn tx_hash(&self) -> Hash { - self.deposit_tx_hash - } - - pub fn identity_key(&self) -> &identity::PrivateKey { - &self.signing_key - } - - pub fn encryption_key(&self) -> &encryption::PrivateKey { - &self.unused_ed25519 - } -} diff --git a/common/credentials/src/coconut/utils.rs b/common/credentials/src/coconut/utils.rs deleted file mode 100644 index 3192804530..0000000000 --- a/common/credentials/src/coconut/utils.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::bandwidth::IssuanceBandwidthCredential; -use crate::error::Error; -use log::{debug, warn}; -use nym_credentials_interface::{ - aggregate_verification_keys, Signature, SignatureShare, VerificationKey, -}; -use nym_validator_client::client::CoconutApiClient; - -pub fn obtain_aggregate_verification_key( - api_clients: &[CoconutApiClient], -) -> Result { - if api_clients.is_empty() { - return Err(Error::NoValidatorsAvailable); - } - - let indices: Vec<_> = api_clients - .iter() - .map(|api_client| api_client.node_id) - .collect(); - let shares: Vec<_> = api_clients - .iter() - .map(|api_client| api_client.verification_key.clone()) - .collect(); - - Ok(aggregate_verification_keys(&shares, Some(&indices))?) -} - -pub async fn obtain_aggregate_signature( - voucher: &IssuanceBandwidthCredential, - coconut_api_clients: &[CoconutApiClient], - threshold: u64, -) -> Result { - if coconut_api_clients.is_empty() { - return Err(Error::NoValidatorsAvailable); - } - let mut shares = Vec::with_capacity(coconut_api_clients.len()); - let verification_key = obtain_aggregate_verification_key(coconut_api_clients)?; - - let request = voucher.prepare_for_signing(); - - for coconut_api_client in coconut_api_clients.iter() { - debug!( - "attempting to obtain partial credential from {}", - coconut_api_client.api_client.api_url() - ); - - match voucher - .obtain_partial_bandwidth_voucher_credential( - &coconut_api_client.api_client, - &coconut_api_client.verification_key, - Some(request.clone()), - ) - .await - { - Ok(signature) => { - let share = SignatureShare::new(signature, coconut_api_client.node_id); - shares.push(share) - } - Err(err) => { - warn!( - "failed to obtain partial credential from {}: {err}", - coconut_api_client.api_client.api_url() - ); - } - }; - } - if shares.len() < threshold as usize { - return Err(Error::NotEnoughShares); - } - - voucher.aggregate_signature_shares(&verification_key, &shares) -} - -pub(crate) mod scalar_serde_helper { - use bls12_381::Scalar; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use zeroize::Zeroizing; - - pub fn serialize(scalar: &Scalar, serializer: S) -> Result { - scalar.to_bytes().serialize(serializer) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { - let b = <[u8; 32]>::deserialize(deserializer)?; - - // make sure the bytes get zeroed - let bytes = Zeroizing::new(b); - - let maybe_scalar: Option = Scalar::from_bytes(&bytes).into(); - maybe_scalar.ok_or(serde::de::Error::custom( - "did not construct a valid bls12-381 scalar out of the provided bytes", - )) - } -} diff --git a/common/credentials/src/ecash/bandwidth/issuance.rs b/common/credentials/src/ecash/bandwidth/issuance.rs new file mode 100644 index 0000000000..5cbdea45bf --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/issuance.rs @@ -0,0 +1,239 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::issued::IssuedTicketBook; +use crate::ecash::bandwidth::CredentialSigningData; +use crate::ecash::utils::cred_exp_date; +use crate::error::Error; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_credentials_interface::{ + aggregate_wallets, generate_keypair_user_from_seed, issue_verify, withdrawal_request, + BlindedSignature, KeyPairUser, PartialWallet, VerificationKeyAuth, WalletSignatures, + WithdrawalRequest, +}; +use nym_crypto::asymmetric::identity; +use nym_ecash_contract_common::deposit::DepositId; +use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime}; +use nym_validator_client::nym_api::EpochId; +use serde::{Deserialize, Serialize}; +use time::Date; + +use crate::ecash::bandwidth::serialiser::VersionedSerialise; +pub use nym_validator_client::nyxd::{Coin, Hash}; + +#[derive(Serialize, Deserialize)] +pub struct IssuanceTicketBook { + /// the id of the associated deposit + deposit_id: DepositId, + + /// base58 encoded private key ensuring the depositer requested these attributes + signing_key: identity::PrivateKey, + + /// ecash keypair related to the credential + ecash_keypair: KeyPairUser, + + /// expiration_date of that credential + expiration_date: Date, +} + +impl IssuanceTicketBook { + pub fn new>( + deposit_id: DepositId, + identifier: M, + signing_key: identity::PrivateKey, + ) -> Self { + //this expiration date will get fed to the ecash library, force midnight to be set + Self::new_with_expiration( + deposit_id, + identifier, + signing_key, + ecash_default_expiration_date(), + ) + } + + pub fn new_with_expiration>( + deposit_id: DepositId, + identifier: M, + signing_key: identity::PrivateKey, + expiration_date: Date, + ) -> Self { + let ecash_keypair = generate_keypair_user_from_seed(identifier); + IssuanceTicketBook { + deposit_id, + signing_key, + ecash_keypair, + expiration_date, + } + } + + pub fn ecash_pubkey_bs58(&self) -> String { + use nym_credentials_interface::Base58; + + self.ecash_keypair.public_key().to_bs58() + } + + pub fn expiration_date(&self) -> Date { + self.expiration_date + } + + pub fn request_plaintext(request: &WithdrawalRequest, deposit_id: DepositId) -> Vec { + let mut message = request.to_bytes(); + message.extend_from_slice(&deposit_id.to_be_bytes()); + message + } + + fn request_signature(&self, signing_request: &CredentialSigningData) -> identity::Signature { + let message = Self::request_plaintext(&signing_request.withdrawal_request, self.deposit_id); + self.signing_key.sign(message) + } + + pub fn create_blind_sign_request_body( + &self, + signing_request: &CredentialSigningData, + ) -> BlindSignRequestBody { + let request_signature = self.request_signature(signing_request); + + BlindSignRequestBody::new( + signing_request.withdrawal_request.clone(), + self.deposit_id, + request_signature, + signing_request.ecash_pub_key.clone(), + signing_request.expiration_date, + ) + } + + pub async fn obtain_blinded_credential( + &self, + client: &nym_validator_client::client::NymApiClient, + request_body: &BlindSignRequestBody, + ) -> Result { + let server_response = client.blind_sign(request_body).await?; + Ok(server_response.blinded_signature) + } + + pub fn deposit_id(&self) -> DepositId { + self.deposit_id + } + + pub fn identity_key(&self) -> &identity::PrivateKey { + &self.signing_key + } + + pub fn check_expiration_date(&self) -> bool { + self.expiration_date != cred_exp_date().ecash_date() + } + + pub fn expired(&self) -> bool { + self.expiration_date < ecash_today().date() + } + + pub fn prepare_for_signing(&self) -> CredentialSigningData { + // safety: the creation of the request can only fail if one provided invalid parameters + // and we created then specific to this type of the credential so the unwrap is fine + let (withdrawal_request, request_info) = withdrawal_request( + self.ecash_keypair.secret_key(), + self.expiration_date.ecash_unix_timestamp(), + ) + .unwrap(); + + CredentialSigningData { + withdrawal_request, + request_info, + ecash_pub_key: self.ecash_keypair.public_key(), + expiration_date: self.expiration_date, + } + } + + pub fn unblind_signature( + &self, + validator_vk: &VerificationKeyAuth, + signing_data: &CredentialSigningData, + blinded_signature: BlindedSignature, + signer_index: u64, + ) -> Result { + let unblinded_signature = issue_verify( + validator_vk, + self.ecash_keypair.secret_key(), + &blinded_signature, + &signing_data.request_info, + signer_index, + )?; + + Ok(unblinded_signature) + } + + // ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers + pub async fn obtain_partial_bandwidth_voucher_credential( + &self, + client: &nym_validator_client::client::NymApiClient, + signer_index: u64, + validator_vk: &VerificationKeyAuth, + signing_data: CredentialSigningData, + ) -> Result { + // We need signing data, because they will be used at the aggregation step + + let request = self.create_blind_sign_request_body(&signing_data); + let blinded_signature = self.obtain_blinded_credential(client, &request).await?; + self.unblind_signature(validator_vk, &signing_data, blinded_signature, signer_index) + } + + // pub fn unchecked_aggregate_signature_shares( + // &self, + // shares: &[SignatureShare], + // ) -> Result { + // aggregate_signature_shares(shares).map_err(Error::SignatureAggregationError) + // } + + pub fn aggregate_signature_shares( + &self, + verification_key: &VerificationKeyAuth, + shares: &[PartialWallet], + signing_data: CredentialSigningData, + ) -> Result { + aggregate_wallets( + verification_key, + self.ecash_keypair.secret_key(), + shares, + &signing_data.request_info, + ) + .map_err(Error::SignatureAggregationError) + .map(|w| w.into_wallet_signatures()) + } + + // also drops self after the conversion + pub fn into_issued_ticketbook( + self, + wallet: WalletSignatures, + epoch_id: EpochId, + ) -> IssuedTicketBook { + self.to_issued_ticketbook(wallet, epoch_id) + } + + pub fn to_issued_ticketbook( + &self, + wallet: WalletSignatures, + epoch_id: EpochId, + ) -> IssuedTicketBook { + IssuedTicketBook::new( + wallet, + epoch_id, + self.ecash_keypair.secret_key().clone(), + self.expiration_date, + ) + } +} + +impl VersionedSerialise for IssuanceTicketBook { + const CURRENT_SERIALISATION_REVISION: u8 = 1; + + fn try_unpack(b: &[u8], revision: impl Into>) -> Result { + let revision = revision + .into() + .unwrap_or(::CURRENT_SERIALISATION_REVISION); + + match revision { + 1 => Self::try_unpack_current(b), + _ => Err(Error::UnknownSerializationRevision { revision }), + } + } +} diff --git a/common/credentials/src/ecash/bandwidth/issued.rs b/common/credentials/src/ecash/bandwidth/issued.rs new file mode 100644 index 0000000000..5d20af3ee8 --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/issued.rs @@ -0,0 +1,174 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::serialiser::VersionedSerialise; +use crate::ecash::bandwidth::CredentialSpendingData; +use crate::ecash::utils::ecash_today; +use crate::error::Error; +use nym_credentials_interface::{ + CoinIndexSignature, ExpirationDateSignature, PayInfo, SecretKeyUser, VerificationKeyAuth, + Wallet, WalletSignatures, +}; +use nym_ecash_time::EcashTime; +use nym_validator_client::nym_api::EpochId; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; +use time::Date; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +pub const CURRENT_SERIALIZATION_REVISION: u8 = 1; + +// the only important thing to zeroize here are the private attributes, the rest can be made fully public for what we're concerned +#[derive(Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] +pub struct IssuedTicketBook { + /// the underlying wallet signatures + signatures_wallet: WalletSignatures, + + /// the counter indicating how many tickets have been spent so far + spent_tickets: u64, + + /// Specifies the (DKG) epoch id when this credential has been issued + epoch_id: EpochId, + + /// secret ecash key used to generate this wallet + ecash_secret_key: SecretKeyUser, + + /// expiration_date for easier discarding + #[zeroize(skip)] + expiration_date: Date, +} + +impl IssuedTicketBook { + pub fn new( + wallet: WalletSignatures, + epoch_id: EpochId, + ecash_secret_key: SecretKeyUser, + expiration_date: Date, + ) -> Self { + IssuedTicketBook { + signatures_wallet: wallet, + spent_tickets: 0, + epoch_id, + ecash_secret_key, + expiration_date, + } + } + + pub fn from_parts( + signatures_wallet: WalletSignatures, + epoch_id: EpochId, + ecash_secret_key: SecretKeyUser, + expiration_date: Date, + spent_tickets: u64, + ) -> Self { + IssuedTicketBook { + signatures_wallet, + spent_tickets, + epoch_id, + ecash_secret_key, + expiration_date, + } + } + + pub fn update_spent_tickets(&mut self, spent_tickets: u64) { + self.spent_tickets = spent_tickets + } + + pub fn epoch_id(&self) -> EpochId { + self.epoch_id + } + + pub fn current_serialization_revision(&self) -> u8 { + CURRENT_SERIALIZATION_REVISION + } + + pub fn expiration_date(&self) -> Date { + self.expiration_date + } + + pub fn expired(&self) -> bool { + self.expiration_date < ecash_today().date() + } + + pub fn params_total_tickets(&self) -> u64 { + nym_credentials_interface::ecash_parameters().get_total_coins() + } + + pub fn spent_tickets(&self) -> u64 { + self.spent_tickets + } + + pub fn wallet(&self) -> &WalletSignatures { + &self.signatures_wallet + } + + pub fn prepare_for_spending( + &mut self, + verification_key: &VerificationKeyAuth, + pay_info: PayInfo, + coin_indices_signatures: &[BI], + expiration_date_signatures: &[BE], + tickets_to_spend: u64, + ) -> Result + where + BI: Borrow, + BE: Borrow, + { + let params = nym_credentials_interface::ecash_parameters(); + let spend_date = ecash_today(); + + // make sure we still have enough tickets to spend + Wallet::ensure_allowance(params, self.spent_tickets, tickets_to_spend)?; + + let payment = self.signatures_wallet.spend( + params, + verification_key, + &self.ecash_secret_key, + &pay_info, + self.spent_tickets, + tickets_to_spend, + expiration_date_signatures, + coin_indices_signatures, + spend_date.ecash_unix_timestamp(), + )?; + + self.spent_tickets += tickets_to_spend; + + Ok(CredentialSpendingData { + payment, + pay_info, + spend_date: spend_date.ecash_date(), + epoch_id: self.epoch_id, + }) + } +} + +impl VersionedSerialise for IssuedTicketBook { + const CURRENT_SERIALISATION_REVISION: u8 = 1; + + fn try_unpack(b: &[u8], revision: impl Into>) -> Result { + let revision = revision + .into() + .unwrap_or(::CURRENT_SERIALISATION_REVISION); + + match revision { + 1 => Self::try_unpack_current(b), + _ => Err(Error::UnknownSerializationRevision { revision }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_zeroize_on_drop() {} + + fn assert_zeroize() {} + + #[test] + fn credential_is_zeroized() { + assert_zeroize::(); + assert_zeroize_on_drop::(); + } +} diff --git a/common/credentials/src/ecash/bandwidth/mod.rs b/common/credentials/src/ecash/bandwidth/mod.rs new file mode 100644 index 0000000000..ce75507df6 --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use issuance::IssuanceTicketBook; +pub use issued::IssuedTicketBook; +pub use nym_credentials_interface::{CredentialSigningData, CredentialSpendingData}; + +pub mod issuance; +pub mod issued; +pub mod serialiser; diff --git a/common/credentials/src/ecash/bandwidth/serialiser.rs b/common/credentials/src/ecash/bandwidth/serialiser.rs new file mode 100644 index 0000000000..acb7629002 --- /dev/null +++ b/common/credentials/src/ecash/bandwidth/serialiser.rs @@ -0,0 +1,65 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; +use crate::Error; +use bincode::Options; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::marker::PhantomData; + +pub struct VersionSerialised { + pub data: Vec, + pub revision: u8, + + // still wondering if there's any point in having the phantom in here + _phantom: PhantomData, +} + +pub trait VersionedSerialise { + const CURRENT_SERIALISATION_REVISION: u8; + + fn current_serialization_revision(&self) -> u8 { + CURRENT_SERIALIZATION_REVISION + } + + // implicitly always uses current revision + fn pack(&self) -> VersionSerialised + where + Self: Serialize, + { + let data = make_current_storable_bincode_serializer() + .serialize(self) + .expect("serialisation failure"); + + VersionSerialised { + data, + revision: Self::CURRENT_SERIALISATION_REVISION, + _phantom: Default::default(), + } + } + + fn try_unpack_current(b: &[u8]) -> Result + where + Self: DeserializeOwned, + { + make_current_storable_bincode_serializer() + .deserialize(b) + .map_err(|source| Error::SerializationFailure { + source, + revision: Self::CURRENT_SERIALISATION_REVISION, + }) + } + + // this is up to whoever implements the trait to provide function implementation, + // as they might have to have different implementations per revision + fn try_unpack(b: &[u8], revision: impl Into>) -> Result + where + Self: DeserializeOwned; +} + +fn make_current_storable_bincode_serializer() -> impl bincode::Options { + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/credentials/src/coconut/mod.rs b/common/credentials/src/ecash/mod.rs similarity index 100% rename from common/credentials/src/coconut/mod.rs rename to common/credentials/src/ecash/mod.rs diff --git a/common/credentials/src/ecash/utils.rs b/common/credentials/src/ecash/utils.rs new file mode 100644 index 0000000000..38ee31f2c1 --- /dev/null +++ b/common/credentials/src/ecash/utils.rs @@ -0,0 +1,210 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::bandwidth::IssuanceTicketBook; +use crate::error::Error; +use log::{debug, warn}; +use nym_credentials_interface::{ + aggregate_expiration_signatures, aggregate_indices_signatures, Base58, CoinIndexSignature, + CoinIndexSignatureShare, ExpirationDateSignature, ExpirationDateSignatureShare, + VerificationKeyAuth, WalletSignatures, +}; +use nym_validator_client::client::EcashApiClient; + +// so we wouldn't break all the existing imports +pub use nym_ecash_time::{cred_exp_date, ecash_date_offset, ecash_today, EcashTime}; + +pub fn aggregate_verification_keys( + api_clients: &[EcashApiClient], +) -> Result { + if api_clients.is_empty() { + return Err(Error::NoValidatorsAvailable); + } + + let indices: Vec<_> = api_clients + .iter() + .map(|api_client| api_client.node_id) + .collect(); + let shares: Vec<_> = api_clients + .iter() + .map(|api_client| api_client.verification_key.clone()) + .collect(); + + Ok(nym_credentials_interface::aggregate_verification_keys( + &shares, + Some(&indices), + )?) +} + +pub fn obtain_aggregated_verification_key( + _api_clients: &[EcashApiClient], +) -> Result { + // TODO: + // let total = api_clients.len(); + // let mut rng = thread_rng(); + // let indices = sample(&mut rng, total, total); + // for index in indices { + // // randomly try apis until we succeed + // // if let Ok(res) = api_clients[index].api_client.get_aggregated_verification_key().await { + // // // + // // } + // } + todo!() +} + +pub async fn obtain_expiration_date_signatures( + ecash_api_clients: &[EcashApiClient], + verification_key: &VerificationKeyAuth, + threshold: u64, +) -> Result, Error> { + if ecash_api_clients.is_empty() { + return Err(Error::NoValidatorsAvailable); + } + + let mut signatures_shares: Vec<_> = Vec::with_capacity(ecash_api_clients.len()); + + let expiration_date = cred_exp_date().unix_timestamp() as u64; + for ecash_api_client in ecash_api_clients.iter() { + match ecash_api_client + .api_client + .partial_expiration_date_signatures(None) + .await + { + Ok(signature) => { + let index = ecash_api_client.node_id; + let key_share = ecash_api_client.verification_key.clone(); + signatures_shares.push(ExpirationDateSignatureShare { + index, + key: key_share, + signatures: signature.signatures, + }); + } + Err(err) => { + warn!( + "failed to obtain expiration date signature from {}: {err}", + ecash_api_client.api_client.api_url() + ); + } + } + } + + if signatures_shares.len() < threshold as usize { + return Err(Error::NotEnoughShares); + } + + //this already takes care of partial signatures validation + aggregate_expiration_signatures(verification_key, expiration_date, &signatures_shares) + .map_err(Error::CompactEcashError) +} + +pub async fn obtain_coin_indices_signatures( + ecash_api_clients: &[EcashApiClient], + verification_key: &VerificationKeyAuth, + threshold: u64, +) -> Result, Error> { + if ecash_api_clients.is_empty() { + return Err(Error::NoValidatorsAvailable); + } + + let mut signatures_shares: Vec<_> = Vec::with_capacity(ecash_api_clients.len()); + + for ecash_api_client in ecash_api_clients.iter() { + match ecash_api_client + .api_client + .partial_coin_indices_signatures(None) + .await + { + Ok(signature) => { + let index = ecash_api_client.node_id; + let key_share = ecash_api_client.verification_key.clone(); + signatures_shares.push(CoinIndexSignatureShare { + index, + key: key_share, + signatures: signature.signatures, + }); + } + Err(err) => { + warn!( + "failed to obtain expiration date signature from {}: {err}", + ecash_api_client.api_client.api_url() + ); + } + } + } + + if signatures_shares.len() < threshold as usize { + return Err(Error::NotEnoughShares); + } + + //this takes care of validating partial signatures + aggregate_indices_signatures( + nym_credentials_interface::ecash_parameters(), + verification_key, + &signatures_shares, + ) + .map_err(Error::CompactEcashError) +} + +pub async fn obtain_aggregate_wallet( + voucher: &IssuanceTicketBook, + ecash_api_clients: &[EcashApiClient], + threshold: u64, +) -> Result { + if ecash_api_clients.len() < threshold as usize { + return Err(Error::NoValidatorsAvailable); + } + let verification_key = aggregate_verification_keys(ecash_api_clients)?; + + let request = voucher.prepare_for_signing(); + + let mut wallets = Vec::with_capacity(ecash_api_clients.len()); + + // TODO: optimise and query just threshold + for ecash_api_client in ecash_api_clients.iter() { + debug!( + "attempting to obtain partial credential from {}", + ecash_api_client.api_client.api_url() + ); + + match voucher + .obtain_partial_bandwidth_voucher_credential( + &ecash_api_client.api_client, + ecash_api_client.node_id, + &ecash_api_client.verification_key, + request.clone(), + ) + .await + { + Ok(wallet) => wallets.push(wallet), + Err(err) => { + warn!("failed to obtain partial credential from API {ecash_api_client}: {err}",); + } + }; + } + if wallets.len() < threshold as usize { + return Err(Error::NotEnoughShares); + } + + voucher.aggregate_signature_shares(&verification_key, &wallets, request) +} + +pub fn signatures_to_string(sigs: &[B]) -> String +where + B: Base58, +{ + sigs.iter() + .map(|sig| sig.to_bs58()) + .collect::>() + .join(",") +} + +pub fn signatures_from_string(bs58_sigs: String) -> Result, Error> +where + B: Base58, +{ + bs58_sigs + .split(',') + .map(B::try_from_bs58) + .collect::, _>>() + .map_err(Error::CompactEcashError) +} diff --git a/common/credentials/src/error.rs b/common/credentials/src/error.rs index 6487d23870..c1f99608ee 100644 --- a/common/credentials/src/error.rs +++ b/common/credentials/src/error.rs @@ -1,11 +1,10 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_credentials_interface::CoconutError; +use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; +use nym_credentials_interface::CompactEcashError; use nym_crypto::asymmetric::encryption::KeyRecoveryError; use nym_validator_client::ValidatorClientError; - -use crate::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; use thiserror::Error; #[derive(Debug, Error)] @@ -26,14 +25,11 @@ pub enum Error { #[error("unknown credential serializatio revision {revision}. the current (and max supported) version is {CURRENT_SERIALIZATION_REVISION}")] UnknownSerializationRevision { revision: u8 }, - #[error("The detailed description is yet to be determined")] - BandwidthCredentialError, - #[error("Could not contact any validator")] NoValidatorsAvailable, - #[error("Ran into a coconut error - {0}")] - CoconutError(#[from] CoconutError), + #[error("Ran into a Compact ecash error - {0}")] + CompactEcashError(#[from] CompactEcashError), #[error("Ran into a validator client error - {0}")] ValidatorClientError(#[from] ValidatorClientError), @@ -51,7 +47,7 @@ pub enum Error { NotEnoughShares, #[error("Could not aggregate signature shares - {0}. Try again using the recovery command")] - SignatureAggregationError(CoconutError), + SignatureAggregationError(CompactEcashError), #[error("Could not deserialize bandwidth voucher - {0}")] BandwidthVoucherDeserializationError(String), @@ -59,9 +55,6 @@ pub enum Error { #[error("the provided issuance data wasn't prepared for a bandwidth voucher")] NotABandwdithVoucher, - #[error("the provided issuance data wasn't prepared for a free pass")] - NotAFreePass, - #[error("failed to create a secp256k1 signature")] Secp256k1SignFailure, } diff --git a/common/credentials/src/lib.rs b/common/credentials/src/lib.rs index c06eeb0a05..7f2c3ef2d4 100644 --- a/common/credentials/src/lib.rs +++ b/common/credentials/src/lib.rs @@ -1,12 +1,11 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod coconut; +pub mod ecash; pub mod error; -pub use coconut::bandwidth::{ - CredentialSigningData, CredentialSpendingData, IssuanceBandwidthCredential, - IssuedBandwidthCredential, +pub use ecash::bandwidth::{ + CredentialSigningData, CredentialSpendingData, IssuanceTicketBook, IssuedTicketBook, }; -pub use coconut::utils::{obtain_aggregate_signature, obtain_aggregate_verification_key}; +pub use ecash::utils::{aggregate_verification_keys, obtain_aggregate_wallet}; pub use error::Error; diff --git a/common/crypto/src/shared_key.rs b/common/crypto/src/shared_key.rs index bcc707489b..f90e99feb2 100644 --- a/common/crypto/src/shared_key.rs +++ b/common/crypto/src/shared_key.rs @@ -3,12 +3,13 @@ use crate::asymmetric::encryption; use crate::hkdf; -#[cfg(feature = "rand")] -use cipher::crypto_common::rand_core::{CryptoRng, RngCore}; use cipher::{Key, KeyIvInit, StreamCipher}; use digest::crypto_common::BlockSizeUser; use digest::Digest; +#[cfg(feature = "rand")] +use rand::{CryptoRng, RngCore}; + /// Generate an ephemeral encryption keypair and perform diffie-hellman to establish /// shared key with the remote. #[cfg(feature = "rand")] diff --git a/common/ecash-double-spending/Cargo.toml b/common/ecash-double-spending/Cargo.toml new file mode 100644 index 0000000000..feb237f5d6 --- /dev/null +++ b/common/ecash-double-spending/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nym-ecash-double-spending" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +bit-vec = { workspace = true } +bloomfilter = { workspace = true } +nym-network-defaults = { path = "../network-defaults" } diff --git a/common/ecash-double-spending/src/lib.rs b/common/ecash-double-spending/src/lib.rs new file mode 100644 index 0000000000..78f24dba38 --- /dev/null +++ b/common/ecash-double-spending/src/lib.rs @@ -0,0 +1,136 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bit_vec::BitVec; +use bloomfilter::Bloom; +use nym_network_defaults::{BloomfilterParameters, ECASH_DS_BLOOMFILTER_PARAMS}; + +pub struct DoubleSpendingFilter { + params: BloomfilterParameters, + inner: Bloom>, +} + +impl Default for DoubleSpendingFilter { + fn default() -> Self { + DoubleSpendingFilter::new_empty_ecash() + } +} + +pub fn bloom_from_params(params: &BloomfilterParameters, bitvec: BitVec) -> Bloom> { + assert_eq!(params.bitmap_size, bitvec.len() as u64); + + Bloom::from_bit_vec( + bitvec, + params.bitmap_size, + params.num_hashes, + params.sip_keys, + ) +} + +impl DoubleSpendingFilter { + pub fn new_empty(params: BloomfilterParameters) -> Self { + let bitvec = BitVec::from_elem(params.bitmap_size as usize, false); + DoubleSpendingFilter { + inner: bloom_from_params(¶ms, bitvec), + params, + } + } + + pub fn params(&self) -> BloomfilterParameters { + self.params + } + + pub fn rebuild(&self) -> DoubleSpendingFilterBuilder { + DoubleSpendingFilterBuilder::new(self.params) + } + + pub fn reset(&mut self) { + self.inner.clear() + } + + pub fn new_empty_ecash() -> Self { + DoubleSpendingFilter::new_empty(ECASH_DS_BLOOMFILTER_PARAMS) + } + + pub fn builder(params: BloomfilterParameters) -> DoubleSpendingFilterBuilder { + DoubleSpendingFilterBuilder::new(params) + } + + pub fn from_bytes(params: BloomfilterParameters, bitmap: &[u8]) -> Self { + DoubleSpendingFilter { + inner: bloom_from_params(¶ms, BitVec::from_bytes(bitmap)), + params, + } + } + + pub fn replace_bitvec(&mut self, new: BitVec) { + self.inner = bloom_from_params(&self.params, new) + } + + pub fn dump_bitmap(&self) -> Vec { + self.inner.bitmap() + } + + pub fn set(&mut self, b: &Vec) { + self.inner.set(b); + } + + pub fn check(&self, b: &Vec) -> bool { + self.inner.check(b) + } +} + +pub struct DoubleSpendingFilterBuilder { + params: BloomfilterParameters, + bit_vec_builder: Option, +} + +impl DoubleSpendingFilterBuilder { + pub fn new(params: BloomfilterParameters) -> Self { + DoubleSpendingFilterBuilder { + params, + bit_vec_builder: None, + } + } + + pub fn add_bytes(&mut self, b: &[u8]) -> bool { + match &mut self.bit_vec_builder { + None => { + self.bit_vec_builder = Some(BitVecBuilder::new(b)); + true + } + Some(builder) => builder.add_bytes(b), + } + } + + pub fn build(self) -> DoubleSpendingFilter { + match self.bit_vec_builder { + None => DoubleSpendingFilter::new_empty(self.params), + Some(builder) => DoubleSpendingFilter { + inner: bloom_from_params(&self.params, builder.finish()), + params: self.params, + }, + } + } +} + +pub struct BitVecBuilder(BitVec); + +impl BitVecBuilder { + pub fn new(initial_bitmap: &[u8]) -> Self { + BitVecBuilder(BitVec::from_bytes(initial_bitmap)) + } + + pub fn add_bytes(&mut self, b: &[u8]) -> bool { + let add = BitVec::from_bytes(b); + if self.0.len() != add.len() { + return false; + } + self.0.or(&add); + true + } + + pub fn finish(self) -> BitVec { + self.0 + } +} diff --git a/common/ecash-time/Cargo.toml b/common/ecash-time/Cargo.toml new file mode 100644 index 0000000000..4eaee90438 --- /dev/null +++ b/common/ecash-time/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nym-ecash-time" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +time.workspace = true + +nym-compact-ecash = { path = "../nym_offline_compact_ecash", optional = true } + +[features] +expiration = ["nym-compact-ecash"] \ No newline at end of file diff --git a/common/ecash-time/src/lib.rs b/common/ecash-time/src/lib.rs new file mode 100644 index 0000000000..e6ac459508 --- /dev/null +++ b/common/ecash-time/src/lib.rs @@ -0,0 +1,71 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use time::{Duration, PrimitiveDateTime, Time}; + +pub use time::{Date, OffsetDateTime}; + +pub trait EcashTime { + fn ecash_unix_timestamp(&self) -> u64 { + let ts = self.ecash_datetime().unix_timestamp(); + + // just panic on pre-1970 timestamps... + assert!(ts > 0); + + ts as u64 + } + + fn ecash_date(&self) -> Date { + self.ecash_datetime().date() + } + + fn ecash_datetime(&self) -> OffsetDateTime; +} + +impl EcashTime for OffsetDateTime { + fn ecash_datetime(&self) -> OffsetDateTime { + self.replace_time(Time::MIDNIGHT) + } +} + +impl EcashTime for PrimitiveDateTime { + fn ecash_datetime(&self) -> OffsetDateTime { + self.assume_utc().ecash_datetime() + } +} + +impl EcashTime for Date { + fn ecash_datetime(&self) -> OffsetDateTime { + OffsetDateTime::new_utc(*self, Time::MIDNIGHT) + } +} + +pub fn ecash_today() -> OffsetDateTime { + OffsetDateTime::now_utc().ecash_datetime() +} + +pub fn ecash_today_date() -> Date { + ecash_today().ecash_date() +} + +// no point in supporting more than i8 variance +pub fn ecash_date_offset(offset: i8) -> OffsetDateTime { + let today = ecash_today(); + + let day = today + Duration::days(offset as i64); + + // make sure to correct the time in case of DST + day.replace_time(Time::MIDNIGHT) +} + +#[cfg(feature = "expiration")] +pub fn cred_exp_date() -> OffsetDateTime { + //count today as well + ecash_date_offset(nym_compact_ecash::constants::CRED_VALIDITY_PERIOD_DAYS as i8 - 1) + // ecash_today() + Duration::days(constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 1) +} + +#[cfg(feature = "expiration")] +pub fn ecash_default_expiration_date() -> Date { + cred_exp_date().ecash_date() +} diff --git a/common/network-defaults/Cargo.toml b/common/network-defaults/Cargo.toml index 4de02982ec..562f12559c 100644 --- a/common/network-defaults/Cargo.toml +++ b/common/network-defaults/Cargo.toml @@ -14,6 +14,6 @@ hex-literal = { workspace = true } log = { workspace = true } once_cell = { workspace = true } schemars = { workspace = true, features = ["preserve_order"] } -serde = { workspace = true, features = ["derive"]} +serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } url = { workspace = true } diff --git a/common/network-defaults/src/ecash.rs b/common/network-defaults/src/ecash.rs new file mode 100644 index 0000000000..944abdff27 --- /dev/null +++ b/common/network-defaults/src/ecash.rs @@ -0,0 +1,39 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +/// How much bandwidth (in bytes) one ticket can buy +pub const TICKET_BANDWIDTH_VALUE: u64 = 100 * 1024 * 1024; // 100 MB + +///Tickets to spend per payment +pub const SPEND_TICKETS: u64 = 1; +/// Threshold for claiming more bandwidth: 1 MB +pub const REMAINING_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024; + +// Constants for bloom filter for double spending detection +//Chosen for FP of +//Calculator at https://hur.st/bloomfilter/ +pub const ECASH_DS_BLOOMFILTER_PARAMS: BloomfilterParameters = BloomfilterParameters { + num_hashes: 13, + bitmap_size: 250_000, + sip_keys: [ + (12345678910111213141, 1415926535897932384), + (7182818284590452353, 3571113171923293137), + ], +}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct BloomfilterParameters { + pub num_hashes: u32, + pub bitmap_size: u64, + pub sip_keys: [(u64, u64); 2], +} + +impl BloomfilterParameters { + pub const fn byte_size(&self) -> u64 { + self.bitmap_size / 8 + } + + pub const fn default_ecash() -> Self { + ECASH_DS_BLOOMFILTER_PARAMS + } +} diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index 33a416e4ae..edbcbdc498 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -13,9 +13,12 @@ use std::{ }; use url::Url; +pub mod ecash; pub mod mainnet; pub mod var_names; +pub use ecash::*; + #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] pub struct ChainDetails { pub bech32_account_prefix: String, @@ -27,7 +30,7 @@ pub struct ChainDetails { pub struct NymContracts { pub mixnet_contract_address: Option, pub vesting_contract_address: Option, - pub coconut_bandwidth_contract_address: Option, + pub ecash_contract_address: Option, pub group_contract_address: Option, pub multisig_contract_address: Option, pub coconut_dkg_contract_address: Option, @@ -119,9 +122,7 @@ impl NymNetworkDetails { )) .with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS)) .with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS)) - .with_coconut_bandwidth_contract(get_optional_env( - var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - )) + .with_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS)) .with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS)) .with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS)) .with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS)) @@ -145,9 +146,7 @@ impl NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS), - coconut_bandwidth_contract_address: parse_optional_str( - mainnet::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ), + ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str( @@ -235,8 +234,8 @@ impl NymNetworkDetails { } #[must_use] - pub fn with_coconut_bandwidth_contract>(mut self, contract: Option) -> Self { - self.contracts.coconut_bandwidth_contract_address = contract.map(Into::into); + pub fn with_ecash_contract>(mut self, contract: Option) -> Self { + self.contracts.ecash_contract_address = contract.map(Into::into); self } @@ -421,19 +420,6 @@ pub fn setup_env>(config_env_file: Option

) { } } -/// How much bandwidth (in bytes) one token can buy -pub const BYTES_PER_UTOKEN: u64 = 1024; -/// How much bandwidth (in bytes) one freepass provides -pub const BYTES_PER_FREEPASS: u64 = 1024 * 1024 * 1024; // 1GB -/// Threshold for claiming more bandwidth: 1 MB -pub const REMAINING_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024; -/// How many tokens should be burned to buy bandwidth -pub const TOKENS_TO_BURN: u64 = 1; -/// How many ERC20 utokens should be burned to buy bandwidth -pub const UTOKENS_TO_BURN: u64 = TOKENS_TO_BURN * 1000000; -/// Default bandwidth (in bytes) that we try to buy -pub const BANDWIDTH_VALUE: u64 = UTOKENS_TO_BURN * BYTES_PER_UTOKEN; - /// Defaults Cosmos Hub/ATOM path pub const COSMOS_DERIVATION_PATH: &str = "m/44'/118'/0'/0/0"; // as set by validators in their configs diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index a9200d0239..10575c2729 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -17,7 +17,7 @@ pub const MIXNET_CONTRACT_ADDRESS: &str = pub const VESTING_CONTRACT_ADDRESS: &str = "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; -pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = ""; +pub const ECASH_CONTRACT_ADDRESS: &str = ""; pub const GROUP_CONTRACT_ADDRESS: &str = "n1e2zq4886zzewpvpucmlw8v9p7zv692f6yck4zjzxh699dkcmlrfqk2knsr"; pub const MULTISIG_CONTRACT_ADDRESS: &str = @@ -92,10 +92,7 @@ pub fn export_to_env() { var_names::VESTING_CONTRACT_ADDRESS, VESTING_CONTRACT_ADDRESS, ); - set_var_to_default( - var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ); + set_var_to_default(var_names::ECASH_CONTRACT_ADDRESS, ECASH_CONTRACT_ADDRESS); set_var_to_default(var_names::GROUP_CONTRACT_ADDRESS, GROUP_CONTRACT_ADDRESS); set_var_to_default( var_names::MULTISIG_CONTRACT_ADDRESS, @@ -133,10 +130,7 @@ pub fn export_to_env_if_not_set() { var_names::VESTING_CONTRACT_ADDRESS, VESTING_CONTRACT_ADDRESS, ); - set_var_conditionally_to_default( - var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ); + set_var_conditionally_to_default(var_names::ECASH_CONTRACT_ADDRESS, ECASH_CONTRACT_ADDRESS); set_var_conditionally_to_default(var_names::GROUP_CONTRACT_ADDRESS, GROUP_CONTRACT_ADDRESS); set_var_conditionally_to_default( var_names::MULTISIG_CONTRACT_ADDRESS, diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index 8a629ee314..64f2e58f48 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -14,7 +14,7 @@ pub const STAKE_DENOM_DISPLAY: &str = "STAKE_DENOM_DISPLAY"; pub const DENOMS_EXPONENT: &str = "DENOMS_EXPONENT"; pub const MIXNET_CONTRACT_ADDRESS: &str = "MIXNET_CONTRACT_ADDRESS"; pub const VESTING_CONTRACT_ADDRESS: &str = "VESTING_CONTRACT_ADDRESS"; -pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "COCONUT_BANDWIDTH_CONTRACT_ADDRESS"; +pub const ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS"; pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS"; pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS"; pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS"; diff --git a/common/nym-id/src/error.rs b/common/nym-id/src/error.rs index 2edc03fd0e..780658944e 100644 --- a/common/nym-id/src/error.rs +++ b/common/nym-id/src/error.rs @@ -3,7 +3,7 @@ use std::error::Error; use thiserror::Error; -use time::OffsetDateTime; +use time::Date; #[derive(Debug, Error)] pub enum NymIdError { @@ -11,7 +11,7 @@ pub enum NymIdError { CredentialDeserializationFailure { source: nym_credentials::Error }, #[error("attempted to import an expired credential (it expired on {expiration})")] - ExpiredCredentialImport { expiration: OffsetDateTime }, + ExpiredCredentialImport { expiration: Date }, #[error("failed to store credential in the provided store: {source}")] StorageError { diff --git a/common/nym-id/src/import_credential.rs b/common/nym-id/src/import_credential.rs index 0d3a9abce4..867db15749 100644 --- a/common/nym-id/src/import_credential.rs +++ b/common/nym-id/src/import_credential.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::NymIdError; -use nym_credential_storage::models::StorableIssuedCredential; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::IssuedBandwidthCredential; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials::ecash::utils::EcashTime; +use nym_credentials::IssuedTicketBook; use time::OffsetDateTime; use tracing::{debug, warn}; use zeroize::Zeroizing; @@ -14,7 +14,7 @@ pub async fn import_credential( credentials_store: S, raw_credential: Vec, credential_version: impl Into>, -) -> Result, NymIdError> +) -> Result where S: Storage, ::StorageError: Send + Sync + 'static, @@ -22,54 +22,28 @@ where let raw_credential = Zeroizing::new(raw_credential); // note: the type itself implements ZeroizeOnDrop - let credential = IssuedBandwidthCredential::try_unpack(&raw_credential, credential_version) + let ticketbook = IssuedTicketBook::try_unpack(&raw_credential, credential_version) .map_err(|source| NymIdError::CredentialDeserializationFailure { source })?; debug!( - "attempting to import credential of type {}", - credential.typ() + "attempting to import credential with expiration date at {}", + ticketbook.expiration_date() ); - let expiry_date = match credential.variant_data() { - BandwidthCredentialIssuedDataVariant::Voucher(voucher_info) => { - debug!("with value of {}", voucher_info.value()); - None - } - BandwidthCredentialIssuedDataVariant::FreePass(freepass_info) => { - debug!("with expiry at {}", freepass_info.expiry_date()); - if freepass_info.expired() { - warn!("the free pass has already expired!"); + if ticketbook.expired() { + warn!("the credential has already expired!"); - // technically we can import it, but the gateway will just reject it so what's the point - return Err(NymIdError::ExpiredCredentialImport { - expiration: freepass_info.expiry_date(), - }); - } else { - Some(freepass_info.expiry_date()) - } - } - }; - - // SAFETY: - // for the epoch to run over u32::MAX, we'd have to advance it for few centuries every block... - // the alternative is a very particularly malformed serialized data, but at that point blowing up is the right call - // because we can't rely on it anyway - #[allow(clippy::expect_used)] - let storable = StorableIssuedCredential { - serialization_revision: credential.current_serialization_revision(), - credential_data: &raw_credential, - credential_type: credential.typ().to_string(), - epoch_id: credential - .epoch_id() - .try_into() - .expect("our epoch is has run over u32::MAX!"), - }; + // technically we can import it, but the gateway will just reject it so what's the point + return Err(NymIdError::ExpiredCredentialImport { + expiration: ticketbook.expiration_date(), + }); + } credentials_store - .insert_issued_credential(storable) + .insert_issued_ticketbook(&ticketbook) .await .map_err(|source| NymIdError::StorageError { source: Box::new(source), })?; - Ok(expiry_date) + Ok(ticketbook.expiration_date().ecash_datetime()) } diff --git a/common/nym_offline_compact_ecash/Cargo.toml b/common/nym_offline_compact_ecash/Cargo.toml new file mode 100644 index 0000000000..d296effaca --- /dev/null +++ b/common/nym_offline_compact_ecash/Cargo.toml @@ -0,0 +1,64 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nym-compact-ecash" +version = "0.1.0" +authors = ["Ania Piotrowska "] +edition = "2021" +license = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bls12_381 = { workspace = true, features = ["alloc", "pairings", "experimental", "zeroize", "experimental_serde"] } +bincode.workspace = true +cfg-if.workspace = true +itertools = "0.12.1" +digest = "0.9" +rand = { workspace = true } +thiserror = { workspace = true } +sha2 = "0.9" +bs58 = { workspace = true } +serde = { workspace = true, features = ["derive"] } +rayon = { version = "1.5.0", optional = true } +zeroize = { workspace = true, features = ["zeroize_derive"] } +ff = { workspace = true } +group = { workspace = true } + +nym-pemstore = { path = "../pemstore" } + +[dev-dependencies] +criterion = { version = "0.5.1", features = ["html_reports"] } + + +[[bench]] +name = "benchmarks_group_operations" +path = "benches/benchmarks_group_operations.rs" +harness = false + +[[bench]] +name = "benchmarks_expiration_date_signatures" +path = "benches/benchmarks_expiration_date_signatures.rs" +harness = false + +[[bench]] +name = "benchmarks_coin_indices_signatures" +path = "benches/benchmarks_coin_indices_signatures.rs" +harness = false + +[[bench]] +name = "benchmarks_ecash_e2e" +path = "benches/benchmarks_ecash_e2e.rs" +harness = false + +[features] +# for 1000 coin indices it goes from ~50ms to ~400ms, but we only have to issue them once per epoch +# so it's not really worth it +par_signing = ["rayon"] + +# for this one there's an argument for it since the verification of 1000 indices can take over 6s, +# but given it's not done very frequently, it shouldn't be too much of a problem +# furthermore, we can't and shouldn't dedicate the entire nym-api CPU just for verification, +# but this feature might potentially be desirable for clients. +par_verify = ["rayon"] \ No newline at end of file diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_coin_indices_signatures.rs b/common/nym_offline_compact_ecash/benches/benchmarks_coin_indices_signatures.rs new file mode 100644 index 0000000000..6b7628814d --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_coin_indices_signatures.rs @@ -0,0 +1,116 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use criterion::{criterion_group, criterion_main, Criterion}; +use nym_compact_ecash::scheme::coin_indices_signatures::{ + aggregate_indices_signatures, sign_coin_indices, verify_coin_indices_signatures, + CoinIndexSignatureShare, +}; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::setup::Parameters; +use nym_compact_ecash::{aggregate_verification_keys, ttp_keygen, VerificationKeyAuth}; + +fn bench_coin_signing(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-sign-verify-coin-signing"); + + let ll = 32; + let params = Parameters::new(ll); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // Pick one authority to do the signing + let sk_i_auth = authorities_keypairs[0].secret_key(); + let vk_i_auth = authorities_keypairs[0].verification_key(); + + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let partial_signatures = sign_coin_indices(¶ms, &verification_key, sk_i_auth).unwrap(); + + // ISSUING AUTHORITY BENCHMARK: issue a set of (partial) signatures for coin indices + group.bench_function( + &format!( + "[IssuingAuthority] sign_coin_indices_L_{}", + params.get_total_coins() + ), + |b| b.iter(|| sign_coin_indices(¶ms, &verification_key, sk_i_auth)), + ); + + // CLIENT: verify the correctness of the (partial)) signatures for coin indices + assert!( + verify_coin_indices_signatures(&verification_key, &vk_i_auth, &partial_signatures).is_ok() + ); + group.bench_function( + &format!( + "[Client] verify_coin_indices_signatures_L_{}", + params.get_total_coins() + ), + |b| { + b.iter(|| { + verify_coin_indices_signatures(&verification_key, &vk_i_auth, &partial_signatures) + }) + }, + ); +} + +fn bench_aggregate_coin_indices_signatures(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-aggregate-coin-signing"); + + let ll = 32; + let params = Parameters::new(ll); + let authorities_keypairs = ttp_keygen(7, 10).unwrap(); + let indices: [u64; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + // create the partial signatures from each authority + let partial_signatures: Vec> = secret_keys_authorities + .iter() + .map(|sk_auth| sign_coin_indices(¶ms, &verification_key, sk_auth).unwrap()) + .collect(); + + let combined_data: Vec<_> = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| CoinIndexSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + // CLIENT: verify all the partial signature vectors and aggregate into a single vector of signed coin indices + group.bench_function( + &format!( + "[Client] aggregate_coin_indices_signatures_from_{}_issuing_authorities_L_{}", + authorities_keypairs.len(), + params.get_total_coins(), + ), + |b| b.iter(|| aggregate_indices_signatures(¶ms, &verification_key, &combined_data)), + ); +} + +criterion_group!( + benches, + bench_coin_signing, + bench_aggregate_coin_indices_signatures +); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_ecash_e2e.rs b/common/nym_offline_compact_ecash/benches/benchmarks_ecash_e2e.rs new file mode 100644 index 0000000000..8beefea27e --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_ecash_e2e.rs @@ -0,0 +1,283 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use criterion::{criterion_group, criterion_main, Criterion}; +use itertools::izip; +use nym_compact_ecash::identify::{identify, IdentifyResult}; +use nym_compact_ecash::scheme::expiration_date_signatures::date_scalar; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::setup::Parameters; +use nym_compact_ecash::tests::helpers::{ + generate_coin_indices_signatures, generate_expiration_date_signatures, +}; +use nym_compact_ecash::{ + aggregate_verification_keys, aggregate_wallets, generate_keypair_user, issue, issue_verify, + ttp_keygen, withdrawal_request, PartialWallet, PayInfo, PublicKeyUser, SecretKeyUser, + VerificationKeyAuth, +}; +use rand::seq::SliceRandom; + +struct BenchCase { + num_authorities: u64, + threshold_p: f32, + ll: u64, + spend_vv: u64, + case_nr_pub_keys: u64, +} + +fn bench_compact_ecash(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-compact-ecash"); + // group.sample_size(300); + // group.measurement_time(Duration::from_secs(1500)); + + let expiration_date = 1703721600; // Dec 28 2023 + let spend_date = 1701907200; // Dec 07 2023 + + let case = BenchCase { + num_authorities: 100, + threshold_p: 0.7, + ll: 1000, + spend_vv: 1, + case_nr_pub_keys: 99, + }; + + // SETUP PHASE and KEY GENERATION + let params = Parameters::new(case.ll); + + let grp = params.grp(); + let user_keypair = generate_keypair_user(); + let threshold = (case.threshold_p * case.num_authorities as f32).round() as u64; + let authorities_keypairs = ttp_keygen(threshold, case.num_authorities).unwrap(); + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let indices: Vec = (1..case.num_authorities + 1).collect(); + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + // PRE-GENERATION OF THE EXPORATION DATE SIGNATURES AND THE COIN INDICES SIGNATURES + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // ISSUANCE PHASE + let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + + // CLIENT BENCHMARK: prepare a single withdrawal request + group.bench_function( + &format!( + "[Client] withdrawal_request_{}_authorities_{}_L_{}_threshold", + case.num_authorities, case.ll, case.threshold_p, + ), + |b| b.iter(|| withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap()), + ); + + // ISSUING AUTHRORITY BENCHMARK: Benchmark the issue function + // called by an authority to issue a blind signature on a partial wallet + let mut rng = rand::thread_rng(); + let keypair = authorities_keypairs.choose(&mut rng).unwrap(); + group.bench_function( + &format!( + "[Issuing Authority] issue_partial_wallet_with_L_{}", + case.ll, + ), + |b| { + b.iter(|| { + issue( + keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ) + }) + }, + ); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in &authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + // CLIENT BENCHMARK: verify the issued partial wallet + let w = wallet_blinded_signatures.first().unwrap(); + let vk = verification_keys_auth.first().unwrap(); + group.bench_function( + &format!("[Client] issue_verify_a_partial_wallet_with_L_{}", case.ll,), + |b| b.iter(|| issue_verify(vk, user_keypair.secret_key(), w, &req_info, 1).unwrap()), + ); + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // CLIENT BENCHMARK: aggregating all partial wallets + group.bench_function( + &format!( + "[Client] aggregate_wallets_with_L_{}_threshold_{}", + case.ll, case.threshold_p, + ), + |b| { + b.iter(|| { + aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap() + }) + }, + ); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + let wallet_clone = aggr_wallet.clone(); + + // SPENDING PHASE + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; + // CLIENT BENCHMARK: spend a single coin from the wallet + group.bench_function( + &format!( + "[Client] spend_a_single_coin_L_{}_threshold_{}", + case.ll, case.threshold_p, + ), + |b| { + b.iter(|| { + aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + case.spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap() + }) + }, + ); + + let payment = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + case.spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + // MERCHANT BENCHMARK: verify whether the submitted payment is legit + group.bench_function( + &format!( + "[Merchant] spend_verify_of_a_single_payment_L_{}_threshold_{}", + case.ll, case.threshold_p, + ), + |b| { + b.iter(|| { + payment + .spend_verify(&verification_key, &pay_info, date_scalar(spend_date)) + .unwrap() + }) + }, + ); + + // BENCHMARK IDENTIFICATION + // Let's generate a double spending payment + + // let's reverse the spending counter in the wallet to create a double spending payment + let mut aggr_wallet = wallet_clone; + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + case.spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + // GENERATE KEYS FOR OTHER USERS + let mut public_keys: Vec = Default::default(); + for _ in 0..case.case_nr_pub_keys { + let sk = grp.random_scalar(); + let sk_user = SecretKeyUser::from_bytes(&sk.to_bytes()).unwrap(); + let pk_user = sk_user.public_key(); + public_keys.push(pk_user); + } + public_keys.push(user_keypair.public_key()); + + // MERCHANT BENCHMARK: identify double spending + group.bench_function( + &format!( + "[Merchant] identify_L_{}_threshold_{}_spend_vv_{}_pks_{}", + case.ll, + case.threshold_p, + case.spend_vv, + public_keys.len() + ), + |b| b.iter(|| identify(&payment, &payment2, pay_info, pay_info2)), + ); + let identify_result = identify(&payment, &payment2, pay_info, pay_info2); + assert_eq!( + identify_result, + IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key()) + ); +} + +criterion_group!(benches, bench_compact_ecash); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_expiration_date_signatures.rs b/common/nym_offline_compact_ecash/benches/benchmarks_expiration_date_signatures.rs new file mode 100644 index 0000000000..8c049a96a5 --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_expiration_date_signatures.rs @@ -0,0 +1,102 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use criterion::{criterion_group, criterion_main, Criterion}; +use nym_compact_ecash::constants; +use nym_compact_ecash::scheme::expiration_date_signatures::{ + aggregate_expiration_signatures, sign_expiration_date, verify_valid_dates_signatures, + ExpirationDateSignatureShare, +}; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::{aggregate_verification_keys, ttp_keygen, VerificationKeyAuth}; + +fn bench_partial_sign_expiration_date(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-sign-verify-expiration-date"); + let expiration_date = 1703183958; + + let authorities_keys = ttp_keygen(2, 3).unwrap(); + let sk_i_auth = authorities_keys[0].secret_key(); + let vk_i_auth = authorities_keys[0].verification_key(); + let partial_exp_sig = sign_expiration_date(sk_i_auth, expiration_date).unwrap(); + + // ISSUING AUTHORITY BENCHMARK: issue a set of (partial) signatures for a given expiration date + group.bench_function( + &format!( + "[IssuingAuthority] sign_expiration_date_{}_validity_period", + constants::CRED_VALIDITY_PERIOD_DAYS, + ), + |b| b.iter(|| sign_expiration_date(sk_i_auth, expiration_date)), + ); + + // CLIENT: verify the correctness of the set of (partial) signatures for a given expiration date + assert!(verify_valid_dates_signatures(&vk_i_auth, &partial_exp_sig, expiration_date).is_ok()); + group.bench_function( + &format!( + "[Client] verify_valid_dates_signatures_{}_validity_period", + constants::CRED_VALIDITY_PERIOD_DAYS, + ), + |b| b.iter(|| verify_valid_dates_signatures(&vk_i_auth, &partial_exp_sig, expiration_date)), + ); +} + +fn bench_aggregate_expiration_date_signatures(c: &mut Criterion) { + let mut group = c.benchmark_group("benchmark-aggregate-verify-expiration-date-signatures"); + let expiration_date = 1703183958; + + let authorities_keypairs = ttp_keygen(7, 10).unwrap(); + let indices: [u64; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let mut partial_signatures: Vec> = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + for sk in secret_keys_authorities.iter() { + let sign = sign_expiration_date(sk, expiration_date).unwrap(); + partial_signatures.push(sign); + } + + let combined_data: Vec<_> = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| ExpirationDateSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + // CLIENT: verify all the partial signature vectors and aggregate into a single vector of signed valid dates + group.bench_function( + &format!( + "[Client] aggregate_expiration_signatures_from_{}_issuing_authorities_{}_validity_period", + constants::CRED_VALIDITY_PERIOD_DAYS, authorities_keypairs.len(), + ), + |b| { + b.iter(|| { + aggregate_expiration_signatures( + &verification_key, + expiration_date, + &combined_data, + ) + }) + }, + ); +} + +criterion_group!( + benches, + bench_partial_sign_expiration_date, + bench_aggregate_expiration_date_signatures +); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/benches/benchmarks_group_operations.rs b/common/nym_offline_compact_ecash/benches/benchmarks_group_operations.rs new file mode 100644 index 0000000000..73c74472e7 --- /dev/null +++ b/common/nym_offline_compact_ecash/benches/benchmarks_group_operations.rs @@ -0,0 +1,189 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Neg; +use std::time::Duration; + +use bls12_381::{ + multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt, Scalar, +}; +use criterion::{criterion_group, criterion_main, Criterion}; +use ff::Field; +use group::{Curve, Group}; +use nym_compact_ecash::utils::check_bilinear_pairing; + +#[allow(unused)] +fn double_pairing(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) { + let gt1 = bls12_381::pairing(g11, g21); + let gt2 = bls12_381::pairing(g12, g22); + assert_eq!(gt1, gt2) +} + +#[allow(unused)] +fn single_pairing(g11: &G1Affine, g21: &G2Affine) { + let gt1 = bls12_381::pairing(g11, g21); +} + +#[allow(unused)] +fn exponent_in_g1(g1: G1Projective, r: Scalar) { + let g11 = (g1 * r); +} + +#[allow(unused)] +fn exponent_in_g2(g2: G2Projective, r: Scalar) { + let g22 = (g2 * r); +} + +#[allow(unused)] +fn exponent_in_gt(gt: Gt, r: Scalar) { + let gtt = (gt * r); +} + +#[allow(unused)] +fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) { + let miller_loop_result = multi_miller_loop(&[ + (g11, &G2Prepared::from(*g21)), + (&g12.neg(), &G2Prepared::from(*g22)), + ]); + assert!(bool::from( + miller_loop_result.final_exponentiation().is_identity() + )) +} + +#[allow(unused)] +fn multi_miller_pairing_with_prepared( + g11: &G1Affine, + g21: &G2Prepared, + g12: &G1Affine, + g22: &G2Prepared, +) { + let miller_loop_result = multi_miller_loop(&[(g11, g21), (&g12.neg(), g22)]); + assert!(bool::from( + miller_loop_result.final_exponentiation().is_identity() + )) +} + +// the case of being able to prepare G2 generator +#[allow(unused)] +fn multi_miller_pairing_with_semi_prepared( + g11: &G1Affine, + g21: &G2Affine, + g12: &G1Affine, + g22: &G2Prepared, +) { + let miller_loop_result = + multi_miller_loop(&[(g11, &G2Prepared::from(*g21)), (&g12.neg(), g22)]); + assert!(bool::from( + miller_loop_result.final_exponentiation().is_identity() + )) +} + +#[allow(unused)] +fn bench_group_operations(c: &mut Criterion) { + let mut group = c.benchmark_group("bench_group_operations"); + group.measurement_time(Duration::from_secs(200)); + + let mut rng = rand::thread_rng(); + + let g1 = G1Affine::generator(); + let g2 = G2Affine::generator(); + let r = Scalar::random(&mut rng); + let s = Scalar::random(&mut rng); + + let g11 = (g1 * r).to_affine(); + let g21 = (g2 * s).to_affine(); + let g21_prep = G2Prepared::from(g21); + + let g12 = (g1 * s).to_affine(); + let g22 = (g2 * r).to_affine(); + let g22_prep = G2Prepared::from(g22); + + let gt = bls12_381::pairing(&g11, &g21); + let gen1 = G1Projective::generator(); + let gen2 = G2Projective::generator(); + + group.bench_function("exponent operation in G1", |b| { + b.iter(|| exponent_in_g1(gen1, r)) + }); + + group.bench_function("exponent operation in G2", |b| { + b.iter(|| exponent_in_g2(gen2, r)) + }); + + group.bench_function("exponent operation in Gt", |b| { + b.iter(|| exponent_in_gt(gt, r)) + }); + + group.bench_function("single pairing", |b| b.iter(|| single_pairing(&g11, &g21))); + + group.bench_function("double pairing", |b| { + b.iter(|| double_pairing(&g11, &g21, &g12, &g22)) + }); + + group.bench_function("multi miller in affine", |b| { + b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22)) + }); + + group.bench_function("multi miller with prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep)) + }); + + group.bench_function("multi miller with semi-prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep)) + }); + + // bench_checking_vk_pairing + // assume key of size 5 + let scalars = [ + Scalar::random(&mut rng), + Scalar::random(&mut rng), + Scalar::random(&mut rng), + Scalar::random(&mut rng), + Scalar::random(&mut rng), + ]; + let gen1 = G1Affine::generator(); + let gen2_prep = G2Prepared::from(G2Affine::generator()); + + let g1 = scalars + .iter() + .map(|s| G1Affine::generator() * s) + .collect::>(); + let g2 = scalars + .iter() + .map(|s| G2Affine::generator() * s) + .collect::>(); + + group.bench_function("individual pairings", |b| { + b.iter(|| { + for (g1, g2) in g1.iter().zip(g2.iter()) { + let _ = check_bilinear_pairing( + &gen1, + &G2Prepared::from(g2.to_affine()), + &g1.to_affine(), + &gen2_prep, + ); + } + }) + }); + + group.bench_function("miller loop with duplicate elements", |b| { + b.iter(|| { + let mut terms = vec![]; + let neg_g1 = gen1.neg(); + for (g1, g2) in g1.iter().zip(g2.iter()) { + // TODO: optimise refs + terms.push((neg_g1, G2Prepared::from(g2.to_affine()))); + terms.push((g1.to_affine(), gen2_prep.clone())); + } + let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::>(); + + let _: bool = multi_miller_loop(&terms_refs) + .final_exponentiation() + .is_identity() + .into(); + }) + }); +} + +criterion_group!(benches, bench_group_operations); +criterion_main!(benches); diff --git a/common/nym_offline_compact_ecash/src/common_types.rs b/common/nym_offline_compact_ecash/src/common_types.rs new file mode 100644 index 0000000000..6aaf81f673 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/common_types.rs @@ -0,0 +1,97 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::error::Result; +use crate::helpers::{g1_tuple_to_bytes, recover_g1_tuple}; +use bls12_381::{G1Projective, Scalar}; +use serde::{Deserialize, Serialize}; + +pub type SignerIndex = u64; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Signature { + pub(crate) h: G1Projective, + pub(crate) s: G1Projective, +} + +pub type PartialSignature = Signature; + +impl Signature { + pub(crate) fn sig1(&self) -> &G1Projective { + &self.h + } + + pub(crate) fn sig2(&self) -> &G1Projective { + &self.s + } + + /// Function randomises the signature. + /// + /// # Returns + /// + /// A tuple containing the randomised signature and the blinding scalar. + pub fn blind_and_randomise(&self) -> (Signature, Scalar) { + let params = ecash_group_parameters(); + + // Generate random blinding scalars + let r = params.random_scalar(); + let r_prime = params.random_scalar(); + + // Calculate h_prime and s_prime using the random scalars + let h_prime = self.h * r_prime; + let s_prime = (self.s * r_prime) + (h_prime * r); + ( + Signature { + h: h_prime, + s: s_prime, + }, + r, + ) + } + + pub fn to_bytes(self) -> [u8; 96] { + g1_tuple_to_bytes((self.h, self.s)) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (h, s) = recover_g1_tuple::(bytes)?; + Ok(Signature { h, s }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct BlindedSignature { + pub(crate) h: G1Projective, + pub(crate) c: G1Projective, +} + +impl BlindedSignature { + pub fn to_bytes(self) -> [u8; 96] { + g1_tuple_to_bytes((self.h, self.c)) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (h, c) = recover_g1_tuple::(bytes)?; + Ok(BlindedSignature { h, c }) + } +} + +pub struct SignatureShare { + signature: Signature, + index: SignerIndex, +} + +impl SignatureShare { + pub fn new(signature: Signature, index: SignerIndex) -> Self { + SignatureShare { signature, index } + } + + pub fn signature(&self) -> &Signature { + &self.signature + } + + pub fn index(&self) -> SignerIndex { + self.index + } +} diff --git a/common/nym_offline_compact_ecash/src/constants.rs b/common/nym_offline_compact_ecash/src/constants.rs new file mode 100644 index 0000000000..fd053bc9d9 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/constants.rs @@ -0,0 +1,28 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bls12_381::Scalar; + +pub const PUBLIC_ATTRIBUTES_LEN: usize = 1; //expiration date +pub const PRIVATE_ATTRIBUTES_LEN: usize = 2; //user and wallet secret +pub const ATTRIBUTES_LEN: usize = PUBLIC_ATTRIBUTES_LEN + PRIVATE_ATTRIBUTES_LEN; // number of attributes encoded in a single zk-nym credential + +pub const CRED_VALIDITY_PERIOD_DAYS: u64 = 30; + +pub(crate) const SECONDS_PER_DAY: u64 = 86400; + +/// Total number of tickets in each issued ticket book. +pub const NB_TICKETS: u64 = 1000; + +pub const TYPE_EXP: Scalar = Scalar::from_raw([ + u64::from_le_bytes(*b"ZKNYMEXP"), + u64::from_le_bytes(*b"IRATIOND"), + u64::from_le_bytes(*b"ATE4llCB"), + u64::from_le_bytes(*b"MEypAxr3"), +]); +pub const TYPE_IDX: Scalar = Scalar::from_raw([ + u64::from_le_bytes(*b"ZKNYMSIN"), + u64::from_le_bytes(*b"DICESh^7"), + u64::from_le_bytes(*b"gTYbhnap"), + u64::from_le_bytes(*b"*12n5GG6"), +]); diff --git a/common/nym_offline_compact_ecash/src/error.rs b/common/nym_offline_compact_ecash/src/error.rs new file mode 100644 index 0000000000..0d49581ec6 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/error.rs @@ -0,0 +1,125 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum CompactEcashError { + #[error("failed to verify expiration date signatures")] + ExpirationDateSignatureVerification, + + #[error("failed to validate expiration date signatures")] + ExpirationDateSignatureValidity, + + #[error("empty set for aggregation")] + AggregationEmptySet, + + #[error("duplicate indices for aggregation")] + AggregationDuplicateIndices, + + #[error("aggregation verification error")] + AggregationVerification, + + #[error("different element size for aggregation")] + AggregationSizeMismatch, + + #[error("withdrawal request failed to verify")] + WithdrawalRequestVerification, + + #[error("invalid key generation parameters")] + KeygenParameters, + + #[error("signing authority's key is too short")] + KeyTooShort, + + #[error("empty/incomplete set of coordinates for interpolation")] + InterpolationSetSize, + + #[error("issuance verification failed")] + IssuanceVerification, + + #[error("trying to spend more than what's available. Spending : {spending}, available : {remaining}")] + SpendExceedsAllowance { spending: u64, remaining: u64 }, + + #[error("signature failed validity check")] + SpendSignaturesValidity, + + #[error("signature failed verification check")] + SpendSignaturesVerification, + + #[error("duplicate serial number in the payment")] + SpendDuplicateSerialNumber, + + #[error("given spend date is too late")] + SpendDateTooLate, + + #[error("given spend date is too early")] + SpendDateTooEarly, + + #[error("ZK proof failed to verify")] + SpendZKProofVerification, + + #[error("could not decode base 58 string - {0}")] + MalformedString(#[from] bs58::decode::Error), + + #[error("failed to verify coin indices signatures")] + CoinIndicesSignatureVerification, + + #[error("failed to deserialize a {object}")] + DeserializationFailure { object: String }, + + #[error("failed to deserialise {type_name}: {source}")] + BinaryDeserialisationFailure { + type_name: String, + source: bincode::Error, + }, + + #[error( + "deserialization error, expected at least {} bytes, got {}", + min, + actual + )] + DeserializationMinLength { min: usize, actual: usize }, + + #[error("{type_name} deserialization error, expected {expected} bytes, got {actual}")] + DeserializationLengthMismatch { + type_name: String, + expected: usize, + actual: usize, + }, + + #[error("tried to deserialize {object} with bytes of invalid length. Expected {actual} < {target} or {modulus_target} % {modulus} == 0")] + DeserializationInvalidLength { + actual: usize, + target: usize, + modulus_target: usize, + modulus: usize, + object: String, + }, + + #[error("failed to deserialize scalar from the received bytes - it might not have been canonically encoded")] + ScalarDeserializationFailure, + + #[error("failed to deserialize G1Projective point from the received bytes - it might not have been canonically encoded")] + G1ProjectiveDeserializationFailure, + + #[error("failed to deserialize G2Projective point from the received bytes - it might not have been canonically encoded")] + G2ProjectiveDeserializationFailure, + + #[error("verification key is invalid for this operation")] + VerificationKeyTooShort, + + #[error("did not provide the sufficient number of coin index signatures")] + InsufficientNumberOfIndexSignatures, + + #[error("did not provide the sufficient number of expiration date signatures")] + InsufficientNumberOfExpirationSignatures, + + //context : This can happen only if the wallet secret `v` was picked such that `v + coin_index + 1 == 0`. + //The chance of this happening is of the order 2^-381 and not failing is waay too much work. + //TLDR: this event can happen, but with probability 0 + #[error("you're one of the most unluck person on your planet and your wallet cannot complete this payment")] + UnluckiestError, +} diff --git a/common/nym_offline_compact_ecash/src/helpers.rs b/common/nym_offline_compact_ecash/src/helpers.rs new file mode 100644 index 0000000000..1fa0a15245 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/helpers.rs @@ -0,0 +1,37 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::utils::try_deserialize_g1_projective; +use crate::CompactEcashError; +use bls12_381::G1Projective; +use group::Curve; +use std::any::{type_name, Any}; + +pub(crate) fn g1_tuple_to_bytes(el: (G1Projective, G1Projective)) -> [u8; 96] { + let mut bytes = [0u8; 96]; + bytes[..48].copy_from_slice(&el.0.to_affine().to_compressed()); + bytes[48..].copy_from_slice(&el.1.to_affine().to_compressed()); + bytes +} + +pub(crate) fn recover_g1_tuple( + bytes: &[u8], +) -> crate::error::Result<(G1Projective, G1Projective)> { + if bytes.len() != 96 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: type_name::().into(), + expected: 96, + actual: bytes.len(), + }); + } + //SAFETY : [0..48] into 48 sized array and [48..96] into 48 sized array + #[allow(clippy::unwrap_used)] + let first_bytes: &[u8; 48] = &bytes[..48].try_into().unwrap(); + #[allow(clippy::unwrap_used)] + let second_bytes: &[u8; 48] = &bytes[48..].try_into().unwrap(); + + let first = try_deserialize_g1_projective(first_bytes)?; + let second = try_deserialize_g1_projective(second_bytes)?; + + Ok((first, second)) +} diff --git a/common/nym_offline_compact_ecash/src/lib.rs b/common/nym_offline_compact_ecash/src/lib.rs new file mode 100644 index 0000000000..b47ee4b56e --- /dev/null +++ b/common/nym_offline_compact_ecash/src/lib.rs @@ -0,0 +1,62 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +use bls12_381::Scalar; +use std::sync::OnceLock; + +pub use crate::error::CompactEcashError; +pub use crate::traits::Bytable; +pub use bls12_381::G1Projective; +pub use common_types::{BlindedSignature, Signature}; +pub use scheme::aggregation::aggregate_verification_keys; +pub use scheme::aggregation::aggregate_wallets; +pub use scheme::identify; +pub use scheme::keygen::ttp_keygen; +pub use scheme::keygen::{generate_keypair_user, generate_keypair_user_from_seed}; +pub use scheme::keygen::{ + KeyPairAuth, PublicKeyUser, SecretKeyAuth, SecretKeyUser, VerificationKeyAuth, +}; +pub use scheme::setup; +pub use scheme::withdrawal::issue; +pub use scheme::withdrawal::issue_verify; +pub use scheme::withdrawal::withdrawal_request; +pub use scheme::withdrawal::WithdrawalRequest; +pub use scheme::PartialWallet; +pub use scheme::PayInfo; +pub use setup::GroupParameters; +pub use traits::Base58; + +pub mod common_types; +pub mod constants; +pub mod error; +mod helpers; +mod proofs; +pub mod scheme; +pub mod tests; +mod traits; +pub mod utils; + +pub type Attribute = Scalar; + +pub fn ecash_parameters() -> &'static setup::Parameters { + static ECASH_PARAMS: OnceLock = OnceLock::new(); + ECASH_PARAMS.get_or_init(|| setup::Parameters::new(constants::NB_TICKETS)) +} + +pub fn ecash_group_parameters() -> &'static setup::GroupParameters { + static ECASH_PARAMS: OnceLock = OnceLock::new(); + ECASH_PARAMS.get_or_init(|| setup::GroupParameters::new(constants::ATTRIBUTES_LEN)) +} + +// if anything changes here you MUST correctly increase semver of this library +pub(crate) fn binary_serialiser() -> impl bincode::Options { + use bincode::Options; + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/nym_offline_compact_ecash/src/proofs/mod.rs b/common/nym_offline_compact_ecash/src/proofs/mod.rs new file mode 100644 index 0000000000..5133c2bedd --- /dev/null +++ b/common/nym_offline_compact_ecash/src/proofs/mod.rs @@ -0,0 +1,59 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::borrow::Borrow; + +use bls12_381::Scalar; +use digest::generic_array::typenum::Unsigned; +use digest::Digest; +use sha2::Sha256; + +pub mod proof_spend; +pub mod proof_withdrawal; + +type ChallengeDigest = Sha256; + +/// Generates a Scalar [or Fp] challenge by hashing a number of elliptic curve points. +fn compute_challenge(iter: I) -> Scalar +where + D: Digest, + I: Iterator, + B: AsRef<[u8]>, +{ + let mut h = D::new(); + for point_representation in iter { + h.update(point_representation); + } + let digest = h.finalize(); + + // TODO: I don't like the 0 padding here (though it's what we've been using before, + // but we never had a security audit anyway...) + // instead we could maybe use the `from_bytes` variant and adding some suffix + // when computing the digest until we produce a valid scalar. + let mut bytes = [0u8; 64]; + let pad_size = 64usize + .checked_sub(D::OutputSize::to_usize()) + .unwrap_or_default(); + + bytes[pad_size..].copy_from_slice(&digest); + + Scalar::from_bytes_wide(&bytes) +} + +fn produce_response(witness_replacement: &Scalar, challenge: &Scalar, secret: &Scalar) -> Scalar { + witness_replacement - challenge * secret +} + +// note: it's caller's responsibility to ensure witnesses.len() = secrets.len() +fn produce_responses(witnesses: &[Scalar], challenge: &Scalar, secrets: &[S]) -> Vec +where + S: Borrow, +{ + debug_assert_eq!(witnesses.len(), secrets.len()); + + witnesses + .iter() + .zip(secrets.iter()) + .map(|(w, x)| produce_response(w, challenge, x.borrow())) + .collect() +} diff --git a/common/nym_offline_compact_ecash/src/proofs/proof_spend.rs b/common/nym_offline_compact_ecash/src/proofs/proof_spend.rs new file mode 100644 index 0000000000..966dd2211c --- /dev/null +++ b/common/nym_offline_compact_ecash/src/proofs/proof_spend.rs @@ -0,0 +1,397 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::proofs::{compute_challenge, produce_response, produce_responses, ChallengeDigest}; +use crate::scheme::keygen::VerificationKeyAuth; +use crate::scheme::PayInfo; +use bls12_381::{G1Projective, G2Projective, Scalar}; +use group::GroupEncoding; +use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct SpendInstance { + pub kappa: G2Projective, + pub cc: G1Projective, + pub aa: Vec, + pub ss: Vec, + pub tt: Vec, + pub kappa_k: Vec, + pub kappa_e: G2Projective, +} + +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct SpendWitness<'a> { + // includes skUser, v, t + #[zeroize(skip)] + pub(crate) attributes: &'a [&'a Scalar], + + // signature randomizing element + pub(crate) r: Scalar, + pub(crate) o_c: Scalar, + pub(crate) lk: Vec, + pub(crate) o_a: Vec, + pub(crate) mu: Vec, + pub(crate) o_mu: Vec, + pub(crate) r_k: Vec, + pub(crate) r_e: Scalar, +} + +pub struct WitnessReplacement { + pub r_attributes: Vec, + pub r_r: Scalar, + pub r_r_e: Scalar, + pub r_o_c: Scalar, + pub r_r_k: Vec, + pub r_lk: Vec, + pub r_o_a: Vec, + pub r_mu: Vec, + pub r_o_mu: Vec, +} + +pub struct InstanceCommitments { + pub tt_kappa: G2Projective, + pub tt_kappa_e: G2Projective, + pub tt_cc: G1Projective, + pub tt_aa: Vec, + pub tt_ss: Vec, + pub tt_tt: Vec, + pub tt_gamma1: Vec, + pub tt_kappa_k: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SpendProof { + challenge: Scalar, + response_r: Scalar, + response_r_e: Scalar, + responses_r_k: Vec, + responses_l: Vec, + responses_o_a: Vec, + response_o_c: Scalar, + responses_mu: Vec, + responses_o_mu: Vec, + responses_attributes: Vec, +} + +pub fn generate_witness_replacement(witness: &SpendWitness) -> WitnessReplacement { + let grp_params = ecash_group_parameters(); + let r_attributes = grp_params.n_random_scalars(witness.attributes.len()); + let r_r = grp_params.random_scalar(); + let r_r_e = grp_params.random_scalar(); + let r_o_c = grp_params.random_scalar(); + + let r_r_k = grp_params.n_random_scalars(witness.r_k.len()); + let r_lk = grp_params.n_random_scalars(witness.lk.len()); + let r_o_a = grp_params.n_random_scalars(witness.o_a.len()); + let r_mu = grp_params.n_random_scalars(witness.mu.len()); + let r_o_mu = grp_params.n_random_scalars(witness.o_mu.len()); + WitnessReplacement { + r_attributes, + r_r, + r_r_e, + r_o_c, + r_r_k, + r_lk, + r_o_a, + r_mu, + r_o_mu, + } +} + +pub fn compute_instance_commitments( + witness_replacement: &WitnessReplacement, + instance: &SpendInstance, + verification_key: &VerificationKeyAuth, + rr: &[Scalar], +) -> InstanceCommitments { + let grp_params = ecash_group_parameters(); + let g1 = *grp_params.gen1(); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let gamma1 = grp_params.gamma_idx(1).unwrap(); + + let tt_kappa = grp_params.gen2() * witness_replacement.r_r + + verification_key.alpha + + witness_replacement + .r_attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + let tt_cc = g1 * witness_replacement.r_o_c + gamma1 * witness_replacement.r_attributes[1]; + + let tt_kappa_e = grp_params.gen2() * witness_replacement.r_r_e + + verification_key.alpha + + verification_key.beta_g2[0] * witness_replacement.r_attributes[2]; + + let tt_aa: Vec = witness_replacement + .r_o_a + .iter() + .zip(witness_replacement.r_lk.iter()) + .map(|(r_o_a_k, r_l_k)| g1 * r_o_a_k + gamma1 * r_l_k) + .collect::>(); + + let tt_kappa_k = witness_replacement + .r_lk + .iter() + .zip(witness_replacement.r_r_k.iter()) + .map(|(r_l_k, r_r_k)| { + verification_key.alpha + verification_key.beta_g2[0] * r_l_k + grp_params.gen2() * r_r_k + }) + .collect::>(); + + let tt_ss = witness_replacement + .r_mu + .iter() + .map(|r_mu_k| grp_params.delta() * r_mu_k) + .collect::>(); + + let tt_tt = rr + .iter() + .zip(witness_replacement.r_mu.iter()) + .map(|(rr_k, r_mu_k)| g1 * witness_replacement.r_attributes[0] + (g1 * rr_k) * r_mu_k) + .collect::>(); + + let tt_gamma1 = instance + .aa + .iter() + .zip(witness_replacement.r_mu.iter()) + .zip(witness_replacement.r_o_mu.iter()) + .map(|((aa_k, r_mu_k), r_o_mu_k)| (aa_k + instance.cc + gamma1) * r_mu_k + g1 * r_o_mu_k) + .collect::>(); + + InstanceCommitments { + tt_kappa, + tt_kappa_e, + tt_cc, + tt_aa, + tt_ss, + tt_tt, + tt_gamma1, + tt_kappa_k, + } +} + +impl SpendProof { + pub fn construct( + instance: &SpendInstance, + witness: &SpendWitness, + verification_key: &VerificationKeyAuth, + rr: &[Scalar], + pay_info: &PayInfo, + spend_value: u64, + ) -> Self { + let grp_params = ecash_group_parameters(); + // generate random values to replace each witness + let witness_replacement = generate_witness_replacement(witness); + + // compute zkp commitment for each instance + let instance_commitments = + compute_instance_commitments(&witness_replacement, instance, verification_key, rr); + + let tt_aa_bytes = instance_commitments + .tt_aa + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_ss_bytes = instance_commitments + .tt_ss + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_tt_bytes = instance_commitments + .tt_tt + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_gamma1_bytes = instance_commitments + .tt_gamma1 + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + let tt_kappa_k_bytes = instance_commitments + .tt_kappa_k + .iter() + .map(|x| x.to_bytes()) + .collect::>(); + + // compute the challenge + let challenge = compute_challenge::( + std::iter::once(grp_params.gen1().to_bytes().as_ref()) + .chain(std::iter::once(grp_params.gen2().to_bytes().as_ref())) + .chain(std::iter::once(grp_params.gammas_to_bytes().as_ref())) + .chain(std::iter::once(verification_key.to_bytes().as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once( + instance_commitments.tt_kappa.to_bytes().as_ref(), + )) + .chain(std::iter::once( + instance_commitments.tt_kappa_e.to_bytes().as_ref(), + )) + .chain(std::iter::once( + instance_commitments.tt_cc.to_bytes().as_ref(), + )) + .chain(tt_aa_bytes.iter().map(|x| x.as_ref())) + .chain(tt_ss_bytes.iter().map(|x| x.as_ref())) + .chain(tt_kappa_k_bytes.iter().map(|x| x.as_ref())) + .chain(tt_gamma1_bytes.iter().map(|x| x.as_ref())) + .chain(tt_tt_bytes.iter().map(|x| x.as_ref())) + .chain(std::iter::once(pay_info.pay_info_bytes.as_ref())) + .chain(std::iter::once(spend_value.to_le_bytes().as_ref())), + ); + + // compute response for each witness + let responses_attributes = produce_responses( + &witness_replacement.r_attributes, + &challenge, + witness.attributes, + ); + let response_r = produce_response(&witness_replacement.r_r, &challenge, &witness.r); + let response_r_e = produce_response(&witness_replacement.r_r_e, &challenge, &witness.r_e); + let response_o_c = produce_response(&witness_replacement.r_o_c, &challenge, &witness.o_c); + + let responses_r_k = produce_responses(&witness_replacement.r_r_k, &challenge, &witness.r_k); + let responses_l = produce_responses(&witness_replacement.r_lk, &challenge, &witness.lk); + let responses_o_a = produce_responses(&witness_replacement.r_o_a, &challenge, &witness.o_a); + let responses_mu = produce_responses(&witness_replacement.r_mu, &challenge, &witness.mu); + let responses_o_mu = + produce_responses(&witness_replacement.r_o_mu, &challenge, &witness.o_mu); + + SpendProof { + challenge, + response_r, + response_r_e, + responses_r_k, + responses_l, + responses_o_a, + response_o_c, + responses_mu, + responses_o_mu, + responses_attributes, + } + } + + pub fn verify( + &self, + instance: &SpendInstance, + verification_key: &VerificationKeyAuth, + rr: &[Scalar], + pay_info: &PayInfo, + spend_value: u64, + ) -> bool { + let grp_params = ecash_group_parameters(); + let g1 = *grp_params.gen1(); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let gamma1 = grp_params.gamma_idx(1).unwrap(); + + // re-compute each zkp commitment + let tt_kappa = instance.kappa * self.challenge + + verification_key.alpha * (self.challenge.neg()) + + verification_key.alpha + + grp_params.gen2() * self.response_r + + self + .responses_attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + let tt_cc = g1 * self.response_o_c + + gamma1 * self.responses_attributes[1] + + instance.cc * self.challenge; + + let tt_kappa_e = instance.kappa_e * self.challenge + + verification_key.alpha * (self.challenge.neg()) + + verification_key.alpha + + verification_key.beta_g2[0] * self.responses_attributes[2] + + grp_params.gen2() * self.response_r_e; + + let tt_aa = self + .responses_o_a + .iter() + .zip(self.responses_l.iter()) + .zip(instance.aa.iter()) + .map(|((resp_o_a_k, resp_l_k), aa_k)| { + g1 * resp_o_a_k + gamma1 * resp_l_k + aa_k * self.challenge + }) + .collect::>(); + + let tt_aa_bytes = tt_aa.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_ss = self + .responses_mu + .iter() + .zip(instance.ss.iter()) + .map(|(resp_mu_k, ss_k)| grp_params.delta() * resp_mu_k + ss_k * self.challenge) + .collect::>(); + + let tt_ss_bytes = tt_ss.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_tt = self + .responses_mu + .iter() + .zip(rr.iter()) + .zip(instance.tt.iter()) + .map(|((resp_mu_k, rr_k), tt_k)| { + g1 * self.responses_attributes[0] + (g1 * rr_k) * resp_mu_k + tt_k * self.challenge + }) + .collect::>(); + + let tt_tt_bytes = tt_tt.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_gamma1 = instance + .aa + .iter() + .zip(self.responses_mu.iter()) + .zip(self.responses_o_mu.iter()) + .map(|((aa_k, resp_mu_k), resp_o_mu_k)| { + (aa_k + instance.cc + gamma1) * resp_mu_k + + g1 * resp_o_mu_k + + gamma1 * self.challenge + }) + .collect::>(); + + let tt_gamma1_bytes = tt_gamma1.iter().map(|x| x.to_bytes()).collect::>(); + + let tt_kappa_k = instance + .kappa_k + .iter() + .zip(self.responses_r_k.iter()) + .zip(self.responses_l.iter()) + .map(|((kappa_k, resp_r_k), resp_r_l_k)| { + kappa_k * self.challenge + + grp_params.gen2() * resp_r_k + + verification_key.alpha * (Scalar::one() - self.challenge) + + verification_key.beta_g2[0] * resp_r_l_k + }) + .collect::>(); + + let tt_kappa_k_bytes = tt_kappa_k.iter().map(|x| x.to_bytes()).collect::>(); + + // re-compute the challenge + let challenge = compute_challenge::( + std::iter::once(grp_params.gen1().to_bytes().as_ref()) + .chain(std::iter::once(grp_params.gen2().to_bytes().as_ref())) + .chain(std::iter::once(grp_params.gammas_to_bytes().as_ref())) + .chain(std::iter::once(verification_key.to_bytes().as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once(tt_kappa.to_bytes().as_ref())) + .chain(std::iter::once(tt_kappa_e.to_bytes().as_ref())) + .chain(std::iter::once(tt_cc.to_bytes().as_ref())) + .chain(tt_aa_bytes.iter().map(|x| x.as_ref())) + .chain(tt_ss_bytes.iter().map(|x| x.as_ref())) + .chain(tt_kappa_k_bytes.iter().map(|x| x.as_ref())) + .chain(tt_gamma1_bytes.iter().map(|x| x.as_ref())) + .chain(tt_tt_bytes.iter().map(|x| x.as_ref())) + .chain(std::iter::once(pay_info.pay_info_bytes.as_ref())) + .chain(std::iter::once(spend_value.to_le_bytes().as_ref())), + ); + + challenge == self.challenge + } +} diff --git a/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs b/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs new file mode 100644 index 0000000000..cba7161713 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs @@ -0,0 +1,248 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::proofs::{compute_challenge, produce_response, produce_responses, ChallengeDigest}; +use crate::scheme::keygen::PublicKeyUser; +use bls12_381::{G1Projective, Scalar}; +use group::GroupEncoding; +use itertools::izip; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +// instance: g, gamma1, gamma2, gamma3, com, h, com1, com2, com3, pkUser +pub struct WithdrawalReqInstance { + // Joined commitment to all attributes + pub(crate) joined_commitment: G1Projective, + // Hash of the joined commitment com + pub(crate) joined_commitment_hash: G1Projective, + // Pedersen commitments to each attribute + pub(crate) private_attributes_commitments: Vec, + // Public key of a user + pub(crate) pk_user: PublicKeyUser, +} + +// witness: m1, m2, m3, o, o1, o2, o3, +pub struct WithdrawalReqWitness { + pub private_attributes: Vec, + // Opening for the joined commitment com + pub joined_commitment_opening: Scalar, + // Openings for the pedersen commitments of private attributes + pub private_attributes_openings: Vec, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct WithdrawalReqProof { + challenge: Scalar, + response_opening: Scalar, + response_openings: Vec, + response_attributes: Vec, +} + +impl WithdrawalReqProof { + pub(crate) fn construct( + instance: &WithdrawalReqInstance, + witness: &WithdrawalReqWitness, + ) -> Self { + let params = ecash_group_parameters(); + // generate random values to replace the witnesses + let r_com_opening = params.random_scalar(); + let r_pedcom_openings = params.n_random_scalars(witness.private_attributes_openings.len()); + let r_attributes = params.n_random_scalars(witness.private_attributes.len()); + + // compute zkp commitments for each instance + let zkcm_com = params.gen1() * r_com_opening + + r_attributes + .iter() + .zip(params.gammas().iter()) + .map(|(rm_i, gamma_i)| gamma_i * rm_i) + .sum::(); + + let zkcm_pedcom = r_pedcom_openings + .iter() + .zip(r_attributes.iter()) + .map(|(o_j, m_j)| params.gen1() * o_j + instance.joined_commitment_hash * m_j) + .collect::>(); + + let zkcm_user_sk = params.gen1() * r_attributes[0]; + + // covert to bytes + let gammas_bytes = params + .gammas() + .iter() + .map(|gamma| gamma.to_bytes()) + .collect::>(); + + let zkcm_pedcom_bytes = zkcm_pedcom + .iter() + .map(|cm| cm.to_bytes()) + .collect::>(); + + // compute zkp challenge using g1, gammas, c, h, c1, c2, c3, zk commitments + let challenge = compute_challenge::( + std::iter::once(params.gen1().to_bytes().as_ref()) + .chain(gammas_bytes.iter().map(|gamma| gamma.as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once(zkcm_com.to_bytes().as_ref())) + .chain(zkcm_pedcom_bytes.iter().map(|c| c.as_ref())) + .chain(std::iter::once(zkcm_user_sk.to_bytes().as_ref())), + ); + + // compute response + let response_opening = produce_response( + &r_com_opening, + &challenge, + &witness.joined_commitment_opening, + ); + let response_openings = produce_responses( + &r_pedcom_openings, + &challenge, + &witness + .private_attributes_openings + .iter() + .collect::>(), + ); + let response_attributes = produce_responses( + &r_attributes, + &challenge, + &witness.private_attributes.iter().collect::>(), + ); + + WithdrawalReqProof { + challenge, + response_opening, + response_openings, + response_attributes, + } + } + + pub(crate) fn verify(&self, instance: &WithdrawalReqInstance) -> bool { + let params = ecash_group_parameters(); + // recompute zk commitments for each instance + let zkcm_com = instance.joined_commitment * self.challenge + + params.gen1() * self.response_opening + + self + .response_attributes + .iter() + .zip(params.gammas().iter()) + .map(|(m_i, gamma_i)| gamma_i * m_i) + .sum::(); + + let zkcm_pedcom = izip!( + instance.private_attributes_commitments.iter(), + self.response_openings.iter(), + self.response_attributes.iter() + ) + .map(|(cm_j, resp_o_j, resp_m_j)| { + cm_j * self.challenge + + params.gen1() * resp_o_j + + instance.joined_commitment_hash * resp_m_j + }) + .collect::>(); + + let zk_commitment_user_sk = + instance.pk_user.pk * self.challenge + params.gen1() * self.response_attributes[0]; + + // covert to bytes + let gammas_bytes = params + .gammas() + .iter() + .map(|gamma| gamma.to_bytes()) + .collect::>(); + + let zkcm_pedcom_bytes = zkcm_pedcom + .iter() + .map(|cm| cm.to_bytes()) + .collect::>(); + + // recompute zkp challenge + let challenge = compute_challenge::( + std::iter::once(params.gen1().to_bytes().as_ref()) + .chain(gammas_bytes.iter().map(|hs| hs.as_ref())) + .chain(std::iter::once(instance.to_bytes().as_ref())) + .chain(std::iter::once(zkcm_com.to_bytes().as_ref())) + .chain(zkcm_pedcom_bytes.iter().map(|c| c.as_ref())) + .chain(std::iter::once(zk_commitment_user_sk.to_bytes().as_ref())), + ); + + challenge == self.challenge + } +} + +#[cfg(test)] +mod tests { + use group::Group; + use rand::thread_rng; + + use crate::GroupParameters; + use crate::{constants, utils::hash_g1}; + + use super::*; + + #[test] + fn withdrawal_request_instance_roundtrip() { + let mut rng = thread_rng(); + let params = GroupParameters::new(constants::ATTRIBUTES_LEN); + let instance = WithdrawalReqInstance { + joined_commitment: G1Projective::random(&mut rng), + joined_commitment_hash: G1Projective::random(&mut rng), + private_attributes_commitments: vec![ + G1Projective::random(&mut rng), + G1Projective::random(&mut rng), + G1Projective::random(&mut rng), + ], + pk_user: PublicKeyUser { + pk: params.gen1() * params.random_scalar(), + }, + }; + + let instance_bytes = instance.to_bytes(); + let instance_p = WithdrawalReqInstance::from_bytes(&instance_bytes).unwrap(); + assert_eq!(instance, instance_p) + } + + #[test] + fn withdrawal_proof_construct_and_verify() { + let _rng = thread_rng(); + let params = GroupParameters::new(constants::ATTRIBUTES_LEN); + let sk = params.random_scalar(); + let pk_user = PublicKeyUser { + pk: params.gen1() * sk, + }; + let v = params.random_scalar(); + let t = params.random_scalar(); + let private_attributes = vec![sk, v, t]; + + let joined_commitment_opening = params.random_scalar(); + let joined_commitment = params.gen1() * joined_commitment_opening + + private_attributes + .iter() + .zip(params.gammas()) + .map(|(&m, gamma)| gamma * m) + .sum::(); + let joined_commitment_hash = hash_g1(joined_commitment.to_bytes()); + + let private_attributes_openings = params.n_random_scalars(private_attributes.len()); + let private_attributes_commitments = private_attributes_openings + .iter() + .zip(private_attributes.iter()) + .map(|(o_j, m_j)| params.gen1() * o_j + joined_commitment_hash * m_j) + .collect::>(); + + let instance = WithdrawalReqInstance { + joined_commitment, + joined_commitment_hash, + private_attributes_commitments, + pk_user, + }; + + let witness = WithdrawalReqWitness { + private_attributes, + joined_commitment_opening, + private_attributes_openings, + }; + let zk_proof = WithdrawalReqProof::construct(&instance, &witness); + assert!(zk_proof.verify(&instance)) + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/aggregation.rs b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs new file mode 100644 index 0000000000..d4441179bb --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs @@ -0,0 +1,161 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use core::iter::Sum; +use core::ops::Mul; + +use bls12_381::{G2Prepared, G2Projective, Scalar}; +use group::Curve; +use itertools::Itertools; + +use crate::common_types::{PartialSignature, Signature, SignatureShare, SignerIndex}; +use crate::error::{CompactEcashError, Result}; +use crate::scheme::expiration_date_signatures::scalar_date; +use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth}; +use crate::scheme::withdrawal::RequestInfo; +use crate::scheme::{PartialWallet, Wallet, WalletSignatures}; +use crate::utils::{check_bilinear_pairing, perform_lagrangian_interpolation_at_origin}; +use crate::{ecash_group_parameters, Attribute}; + +pub(crate) trait Aggregatable: Sized { + fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result; + + fn check_unique_indices(indices: &[SignerIndex]) -> bool { + // if aggregation is a threshold one, all indices should be unique + indices.iter().unique_by(|&index| index).count() == indices.len() + } +} + +impl Aggregatable for T +where + T: Sum, + for<'a> T: Sum<&'a T>, + for<'a> &'a T: Mul, +{ + fn aggregate(aggregatable: &[T], indices: Option<&[u64]>) -> Result { + if aggregatable.is_empty() { + return Err(CompactEcashError::AggregationEmptySet); + } + + if let Some(indices) = indices { + if !Self::check_unique_indices(indices) { + return Err(CompactEcashError::AggregationDuplicateIndices); + } + perform_lagrangian_interpolation_at_origin(indices, aggregatable) + } else { + // non-threshold + Ok(aggregatable.iter().sum()) + } + } +} + +impl Aggregatable for PartialSignature { + fn aggregate(sigs: &[PartialSignature], indices: Option<&[u64]>) -> Result { + let h = sigs + .first() + .ok_or(CompactEcashError::AggregationEmptySet)? + .sig1(); + + // TODO: is it possible to avoid this allocation? + let sigmas = sigs.iter().map(|sig| *sig.sig2()).collect::>(); + let aggr_sigma = Aggregatable::aggregate(&sigmas, indices)?; + + Ok(Signature { + h: *h, + s: aggr_sigma, + }) + } +} + +/// Ensures all provided verification keys were generated to verify the same number of attributes. +fn check_same_key_size(keys: &[VerificationKeyAuth]) -> bool { + keys.iter().map(|vk| vk.beta_g1.len()).all_equal() + && keys.iter().map(|vk| vk.beta_g2.len()).all_equal() +} + +pub fn aggregate_verification_keys( + keys: &[VerificationKeyAuth], + indices: Option<&[SignerIndex]>, +) -> Result { + if !check_same_key_size(keys) { + return Err(CompactEcashError::AggregationSizeMismatch); + } + Aggregatable::aggregate(keys, indices) +} + +pub fn aggregate_signature_shares( + verification_key: &VerificationKeyAuth, + attributes: &[Attribute], + shares: &[SignatureShare], +) -> Result { + let (signatures, indices): (Vec<_>, Vec<_>) = shares + .iter() + .map(|share| (*share.signature(), share.index())) + .unzip(); + + aggregate_signatures(verification_key, attributes, &signatures, Some(&indices)) +} + +pub fn aggregate_signatures( + verification_key: &VerificationKeyAuth, + attributes: &[Attribute], + signatures: &[PartialSignature], + indices: Option<&[SignerIndex]>, +) -> Result { + let params = ecash_group_parameters(); + // aggregate the signature + + let signature = match Aggregatable::aggregate(signatures, indices) { + Ok(res) => res, + Err(err) => return Err(err), + }; + + // Verify the signature + let tmp = attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + if !check_bilinear_pairing( + &signature.h.to_affine(), + &G2Prepared::from((verification_key.alpha + tmp).to_affine()), + &signature.s.to_affine(), + params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::AggregationVerification); + } + Ok(signature) +} + +pub fn aggregate_wallets( + verification_key: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + wallets: &[PartialWallet], + req_info: &RequestInfo, +) -> Result { + // Aggregate partial wallets + let signature_shares: Vec = wallets + .iter() + .map(|wallet| SignatureShare::new(*wallet.signature(), wallet.index())) + .collect(); + + let attributes = vec![ + sk_user.sk, + *req_info.get_v(), + *req_info.get_expiration_date(), + ]; + let aggregated_signature = + aggregate_signature_shares(verification_key, &attributes, &signature_shares)?; + + let expiration_date_timestamp = req_info.get_expiration_date(); + + Ok(Wallet { + signatures: WalletSignatures { + sig: aggregated_signature, + v: *req_info.get_v(), + expiration_date_timestamp: scalar_date(expiration_date_timestamp), + }, + tickets_spent: 0, + }) +} diff --git a/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs b/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs new file mode 100644 index 0000000000..b08ab368dc --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs @@ -0,0 +1,439 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{Signature, SignerIndex}; +use crate::constants; +use crate::error::{CompactEcashError, Result}; +use crate::scheme::keygen::{SecretKeyAuth, VerificationKeyAuth}; +use crate::scheme::setup::Parameters; +use crate::utils::generate_lagrangian_coefficients_at_origin; +use crate::utils::{batch_verify_signatures, hash_g1}; +use bls12_381::{G1Projective, Scalar}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; + +pub type CoinIndexSignature = Signature; +pub type PartialCoinIndexSignature = CoinIndexSignature; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct AnnotatedCoinIndexSignature { + pub signature: CoinIndexSignature, + pub index: u64, +} + +impl Borrow for AnnotatedCoinIndexSignature { + fn borrow(&self) -> &CoinIndexSignature { + &self.signature + } +} + +impl From for CoinIndexSignature { + fn from(value: AnnotatedCoinIndexSignature) -> Self { + value.signature + } +} + +pub struct CoinIndexSignatureShare +where + B: Borrow, +{ + pub index: SignerIndex, + pub key: VerificationKeyAuth, + pub signatures: Vec, +} + +/// Signs coin indices. +/// +/// This function takes cryptographic parameters, a global verification key, and a secret key of the signing authority, +/// and generates partial coin index signatures for a specified number of indices using a parallel fold operation. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the signing process. +/// * `vk` - The global verification key. +/// * `sk_auth` - The secret key associated with the individual signing authority. +/// +/// # Returns +/// +/// A vector containing partial coin index signatures. +pub fn sign_coin_indices( + params: &Parameters, + vk: &VerificationKeyAuth, + sk_auth: &SecretKeyAuth, +) -> Result> { + if sk_auth.ys.len() < 3 { + return Err(CompactEcashError::KeyTooShort); + } + let m1: Scalar = constants::TYPE_IDX; + let m2: Scalar = constants::TYPE_IDX; + + let vk_bytes = vk.to_bytes(); + let partial_s_exponent = sk_auth.x + sk_auth.ys[1] * m1 + sk_auth.ys[2] * m2; + + let sign_index = |index: u64| { + let m0: Scalar = Scalar::from(index); + // Compute the hash h + let mut concatenated_bytes = Vec::with_capacity(vk_bytes.len() + index.to_le_bytes().len()); + concatenated_bytes.extend_from_slice(&vk_bytes); + concatenated_bytes.extend_from_slice(&index.to_le_bytes()); + let h = hash_g1(concatenated_bytes); + + // Sign the attributes + let s_exponent = partial_s_exponent + sk_auth.ys[0] * m0; + + // Create the signature struct + let signature = PartialCoinIndexSignature { + h, + s: h * s_exponent, + }; + AnnotatedCoinIndexSignature { signature, index } + }; + + cfg_if::cfg_if! { + if #[cfg(feature = "par_signing")] { + use rayon::prelude::*; + + Ok((0..params.get_total_coins()) + .into_par_iter() + .map(sign_index) + .collect()) + } else { + Ok((0..params.get_total_coins()).map(sign_index).collect()) + } + } +} + +/// Verifies coin index signatures using parallel iterators. +/// +/// This function takes cryptographic parameters, verification keys, and a list of coin index +/// signatures. It verifies each signature's commitment hash and performs a bilinear pairing check. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the verification process. +/// * `vk` - The global verification key. +/// * `vk_auth` - The verification key associated with the authority which issued the partial signatures. +/// * `signatures` - A slice containing coin index signatures to be verified. +/// +/// # Returns +/// +/// Returns `Ok(())` if all signatures are valid, otherwise returns an error with a description +/// of the verification failure. +pub fn verify_coin_indices_signatures( + vk: &VerificationKeyAuth, + vk_auth: &VerificationKeyAuth, + signatures: &[B], +) -> Result<()> +where + B: Borrow, +{ + if vk_auth.beta_g2.len() < 3 { + return Err(CompactEcashError::KeyTooShort); + } + let m1: Scalar = constants::TYPE_IDX; + let m2: Scalar = constants::TYPE_IDX; + let partially_signed = vk_auth.alpha + vk_auth.beta_g2[1] * m1 + vk_auth.beta_g2[2] * m2; + let vk_bytes = vk.to_bytes(); + + let mut pairing_terms = Vec::with_capacity(signatures.len()); + + for (i, sig) in signatures.iter().enumerate() { + let l = i as u64; + let mut concatenated_bytes = Vec::with_capacity(vk_bytes.len() + l.to_le_bytes().len()); + concatenated_bytes.extend_from_slice(&vk_bytes); + concatenated_bytes.extend_from_slice(&l.to_le_bytes()); + + // Compute the hash h + let h = hash_g1(concatenated_bytes.clone()); + + let sig = *sig.borrow(); + // Check if the hash is matching + if sig.h != h { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + + let m0 = Scalar::from(l); + // push elements for computing + // e(h1, X1) * e(s1, g2^-1) * ... * e(hi, Xi) * e(si, g2^-1) + // where + // h: H(vk, l) + // si: h^{xi + yi[0] * mi0 + yi[1] * m1 + yi[2] * m2} + // X: g2^{x + y[0] * mi0 + yi[1] * m1 + yi[2] * m2} + pairing_terms.push((sig, vk_auth.beta_g2[0] * m0 + partially_signed)); + } + + // computing all pairings in parallel using rayon makes it go from ~45ms to ~30ms, + // but given this function is called very infrequently, the possible interference up the stack is not worth it + if !batch_verify_signatures(pairing_terms.iter()) { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + + Ok(()) +} + +fn _aggregate_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signatures_shares: &[CoinIndexSignatureShare], + validate_shares: bool, +) -> Result> +where + B: Borrow, +{ + // Check if all indices are unique + if signatures_shares + .iter() + .map(|share| share.index) + .unique() + .count() + != signatures_shares.len() + { + return Err(CompactEcashError::AggregationDuplicateIndices); + } + + // Evaluate at 0 the Lagrange basis polynomials k_i + let coefficients = generate_lagrangian_coefficients_at_origin( + &signatures_shares + .iter() + .map(|share| share.index) + .collect::>(), + ); + + // Verify that all signatures are valid + if validate_shares { + cfg_if::cfg_if! { + if #[cfg(feature = "par_verify")] { + use rayon::prelude::*; + + signatures_shares.par_iter().try_for_each(|share| { + verify_coin_indices_signatures(vk, &share.key, &share.signatures) + })?; + } else { + + signatures_shares.iter().try_for_each(|share| verify_coin_indices_signatures(vk, &share.key, &share.signatures))?; + } + } + } + + // Pre-allocate vectors + let mut aggregated_coin_signatures: Vec = + Vec::with_capacity(params.get_total_coins() as usize); + + let vk_bytes = vk.to_bytes(); + for l in 0..params.get_total_coins() { + // Compute the hash h + let mut concatenated_bytes = Vec::with_capacity(vk_bytes.len() + l.to_le_bytes().len()); + concatenated_bytes.extend_from_slice(&vk_bytes); + concatenated_bytes.extend_from_slice(&l.to_le_bytes()); + let h = hash_g1(concatenated_bytes); + + // Collect the partial signatures for the same coin index + let collected_at_l: Vec<_> = signatures_shares + .iter() + .filter_map(|share| share.signatures.get(l as usize)) + .collect(); + + // Aggregate partial signatures for each coin index + let aggr_s: G1Projective = coefficients + .iter() + .zip(collected_at_l.iter()) + .map(|(coeff, &sig)| sig.borrow().s * coeff) + .sum(); + let aggr_sig = CoinIndexSignature { h, s: aggr_s }; + aggregated_coin_signatures.push(aggr_sig); + } + verify_coin_indices_signatures(vk, vk, &aggregated_coin_signatures)?; + Ok(aggregated_coin_signatures) +} + +/// Aggregates and verifies partial coin index signatures. +/// +/// This function takes cryptographic parameters, a master verification key, and a list of tuples +/// containing indices, verification keys, and partial coin index signatures from different authorities. +/// It aggregates these partial signatures into a final set of coin index signatures, and verifying the +/// final aggregated signatures. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the aggregation process. +/// * `vk` - The master verification key against which the partial signatures are verified. +/// * `signatures` - A slice of tuples, where each tuple contains an index, a verification key, and +/// a vector of partial coin index signatures from a specific authority. +/// +/// # Returns +/// +/// Returns a vector of aggregated coin index signatures if the aggregation is successful. +/// Otherwise, returns an error describing the nature of the failure. +pub fn aggregate_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signatures_shares: &[CoinIndexSignatureShare], +) -> Result> +where + B: Borrow, +{ + _aggregate_indices_signatures(params, vk, signatures_shares, true) +} + +/// Perform aggregation and verification of partial coin index signatures with +/// an additional check ensuring correct ordering of provided shares +/// +/// It further annotates the result with index information +pub fn aggregate_annotated_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signature_shares: &[CoinIndexSignatureShare], +) -> Result> { + // it's sufficient to just verify the first share as if the rest of them don't match, + // the aggregation will fail anyway + let Some(share) = signature_shares.first() else { + return Ok(Vec::new()); + }; + + if share.signatures.len() != params.get_total_coins() as usize { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + + for (idx, sig) in share.signatures.iter().enumerate() { + if idx != sig.index as usize { + return Err(CompactEcashError::CoinIndicesSignatureVerification); + } + } + + let aggregated = aggregate_indices_signatures(params, vk, signature_shares)?; + Ok(aggregated + .into_iter() + .enumerate() + .map(|(index, signature)| AnnotatedCoinIndexSignature { + signature, + index: index as u64, + }) + .collect()) +} + +/// An unchecked variant of `aggregate_indices_signatures` that does not perform +/// validation of intermediate signatures. +/// +/// It is expected the caller has already pre-validated them via manual calls to `verify_coin_indices_signatures` +pub fn unchecked_aggregate_indices_signatures( + params: &Parameters, + vk: &VerificationKeyAuth, + signatures_shares: &[CoinIndexSignatureShare], +) -> Result> { + _aggregate_indices_signatures(params, vk, signatures_shares, false) +} + +/// Generates parameters for the scheme setup. +/// +/// # Arguments +/// +/// * `total_coins` - it is the number of coins in a freshly generated wallet. It is the public parameter of the scheme. +/// +/// # Returns +/// +/// A `Parameters` struct containing group parameters, public key, the number of signatures (`total_coins`), +/// and a map of signatures for each index `l`. +/// + +#[cfg(test)] +mod tests { + use super::*; + use crate::scheme::aggregation::aggregate_verification_keys; + use crate::scheme::keygen::ttp_keygen; + + #[test] + fn test_sign_coins() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // Pick one authority to do the signing + let sk_i_auth = authorities_keypairs[0].secret_key(); + let vk_i_auth = authorities_keypairs[0].verification_key(); + + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let partial_signatures = sign_coin_indices(¶ms, &verification_key, sk_i_auth).unwrap(); + assert!( + verify_coin_indices_signatures(&verification_key, &vk_i_auth, &partial_signatures) + .is_ok() + ); + } + + #[test] + fn test_sign_coins_fail() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // Pick one authority to do the signing + let sk_0_auth = authorities_keypairs[0].secret_key(); + let vk_1_auth = authorities_keypairs[1].verification_key(); + + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let partial_signatures = sign_coin_indices(¶ms, &verification_key, sk_0_auth).unwrap(); + // Since we used a non matching verification key to verify the signature, the verification should fail + assert!( + verify_coin_indices_signatures(&verification_key, &vk_1_auth, &partial_signatures) + .is_err() + ); + } + + #[test] + fn test_aggregate_coin_indices_signatures() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + // create the partial signatures from each authority + let partial_signatures: Vec> = secret_keys_authorities + .iter() + .map(|sk_auth| sign_coin_indices(¶ms, &verification_key, sk_auth).unwrap()) + .collect(); + + let combined_data = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| CoinIndexSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect::>(); + + assert!(aggregate_indices_signatures(¶ms, &verification_key, &combined_data).is_ok()); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs b/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs new file mode 100644 index 0000000000..b419d0ef73 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs @@ -0,0 +1,500 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{Signature, SignerIndex}; +use crate::constants; +use crate::error::{CompactEcashError, Result}; +use crate::scheme::keygen::{SecretKeyAuth, VerificationKeyAuth}; +use crate::utils::generate_lagrangian_coefficients_at_origin; +use crate::utils::{batch_verify_signatures, hash_g1}; +use bls12_381::{G1Projective, Scalar}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; + +/// A structure representing an expiration date signature. +pub type ExpirationDateSignature = Signature; +pub type PartialExpirationDateSignature = ExpirationDateSignature; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct AnnotatedExpirationDateSignature { + pub signature: ExpirationDateSignature, + pub expiration_timestamp: u64, + pub spending_timestamp: u64, +} + +impl Borrow for AnnotatedExpirationDateSignature { + fn borrow(&self) -> &ExpirationDateSignature { + &self.signature + } +} + +impl From for ExpirationDateSignature { + fn from(value: AnnotatedExpirationDateSignature) -> Self { + value.signature + } +} + +pub struct ExpirationDateSignatureShare +where + B: Borrow, +{ + pub index: SignerIndex, + pub key: VerificationKeyAuth, + pub signatures: Vec, +} + +/// Signs given expiration date for a specified validity period using the given secret key of a single authority. +/// +/// # Arguments +/// +/// * `params` - The cryptographic parameters used in the signing process. +/// * `sk_auth` - The secret key of the signing authority. +/// * `expiration_unix_timestamp` - The expiration date for which signatures will be generated (as unix timestamp). +/// +/// # Returns +/// +/// A vector containing partial signatures for each date within the validity period (i.e., +/// from expiration_date - CRED_VALIDITY_PERIOD till expiration_date. +/// +/// # Note +/// +/// This function is executed by a single singing authority and generates partial expiration date +/// signatures for a specified validity period. Each signature is created by combining cryptographic +/// attributes derived from the expiration date, and the resulting vector contains signatures for +/// each date within the defined validity period till expiration date. +/// The validity period is determined by the constant `CRED_VALIDITY_PERIOD` in the `constants` module. +pub fn sign_expiration_date( + sk_auth: &SecretKeyAuth, + expiration_unix_timestamp: u64, +) -> Result> { + if sk_auth.ys.len() < 3 { + return Err(CompactEcashError::KeyTooShort); + } + let m0: Scalar = Scalar::from(expiration_unix_timestamp); + let m2: Scalar = constants::TYPE_EXP; + + let partial_s_exponent = sk_auth.x + sk_auth.ys[0] * m0 + sk_auth.ys[2] * m2; + + let sign_expiration = |offset: u64| { + // we produce tuples of (assuming CRED_VALIDITY_PERIOD_DAYS = 30): + // (expiration, expiration - 29) + // (expiration, expiration - 28) + // ... + // (expiration, expiration) + let spending_unix_timestamp = expiration_unix_timestamp + - ((constants::CRED_VALIDITY_PERIOD_DAYS - offset - 1) * constants::SECONDS_PER_DAY); + let m1: Scalar = Scalar::from(spending_unix_timestamp); + // Compute the hash + let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat()); + // Sign the attributes by performing scalar-point multiplications and accumulating the result + let s_exponent = partial_s_exponent + sk_auth.ys[1] * m1; + + // Create the signature struct on the expiration date + let signature = PartialExpirationDateSignature { + h, + s: h * s_exponent, + }; + + AnnotatedExpirationDateSignature { + signature, + expiration_timestamp: expiration_unix_timestamp, + spending_timestamp: spending_unix_timestamp, + } + }; + + cfg_if::cfg_if! { + if #[cfg(feature = "par_signing")] { + use rayon::prelude::*; + + Ok((0..constants::CRED_VALIDITY_PERIOD_DAYS) + .into_par_iter() + .map(sign_expiration) + .collect()) + } else { + Ok((0..constants::CRED_VALIDITY_PERIOD_DAYS).map(sign_expiration).collect()) + } + } +} + +/// Verifies the expiration date signatures against the given verification key. +/// +/// This function iterates over the provided valid date signatures and verifies each one +/// against the provided verification key. It computes the hash and checks the correctness of the +/// signature using bilinear pairings. +/// +/// # Arguments +/// +/// * `vkey` - The verification key of the signing authority. +/// * `signatures` - The list of date signatures to be verified. +/// * `expiration_date` - The expiration date for which signatures are being issued (as unix timestamp). +/// +/// # Returns +/// +/// Returns `Ok(true)` if all signatures are verified successfully, otherwise returns an error +/// +pub fn verify_valid_dates_signatures( + vk: &VerificationKeyAuth, + signatures: &[B], + expiration_date: u64, +) -> Result<()> +where + B: Borrow, +{ + let m0: Scalar = Scalar::from(expiration_date); + let m2: Scalar = constants::TYPE_EXP; + + let partially_signed = vk.alpha + vk.beta_g2[0] * m0 + vk.beta_g2[2] * m2; + let mut pairing_terms = Vec::with_capacity(signatures.len()); + + for (i, sig) in signatures.iter().enumerate() { + let l = i as u64; + let valid_date = expiration_date + - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY); + let m1: Scalar = Scalar::from(valid_date); + + // Compute the hash + let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat()); + + let sig = *sig.borrow(); + // Check if the hash is matching + if sig.h != h { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + + // let partially_signed_attributes = partially_signed + vk.beta_g2[1] * m1; + pairing_terms.push((sig, partially_signed + vk.beta_g2[1] * m1)); + } + + if !batch_verify_signatures(pairing_terms.iter()) { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + Ok(()) +} + +/// Aggregates partial expiration date signatures into a list of aggregated expiration date signatures. +/// +/// # Arguments +/// +/// * `vk_auth` - The global verification key. +/// * `expiration_date` - The expiration date for which the signatures are being aggregated (as unix timestamp). +/// * `signatures_shares` - A list of tuples containing unique indices, verification keys, and partial expiration date signatures corresponding to the signing authorities. +/// +/// # Returns +/// +/// A `Result` containing a vector of `ExpirationDateSignature` if the aggregation is successful, +/// or an `Err` variant with a description of the encountered error. +/// +/// # Errors +/// +/// This function returns an error if there is a mismatch in the lengths of `signatures`. This occurs +/// when the number of tuples in `signatures` is not equal to the expected number of signing authorities. +/// Each tuple should contain a unique index, a verification key, and a list of partial signatures. +/// +/// It also returns an error if there are not enough unique indices. This happens when the number +/// of unique indices in the tuples is less than the total number of signing authorities. +/// +/// Additionally, an error is returned if the verification of the partial or aggregated signatures fails. +/// This can occur if the cryptographic verification process fails for any of the provided signatures. +/// +fn _aggregate_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: u64, + signatures_shares: &[ExpirationDateSignatureShare], + validate_shares: bool, +) -> Result> +where + B: Borrow, +{ + // Check if all indices are unique + if signatures_shares + .iter() + .map(|share| share.index) + .unique() + .count() + != signatures_shares.len() + { + return Err(CompactEcashError::AggregationDuplicateIndices); + } + + // Evaluate at 0 the Lagrange basis polynomials k_i + let coefficients = generate_lagrangian_coefficients_at_origin( + &signatures_shares + .iter() + .map(|share| share.index) + .collect::>(), + ); + + // Verify that all signatures are valid + if validate_shares { + cfg_if::cfg_if! { + if #[cfg(feature = "par_verify")] { + use rayon::prelude::*; + + signatures_shares.par_iter().try_for_each(|share| { + verify_valid_dates_signatures(&share.key, &share.signatures, expiration_date) + })?; + } else { + signatures_shares.iter().try_for_each(|share| verify_valid_dates_signatures(&share.key, &share.signatures, expiration_date))?; + } + } + } + + // Pre-allocate vectors + let mut aggregated_date_signatures: Vec = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + + let m0: Scalar = Scalar::from(expiration_date); + + for l in 0..constants::CRED_VALIDITY_PERIOD_DAYS { + let valid_date = expiration_date + - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY); + let m1: Scalar = Scalar::from(valid_date); + // Compute the hash + let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat()); + + // Collect the partial signatures for the same valid date + let collected_at_l: Vec<_> = signatures_shares + .iter() + .filter_map(|share| share.signatures.get(l as usize)) + .collect(); + + // Aggregate partial signatures for each validity date + let aggr_s: G1Projective = coefficients + .iter() + .zip(collected_at_l.iter()) + .map(|(coeff, &sig)| sig.borrow().s * coeff) + .sum(); + let aggr_sig = ExpirationDateSignature { h, s: aggr_s }; + aggregated_date_signatures.push(aggr_sig); + } + verify_valid_dates_signatures(vk, &aggregated_date_signatures, expiration_date)?; + Ok(aggregated_date_signatures) +} + +/// Aggregates partial expiration date signatures into a list of aggregated expiration date signatures. +/// +/// # Arguments +/// +/// * `vk_auth` - The global verification key. +/// * `expiration_date` - The expiration date for which the signatures are being aggregated (as unix timestamp). +/// * `signatures_shares` - A list of tuples containing unique indices, verification keys, and partial expiration date signatures corresponding to the signing authorities. +/// +/// # Returns +/// +/// A `Result` containing a vector of `ExpirationDateSignature` if the aggregation is successful, +/// or an `Err` variant with a description of the encountered error. +/// +/// # Errors +/// +/// This function returns an error if there is a mismatch in the lengths of `signatures`. This occurs +/// when the number of tuples in `signatures` is not equal to the expected number of signing authorities. +/// Each tuple should contain a unique index, a verification key, and a list of partial signatures. +/// +/// It also returns an error if there are not enough unique indices. This happens when the number +/// of unique indices in the tuples is less than the total number of signing authorities. +/// +/// Additionally, an error is returned if the verification of the partial or aggregated signatures fails. +/// This can occur if the cryptographic verification process fails for any of the provided signatures. +/// +pub fn aggregate_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: u64, + signatures_shares: &[ExpirationDateSignatureShare], +) -> Result> +where + B: Borrow, +{ + _aggregate_expiration_signatures(vk, expiration_date, signatures_shares, true) +} + +/// Perform aggregation and verification of partial expiration date signatures with +/// an additional check ensuring correct ordering of provided shares +/// +/// It further annotates the result with timestamp information +pub fn aggregate_annotated_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: u64, + signatures_shares: &[ExpirationDateSignatureShare], +) -> Result> { + // it's sufficient to just verify the first share as if the rest of them don't match, + // the aggregation will fail anyway + let Some(share) = signatures_shares.first() else { + return Ok(Vec::new()); + }; + + if share.signatures.len() != constants::CRED_VALIDITY_PERIOD_DAYS as usize { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + + for (i, sig) in share.signatures.iter().enumerate() { + if sig.expiration_timestamp != expiration_date { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + + let l = i as u64; + let expected_spending = sig.expiration_timestamp + - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY); + + if sig.spending_timestamp != expected_spending { + return Err(CompactEcashError::ExpirationDateSignatureVerification); + } + } + + let aggregated = aggregate_expiration_signatures(vk, expiration_date, signatures_shares)?; + assert_eq!(aggregated.len(), share.signatures.len()); + + Ok(aggregated + .into_iter() + .zip(share.signatures.iter()) + .map(|(signature, sh)| AnnotatedExpirationDateSignature { + signature, + expiration_timestamp: sh.expiration_timestamp, + spending_timestamp: sh.spending_timestamp, + }) + .collect()) +} + +/// An unchecked variant of `aggregate_expiration_signatures` that does not perform +/// validation of intermediate signatures. +/// +/// It is expected the caller has already pre-validated them via manual calls to `verify_valid_dates_signatures` +pub fn unchecked_aggregate_expiration_signatures( + vk: &VerificationKeyAuth, + expiration_date: u64, + signatures_shares: &[ExpirationDateSignatureShare], +) -> Result> { + _aggregate_expiration_signatures(vk, expiration_date, signatures_shares, false) +} + +/// Finds the index corresponding to the given spend date based on the expiration date. +/// +/// This function calculates the index such that the following equality holds: +/// `spend_date = expiration_date - 30 + index` +/// This index is used to retrieve a corresponding signature. +/// +/// # Arguments +/// +/// * `spend_date` - The spend date for which to find the index. +/// * `expiration_date` - The expiration date used in the calculation. +/// +/// # Returns +/// +/// If a valid index is found, returns `Ok(index)`. If no valid index is found +/// (i.e., `spend_date` is earlier than `expiration_date - 30`), returns `Err(InvalidDateError)`. +/// +pub fn find_index(spend_date: u64, expiration_date: u64) -> Result { + let start_date = + expiration_date - ((constants::CRED_VALIDITY_PERIOD_DAYS - 1) * constants::SECONDS_PER_DAY); + + if spend_date >= start_date { + let index_a = ((spend_date - start_date) / constants::SECONDS_PER_DAY) as usize; + if index_a as u64 >= constants::CRED_VALIDITY_PERIOD_DAYS { + Err(CompactEcashError::SpendDateTooLate) + } else { + Ok(index_a) + } + } else { + Err(CompactEcashError::SpendDateTooEarly) + } +} + +pub fn date_scalar(date: u64) -> Scalar { + Scalar::from(date) +} + +// TODO: this will not work for **all** scalars, +// but timestamps have extremely (relatively speaking) limited range, +// so this should be fine +pub fn scalar_date(scalar: &Scalar) -> u64 { + let b = scalar.to_bytes(); + u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scheme::aggregation::aggregate_verification_keys; + use crate::scheme::keygen::ttp_keygen; + + #[test] + fn test_find_index() { + let expiration_date = 1701993600; // Dec 8 2023 + for i in 0..constants::CRED_VALIDITY_PERIOD_DAYS { + let current_spend_date = expiration_date - i * 86400; + assert_eq!( + find_index(current_spend_date, expiration_date).unwrap(), + (constants::CRED_VALIDITY_PERIOD_DAYS - 1 - i) as usize + ) + } + + let late_spend_date = expiration_date + 86400; + assert!(find_index(late_spend_date, expiration_date).is_err()); + + let early_spend_date = expiration_date - (constants::CRED_VALIDITY_PERIOD_DAYS) * 86400; + assert!(find_index(early_spend_date, expiration_date).is_err()); + } + + #[test] + fn test_sign_expiration_date() { + let expiration_date = 1702050209; // Dec 8 2023 + + let authorities_keys = ttp_keygen(2, 3).unwrap(); + let sk_i_auth = authorities_keys[0].secret_key(); + let vk_i_auth = authorities_keys[0].verification_key(); + let partial_exp_sig = sign_expiration_date(sk_i_auth, expiration_date).unwrap(); + + assert!( + verify_valid_dates_signatures(&vk_i_auth, &partial_exp_sig, expiration_date).is_ok() + ); + } + + #[test] + fn test_aggregate_expiration_signatures() { + let expiration_date = 1702050209; // Dec 8 2023 + + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + // list of secret keys of each authority + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + // list of verification keys of each authority + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + // the global master verification key + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap(); + + let mut edt_partial_signatures: Vec> = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + for sk_auth in secret_keys_authorities.iter() { + let sign = sign_expiration_date(sk_auth, expiration_date).unwrap(); + edt_partial_signatures.push(sign); + } + + let combined_data = indices + .iter() + .zip( + verification_keys_auth + .iter() + .zip(edt_partial_signatures.iter()), + ) + .map(|(i, (vk, sigs))| ExpirationDateSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect::>(); + + assert!(aggregate_expiration_signatures( + &verification_key, + expiration_date, + &combined_data, + ) + .is_ok()); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/identify.rs b/common/nym_offline_compact_ecash/src/scheme/identify.rs new file mode 100644 index 0000000000..1dd2779137 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/identify.rs @@ -0,0 +1,574 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::scheme::keygen::PublicKeyUser; +use crate::scheme::{compute_pay_info_hash, Payment}; + +use crate::PayInfo; + +#[derive(Debug, Eq, PartialEq)] +pub enum IdentifyResult { + NotADuplicatePayment, + DuplicatePayInfo(PayInfo), + DoubleSpendingPublicKeys(PublicKeyUser), +} + +pub fn identify( + payment1: &Payment, + payment2: &Payment, + pay_info1: PayInfo, + pay_info2: PayInfo, +) -> IdentifyResult { + let mut k = 0; + let mut j = 0; + for (id1, pay1_ss) in payment1.ss.iter().enumerate() { + for (id2, pay2_ss) in payment2.ss.iter().enumerate() { + if pay1_ss == pay2_ss { + k = id1; + j = id2; + break; + } + } + } + if payment1 + .ss + .iter() + .any(|pay1_ss| payment2.ss.contains(pay1_ss)) + { + if pay_info1 == pay_info2 { + IdentifyResult::DuplicatePayInfo(pay_info1) + } else { + let rr_k_payment1 = compute_pay_info_hash(&pay_info1, k as u64); + let rr_j_payment2 = compute_pay_info_hash(&pay_info2, j as u64); + let rr_diff = rr_k_payment1 - rr_j_payment2; + //SAFETY: `pay_info1` and `pay_info2` are different here, so rr_diff will not be zero, invert is then fine + let pk = (payment2.tt[j] * rr_k_payment1 - payment1.tt[k] * rr_j_payment2) + * rr_diff.invert().unwrap(); + let pk_user = PublicKeyUser { pk }; + IdentifyResult::DoubleSpendingPublicKeys(pk_user) + } + } else { + IdentifyResult::NotADuplicatePayment + } +} + +#[cfg(test)] +mod tests { + use crate::scheme::expiration_date_signatures::date_scalar; + use crate::scheme::identify::{identify, IdentifyResult}; + use crate::scheme::keygen::{PublicKeyUser, SecretKeyAuth, SecretKeyUser}; + use crate::setup::Parameters; + use crate::tests::helpers::{ + generate_coin_indices_signatures, generate_expiration_date_signatures, + }; + use crate::{ + aggregate_verification_keys, aggregate_wallets, generate_keypair_user, issue, issue_verify, + ttp_keygen, withdrawal_request, PartialWallet, PayInfo, VerificationKeyAuth, + }; + use itertools::izip; + + #[test] + fn duplicate_payments_with_the_same_pay_info() { + let total_coins = 32; + let params = Parameters::new(total_coins); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let expiration_date = 1703721600; // Dec 28 2023 00:00:00 + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let user_keypair = generate_keypair_user(); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, date_scalar(spend_date)) + .is_ok()); + + let payment2 = payment1.clone(); + assert!(payment2 + .spend_verify(&verification_key, &pay_info1, date_scalar(spend_date)) + .is_ok()); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info1); + assert_eq!(identify_result, IdentifyResult::DuplicatePayInfo(pay_info1)); + } + + #[test] + fn ok_if_two_different_payments() { + let total_coins = 32; + let params = Parameters::new(total_coins); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let expiration_date = 1703721600; // Dec 28 2023 00:00:00 + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let user_keypair = generate_keypair_user(); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, date_scalar(spend_date)) + .is_ok()); + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment2 + .spend_verify(&verification_key, &pay_info2, date_scalar(spend_date)) + .is_ok()); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info2); + assert_eq!(identify_result, IdentifyResult::NotADuplicatePayment); + } + + #[test] + fn two_payments_with_one_repeating_serial_number_but_different_pay_info() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let grp = params.grp(); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let expiration_date = 1703721600; // Dec 28 2023 00:00:00 + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let user_keypair = generate_keypair_user(); + + // GENERATE KEYS FOR OTHER USERS + let mut public_keys: Vec = Default::default(); + for _i in 0..50 { + let sk = grp.random_scalar(); + let sk_user = SecretKeyUser { sk }; + let pk_user = sk_user.public_key(); + public_keys.push(pk_user.clone()); + } + public_keys.push(user_keypair.public_key().clone()); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, date_scalar(spend_date)) + .is_ok()); + + // let's reverse the spending counter in the wallet to create a double spending payment + aggr_wallet.tickets_spent -= 1; + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment2 + .spend_verify(&verification_key, &pay_info2, date_scalar(spend_date)) + .is_ok()); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info2); + assert_eq!( + identify_result, + IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key()) + ); + } + + #[test] + fn two_payments_with_multiple_repeating_serial_numbers_but_different_pay_info() { + let total_coins = 32; + let params = Parameters::new(total_coins); + let grp = params.grp(); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let expiration_date = 1703721600; // Dec 28 2023 00:00:00 + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let user_keypair = generate_keypair_user(); + + // GENERATE KEYS FOR OTHER USERS + let mut public_keys: Vec = Default::default(); + for _ in 0..50 { + let sk = grp.random_scalar(); + let sk_user = SecretKeyUser { sk }; + let pk_user = sk_user.public_key(); + public_keys.push(pk_user.clone()); + } + public_keys.push(user_keypair.public_key().clone()); + + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3])).unwrap(); + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + ) + .unwrap(); + + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + ) + .unwrap(); + + // Let's try to spend some coins + let pay_info1 = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 10; + + let payment1 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info1, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + assert!(payment1 + .spend_verify(&verification_key, &pay_info1, date_scalar(spend_date)) + .is_ok()); + + // let's reverse the spending counter in the wallet to create a double spending payment + aggr_wallet.tickets_spent -= 10; + + let pay_info2 = PayInfo { + pay_info_bytes: [7u8; 72], + }; + let payment2 = aggr_wallet + .spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info2, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + ) + .unwrap(); + + let identify_result = identify(&payment1, &payment2, pay_info1, pay_info2); + assert_eq!( + identify_result, + IdentifyResult::DoubleSpendingPublicKeys(user_keypair.public_key()) + ); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/keygen.rs b/common/nym_offline_compact_ecash/src/scheme/keygen.rs new file mode 100644 index 0000000000..375d974e9c --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/keygen.rs @@ -0,0 +1,657 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::{CompactEcashError, Result}; +use crate::scheme::aggregation::aggregate_verification_keys; +use crate::scheme::SignerIndex; +use crate::traits::Bytable; +use crate::utils::{hash_to_scalar, Polynomial}; +use crate::utils::{ + try_deserialize_g1_projective, try_deserialize_g2_projective, try_deserialize_scalar, + try_deserialize_scalar_vec, +}; +use crate::{ecash_group_parameters, Base58}; +use bls12_381::{G1Projective, G2Projective, Scalar}; +use core::borrow::Borrow; +use core::iter::Sum; +use core::ops::{Add, Mul}; +use group::{Curve, GroupEncoding}; +use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; +use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Debug, PartialEq, Clone, Zeroize, ZeroizeOnDrop)] +pub struct SecretKeyAuth { + pub(crate) x: Scalar, + pub(crate) ys: Vec, +} + +impl PemStorableKey for SecretKeyAuth { + type Error = CompactEcashError; + + fn pem_type() -> &'static str { + "ECASH SECRET KEY" + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn from_bytes(bytes: &[u8]) -> std::result::Result { + Self::from_bytes(bytes) + } +} + +impl TryFrom<&[u8]> for SecretKeyAuth { + type Error = CompactEcashError; + + fn try_from(bytes: &[u8]) -> Result { + // There should be x and at least one y + if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 { + return Err(CompactEcashError::DeserializationInvalidLength { + actual: bytes.len(), + modulus_target: bytes.len() - 8, + target: 32 * 2 + 8, + modulus: 32, + object: "secret key".to_string(), + }); + } + + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let x_bytes: [u8; 32] = bytes[..32].try_into().unwrap(); + + #[allow(clippy::unwrap_used)] + let ys_len = u64::from_le_bytes(bytes[32..40].try_into().unwrap()); + let actual_ys_len = (bytes.len() - 40) / 32; + + if ys_len as usize != actual_ys_len { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "Secret_key ys".into(), + expected: ys_len as usize, + actual: actual_ys_len, + }); + } + + let x = try_deserialize_scalar(&x_bytes)?; + let ys = try_deserialize_scalar_vec(ys_len, &bytes[40..])?; + + Ok(SecretKeyAuth { x, ys }) + } +} + +impl SecretKeyAuth { + /// Following a (distributed) key generation process, scalar values can be obtained + /// outside of the normal key generation process. + pub fn create_from_raw(x: Scalar, ys: Vec) -> Self { + Self { x, ys } + } + + /// Extract the Scalar copy of the underlying secrets. + /// The caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized + pub fn hazmat_to_raw(&self) -> (Scalar, Vec) { + (self.x, self.ys.clone()) + } + + pub fn size(&self) -> usize { + self.ys.len() + } + + pub(crate) fn get_y_by_idx(&self, i: usize) -> Option<&Scalar> { + self.ys.get(i) + } + + pub fn verification_key(&self) -> VerificationKeyAuth { + let params = ecash_group_parameters(); + let g1 = params.gen1(); + let g2 = params.gen2(); + VerificationKeyAuth { + alpha: g2 * self.x, + beta_g1: self.ys.iter().map(|y| g1 * y).collect(), + beta_g2: self.ys.iter().map(|y| g2 * y).collect(), + } + } + + pub fn to_bytes(&self) -> Vec { + let ys_len = self.ys.len(); + let mut bytes = Vec::with_capacity(8 + (ys_len + 1) * 32); + bytes.extend_from_slice(&self.x.to_bytes()); + bytes.extend_from_slice(&ys_len.to_le_bytes()); + for y in self.ys.iter() { + bytes.extend_from_slice(&y.to_bytes()) + } + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + SecretKeyAuth::try_from(bytes) + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct VerificationKeyAuth { + pub(crate) alpha: G2Projective, + pub(crate) beta_g1: Vec, + pub(crate) beta_g2: Vec, +} + +impl PemStorableKey for VerificationKeyAuth { + type Error = CompactEcashError; + + fn pem_type() -> &'static str { + "ECASH VERIFICATION KEY" + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn from_bytes(bytes: &[u8]) -> std::result::Result { + Self::from_bytes(bytes) + } +} + +impl TryFrom<&[u8]> for VerificationKeyAuth { + type Error = CompactEcashError; + + fn try_from(bytes: &[u8]) -> Result { + // There should be at least alpha, one betaG1 and one betaG2 and their length + if bytes.len() < 96 * 2 + 48 + 8 || (bytes.len() - 8 - 96) % (96 + 48) != 0 { + return Err(CompactEcashError::DeserializationInvalidLength { + actual: bytes.len(), + modulus_target: bytes.len() - 8 - 96, + target: 96 * 2 + 48 + 8, + modulus: 96 + 48, + object: "verification key".to_string(), + }); + } + + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let alpha_bytes: [u8; 96] = bytes[..96].try_into().unwrap(); + #[allow(clippy::unwrap_used)] + let betas_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap()); + + let actual_betas_len = (bytes.len() - 104) / (96 + 48); + + if betas_len as usize != actual_betas_len { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "Verification_key betas".into(), + expected: betas_len as usize, + actual: actual_betas_len, + }); + } + + let alpha = try_deserialize_g2_projective(&alpha_bytes)?; + + let mut beta_g1 = Vec::with_capacity(betas_len as usize); + let mut beta_g1_end: u64 = 0; + for i in 0..betas_len { + let start = (104 + i * 48) as usize; + let end = start + 48; + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let beta_i_bytes = bytes[start..end].try_into().unwrap(); + let beta_i = try_deserialize_g1_projective(&beta_i_bytes)?; + + beta_g1_end = end as u64; + beta_g1.push(beta_i) + } + + let mut beta_g2 = Vec::with_capacity(betas_len as usize); + for i in 0..betas_len { + let start = (beta_g1_end + i * 96) as usize; + let end = start + 96; + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let beta_i_bytes = bytes[start..end].try_into().unwrap(); + let beta_i = try_deserialize_g2_projective(&beta_i_bytes)?; + + beta_g2.push(beta_i) + } + + Ok(VerificationKeyAuth { + alpha, + beta_g1, + beta_g2, + }) + } +} + +impl<'b> Add<&'b VerificationKeyAuth> for VerificationKeyAuth { + type Output = VerificationKeyAuth; + + #[inline] + fn add(self, rhs: &'b VerificationKeyAuth) -> VerificationKeyAuth { + // If you're trying to add two keys together that were created + // for different number of attributes, just panic as it's a + // nonsense operation. + assert_eq!( + self.beta_g1.len(), + rhs.beta_g1.len(), + "trying to add verification keys generated for different number of attributes [G1]" + ); + + assert_eq!( + self.beta_g2.len(), + rhs.beta_g2.len(), + "trying to add verification keys generated for different number of attributes [G2]" + ); + + assert_eq!( + self.beta_g1.len(), + self.beta_g2.len(), + "this key is incorrect - the number of elements G1 and G2 does not match" + ); + + assert_eq!( + rhs.beta_g1.len(), + rhs.beta_g2.len(), + "they key you want to add is incorrect - the number of elements G1 and G2 does not match" + ); + + VerificationKeyAuth { + alpha: self.alpha + rhs.alpha, + beta_g1: self + .beta_g1 + .iter() + .zip(rhs.beta_g1.iter()) + .map(|(self_beta_g1, rhs_beta_g1)| self_beta_g1 + rhs_beta_g1) + .collect(), + beta_g2: self + .beta_g2 + .iter() + .zip(rhs.beta_g2.iter()) + .map(|(self_beta_g2, rhs_beta_g2)| self_beta_g2 + rhs_beta_g2) + .collect(), + } + } +} + +impl<'a> Mul for &'a VerificationKeyAuth { + type Output = VerificationKeyAuth; + + #[inline] + fn mul(self, rhs: Scalar) -> Self::Output { + VerificationKeyAuth { + alpha: self.alpha * rhs, + beta_g1: self.beta_g1.iter().map(|b_i| b_i * rhs).collect(), + beta_g2: self.beta_g2.iter().map(|b_i| b_i * rhs).collect(), + } + } +} + +impl Sum for VerificationKeyAuth +where + T: Borrow, +{ + #[inline] + fn sum(iter: I) -> Self + where + I: Iterator, + { + let mut peekable = iter.peekable(); + let head_attributes = match peekable.peek() { + Some(head) => head.borrow().beta_g2.len(), + None => { + // TODO: this is a really weird edge case. You're trying to sum an EMPTY iterator + // of VerificationKey. So should it panic here or just return some nonsense value? + return VerificationKeyAuth::identity(0); + } + }; + + peekable.fold( + VerificationKeyAuth::identity(head_attributes), + |acc, item| acc + item.borrow(), + ) + } +} + +impl VerificationKeyAuth { + /// Create a (kinda) identity verification key using specified + /// number of 'beta' elements + pub(crate) fn identity(beta_size: usize) -> Self { + VerificationKeyAuth { + alpha: G2Projective::identity(), + beta_g1: vec![G1Projective::identity(); beta_size], + beta_g2: vec![G2Projective::identity(); beta_size], + } + } + + pub fn aggregate(sigs: &[Self], indices: Option<&[SignerIndex]>) -> Result { + aggregate_verification_keys(sigs, indices) + } + + pub fn alpha(&self) -> &G2Projective { + &self.alpha + } + + pub fn beta_g1(&self) -> &Vec { + &self.beta_g1 + } + + pub fn beta_g2(&self) -> &Vec { + &self.beta_g2 + } + + pub fn to_bytes(&self) -> Vec { + let beta_g1_len = self.beta_g1.len(); + let beta_g2_len = self.beta_g2.len(); + let mut bytes = Vec::with_capacity(96 + 8 + beta_g1_len * 48 + beta_g2_len * 96); + + bytes.extend_from_slice(&self.alpha.to_affine().to_compressed()); + + bytes.extend_from_slice(&beta_g1_len.to_le_bytes()); + + for beta_g1 in self.beta_g1.iter() { + bytes.extend_from_slice(&beta_g1.to_affine().to_compressed()) + } + + for beta_g2 in self.beta_g2.iter() { + bytes.extend_from_slice(&beta_g2.to_affine().to_compressed()) + } + + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + VerificationKeyAuth::try_from(bytes) + } +} + +impl Bytable for VerificationKeyAuth { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> std::result::Result { + Self::from_bytes(slice) + } +} + +impl Base58 for VerificationKeyAuth {} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] +pub struct SecretKeyUser { + pub(crate) sk: Scalar, +} + +impl SecretKeyUser { + pub fn public_key(&self) -> PublicKeyUser { + PublicKeyUser { + pk: ecash_group_parameters().gen1() * self.sk, + } + } + + pub fn to_bytes(&self) -> Vec { + self.sk.to_bytes().to_vec() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let sk = Scalar::try_from_byte_slice(bytes)?; + Ok(SecretKeyUser { sk }) + } +} + +impl Bytable for SecretKeyUser { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> std::result::Result { + Self::from_bytes(slice) + } +} + +impl Base58 for SecretKeyUser {} + +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +pub struct PublicKeyUser { + pub(crate) pk: G1Projective, +} + +impl PublicKeyUser { + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.pk.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: I) -> Result { + let bytes = bs58::decode(val).into_vec()?; + Self::from_bytes(&bytes) + } + + pub fn to_bytes(&self) -> Vec { + self.pk.to_affine().to_compressed().to_vec() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 48 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "PublicKeyUser".into(), + expected: 48, + actual: bytes.len(), + }); + } + //SAFETY : slice to array conversion after a length check + #[allow(clippy::unwrap_used)] + let pk_bytes: &[u8; 48] = bytes[..48].try_into().unwrap(); + let pk = try_deserialize_g1_projective(pk_bytes)?; + Ok(PublicKeyUser { pk }) + } +} + +impl Bytable for PublicKeyUser { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + Self::from_bytes(slice) + } +} + +impl Base58 for PublicKeyUser {} + +#[derive(Debug, Zeroize, ZeroizeOnDrop)] +pub struct KeyPairAuth { + secret_key: SecretKeyAuth, + #[zeroize(skip)] + verification_key: VerificationKeyAuth, + /// Optional index value specifying polynomial point used during threshold key generation. + pub index: Option, +} + +impl From for KeyPairAuth { + fn from(secret_key: SecretKeyAuth) -> Self { + KeyPairAuth { + verification_key: secret_key.verification_key(), + secret_key, + index: None, + } + } +} + +impl PemStorableKeyPair for KeyPairAuth { + type PrivatePemKey = SecretKeyAuth; + type PublicPemKey = VerificationKeyAuth; + + fn private_key(&self) -> &Self::PrivatePemKey { + &self.secret_key + } + + fn public_key(&self) -> &Self::PublicPemKey { + &self.verification_key + } + + fn from_keys(secret_key: Self::PrivatePemKey, verification_key: Self::PublicPemKey) -> Self { + Self::from_keys(secret_key, verification_key) + } +} + +impl KeyPairAuth { + pub fn new( + sk: SecretKeyAuth, + vk: VerificationKeyAuth, + index: Option, + ) -> KeyPairAuth { + KeyPairAuth { + secret_key: sk, + verification_key: vk, + index, + } + } + + pub fn from_keys(secret_key: SecretKeyAuth, verification_key: VerificationKeyAuth) -> Self { + Self { + secret_key, + verification_key, + index: None, + } + } + + pub fn secret_key(&self) -> &SecretKeyAuth { + &self.secret_key + } + + pub fn verification_key(&self) -> VerificationKeyAuth { + self.verification_key.clone() + } + + pub fn verification_key_ref(&self) -> &VerificationKeyAuth { + &self.verification_key + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct KeyPairUser { + secret_key: SecretKeyUser, + public_key: PublicKeyUser, +} + +impl KeyPairUser { + pub fn secret_key(&self) -> &SecretKeyUser { + &self.secret_key + } + + pub fn public_key(&self) -> PublicKeyUser { + self.public_key.clone() + } + + pub fn to_bytes(&self) -> Vec { + [self.secret_key.to_bytes(), self.public_key.to_bytes()].concat() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 32 + 48 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "KeyPairUser".into(), + expected: 80, + actual: bytes.len(), + }); + } + let sk = SecretKeyUser::from_bytes(&bytes[..32])?; + let pk = PublicKeyUser::from_bytes(&bytes[32..32 + 48])?; + Ok(KeyPairUser { + secret_key: sk, + public_key: pk, + }) + } +} + +pub fn generate_keypair_user() -> KeyPairUser { + let params = ecash_group_parameters(); + let sk_user = SecretKeyUser { + sk: params.random_scalar(), + }; + let pk_user = PublicKeyUser { + pk: params.gen1() * sk_user.sk, + }; + + KeyPairUser { + secret_key: sk_user, + public_key: pk_user, + } +} + +pub fn generate_keypair_user_from_seed>(seed: M) -> KeyPairUser { + let params = ecash_group_parameters(); + let sk_user = SecretKeyUser { + sk: hash_to_scalar(seed), + }; + let pk_user = PublicKeyUser { + pk: params.gen1() * sk_user.sk, + }; + + KeyPairUser { + secret_key: sk_user, + public_key: pk_user, + } +} + +pub fn ttp_keygen(threshold: u64, num_authorities: u64) -> Result> { + let params = ecash_group_parameters(); + if threshold == 0 { + return Err(CompactEcashError::KeygenParameters); + } + + if threshold > num_authorities { + return Err(CompactEcashError::KeygenParameters); + } + + let attributes = params.gammas().len(); + + // generate polynomials + let v = Polynomial::new_random(params, threshold - 1); + let ws = (0..attributes + 1) + .map(|_| Polynomial::new_random(params, threshold - 1)) + .collect::>(); + + // TODO: potentially if we had some known authority identifier we could use that instead + // of the increasing (1,2,3,...) sequence + let polynomial_indices = (1..=num_authorities).collect::>(); + + // generate polynomial shares + let x = polynomial_indices + .iter() + .map(|&id| v.evaluate(&Scalar::from(id))); + let ys = polynomial_indices.iter().map(|&id| { + ws.iter() + .map(|w| w.evaluate(&Scalar::from(id))) + .collect::>() + }); + + // finally set the keys + let secret_keys = x.zip(ys).map(|(x, ys)| SecretKeyAuth { x, ys }); + + let keypairs = secret_keys + .zip(polynomial_indices.iter()) + .map(|(secret_key, index)| { + let verification_key = secret_key.verification_key(); + KeyPairAuth { + secret_key, + verification_key, + index: Some(*index), + } + }) + .collect(); + + Ok(keypairs) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_zeroize_on_drop() {} + + fn assert_zeroize() {} + + #[test] + fn secret_key_is_zeroized() { + assert_zeroize::(); + assert_zeroize_on_drop::(); + + assert_zeroize::(); + assert_zeroize_on_drop::(); + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/mod.rs b/common/nym_offline_compact_ecash/src/scheme/mod.rs new file mode 100644 index 0000000000..7440c3641c --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/mod.rs @@ -0,0 +1,979 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{Signature, SignerIndex}; +use crate::error::{CompactEcashError, Result}; +use crate::proofs::proof_spend::{SpendInstance, SpendProof, SpendWitness}; +use crate::scheme::coin_indices_signatures::CoinIndexSignature; +use crate::scheme::expiration_date_signatures::{date_scalar, find_index, ExpirationDateSignature}; +use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth}; +use crate::scheme::setup::{GroupParameters, Parameters}; +use crate::traits::Bytable; +use crate::utils::{ + batch_verify_signatures, check_bilinear_pairing, hash_to_scalar, try_deserialize_scalar, +}; +use crate::Base58; +use crate::{constants, ecash_group_parameters}; +use bls12_381::{G1Projective, G2Prepared, G2Projective, Scalar}; +use group::Curve; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::borrow::Borrow; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +pub mod aggregation; +pub mod coin_indices_signatures; +pub mod expiration_date_signatures; +pub mod identify; +pub mod keygen; +pub mod setup; +pub mod withdrawal; + +/// The struct represents a partial wallet with essential components for a payment transaction. +/// +/// A `PartialWallet` includes a Pointcheval-Sanders signature (`sig`), +/// a scalar value (`v`) representing the wallet's secret, an optional +/// `SignerIndex` (`idx`) indicating the signer's index, and an expiration date (`expiration_date`). +/// +#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)] +pub struct PartialWallet { + #[zeroize(skip)] + sig: Signature, + v: Scalar, + idx: SignerIndex, + expiration_date: Scalar, +} + +impl PartialWallet { + pub fn signature(&self) -> &Signature { + &self.sig + } + + pub fn index(&self) -> SignerIndex { + self.idx + } + pub fn expiration_date(&self) -> Scalar { + self.expiration_date + } + + /// Converts the `PartialWallet` to a fixed-size byte array. + /// + /// The resulting byte array has a length of 168 bytes and contains serialized + /// representations of the `Signature` (`sig`), scalar value (`v`), + /// expiration date (`expiration_date`), and `idx` fields of the `PartialWallet` struct. + /// + /// # Returns + /// + /// A fixed-size byte array (`[u8; 168]`) representing the serialized form of the `PartialWallet`. + /// + pub fn to_bytes(&self) -> [u8; 168] { + let mut bytes = [0u8; 168]; + bytes[0..96].copy_from_slice(&self.sig.to_bytes()); + bytes[96..128].copy_from_slice(&self.v.to_bytes()); + bytes[128..160].copy_from_slice(&self.expiration_date.to_bytes()); + bytes[160..168].copy_from_slice(&self.idx.to_le_bytes()); + bytes + } + + /// Convert a byte slice into a `PartialWallet` instance. + /// + /// This function performs deserialization on the provided byte slice, which + /// represent a serialized `PartialWallet`. + /// + /// # Arguments + /// + /// * `bytes` - A reference to the byte slice to be deserialized. + /// + /// # Returns + /// + /// A `Result` containing the deserialized `PartialWallet` if successful, or a + /// `CompactEcashError` indicating the reason for failure. + pub fn from_bytes(bytes: &[u8]) -> Result { + const SIGNATURE_BYTES: usize = 96; + const V_BYTES: usize = 32; + const EXPIRATION_DATE_BYTES: usize = 32; + const IDX_BYTES: usize = 8; + const EXPECTED_LENGTH: usize = + SIGNATURE_BYTES + V_BYTES + EXPIRATION_DATE_BYTES + IDX_BYTES; + + if bytes.len() != EXPECTED_LENGTH { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "PartialWallet".into(), + expected: EXPECTED_LENGTH, + actual: bytes.len(), + }); + } + + let mut j = 0; + + let sig = Signature::try_from(&bytes[j..j + SIGNATURE_BYTES])?; + j += SIGNATURE_BYTES; + + //SAFETY: slice to array after length check + #[allow(clippy::unwrap_used)] + let v_bytes = bytes[j..j + V_BYTES].try_into().unwrap(); + let v = try_deserialize_scalar(v_bytes)?; + j += V_BYTES; + + //SAFETY: slice to array after length check + #[allow(clippy::unwrap_used)] + let expiration_date_bytes = bytes[j..j + EXPIRATION_DATE_BYTES].try_into().unwrap(); + let expiration_date = try_deserialize_scalar(expiration_date_bytes)?; + j += EXPIRATION_DATE_BYTES; + + //SAFETY: slice to array after length check + #[allow(clippy::unwrap_used)] + let idx_bytes = bytes[j..].try_into().unwrap(); + let idx = u64::from_le_bytes(idx_bytes); + + Ok(PartialWallet { + sig, + v, + idx, + expiration_date, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Zeroize, Serialize, Deserialize)] +pub struct Wallet { + /// The cryptographic materials required for producing spending proofs and payments. + signatures: WalletSignatures, + + /// Also known as `l` parameter in the paper + tickets_spent: u64, +} + +impl Wallet { + pub fn new(signatures: WalletSignatures, tickets_spent: u64) -> Self { + Wallet { + signatures, + tickets_spent, + } + } + + pub fn into_wallet_signatures(self) -> WalletSignatures { + self.into() + } + + pub fn to_bytes(&self) -> [u8; WalletSignatures::SERIALISED_SIZE + 8] { + let mut bytes = [0u8; WalletSignatures::SERIALISED_SIZE + 8]; + bytes[0..WalletSignatures::SERIALISED_SIZE].copy_from_slice(&self.signatures.to_bytes()); + bytes[WalletSignatures::SERIALISED_SIZE..] + .copy_from_slice(&self.tickets_spent.to_be_bytes()); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != WalletSignatures::SERIALISED_SIZE + 8 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "Wallet".into(), + expected: WalletSignatures::SERIALISED_SIZE + 8, + actual: bytes.len(), + }); + } + + //SAFETY : slice to array conversions after a length check + #[allow(clippy::unwrap_used)] + let tickets_bytes = bytes[WalletSignatures::SERIALISED_SIZE..] + .try_into() + .unwrap(); + + let signatures = WalletSignatures::from_bytes(&bytes[..WalletSignatures::SERIALISED_SIZE])?; + let tickets_spent = u64::from_be_bytes(tickets_bytes); + + Ok(Wallet { + signatures, + tickets_spent, + }) + } + + pub fn ensure_allowance( + params: &Parameters, + tickets_spent: u64, + spend_value: u64, + ) -> Result<()> { + if tickets_spent + spend_value > params.get_total_coins() { + Err(CompactEcashError::SpendExceedsAllowance { + spending: spend_value, + remaining: params.get_total_coins() - tickets_spent, + }) + } else { + Ok(()) + } + } + + pub fn check_remaining_allowance(&self, params: &Parameters, spend_value: u64) -> Result<()> { + Self::ensure_allowance(params, self.tickets_spent, spend_value) + } + + #[allow(clippy::too_many_arguments)] + pub fn spend( + &mut self, + params: &Parameters, + verification_key: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + pay_info: &PayInfo, + spend_value: u64, + valid_dates_signatures: &[ExpirationDateSignature], + coin_indices_signatures: &[CoinIndexSignature], + spend_date_timestamp: u64, + ) -> Result { + self.check_remaining_allowance(params, spend_value)?; + + // produce payment + let payment = self.signatures.spend( + params, + verification_key, + sk_user, + pay_info, + self.tickets_spent, + spend_value, + valid_dates_signatures, + coin_indices_signatures, + spend_date_timestamp, + )?; + + // update the ticket counter + self.tickets_spent += spend_value; + Ok(payment) + } +} + +impl From for WalletSignatures { + fn from(value: Wallet) -> Self { + value.signatures + } +} + +/// The struct represents a wallet with essential components for a payment transaction. +/// +/// A `Wallet` includes a Pointcheval-Sanders signature (`sig`), +/// a scalar value (`v`) representing the wallet's secret, an optional +/// an expiration date (`expiration_date`) +/// and an u64 ('l') indicating the total number of spent coins. +/// +#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] +pub struct WalletSignatures { + #[zeroize(skip)] + sig: Signature, + v: Scalar, + expiration_date_timestamp: u64, +} + +impl WalletSignatures { + pub fn with_tickets_spent(self, tickets_spent: u64) -> Wallet { + Wallet { + signatures: self, + tickets_spent, + } + } + + pub fn new_wallet(self) -> Wallet { + self.with_tickets_spent(0) + } + + pub fn encoded_expiration_date(&self) -> Scalar { + date_scalar(self.expiration_date_timestamp) + } +} + +/// Computes the hash of payment information concatenated with a numeric value. +/// +/// This function takes a `PayInfo` structure and a numeric value `k`, and +/// concatenates the serialized `payinfo` field of `PayInfo` with the little-endian +/// byte representation of `k`. The resulting byte sequence is then hashed to produce +/// a scalar value using the `hash_to_scalar` function. +/// +/// # Arguments +/// +/// * `pay_info` - A reference to the `PayInfo` structure containing payment information. +/// * `k` - A numeric value used in the hash computation. +/// +/// # Returns +/// +/// A `Scalar` value representing the hash of the concatenated byte sequence. +/// +pub fn compute_pay_info_hash(pay_info: &PayInfo, k: u64) -> Scalar { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&pay_info.pay_info_bytes); + bytes.extend_from_slice(&k.to_le_bytes()); + hash_to_scalar(bytes) +} + +impl WalletSignatures { + // signature size (96) + secret size (32) + expiration size (8) + pub const SERIALISED_SIZE: usize = 136; + + pub fn signature(&self) -> &Signature { + &self.sig + } + + /// Converts the `WalletSignatures` to a fixed-size byte array. + /// + /// The resulting byte array has a length of 168 bytes and contains serialized + /// representations of the `Signature` (`sig`), scalar value (`v`), and + /// expiration date (`expiration_date`) fields of the `WalletSignatures` struct. + /// + /// # Returns + /// + /// A fixed-size byte array (`[u8; 136]`) representing the serialized form of the `Wallet`. + /// + pub fn to_bytes(&self) -> [u8; Self::SERIALISED_SIZE] { + let mut bytes = [0u8; Self::SERIALISED_SIZE]; + bytes[0..96].copy_from_slice(&self.sig.to_bytes()); + bytes[96..128].copy_from_slice(&self.v.to_bytes()); + bytes[128..136].copy_from_slice(&self.expiration_date_timestamp.to_be_bytes()); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Self::SERIALISED_SIZE { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "WalletSignatures".into(), + expected: Self::SERIALISED_SIZE, + actual: bytes.len(), + }); + } + //SAFETY : slice to array conversions after a length check + #[allow(clippy::unwrap_used)] + let sig_bytes: &[u8; 96] = &bytes[..96].try_into().unwrap(); + + #[allow(clippy::unwrap_used)] + let v_bytes: &[u8; 32] = &bytes[96..128].try_into().unwrap(); + + #[allow(clippy::unwrap_used)] + let expiration_date_bytes = bytes[128..].try_into().unwrap(); + + let sig = Signature::try_from(sig_bytes.as_slice())?; + let v = Scalar::from_bytes(v_bytes).unwrap(); + let expiration_date_timestamp = u64::from_be_bytes(expiration_date_bytes); + + Ok(WalletSignatures { + sig, + v, + expiration_date_timestamp, + }) + } + + /// Performs a spending operation with the given parameters, updating the wallet and generating a payment. + /// + /// # Arguments + /// + /// * `verification_key` - The global verification key. + /// * `sk_user` - The secret key of the user who wants to spend from their wallet. + /// * `pay_info` - Unique information related to the payment. + /// * `current_tickets_spent` - The total number of tickets already spent in the associated wallet. + /// * `spend_value` - The amount to spend from the wallet. + /// * `valid_dates_signatures` - A list of **SORTED** signatures on valid dates during which we can spend from the wallet. + /// * `coin_indices_signatures` - A list of **SORTED** signatures on coin indices. + /// * `spend_date` - The date on which the spending occurs, expressed as unix timestamp. + /// + /// # Returns + /// + /// A tuple containing the generated payment and a reference to the updated wallet, or an error. + #[allow(clippy::too_many_arguments)] + pub fn spend( + &self, + params: &Parameters, + verification_key: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + pay_info: &PayInfo, + current_tickets_spent: u64, + spend_value: u64, + valid_dates_signatures: &[BE], + coin_indices_signatures: &[BI], + spend_date_timestamp: u64, + ) -> Result + where + BI: Borrow, + BE: Borrow, + { + // Extract group parameters + let grp_params = params.grp(); + + if verification_key.beta_g2.is_empty() { + return Err(CompactEcashError::VerificationKeyTooShort); + } + + if valid_dates_signatures.len() != constants::CRED_VALIDITY_PERIOD_DAYS as usize { + return Err(CompactEcashError::InsufficientNumberOfExpirationSignatures); + } + + if coin_indices_signatures.len() != params.get_total_coins() as usize { + return Err(CompactEcashError::InsufficientNumberOfIndexSignatures); + } + + Wallet::ensure_allowance(params, current_tickets_spent, spend_value)?; + + // Wallet attributes needed for spending + let attributes = [&sk_user.sk, &self.v, &self.encoded_expiration_date()]; + + // Randomize wallet signature + let (signature_prime, sign_blinding_factor) = self.signature().blind_and_randomise(); + + // compute kappa (i.e., blinded attributes for show) to prove possession of the wallet signature + let kappa = compute_kappa( + grp_params, + verification_key, + &attributes, + sign_blinding_factor, + ); + + // Randomise the expiration date signature for the date when we want to perform the spending, and compute kappa_e to prove possession of + // the expiration signature + let date_signature_index = + find_index(spend_date_timestamp, self.expiration_date_timestamp)?; + + //SAFETY : find_index eiter returns a valid index or an error. The unwrap is therefore fine + #[allow(clippy::unwrap_used)] + let date_signature = valid_dates_signatures + .get(date_signature_index) + .unwrap() + .borrow(); + let (date_signature_prime, date_sign_blinding_factor) = + date_signature.blind_and_randomise(); + // compute kappa_e to prove possession of the expiration signature + //SAFETY: we checked that verification beta_g2 isn't empty + #[allow(clippy::unwrap_used)] + let kappa_e: G2Projective = grp_params.gen2() * date_sign_blinding_factor + + verification_key.alpha + + verification_key.beta_g2.first().unwrap() * self.encoded_expiration_date(); + + // pick random openings o_c and compute commitments C to v (wallet secret) + let o_c = grp_params.random_scalar(); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let cc = grp_params.gen1() * o_c + grp_params.gamma_idx(1).unwrap() * self.v; + + let mut aa: Vec = Default::default(); + let mut ss: Vec = Default::default(); + let mut tt: Vec = Default::default(); + let mut rr: Vec = Default::default(); + let mut o_a: Vec = Default::default(); + let mut o_mu: Vec = Default::default(); + let mut mu: Vec = Default::default(); + let r_k_vec: Vec = Default::default(); + let mut kappa_k_vec: Vec = Default::default(); + let mut lk_vec: Vec = Default::default(); + + let mut coin_indices_signatures_prime: Vec = Default::default(); + for k in 0..spend_value { + let lk = current_tickets_spent + k; + lk_vec.push(Scalar::from(lk)); + + // compute hashes R_k = H(payinfo, k) + let rr_k = compute_pay_info_hash(pay_info, k); + rr.push(rr_k); + + let o_a_k = grp_params.random_scalar(); + o_a.push(o_a_k); + //SAFETY: grp_params is static with length 3 + #[allow(clippy::unwrap_used)] + let aa_k = + grp_params.gen1() * o_a_k + grp_params.gamma_idx(1).unwrap() * Scalar::from(lk); + aa.push(aa_k); + + // compute the serial numbers + let ss_k = pseudorandom_f_delta_v(grp_params, &self.v, lk)?; + ss.push(ss_k); + // compute the identification tags + let tt_k = grp_params.gen1() * sk_user.sk + + pseudorandom_f_g_v(grp_params, &self.v, lk)? * rr_k; + tt.push(tt_k); + + // compute values mu, o_mu, lambda, o_lambda + let maybe_mu_k: Option = (self.v + Scalar::from(lk) + Scalar::from(1)) + .invert() + .into(); + let mu_k = maybe_mu_k.ok_or(CompactEcashError::UnluckiestError)?; + mu.push(mu_k); + + let o_mu_k = ((o_a_k + o_c) * mu_k).neg(); + o_mu.push(o_mu_k); + + // Randomize the coin index signatures and compute kappa_k to prove possession of each coin's signature + // This involves iterating over the signatures corresponding to the coins we want to spend in this payment. + //SAFETY : Earlier `ensure_allowance` ensures we don't do out of of bound here + #[allow(clippy::unwrap_used)] + let coin_sign = coin_indices_signatures.get(lk as usize).unwrap().borrow(); + let (coin_sign_prime, coin_sign_blinding_factor) = coin_sign.blind_and_randomise(); + coin_indices_signatures_prime.push(coin_sign_prime); + //SAFETY: we checked that verification beta_g2 isn't empty + #[allow(clippy::unwrap_used)] + let kappa_k: G2Projective = grp_params.gen2() * coin_sign_blinding_factor + + verification_key.alpha + + verification_key.beta_g2.first().unwrap() * Scalar::from(lk); + kappa_k_vec.push(kappa_k); + } + + // construct the zkp proof + let spend_instance = SpendInstance { + kappa, + cc, + aa: aa.clone(), + ss: ss.clone(), + tt: tt.clone(), + kappa_k: kappa_k_vec.clone(), + kappa_e, + }; + let spend_witness = SpendWitness { + attributes: &attributes, + r: sign_blinding_factor, + o_c, + lk: lk_vec, + o_a, + mu, + o_mu, + r_k: r_k_vec, + r_e: date_sign_blinding_factor, + }; + + let zk_proof = SpendProof::construct( + &spend_instance, + &spend_witness, + verification_key, + &rr, + pay_info, + spend_value, + ); + + // output pay + let pay = Payment { + kappa, + kappa_e, + sig: signature_prime, + sig_exp: date_signature_prime, + kappa_k: kappa_k_vec.clone(), + omega: coin_indices_signatures_prime, + ss: ss.clone(), + tt: tt.clone(), + aa: aa.clone(), + spend_value, + cc, + zk_proof, + }; + + Ok(pay) + } +} + +fn pseudorandom_f_delta_v(params: &GroupParameters, v: &Scalar, l: u64) -> Result { + let maybe_pow: Option = (v + Scalar::from(l) + Scalar::from(1)).invert().into(); + Ok(params.delta() * maybe_pow.ok_or(CompactEcashError::UnluckiestError)?) +} + +fn pseudorandom_f_g_v(params: &GroupParameters, v: &Scalar, l: u64) -> Result { + let maybe_pow: Option = (v + Scalar::from(l) + Scalar::from(1)).invert().into(); + Ok(params.gen1() * maybe_pow.ok_or(CompactEcashError::UnluckiestError)?) +} + +/// Computes the value of kappa (blinded private attributes for show) for proving possession of the wallet signature. +/// +/// This function calculates the value of kappa, which is used to prove possession of the wallet signature in the zero-knowledge proof. +/// +/// # Arguments +/// +/// * `params` - A reference to the group parameters required for the computation. +/// * `verification_key` - The global verification key of the signing authorities. +/// * `attributes` - A slice of private attributes associated with the wallet. +/// * `blinding_factor` - The blinding factor used used to randomise the wallet's signature. +/// +/// # Returns +/// +/// A `G2Projective` element representing the computed value of kappa. +/// +fn compute_kappa( + params: &GroupParameters, + verification_key: &VerificationKeyAuth, + attributes: &[&Scalar], + blinding_factor: Scalar, +) -> G2Projective { + params.gen2() * blinding_factor + + verification_key.alpha + + attributes + .iter() + .zip(verification_key.beta_g2.iter()) + .map(|(&priv_attr, beta_i)| beta_i * priv_attr) + .sum::() +} + +/// Represents the unique payment information associated with the payment. +/// The bytes representing the payment information encode the public key of the +/// provider with whom you are spending the payment, timestamp and a unique random 32 bytes. +/// +/// # Fields +/// +/// * `payinfo_bytes` - An array of bytes representing the payment information. +/// +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub struct PayInfo { + pub pay_info_bytes: [u8; 72], +} + +impl Serialize for PayInfo { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + self.pay_info_bytes.to_vec().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for PayInfo { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let pay_info_bytes = >::deserialize(deserializer)?; + Ok(PayInfo { + pay_info_bytes: pay_info_bytes + .try_into() + .map_err(|_| serde::de::Error::custom("invalid pay info bytes"))?, + }) + } +} + +impl Bytable for PayInfo { + fn to_byte_vec(&self) -> Vec { + self.pay_info_bytes.to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> std::result::Result { + if slice.len() != 72 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "PayInfo".into(), + expected: 72, + actual: slice.len(), + }); + } + //safety : we checked that slices length is exactly 72, hence this unwrap won't fail + #[allow(clippy::unwrap_used)] + Ok(Self { + pay_info_bytes: slice.try_into().unwrap(), + }) + } +} + +impl Base58 for PayInfo {} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Payment { + pub kappa: G2Projective, + pub kappa_e: G2Projective, + pub sig: Signature, + pub sig_exp: ExpirationDateSignature, + pub kappa_k: Vec, + pub omega: Vec, + pub ss: Vec, + pub tt: Vec, + pub aa: Vec, + pub spend_value: u64, + pub cc: G1Projective, + pub zk_proof: SpendProof, +} + +impl Payment { + /// Checks the validity of the payment signature. + /// + /// This function performs two checks to ensure the payment signature is valid: + /// - Verifies that the element `h` of the payment signature does not equal the identity. + /// - Performs a bilinear pairing check involving the elements of the signature and the payment (`h`, `kappa`, and `s`). + /// + /// # Arguments + /// + /// * `params` - A reference to the system parameters required for the checks. + /// + /// # Returns + /// + /// A `Result` indicating success if the signature is valid or an error if any check fails. + /// + /// # Errors + /// + /// An error is returned if: + /// - The element `h` of the payment signature equals the identity. + /// - The bilinear pairing check for `kappa` fails. + /// + pub fn check_signature_validity(&self) -> Result<()> { + let params = ecash_group_parameters(); + if bool::from(self.sig.h.is_identity()) { + return Err(CompactEcashError::SpendSignaturesValidity); + } + + if !check_bilinear_pairing( + &self.sig.h.to_affine(), + &G2Prepared::from(self.kappa.to_affine()), + &self.sig.s.to_affine(), + params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::SpendSignaturesValidity); + } + Ok(()) + } + + /// Checks the validity of the expiration signature encoded in the payment given a spending date. + /// If the spending date is within the allowed range before the expiration date, the check is successful. + /// + /// This function performs two checks to ensure the payment expiration signature is valid: + /// - Verifies that the element `h` of the expiration signature does not equal the identity. + /// - Performs a bilinear pairing check involving the elements of the expiration signature and the payment (`h`, `kappa_e`, and `s`). + /// + /// # Arguments + /// + /// * `verification_key` - The global verification key of the signing authorities. + /// * `spend_date` - The date associated with the payment. + /// + /// # Returns + /// + /// A `Result` indicating success if the expiration signature is valid or an error if any check fails. + /// + /// # Errors + /// + /// An error is returned if: + /// - The element `h` of the payment expiration signature equals the identity. + /// - The bilinear pairing check for `kappa_e` fails. + /// + pub fn check_exp_signature_validity( + &self, + verification_key: &VerificationKeyAuth, + spend_date: Scalar, + ) -> Result<()> { + let grp_params = ecash_group_parameters(); + // Check if the element h of the payment expiration signature equals the identity. + if bool::from(self.sig_exp.h.is_identity()) { + return Err(CompactEcashError::ExpirationDateSignatureValidity); + } + + if verification_key.beta_g2.len() < 3 { + return Err(CompactEcashError::VerificationKeyTooShort); + } + + // Calculate m1 and m2 values. + let m1: Scalar = spend_date; + let m2: Scalar = constants::TYPE_EXP; + + // Perform a bilinear pairing check for kappa_e + //SAFETY: we checked the size of beta_G2 earlier + let combined_kappa_e = + self.kappa_e + verification_key.beta_g2[1] * m1 + verification_key.beta_g2[2] * m2; + + if !check_bilinear_pairing( + &self.sig_exp.h.to_affine(), + &G2Prepared::from(combined_kappa_e.to_affine()), + &self.sig_exp.s.to_affine(), + grp_params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::ExpirationDateSignatureValidity); + } + + Ok(()) + } + + /// Checks that all serial numbers in the payment are unique. + /// + /// This function verifies that each serial number in the payment's serial number array (`ss`) is unique. + /// + /// # Returns + /// + /// A `Result` indicating success if all serial numbers are unique or an error if any serial number is duplicated. + /// + /// # Errors + /// + /// An error is returned if not all serial numbers in the payment are unique. + /// + pub fn no_duplicate_serial_numbers(&self) -> Result<()> { + let mut seen_serial_numbers = Vec::new(); + + for serial_number in &self.ss { + if seen_serial_numbers.contains(serial_number) { + return Err(CompactEcashError::SpendDuplicateSerialNumber); + } + seen_serial_numbers.push(*serial_number); + } + + Ok(()) + } + + // /// Checks the validity of the coin index signature at a specific index. + // /// + // /// This function performs two checks to ensure the coin index signature at a given index (`k`) is valid: + // /// - Verifies that the element `h` of the coin index signature does not equal the identity. + // /// - Calculates a combined element for the bilinear pairing check involving `kappa_k`, and verifies the pairing with the coin index signature elements (`h`, `kappa_k`, and `s`). + // /// + // /// # Arguments + // /// + // /// * `verification_key` - The global verification key of the signing authorities. + // /// * `k` - The index at which to check the coin index signature. + // /// + // /// # Returns + // /// + // /// A `Result` indicating success if the coin index signature is valid or an error if any check fails. + // /// + // /// # Errors + // /// + // /// An error is returned if: + // /// - The element `h` of the coin index signature at the specified index equals the identity. + // /// - The bilinear pairing check for `kappa_k` at the specified index fails. + // /// - The specified index is out of bounds for the coin index signatures array (`omega`). + // /// + // pub fn check_coin_index_signature( + // &self, + // verification_key: &VerificationKeyAuth, + // k: u64, + // ) -> Result<()> { + // if let Some(coin_idx_sign) = self.omega.get(k as usize) { + // if bool::from(coin_idx_sign.h.is_identity()) { + // return Err(CompactEcashError::SpendSignaturesVerification); + // } + // if verification_key.beta_g2.len() < 3 { + // return Err(CompactEcashError::VerificationKeyTooShort); + // } + // //SAFETY: we checked the size of beta_G2 earlier + // #[allow(clippy::unwrap_used)] + // let combined_kappa_k = self.kappa_k[k as usize].to_affine() + // + verification_key.beta_g2.get(1).unwrap() * constants::TYPE_IDX + // + verification_key.beta_g2.get(2).unwrap() * constants::TYPE_IDX; + // + // if !check_bilinear_pairing( + // &coin_idx_sign.h.to_affine(), + // &G2Prepared::from(combined_kappa_k.to_affine()), + // &coin_idx_sign.s.to_affine(), + // ecash_group_parameters().prepared_miller_g2(), + // ) { + // return Err(CompactEcashError::SpendSignaturesVerification); + // } + // } else { + // return Err(CompactEcashError::SpendSignaturesVerification); + // } + // Ok(()) + // } + + /// Checks the validity of all coin index signatures available. + pub fn batch_check_coin_index_signatures( + &self, + verification_key: &VerificationKeyAuth, + ) -> Result<()> { + if verification_key.beta_g2.len() < 3 { + return Err(CompactEcashError::VerificationKeyTooShort); + } + + if self.omega.len() != self.kappa_k.len() { + return Err(CompactEcashError::SpendSignaturesVerification); + } + + let partially_signed = verification_key.beta_g2[1] * constants::TYPE_IDX + + verification_key.beta_g2[2] * constants::TYPE_IDX; + + let mut pairing_terms = Vec::with_capacity(self.omega.len()); + for (sig, kappa_k) in self.omega.iter().zip(self.kappa_k.iter()) { + pairing_terms.push((sig, partially_signed + kappa_k)) + } + + if !batch_verify_signatures(pairing_terms.iter()) { + return Err(CompactEcashError::SpendSignaturesVerification); + } + Ok(()) + } + + /// Checks the validity of the attached zk proof of spending. + pub fn verify_spend_proof( + &self, + verification_key: &VerificationKeyAuth, + pay_info: &PayInfo, + ) -> Result<()> { + // Compute pay_info hash for each coin + let mut rr = Vec::with_capacity(self.spend_value as usize); + for k in 0..self.spend_value { + // Compute hashes R_k = H(payinfo, k) + let rr_k = compute_pay_info_hash(pay_info, k); + rr.push(rr_k); + } + + // verify the zk proof + let instance = SpendInstance { + kappa: self.kappa, + cc: self.cc, + aa: self.aa.clone(), + ss: self.ss.clone(), + tt: self.tt.clone(), + kappa_k: self.kappa_k.clone(), + kappa_e: self.kappa_e, + }; + + // verify the zk-proof + if !self + .zk_proof + .verify(&instance, verification_key, &rr, pay_info, self.spend_value) + { + return Err(CompactEcashError::SpendZKProofVerification); + } + + Ok(()) + } + + /// Verifies the validity of a spend transaction, including signature checks, + /// expiration date signature checks, serial number uniqueness, coin index signature checks, + /// and zero-knowledge proof verification. + /// + /// # Arguments + /// + /// * `params` - The cryptographic parameters. + /// * `verification_key` - The verification key used for validation. + /// * `pay_info` - The pay information associated with the transaction. + /// * `spend_date` - The date at which the spending transaction occurs. + /// + /// # Returns + /// + /// Returns `Ok(true)` if the spend transaction is valid; otherwise, returns an error. + pub fn spend_verify( + &self, + verification_key: &VerificationKeyAuth, + pay_info: &PayInfo, + spend_date: Scalar, + ) -> Result<()> { + // check if all serial numbers are different + self.no_duplicate_serial_numbers()?; + // verify the zk proof + self.verify_spend_proof(verification_key, pay_info)?; + // Verify whether the payment signature and kappa are correct + self.check_signature_validity()?; + // Verify whether the expiration date signature and kappa_e are correct + self.check_exp_signature_validity(verification_key, spend_date)?; + // Verify whether the coin indices signatures and kappa_k are correct + self.batch_check_coin_index_signatures(verification_key)?; + + Ok(()) + } + + pub fn encoded_serial_number(&self) -> Vec { + SerialNumberRef { inner: &self.ss }.to_bytes() + } + + pub fn serial_number_bs58(&self) -> String { + SerialNumberRef { inner: &self.ss }.to_bs58() + } + + // pub fn has_serial_number(&self, serial_number_bs58: &str) -> Result { + // let serial_number = SerialNumberRef::try_from_bs58(serial_number_bs58)?; + // let ret = self.ss.eq(&serial_number.inner); + // Ok(ret) + // } +} + +pub struct SerialNumberRef<'a> { + pub(crate) inner: &'a [G1Projective], +} + +impl<'a> SerialNumberRef<'a> { + pub fn to_bytes(&self) -> Vec { + let ss_len = self.inner.len(); + let mut bytes: Vec = Vec::with_capacity(ss_len * 48); + for s in self.inner { + bytes.extend_from_slice(&s.to_affine().to_compressed()); + } + bytes + } + + pub fn to_bs58(&self) -> String { + bs58::encode(self.to_bytes()).into_string() + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/setup.rs b/common/nym_offline_compact_ecash/src/scheme/setup.rs new file mode 100644 index 0000000000..dd469fb009 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/setup.rs @@ -0,0 +1,104 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash_group_parameters; +use crate::utils::hash_g1; +use bls12_381::{G1Affine, G1Projective, G2Affine, G2Prepared, Scalar}; +use ff::Field; +use group::GroupEncoding; +use rand::thread_rng; + +#[derive(Debug)] +pub struct GroupParameters { + /// Generator of the G1 group + g1: G1Affine, + /// Generator of the G2 group + g2: G2Affine, + /// Additional generators of the G1 group + gammas: Vec, + // Additional generator of the G1 group + delta: G1Projective, + /// Precomputed G2 generator used for the miller loop + _g2_prepared_miller: G2Prepared, +} + +impl GroupParameters { + pub fn new(attributes: usize) -> GroupParameters { + assert!(attributes > 0); + let gammas = (1..=attributes) + .map(|i| hash_g1(format!("gamma{}", i))) + .collect(); + + let delta = hash_g1("delta"); + + GroupParameters { + g1: G1Affine::generator(), + g2: G2Affine::generator(), + gammas, + delta, + _g2_prepared_miller: G2Prepared::from(G2Affine::generator()), + } + } + + pub(crate) fn gen1(&self) -> &G1Affine { + &self.g1 + } + + pub(crate) fn gen2(&self) -> &G2Affine { + &self.g2 + } + + pub(crate) fn gammas(&self) -> &Vec { + &self.gammas + } + + pub(crate) fn gammas_to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(self.gammas.len() * 48); + for g in &self.gammas { + bytes.extend_from_slice(g.to_bytes().as_ref()); + } + bytes + } + + pub(crate) fn gamma_idx(&self, i: usize) -> Option<&G1Projective> { + self.gammas.get(i) + } + + pub(crate) fn delta(&self) -> &G1Projective { + &self.delta + } + + pub fn random_scalar(&self) -> Scalar { + // lazily-initialized thread-local random number generator, seeded by the system + let mut rng = thread_rng(); + Scalar::random(&mut rng) + } + + pub fn n_random_scalars(&self, n: usize) -> Vec { + (0..n).map(|_| self.random_scalar()).collect() + } + + pub(crate) fn prepared_miller_g2(&self) -> &G2Prepared { + &self._g2_prepared_miller + } +} + +#[derive(Debug)] +pub struct Parameters { + /// Number of coins of fixed denomination in the credential wallet; L in construction + total_coins: u64, +} + +impl Parameters { + pub fn new(total_coins: u64) -> Parameters { + assert!(total_coins > 0); + Parameters { total_coins } + } + pub fn grp(&self) -> &GroupParameters { + ecash_group_parameters() + } + + pub fn get_total_coins(&self) -> u64 { + self.total_coins + } +} diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs new file mode 100644 index 0000000000..1cd116e611 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -0,0 +1,480 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{BlindedSignature, Signature, SignerIndex}; +use crate::error::{CompactEcashError, Result}; +use crate::proofs::proof_withdrawal::{ + WithdrawalReqInstance, WithdrawalReqProof, WithdrawalReqWitness, +}; +use crate::scheme::keygen::{PublicKeyUser, SecretKeyAuth, SecretKeyUser, VerificationKeyAuth}; +use crate::scheme::setup::GroupParameters; +use crate::scheme::PartialWallet; +use crate::utils::{check_bilinear_pairing, hash_g1}; +use crate::{constants, ecash_group_parameters, Attribute}; +use bls12_381::{multi_miller_loop, G1Projective, G2Prepared, G2Projective, Scalar}; +use group::{Curve, Group, GroupEncoding}; +use serde::{Deserialize, Serialize}; +use std::ops::Neg; + +/// Represents a withdrawal request generate by the client who wants to obtain a zk-nym credential. +/// +/// This struct encapsulates the necessary components for a withdrawal request, including the joined commitment hash, the joined commitment, +/// individual Pedersen commitments for private attributes, and a zero-knowledge proof for the withdrawal request. +/// +/// # Fields +/// +/// * `joined_commitment_hash` - The joined commitment hash represented as a G1Projective element. +/// * `joined_commitment` - The joined commitment represented as a G1Projective element. +/// * `private_attributes_commitments` - A vector of individual Pedersen commitments for private attributes represented as G1Projective elements. +/// * `zk_proof` - The zero-knowledge proof for the withdrawal request. +/// +/// # Derives +/// +/// The struct derives `Debug` and `PartialEq` to provide debug output and basic comparison functionality. +/// +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct WithdrawalRequest { + joined_commitment_hash: G1Projective, + joined_commitment: G1Projective, + private_attributes_commitments: Vec, + zk_proof: WithdrawalReqProof, +} + +impl WithdrawalRequest { + pub fn get_private_attributes_commitments(&self) -> &[G1Projective] { + &self.private_attributes_commitments + } +} + +/// Represents information associated with a withdrawal request. +/// +/// This structure holds the commitment hash, commitment opening, private attributes openings, +/// the wallet secret (scalar), and the expiration date related to a withdrawal request. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RequestInfo { + joined_commitment_hash: G1Projective, + joined_commitment_opening: Scalar, + private_attributes_openings: Vec, + wallet_secret: Scalar, + expiration_date: Scalar, +} + +impl RequestInfo { + pub fn get_joined_commitment_hash(&self) -> &G1Projective { + &self.joined_commitment_hash + } + pub fn get_joined_commitment_opening(&self) -> &Scalar { + &self.joined_commitment_opening + } + pub fn get_private_attributes_openings(&self) -> &[Scalar] { + &self.private_attributes_openings + } + pub fn get_v(&self) -> &Scalar { + &self.wallet_secret + } + pub fn get_expiration_date(&self) -> &Scalar { + &self.expiration_date + } +} + +/// Computes Pedersen commitments for private attributes. +/// +/// Given a set of private attributes and the commitment hash for all attributes, +/// this function generates random blinding factors (`openings`) and computes corresponding +/// Pedersen commitments for each private attribute. +/// Pedersen commitments have the hiding and binding properties, providing a secure way +/// to represent private values in a commitment scheme. +/// +/// # Arguments +/// +/// * `params` - Group parameters for the cryptographic group. +/// * `joined_commitment_hash` - The commitment hash to be used in the Pedersen commitments. +/// * `private_attributes` - A slice of private attributes to be committed. +/// +/// # Returns +/// +/// A tuple containing vectors of blinding factors (`openings`) and corresponding +/// Pedersen commitments for each private attribute. +fn compute_private_attribute_commitments( + params: &GroupParameters, + joined_commitment_hash: &G1Projective, + private_attributes: &[Scalar], +) -> (Vec, Vec) { + let (openings, commitments): (Vec, Vec) = private_attributes + .iter() + .map(|m_j| { + let o_j = params.random_scalar(); + (o_j, params.gen1() * o_j + joined_commitment_hash * m_j) + }) + .unzip(); + + (openings, commitments) +} + +/// Generates a withdrawal request for the given user to request a zk-nym credential wallet. +/// +/// # Arguments +/// +/// * `sk_user` - A reference to the user's secret key. +/// * `expiration_date` - The expiration date for the withdrawal request. +/// +/// # Returns +/// +/// A tuple containing the generated `WithdrawalRequest` and `RequestInfo`, or an error if the operation fails. +/// +/// # Details +/// +/// The function starts by generating a random, unique wallet secret `v` and computing the joined commitment for all attributes, +/// including public (expiration date) and private ones (user secret key and wallet secret). +/// It then calculates the commitment hash (`joined_commitment_hash`) and computes Pedersen commitments for private attributes. +/// A zero-knowledge proof of knowledge is constructed to prove possession of specific attributes. +/// +/// The resulting `WithdrawalRequest` includes the commitment hash, joined commitment, commitments for private +/// attributes, and the constructed zero-knowledge proof. +/// +/// The associated `RequestInfo` includes information such as commitment hash, commitment opening, +/// openings for private attributes, `v`, and the expiration date. +pub fn withdrawal_request( + sk_user: &SecretKeyUser, + expiration_date: u64, +) -> Result<(WithdrawalRequest, RequestInfo)> { + let params = ecash_group_parameters(); + // Generate random and unique wallet secret + let v = params.random_scalar(); + let joined_commitment_opening = params.random_scalar(); + // Compute joined commitment for all attributes (public and private) + //SAFETY: params is static with length 3 + #[allow(clippy::unwrap_used)] + let joined_commitment: G1Projective = params.gen1() * joined_commitment_opening + + params.gamma_idx(0).unwrap() * sk_user.sk + + params.gamma_idx(1).unwrap() * v; + + // Compute commitment hash h + #[allow(clippy::unwrap_used)] + let joined_commitment_hash = hash_g1( + (joined_commitment + params.gamma_idx(2).unwrap() * Scalar::from(expiration_date)) + .to_bytes(), + ); + + // Compute Pedersen commitments for private attributes (wallet secret and user's secret) + let private_attributes = vec![sk_user.sk, v]; + let (private_attributes_openings, private_attributes_commitments) = + compute_private_attribute_commitments(params, &joined_commitment_hash, &private_attributes); + + // construct a NIZK proof of knowledge proving possession of m1, m2, o, o1, o2 + let instance = WithdrawalReqInstance { + joined_commitment, + joined_commitment_hash, + private_attributes_commitments: private_attributes_commitments.clone(), + pk_user: PublicKeyUser { + pk: params.gen1() * sk_user.sk, + }, + }; + + let witness = WithdrawalReqWitness { + private_attributes, + joined_commitment_opening, + private_attributes_openings: private_attributes_openings.clone(), + }; + let zk_proof = WithdrawalReqProof::construct(&instance, &witness); + + // Create and return WithdrawalRequest and RequestInfo + Ok(( + WithdrawalRequest { + joined_commitment_hash, + joined_commitment, + private_attributes_commitments, + zk_proof, + }, + RequestInfo { + joined_commitment_hash, + joined_commitment_opening, + private_attributes_openings: private_attributes_openings.clone(), + wallet_secret: v, + expiration_date: Scalar::from(expiration_date), + }, + )) +} + +/// Verifies the integrity of a withdrawal request, including the joined commitment hash +/// and the zero-knowledge proof of knowledge. +/// +/// # Arguments +/// +/// * `req` - The withdrawal request to be verified. +/// * `pk_user` - Public key of the user associated with the withdrawal request. +/// * `expiration_date` - Expiration date for the withdrawal request. +/// +/// # Returns +/// +/// Returns `Ok(true)` if the verification is successful, otherwise returns an error +/// with a specific message indicating the verification failure. +pub fn request_verify( + req: &WithdrawalRequest, + pk_user: PublicKeyUser, + expiration_date: u64, +) -> Result<()> { + let params = ecash_group_parameters(); + // Verify the joined commitment hash + //SAFETY: params is static with length 3 + #[allow(clippy::unwrap_used)] + let expected_commitment_hash = hash_g1( + (req.joined_commitment + params.gamma_idx(2).unwrap() * Scalar::from(expiration_date)) + .to_bytes(), + ); + if req.joined_commitment_hash != expected_commitment_hash { + return Err(CompactEcashError::WithdrawalRequestVerification); + } + // Verify zk proof + let instance = WithdrawalReqInstance { + joined_commitment: req.joined_commitment, + joined_commitment_hash: req.joined_commitment_hash, + private_attributes_commitments: req.private_attributes_commitments.clone(), + pk_user, + }; + if !req.zk_proof.verify(&instance) { + return Err(CompactEcashError::WithdrawalRequestVerification); + } + Ok(()) +} + +/// Signs an expiration date using a joined commitment hash and a secret key. +/// +/// Given a joined commitment hash (`joined_commitment_hash`), an expiration date (`expiration_date`), +/// and a secret key for authentication (`sk_auth`), this function computes the signature of the +/// expiration date by multiplying the commitment hash with the blinding factor derived from the secret key +/// and the expiration date. +/// +/// # Arguments +/// +/// * `joined_commitment_hash` - The G1Projective point representing the joined commitment hash. +/// * `expiration_date` - The expiration date timestamp to be signed. +/// * `sk_auth` - The secret key of the signing authority. Assumes key is long enough. +/// +/// # Returns +/// +/// A `Result` containing the resulting G1Projective point if successful, or an error if the +/// authentication secret key index is out of bounds. +fn sign_expiration_date( + joined_commitment_hash: &G1Projective, + expiration_date: u64, + sk_auth: &SecretKeyAuth, +) -> G1Projective { + //SAFETY : this fn assumes a long enough key + #[allow(clippy::unwrap_used)] + let yi = sk_auth.get_y_by_idx(2).unwrap(); + joined_commitment_hash * (yi * Scalar::from(expiration_date)) +} + +/// Issues a blinded signature for a withdrawal request, after verifying its integrity. +/// +/// This function first verifies the withdrawal request using the provided group parameters, +/// user's public key, and expiration date. If the verification is successful, +/// the function proceeds to blind sign the private attributes and sign the expiration date, +/// combining both signatures into a final signature. +/// +/// # Arguments +/// +/// * `sk_auth` - Secret key of the signing authority. +/// * `pk_user` - Public key of the user associated with the withdrawal request. +/// * `withdrawal_req` - The withdrawal request to be signed. +/// * `expiration_date` - Expiration date for the withdrawal request. +/// +/// # Returns +/// +/// Returns a `BlindedSignature` if the issuance process is successful, otherwise returns an error +/// with a specific message indicating the failure. +pub fn issue( + sk_auth: &SecretKeyAuth, + pk_user: PublicKeyUser, + withdrawal_req: &WithdrawalRequest, + expiration_date: u64, +) -> Result { + // Verify the withdrawal request + request_verify(withdrawal_req, pk_user, expiration_date)?; + // Verify `sk_auth` is long enough + if sk_auth.ys.len() < constants::ATTRIBUTES_LEN { + return Err(CompactEcashError::KeyTooShort); + } + // Blind sign the private attributes + let blind_signatures: G1Projective = withdrawal_req + .private_attributes_commitments + .iter() + .zip(sk_auth.ys.iter().take(2)) + .map(|(pc, yi)| pc * yi) + .sum(); + // Sign the expiration date + //SAFETY: key length was verified before + let expiration_date_sign = sign_expiration_date( + &withdrawal_req.joined_commitment_hash, + expiration_date, + sk_auth, + ); + // Combine both signatures + let signature = + blind_signatures + withdrawal_req.joined_commitment_hash * sk_auth.x + expiration_date_sign; + + Ok(BlindedSignature { + h: withdrawal_req.joined_commitment_hash, + c: signature, + }) +} + +/// Verifies the integrity and correctness of a blinded signature +/// and returns an unblinded partial zk-nym wallet. +/// +/// This function first verifies the integrity of the received blinded signature by checking +/// if the joined commitment hash matches the one provided in the `req_info`. If the verification +/// is successful, it proceeds to unblind the blinded signature and verify its correctness. +/// +/// # Arguments +/// +/// * `vk_auth` - Verification key of the signing authority. +/// * `sk_user` - Secret key of the user. +/// * `blind_signature` - Blinded signature received from the authority. +/// * `req_info` - Information associated with the request, including the joined commitment hash, +/// private attributes openings, v, and expiration date. +/// +/// # Returns +/// +/// Returns a `PartialWallet` if the verification process is successful, otherwise returns an error +/// with a specific message indicating the failure. +pub fn issue_verify( + vk_auth: &VerificationKeyAuth, + sk_user: &SecretKeyUser, + blind_signature: &BlindedSignature, + req_info: &RequestInfo, + signer_index: SignerIndex, +) -> Result { + let params = ecash_group_parameters(); + // Verify the integrity of the response from the authority + if req_info.joined_commitment_hash != blind_signature.h { + return Err(CompactEcashError::IssuanceVerification); + } + + // Unblind the blinded signature on the partial signature + let blinding_removers = vk_auth + .beta_g1 + .iter() + .zip(&req_info.private_attributes_openings) + .map(|(beta, opening)| beta * opening) + .sum::(); + let unblinded_c = blind_signature.c - blinding_removers; + + let attr = [sk_user.sk, req_info.wallet_secret, req_info.expiration_date]; + + let signed_attributes = attr + .iter() + .zip(vk_auth.beta_g2.iter()) + .map(|(attr, beta_i)| beta_i * attr) + .sum::(); + + // Verify the signature correctness on the wallet share + if !check_bilinear_pairing( + &blind_signature.h.to_affine(), + &G2Prepared::from((vk_auth.alpha + signed_attributes).to_affine()), + &unblinded_c.to_affine(), + params.prepared_miller_g2(), + ) { + return Err(CompactEcashError::IssuanceVerification); + } + + Ok(PartialWallet { + sig: Signature { + h: blind_signature.h, + s: unblinded_c, + }, + v: req_info.wallet_secret, + idx: signer_index, + expiration_date: req_info.expiration_date, + }) +} + +/// Verifies a partial blind signature using the provided parameters and validator's verification key. +/// +/// # Arguments +/// +/// * `blind_sign_request` - A reference to the blind signature request signed by the client. +/// * `public_attributes` - A reference to the public attributes included in the client's request. +/// * `blind_sig` - A reference to the issued partial blinded signature to be verified. +/// * `partial_verification_key` - A reference to the validator's partial verification key. +/// +/// # Returns +/// +/// A boolean indicating whether the partial blind signature is valid (`true`) or not (`false`). +/// +/// # Remarks +/// +/// This function verifies the correctness and validity of a partial blind signature using +/// the provided cryptographic parameters, blind signature request, blinded signature, +/// and partial verification key. +/// It calculates pairings based on the provided values and checks whether the partial blind signature +/// is consistent with the verification key and commitments in the blind signature request. +/// The function returns `true` if the partial blind signature is valid, and `false` otherwise. +pub fn verify_partial_blind_signature( + private_attribute_commitments: &[G1Projective], + public_attributes: &[&Attribute], + blind_sig: &BlindedSignature, + partial_verification_key: &VerificationKeyAuth, +) -> bool { + let params = ecash_group_parameters(); + let num_private_attributes = private_attribute_commitments.len(); + if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() { + return false; + } + + // TODO: we're losing some memory here due to extra allocation, + // but worst-case scenario (given SANE amount of attributes), it's just few kb at most + let c_neg = blind_sig.c.to_affine().neg(); + let g2_prep = params.prepared_miller_g2(); + + let mut terms = vec![ + // (c^{-1}, g2) + (c_neg, g2_prep.clone()), + // (s, alpha) + ( + blind_sig.h.to_affine(), + G2Prepared::from(partial_verification_key.alpha.to_affine()), + ), + ]; + + // for each private attribute, add (cm_i, beta_i) to the miller terms + for (private_attr_commit, beta_g2) in private_attribute_commitments + .iter() + .zip(&partial_verification_key.beta_g2) + { + // (cm_i, beta_i) + terms.push(( + private_attr_commit.to_affine(), + G2Prepared::from(beta_g2.to_affine()), + )) + } + + // for each public attribute, add (s^pub_j, beta_{priv + j}) to the miller terms + for (&pub_attr, beta_g2) in public_attributes.iter().zip( + partial_verification_key + .beta_g2 + .iter() + .skip(num_private_attributes), + ) { + // (s^pub_j, beta_j) + terms.push(( + (blind_sig.h * pub_attr).to_affine(), + G2Prepared::from(beta_g2.to_affine()), + )) + } + + // get the references to all the terms to get the arguments the miller loop expects + #[allow(clippy::map_identity)] + let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::>(); + + // since checking whether e(a, b) == e(c, d) + // is equivalent to checking e(a, b) • e(c, d)^{-1} == id + // and thus to e(a, b) • e(c^{-1}, d) == id + // + // compute e(c^{-1}, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j}) + multi_miller_loop(&terms_refs) + .final_exponentiation() + .is_identity() + .into() +} diff --git a/common/nym_offline_compact_ecash/src/tests/e2e.rs b/common/nym_offline_compact_ecash/src/tests/e2e.rs new file mode 100644 index 0000000000..1ce1f84b08 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/tests/e2e.rs @@ -0,0 +1,135 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +mod tests { + use crate::error::Result; + use crate::scheme::aggregation::{aggregate_verification_keys, aggregate_wallets}; + use crate::scheme::expiration_date_signatures::date_scalar; + use crate::scheme::keygen::{ + generate_keypair_user, ttp_keygen, SecretKeyAuth, VerificationKeyAuth, + }; + use crate::scheme::withdrawal::{issue, issue_verify, withdrawal_request, WithdrawalRequest}; + use crate::scheme::{PartialWallet, PayInfo, Payment, Wallet}; + use crate::setup::Parameters; + use crate::tests::helpers::{ + generate_coin_indices_signatures, generate_expiration_date_signatures, + }; + use itertools::izip; + + #[test] + fn main() -> Result<()> { + let total_coins = 32; + let params = Parameters::new(total_coins); + // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! + let expiration_date = 1703721600; // Dec 28 2023 00:00:00 + let spend_date = 1701907200; // Dec 07 2023 00:00:00 + let user_keypair = generate_keypair_user(); + + // generate authorities keys + let authorities_keypairs = ttp_keygen(2, 3).unwrap(); + let indices: [u64; 3] = [1, 2, 3]; + let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = authorities_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + let verification_key = + aggregate_verification_keys(&verification_keys_auth, Some(&[1, 2, 3]))?; + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + )?; + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + &indices, + )?; + + // request a wallet + let (req, req_info) = + withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + let req_bytes = req.to_bytes(); + let req2 = WithdrawalRequest::try_from(req_bytes.as_slice()).unwrap(); + assert_eq!(req, req2); + + // issue partial wallets + let mut wallet_blinded_signatures = Vec::new(); + for auth_keypair in authorities_keypairs { + let blind_signature = issue( + auth_keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + ); + wallet_blinded_signatures.push(blind_signature.unwrap()); + } + + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + let partial_wallet = unblinded_wallet_shares.first().unwrap().clone(); + let partial_wallet_bytes = partial_wallet.to_bytes(); + let partial_wallet2 = PartialWallet::try_from(&partial_wallet_bytes[..]).unwrap(); + assert_eq!(partial_wallet, partial_wallet2); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + )?; + + let wallet_bytes = aggr_wallet.to_bytes(); + let wallet = Wallet::from_bytes(&wallet_bytes).unwrap(); + assert_eq!(aggr_wallet, wallet); + + // Let's try to spend some coins + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment = aggr_wallet.spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + )?; + + assert!(payment + .spend_verify(&verification_key, &pay_info, date_scalar(spend_date)) + .is_ok()); + + let payment_bytes = payment.to_bytes(); + let payment2 = Payment::try_from(&payment_bytes[..]).unwrap(); + assert_eq!(payment, payment2); + + Ok(()) + } +} diff --git a/common/nym_offline_compact_ecash/src/tests/helpers.rs b/common/nym_offline_compact_ecash/src/tests/helpers.rs new file mode 100644 index 0000000000..4e98e7b060 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/tests/helpers.rs @@ -0,0 +1,178 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::SignerIndex; +use crate::error::Result; +use crate::scheme::coin_indices_signatures::{ + aggregate_indices_signatures, sign_coin_indices, CoinIndexSignature, CoinIndexSignatureShare, +}; +use crate::scheme::expiration_date_signatures::{ + aggregate_expiration_signatures, sign_expiration_date, ExpirationDateSignature, + ExpirationDateSignatureShare, +}; +use crate::scheme::keygen::{KeyPairAuth, SecretKeyAuth}; +use crate::scheme::Payment; +use crate::setup::Parameters; +use crate::{ + aggregate_verification_keys, aggregate_wallets, constants, generate_keypair_user, issue, + issue_verify, withdrawal_request, PartialWallet, PayInfo, VerificationKeyAuth, +}; +use itertools::izip; + +pub fn generate_expiration_date_signatures( + expiration_date: u64, + secret_keys_authorities: &[&SecretKeyAuth], + verification_keys_auth: &[VerificationKeyAuth], + verification_key: &VerificationKeyAuth, + indices: &[u64], +) -> Result> { + let mut edt_partial_signatures: Vec> = + Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize); + for sk_auth in secret_keys_authorities.iter() { + //Test helpers + #[allow(clippy::unwrap_used)] + let sign = sign_expiration_date(sk_auth, expiration_date).unwrap(); + edt_partial_signatures.push(sign); + } + let combined_data: Vec<_> = indices + .iter() + .zip( + verification_keys_auth + .iter() + .zip(edt_partial_signatures.iter()), + ) + .map(|(i, (vk, sigs))| ExpirationDateSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + aggregate_expiration_signatures(verification_key, expiration_date, &combined_data) +} + +pub fn generate_coin_indices_signatures( + params: &Parameters, + secret_keys_authorities: &[&SecretKeyAuth], + verification_keys_auth: &[VerificationKeyAuth], + verification_key: &VerificationKeyAuth, + indices: &[u64], +) -> Result> { + // create the partial signatures from each authority + //Test helpers + #[allow(clippy::unwrap_used)] + let partial_signatures: Vec> = secret_keys_authorities + .iter() + .map(|sk_auth| sign_coin_indices(params, verification_key, sk_auth).unwrap()) + .collect(); + + let combined_data: Vec<_> = indices + .iter() + .zip(verification_keys_auth.iter().zip(partial_signatures.iter())) + .map(|(i, (vk, sigs))| CoinIndexSignatureShare { + index: *i, + key: vk.clone(), + signatures: sigs.clone(), + }) + .collect(); + + aggregate_indices_signatures(params, verification_key, &combined_data) +} + +pub fn payment_from_keys_and_expiration_date( + ecash_keypairs: &Vec, + indices: &[SignerIndex], + expiration_date: u64, +) -> Result<(Payment, PayInfo)> { + let total_coins = 32; + let params = Parameters::new(total_coins); + let spend_date = expiration_date - 29 * constants::SECONDS_PER_DAY; + let user_keypair = generate_keypair_user(); + + let secret_keys_authorities: Vec<&SecretKeyAuth> = ecash_keypairs + .iter() + .map(|keypair| keypair.secret_key()) + .collect(); + let verification_keys_auth: Vec = ecash_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + // aggregate verification keys + let verification_key = aggregate_verification_keys(&verification_keys_auth, Some(indices))?; + + // generate valid dates signatures + let dates_signatures = generate_expiration_date_signatures( + expiration_date, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + indices, + )?; + + // generate coin indices signatures + let coin_indices_signatures = generate_coin_indices_signatures( + ¶ms, + &secret_keys_authorities, + &verification_keys_auth, + &verification_key, + indices, + )?; + //SAFETY : method intended for test only + #[allow(clippy::unwrap_used)] + // request a wallet + let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + + // generate blinded signatures + let mut wallet_blinded_signatures = Vec::new(); + + for keypair in ecash_keypairs { + let blinded_signature = issue( + keypair.secret_key(), + user_keypair.public_key(), + &req, + expiration_date, + )?; + wallet_blinded_signatures.push(blinded_signature) + } + + // Unblind + //SAFETY : method intended for test only + #[allow(clippy::unwrap_used)] + let unblinded_wallet_shares: Vec = izip!( + wallet_blinded_signatures.iter(), + verification_keys_auth.iter() + ) + .enumerate() + .map(|(idx, (w, vk))| { + issue_verify(vk, user_keypair.secret_key(), w, &req_info, idx as u64 + 1).unwrap() + }) + .collect(); + + // Aggregate partial wallets + let mut aggr_wallet = aggregate_wallets( + &verification_key, + user_keypair.secret_key(), + &unblinded_wallet_shares, + &req_info, + )?; + + // Let's try to spend some coins + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; + let spend_vv = 1; + + let payment = aggr_wallet.spend( + ¶ms, + &verification_key, + user_keypair.secret_key(), + &pay_info, + spend_vv, + &dates_signatures, + &coin_indices_signatures, + spend_date, + )?; + + Ok((payment, pay_info)) +} diff --git a/common/nym_offline_compact_ecash/src/tests/mod.rs b/common/nym_offline_compact_ecash/src/tests/mod.rs new file mode 100644 index 0000000000..6c3d2a3b0d --- /dev/null +++ b/common/nym_offline_compact_ecash/src/tests/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +mod e2e; +pub mod helpers; diff --git a/common/nym_offline_compact_ecash/src/traits.rs b/common/nym_offline_compact_ecash/src/traits.rs new file mode 100644 index 0000000000..a9a22144b9 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/traits.rs @@ -0,0 +1,140 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::{BlindedSignature, Signature}; +use crate::proofs::proof_spend::{SpendInstance, SpendProof}; +use crate::proofs::proof_withdrawal::{WithdrawalReqInstance, WithdrawalReqProof}; +use crate::scheme::withdrawal::RequestInfo; +use crate::scheme::{Payment, WalletSignatures}; +use crate::{Attribute, CompactEcashError, PartialWallet, WithdrawalRequest}; +use bls12_381::{G1Affine, G1Projective}; +use group::GroupEncoding; + +#[macro_export] +macro_rules! impl_byteable_bs58 { + ($typ:ident) => { + impl $crate::traits::Bytable for $typ { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> $crate::error::Result { + Self::from_bytes(slice) + } + } + + impl $crate::traits::Base58 for $typ {} + + impl TryFrom<&[u8]> for $typ { + type Error = CompactEcashError; + + fn try_from(bytes: &[u8]) -> $crate::error::Result { + Self::from_bytes(bytes) + } + } + }; +} + +macro_rules! impl_complex_binary_bytable { + ($typ:ident) => { + impl $typ { + pub fn to_bytes(&self) -> Vec { + use bincode::Options; + + // all of our manually derived types correctly serialise into bincode + #[allow(clippy::unwrap_used)] + crate::binary_serialiser().serialize(self).unwrap() + } + + pub fn from_bytes(bytes: &[u8]) -> crate::error::Result { + use bincode::Options; + crate::binary_serialiser() + .deserialize(bytes) + .map_err(|source| CompactEcashError::BinaryDeserialisationFailure { + type_name: std::any::type_name::<$typ>().to_string(), + source, + }) + } + } + + impl_byteable_bs58!($typ); + }; +} + +pub trait Bytable +where + Self: Sized, +{ + fn to_byte_vec(&self) -> Vec; + + fn try_from_byte_slice(slice: &[u8]) -> Result; +} + +pub trait Base58 +where + Self: Bytable, +{ + fn try_from_bs58>(x: S) -> Result { + Self::try_from_byte_slice(&bs58::decode(x.as_ref()).into_vec()?) + } + fn to_bs58(&self) -> String { + bs58::encode(self.to_byte_vec()).into_string() + } +} + +impl Bytable for G1Projective { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().as_ref().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + let bytes = slice + .try_into() + .map_err(|_| CompactEcashError::G1ProjectiveDeserializationFailure)?; + + let maybe_g1 = G1Affine::from_compressed(&bytes); + if maybe_g1.is_none().into() { + Err(CompactEcashError::G1ProjectiveDeserializationFailure) + } else { + // safety: this unwrap is fine as we've just checked the element is not none + #[allow(clippy::unwrap_used)] + Ok(maybe_g1.unwrap().into()) + } + } +} + +impl Base58 for G1Projective {} + +impl Bytable for Attribute { + fn to_byte_vec(&self) -> Vec { + self.to_bytes().to_vec() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + let maybe_attribute = Attribute::from_bytes( + slice + .try_into() + .map_err(|_| CompactEcashError::ScalarDeserializationFailure)?, + ); + if maybe_attribute.is_none().into() { + Err(CompactEcashError::ScalarDeserializationFailure) + } else { + // safety: this unwrap is fine as we've just checked the element is not none + #[allow(clippy::unwrap_used)] + Ok(maybe_attribute.unwrap()) + } + } +} + +impl_byteable_bs58!(Signature); +impl_byteable_bs58!(BlindedSignature); +impl_byteable_bs58!(WalletSignatures); +impl_byteable_bs58!(PartialWallet); + +impl_complex_binary_bytable!(SpendProof); +impl_complex_binary_bytable!(SpendInstance); +impl_complex_binary_bytable!(WithdrawalReqProof); +impl_complex_binary_bytable!(WithdrawalReqInstance); +impl_complex_binary_bytable!(Payment); +impl_complex_binary_bytable!(WithdrawalRequest); +impl_complex_binary_bytable!(RequestInfo); diff --git a/common/nym_offline_compact_ecash/src/utils.rs b/common/nym_offline_compact_ecash/src/utils.rs new file mode 100644 index 0000000000..57a648fff2 --- /dev/null +++ b/common/nym_offline_compact_ecash/src/utils.rs @@ -0,0 +1,404 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::common_types::SignerIndex; +use crate::error::{CompactEcashError, Result}; +use crate::scheme::setup::GroupParameters; +use crate::{ecash_group_parameters, Signature, VerificationKeyAuth}; +use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField}; +use bls12_381::{ + multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Scalar, +}; +use core::iter::Sum; +use core::ops::Mul; +use ff::Field; +use group::{Curve, Group}; +use itertools::Itertools; +use std::borrow::Borrow; +use std::ops::Neg; + +pub struct Polynomial { + coefficients: Vec, +} + +impl Polynomial { + // for polynomial of degree n, we generate n+1 values + // (for example for degree 1, like y = x + 2, we need [2,1]) + pub fn new_random(params: &GroupParameters, degree: u64) -> Self { + Polynomial { + coefficients: params.n_random_scalars((degree + 1) as usize), + } + } + + /// Evaluates the polynomial at point x. + pub fn evaluate(&self, x: &Scalar) -> Scalar { + if self.coefficients.is_empty() { + Scalar::zero() + // if x is zero then we can ignore most of the expensive computation and + // just return the last term of the polynomial + } else if x.is_zero().unwrap_u8() == 1 { + // we checked that coefficients are not empty so unwrap here is fine + #[allow(clippy::unwrap_used)] + *self.coefficients.first().unwrap() + } else { + self.coefficients + .iter() + .enumerate() + // coefficient[n] * x ^ n + .map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0])) + .sum() + } + } +} + +#[inline] +pub fn generate_lagrangian_coefficients_at_origin(points: &[u64]) -> Vec { + let x = Scalar::zero(); + + points + .iter() + .enumerate() + .map(|(i, point_i)| { + let mut numerator = Scalar::one(); + let mut denominator = Scalar::one(); + let xi = Scalar::from(*point_i); + + for (j, point_j) in points.iter().enumerate() { + if j != i { + let xj = Scalar::from(*point_j); + + // numerator = (x - xs[0]) * ... * (x - xs[j]), j != i + numerator *= x - xj; + + // denominator = (xs[i] - x[0]) * ... * (xs[i] - x[j]), j != i + denominator *= xi - xj; + } + } + // numerator / denominator + //SAFETY: denominator start as one, and (xi-xj) is guaranteed to be non zero, as we force i != j + numerator * denominator.invert().unwrap() + }) + .collect() +} + +/// Performs a Lagrange interpolation at the origin for a polynomial defined by `points` and `values`. +/// It can be used for Scalars, G1 and G2 points. +pub(crate) fn perform_lagrangian_interpolation_at_origin( + points: &[SignerIndex], + values: &[T], +) -> Result +where + T: Sum, + for<'a> &'a T: Mul, +{ + if points.is_empty() || values.is_empty() { + return Err(CompactEcashError::InterpolationSetSize); + } + + if points.len() != values.len() { + return Err(CompactEcashError::InterpolationSetSize); + } + + let coefficients = generate_lagrangian_coefficients_at_origin(points); + + Ok(coefficients + .into_iter() + .zip(values.iter()) + .map(|(coeff, val)| val * coeff) + .sum()) +} + +//domain name following https://www.rfc-editor.org/rfc/rfc9380.html#name-domain-separation-requireme recommendation +const G1_HASH_DOMAIN: &[u8] = b"NYMECASH-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_"; +const SCALAR_HASH_DOMAIN: &[u8] = b"NYMECASH-V01-CS02-with-expander-SHA256"; + +pub fn hash_g1>(msg: M) -> G1Projective { + >>::hash_to_curve(msg, G1_HASH_DOMAIN) +} + +pub fn hash_to_scalar>(msg: M) -> Scalar { + let mut output = vec![Scalar::zero()]; + + Scalar::hash_to_field::>( + msg.as_ref(), + SCALAR_HASH_DOMAIN, + &mut output, + ); + output[0] +} + +pub fn try_deserialize_scalar_vec(expected_len: u64, bytes: &[u8]) -> Result> { + if bytes.len() != expected_len as usize * 32 { + return Err(CompactEcashError::DeserializationLengthMismatch { + type_name: "Scalar vector".into(), + expected: expected_len as usize * 32, + actual: bytes.len(), + }); + } + + let mut out = Vec::with_capacity(expected_len as usize); + for i in 0..expected_len as usize { + //SAFETY : casting 32 len slice into 32 len array + #[allow(clippy::unwrap_used)] + let s_bytes = bytes[i * 32..(i + 1) * 32].try_into().unwrap(); + let s = match Scalar::from_bytes(&s_bytes).into() { + None => return Err(CompactEcashError::ScalarDeserializationFailure), + Some(scalar) => scalar, + }; + out.push(s) + } + + Ok(out) +} + +pub fn try_deserialize_scalar(bytes: &[u8; 32]) -> Result { + Into::>::into(Scalar::from_bytes(bytes)) + .ok_or(CompactEcashError::ScalarDeserializationFailure) +} + +pub fn try_deserialize_g1_projective(bytes: &[u8; 48]) -> Result { + Into::>::into(G1Affine::from_compressed(bytes)) + .ok_or(CompactEcashError::G1ProjectiveDeserializationFailure) + .map(G1Projective::from) +} + +pub fn try_deserialize_g2_projective(bytes: &[u8; 96]) -> Result { + Into::>::into(G2Affine::from_compressed(bytes)) + .ok_or(CompactEcashError::G2ProjectiveDeserializationFailure) + .map(G2Projective::from) +} + +/// Checks whether e(P, Q) * e(-R, S) == id +pub fn check_bilinear_pairing(p: &G1Affine, q: &G2Prepared, r: &G1Affine, s: &G2Prepared) -> bool { + // checking e(P, Q) * e(-R, S) == id + // is equivalent to checking e(P, Q) == e(R, S) + // but requires only a single final exponentiation rather than two of them + // and therefore, as seen via benchmarks.rs, is almost 50% faster + // (1.47ms vs 2.45ms, tested on R9 5900X) + + let multi_miller = multi_miller_loop(&[(p, q), (&r.neg(), s)]); + multi_miller.final_exponentiation().is_identity().into() +} + +// compute e(h1, X1) * e(s1, g2^-1) * ... == id +// pub fn batch_verify_signatures(iter: Vec<(&Signature, G2Projective)>) -> bool { +pub fn batch_verify_signatures(iter: impl Iterator) -> bool +where + T: Borrow<(S, G2)>, + S: Borrow, + G2: Borrow, +{ + let mut miller_terms_owned = Vec::new(); + for t in iter { + let (sig, q) = t.borrow(); + let sig = sig.borrow(); + miller_terms_owned.push(( + sig.h.to_affine(), + G2Prepared::from(q.borrow().to_affine()), + sig.s.to_affine(), + )); + } + + let params = ecash_group_parameters(); + let g2_prep_neg = G2Prepared::from(params.gen2().neg()); + + let mut miller_terms = Vec::with_capacity(miller_terms_owned.len() * 2); + for (h, q, s) in &miller_terms_owned { + miller_terms.push((h, q)); + miller_terms.push((s, &g2_prep_neg)); + } + + multi_miller_loop(&miller_terms) + .final_exponentiation() + .is_identity() + .into() +} + +pub fn check_vk_pairing( + params: &GroupParameters, + dkg_values: &[G2Projective], + vk: &VerificationKeyAuth, +) -> bool { + let values_len = dkg_values.len(); + if values_len == 0 || values_len - 1 != vk.beta_g1.len() || values_len - 1 != vk.beta_g2.len() { + return false; + } + + // safety: we made an explicit check for if the length of the slice is 0, thus unwrap here is fine + #[allow(clippy::unwrap_used)] + if &vk.alpha != *dkg_values.first().as_ref().unwrap() { + return false; + } + let dkg_betas = &dkg_values[1..]; + if dkg_betas + .iter() + .zip(vk.beta_g2.iter()) + .any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta) + { + return false; + } + + let mut owned_miller_terms = vec![]; + for (i, (g1, g2)) in vk.beta_g1.iter().zip(vk.beta_g2.iter()).enumerate() { + if i % 2 == 0 { + owned_miller_terms.push((g1.to_affine(), G2Prepared::from(g2.to_affine()))); + } else { + // negate every other g1 element + owned_miller_terms.push((g1.neg().to_affine(), G2Prepared::from(g2.to_affine()))); + } + } + + // if our key has odd length, make sure to include the generators to correctly compute the final exponentiation + if owned_miller_terms.len() % 2 == 1 { + let neg_g1 = params.gen1().neg(); + let g2_prep = params.prepared_miller_g2(); + owned_miller_terms.push((neg_g1, g2_prep.to_owned())) + } + + let mut miller_terms = Vec::new(); + for ((p, s), (r, q)) in owned_miller_terms.iter().tuples::<(_, _)>() { + miller_terms.push((p, q)); + miller_terms.push((r, s)); + } + + // check if e(g1^x, g2^y) * e(g1^-y, g2^x) * ... == id + // in case of odd-length key check: + // check if e(g1^x, g2^y) * e(g1^-y, g2^x) * ... * e(g1^-z, g2) * e(g1^1, g2^z) == id + // (this is more than 2x as fast as checking each pairing individually for key of size 5) + multi_miller_loop(&miller_terms) + .final_exponentiation() + .is_identity() + .into() +} + +#[cfg(test)] +mod tests { + use rand::RngCore; + + use super::*; + + #[test] + fn polynomial_evaluation() { + // y = 42 (it should be 42 regardless of x) + let poly = Polynomial { + coefficients: vec![Scalar::from(42)], + }; + + assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(1))); + assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(0))); + assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(10))); + + // y = x + 10, at x = 2 (exp: 12) + let poly = Polynomial { + coefficients: vec![Scalar::from(10), Scalar::from(1)], + }; + + assert_eq!(Scalar::from(12), poly.evaluate(&Scalar::from(2))); + + // y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39) + let poly = Polynomial { + coefficients: vec![ + (-Scalar::from(3)), + Scalar::from(2), + (-Scalar::from(5)), + Scalar::zero(), + Scalar::from(1), + ], + }; + + assert_eq!(Scalar::from(39), poly.evaluate(&Scalar::from(3))); + + // empty polynomial + let poly = Polynomial { + coefficients: vec![], + }; + + // should always be 0 + assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(1))); + assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(0))); + assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(10))); + } + + #[test] + fn performing_lagrangian_scalar_interpolation_at_origin() { + // x^2 + 3 + // x, f(x): + // 1, 4, + // 2, 7, + // 3, 12, + let points = vec![1, 2, 3]; + let values = vec![Scalar::from(4), Scalar::from(7), Scalar::from(12)]; + + assert_eq!( + Scalar::from(3), + perform_lagrangian_interpolation_at_origin(&points, &values).unwrap() + ); + + // x^3 + 3x^2 - 5x + 11 + // x, f(x): + // 1, 10 + // 2, 21 + // 3, 50 + // 4, 103 + let points = vec![1, 2, 3, 4]; + let values = vec![ + Scalar::from(10), + Scalar::from(21), + Scalar::from(50), + Scalar::from(103), + ]; + + assert_eq!( + Scalar::from(11), + perform_lagrangian_interpolation_at_origin(&points, &values).unwrap() + ); + + // more points than it is required + // x^2 + x + 10 + // x, f(x) + // 1, 12 + // 2, 16 + // 3, 22 + // 4, 30 + // 5, 40 + let points = vec![1, 2, 3, 4, 5]; + let values = vec![ + Scalar::from(12), + Scalar::from(16), + Scalar::from(22), + Scalar::from(30), + Scalar::from(40), + ]; + + assert_eq!( + Scalar::from(10), + perform_lagrangian_interpolation_at_origin(&points, &values).unwrap() + ); + } + + #[test] + fn hash_g1_sanity_check() { + let mut rng = rand::thread_rng(); + let mut msg1 = [0u8; 1024]; + rng.fill_bytes(&mut msg1); + let mut msg2 = [0u8; 1024]; + rng.fill_bytes(&mut msg2); + + assert_eq!(hash_g1(msg1), hash_g1(msg1)); + assert_eq!(hash_g1(msg2), hash_g1(msg2)); + assert_ne!(hash_g1(msg1), hash_g1(msg2)); + } + + #[test] + fn hash_scalar_sanity_check() { + let mut rng = rand::thread_rng(); + let mut msg1 = [0u8; 1024]; + rng.fill_bytes(&mut msg1); + let mut msg2 = [0u8; 1024]; + rng.fill_bytes(&mut msg2); + + assert_eq!(hash_to_scalar(msg1), hash_to_scalar(msg1)); + assert_eq!(hash_to_scalar(msg2), hash_to_scalar(msg2)); + assert_ne!(hash_to_scalar(msg1), hash_to_scalar(msg2)); + } +} diff --git a/common/nymcoconut/benches/benchmarks.rs b/common/nymcoconut/benches/benchmarks.rs index e550791969..debda49677 100644 --- a/common/nymcoconut/benches/benchmarks.rs +++ b/common/nymcoconut/benches/benchmarks.rs @@ -33,6 +33,40 @@ fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g )) } +#[allow(unused)] +fn bench_pairings(c: &mut Criterion) { + let mut rng = rand::thread_rng(); + + let g1 = G1Affine::generator(); + let g2 = G2Affine::generator(); + let r = Scalar::random(&mut rng); + let s = Scalar::random(&mut rng); + + let g11 = (g1 * r).to_affine(); + let g21 = (g2 * s).to_affine(); + let g21_prep = G2Prepared::from(g21); + + let g12 = (g1 * s).to_affine(); + let g22 = (g2 * r).to_affine(); + let g22_prep = G2Prepared::from(g22); + + c.bench_function("double pairing", |b| { + b.iter(|| double_pairing(&g11, &g21, &g12, &g22)) + }); + + c.bench_function("multi miller in affine", |b| { + b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22)) + }); + + c.bench_function("multi miller with prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep)) + }); + + c.bench_function("multi miller with semi-prepared g2", |b| { + b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep)) + }); +} + #[allow(unused)] fn multi_miller_pairing_with_prepared( g11: &G1Affine, @@ -125,43 +159,9 @@ impl BenchCase { } } -#[allow(unused)] -fn bench_pairings(c: &mut Criterion) { - let mut rng = rand::thread_rng(); - - let g1 = G1Affine::generator(); - let g2 = G2Affine::generator(); - let r = Scalar::random(&mut rng); - let s = Scalar::random(&mut rng); - - let g11 = (g1 * r).to_affine(); - let g21 = (g2 * s).to_affine(); - let g21_prep = G2Prepared::from(g21); - - let g12 = (g1 * s).to_affine(); - let g22 = (g2 * r).to_affine(); - let g22_prep = G2Prepared::from(g22); - - c.bench_function("double pairing", |b| { - b.iter(|| double_pairing(&g11, &g21, &g12, &g22)) - }); - - c.bench_function("multi miller in affine", |b| { - b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22)) - }); - - c.bench_function("multi miller with prepared g2", |b| { - b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep)) - }); - - c.bench_function("multi miller with semi-prepared g2", |b| { - b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep)) - }); -} - fn bench_coconut(c: &mut Criterion) { let mut group = c.benchmark_group("benchmark-coconut"); - group.measurement_time(Duration::from_secs(100)); + group.measurement_time(Duration::from_secs(1000)); let case = BenchCase { num_authorities: 100, threshold_p: 0.7, diff --git a/common/nymcoconut/src/lib.rs b/common/nymcoconut/src/lib.rs index 3eed2bf88c..dbcbda5b39 100644 --- a/common/nymcoconut/src/lib.rs +++ b/common/nymcoconut/src/lib.rs @@ -47,7 +47,7 @@ mod proofs; mod scheme; pub mod tests; mod traits; -mod utils; +pub mod utils; pub type Attribute = bls12_381::Scalar; pub type PrivateAttribute = Attribute; diff --git a/common/nymcoconut/src/utils.rs b/common/nymcoconut/src/utils.rs index 819156cb23..d12707408c 100644 --- a/common/nymcoconut/src/utils.rs +++ b/common/nymcoconut/src/utils.rs @@ -122,7 +122,7 @@ const G1_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_R // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-K.1 const SCALAR_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-expander"; -pub(crate) fn hash_g1>(msg: M) -> G1Projective { +pub fn hash_g1>(msg: M) -> G1Projective { >>::hash_to_curve(msg, G1_HASH_DOMAIN) } @@ -137,7 +137,7 @@ pub fn hash_to_scalar>(msg: M) -> Scalar { output[0] } -pub(crate) fn try_deserialize_scalar_vec( +pub fn try_deserialize_scalar_vec( expected_len: u64, bytes: &[u8], err: CoconutError, @@ -161,23 +161,17 @@ pub(crate) fn try_deserialize_scalar_vec( Ok(out) } -pub(crate) fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result { +pub fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result { Into::>::into(Scalar::from_bytes(bytes)).ok_or(err) } -pub(crate) fn try_deserialize_g1_projective( - bytes: &[u8; 48], - err: CoconutError, -) -> Result { +pub fn try_deserialize_g1_projective(bytes: &[u8; 48], err: CoconutError) -> Result { Into::>::into(G1Affine::from_compressed(bytes)) .ok_or(err) .map(G1Projective::from) } -pub(crate) fn try_deserialize_g2_projective( - bytes: &[u8; 96], - err: CoconutError, -) -> Result { +pub fn try_deserialize_g2_projective(bytes: &[u8; 96], err: CoconutError) -> Result { Into::>::into(G2Affine::from_compressed(bytes)) .ok_or(err) .map(G2Projective::from) diff --git a/common/pemstore/src/lib.rs b/common/pemstore/src/lib.rs index a89bc628c5..bd36158229 100644 --- a/common/pemstore/src/lib.rs +++ b/common/pemstore/src/lib.rs @@ -9,7 +9,7 @@ use std::path::{Path, PathBuf}; pub mod traits; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct KeyPairPath { pub private_key_path: PathBuf, pub public_key_path: PathBuf, diff --git a/common/serde-helpers/Cargo.toml b/common/serde-helpers/Cargo.toml new file mode 100644 index 0000000000..f6907190b3 --- /dev/null +++ b/common/serde-helpers/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "serde-helpers" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +serde = { workspace = true, default-features = false } +bs58 = { workspace = true, optional = true } +base64 = { workspace = true, optional = true } + + +[features] +bs58 = ["dep:bs58"] +base64 = ["dep:base64"] diff --git a/common/serde-helpers/src/lib.rs b/common/serde-helpers/src/lib.rs new file mode 100644 index 0000000000..f90a65a2f4 --- /dev/null +++ b/common/serde-helpers/src/lib.rs @@ -0,0 +1,33 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "base64")] +pub mod base64 { + use base64::{engine::general_purpose::STANDARD, Engine as _}; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&STANDARD.encode(bytes)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = ::deserialize(deserializer)?; + STANDARD.decode(s).map_err(serde::de::Error::custom) + } +} + +#[cfg(feature = "bs58")] +pub mod bs58 { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&::bs58::encode(bytes).into_string()) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = String::deserialize(deserializer)?; + ::bs58::decode(&s) + .into_vec() + .map_err(serde::de::Error::custom) + } +} diff --git a/common/types/src/transaction.rs b/common/types/src/transaction.rs index a7e4176d7a..9179a0170b 100644 --- a/common/types/src/transaction.rs +++ b/common/types/src/transaction.rs @@ -65,7 +65,7 @@ impl TransactionDetails { #[derive(Deserialize, Serialize, Debug)] pub struct TransactionExecuteResult { pub logs_json: String, - pub data_json: String, + pub msg_responses_json: String, pub transaction_hash: String, pub gas_info: GasInfo, pub fee: Option, @@ -79,7 +79,7 @@ impl TransactionExecuteResult { Ok(TransactionExecuteResult { gas_info: value.gas_info.into(), transaction_hash: value.transaction_hash.to_string(), - data_json: ::serde_json::to_string_pretty(&value.data)?, + msg_responses_json: ::serde_json::to_string_pretty(&value.msg_responses)?, logs_json: ::serde_json::to_string_pretty(&value.logs)?, fee, }) diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index c1440b1c6f..e51183c7a7 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -242,6 +242,21 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_panic" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6051f239ecec86fde3410901ab7860d458d160371533842974fc61f96d15879b" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cosmwasm-crypto" version = "1.4.3" @@ -438,9 +453,9 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.16.4" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a18afd2e201221c6d72a57f0886ef2a22151bbc9e6db7af276fde8a91081042" +checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1" dependencies = [ "anyhow", "cosmwasm-std", @@ -744,7 +759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ "curve25519-dalek 3.2.0", - "hashbrown", + "hashbrown 0.12.3", "hex", "rand_core 0.6.4", "serde", @@ -797,6 +812,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "ff" version = "0.12.1" @@ -891,6 +912,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hex" version = "0.4.3" @@ -931,6 +958,16 @@ dependencies = [ "serde", ] +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + [[package]] name = "inout" version = "0.1.3" @@ -987,6 +1024,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" +[[package]] +name = "konst" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e28ab1dc35e09d60c2b8c90d12a9a8d9666c876c10a3739a3196db0103b6043" + [[package]] name = "libc" version = "0.2.153" @@ -1151,6 +1215,45 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-ecash" +version = "0.1.0" +dependencies = [ + "bs58 0.4.0", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "cw4", + "nym-contracts-common", + "nym-crypto", + "nym-ecash-contract-common", + "nym-multisig-contract-common", + "rand_chacha", + "schemars", + "semver", + "serde", + "sylvia", + "thiserror", +] + +[[package]] +name = "nym-ecash-contract-common" +version = "0.1.0" +dependencies = [ + "bs58 0.5.1", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-multisig-contract-common", + "thiserror", +] + [[package]] name = "nym-group-contract-common" version = "0.1.0" @@ -1333,6 +1436,40 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.81" @@ -1561,6 +1698,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-cw-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" +dependencies = [ + "serde", +] + [[package]] name = "serde-json-wasm" version = "0.5.0" @@ -1724,6 +1870,39 @@ dependencies = [ "zeroize", ] +[[package]] +name = "sylvia" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33388920659b494dab887f3bb40ebb071c602750597575034bea7c63ab12800" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "derivative", + "konst", + "schemars", + "serde", + "serde-cw-value", + "serde-json-wasm", + "sylvia-derive", +] + +[[package]] +name = "sylvia-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8031f53dbfda341acd7bd321e10d0d684b673324145026e23705da4b6d5c4919" +dependencies = [ + "convert_case", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "syn" version = "1.0.109" @@ -1814,18 +1993,56 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typewit" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fb9ae6a3cafaf0a5d14c2302ca525f9ae8e07a0f0e6949de88d882c37a6e24" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "vergen" version = "8.3.1" @@ -1853,6 +2070,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "x25519-dalek" version = "2.0.1" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 175bbcc767..a7c49c9099 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -3,7 +3,8 @@ resolver = "2" members = [ "coconut-bandwidth", "coconut-dkg", - "coconut-test", + "coconut-test", + "ecash", "mixnet", "mixnet-vesting-integration-tests", "multisig/cw3-flex-multisig", @@ -38,7 +39,7 @@ cosmwasm-schema = "=1.4.3" cosmwasm-std = "=1.4.3" cosmwasm-storage = "=1.4.3" cw-controllers = "=1.1.0" -cw-multi-test = "=0.16.4" +cw-multi-test = "=0.16.5" cw-storage-plus = "=1.2.0" cw-utils = "=1.0.1" cw2 = "=1.1.2" @@ -48,5 +49,7 @@ cw4 = "=1.1.2" cw20 = "=1.1.2" semver = "1.0.21" serde = "1.0.196" +sylvia = "0.8.0" +schemars = "0.8.16" thiserror = "1.0.48" diff --git a/contracts/coconut-dkg/schema/nym-coconut-dkg.json b/contracts/coconut-dkg/schema/nym-coconut-dkg.json index 43f3a69975..b33212190b 100644 --- a/contracts/coconut-dkg/schema/nym-coconut-dkg.json +++ b/contracts/coconut-dkg/schema/nym-coconut-dkg.json @@ -374,6 +374,29 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "get_epoch_threshold" + ], + "properties": { + "get_epoch_threshold": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -1843,6 +1866,13 @@ } } }, + "get_epoch_threshold": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "uint64", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "get_registered_dealer": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "RegisteredDealerDetails", diff --git a/contracts/coconut-dkg/schema/raw/query.json b/contracts/coconut-dkg/schema/raw/query.json index 72e1e095cd..386632565c 100644 --- a/contracts/coconut-dkg/schema/raw/query.json +++ b/contracts/coconut-dkg/schema/raw/query.json @@ -41,6 +41,29 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "get_epoch_threshold" + ], + "properties": { + "get_epoch_threshold": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ diff --git a/contracts/coconut-dkg/schema/raw/response_to_get_epoch_threshold.json b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_threshold.json new file mode 100644 index 0000000000..7b729a7b96 --- /dev/null +++ b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_threshold.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "uint64", + "type": "integer", + "format": "uint64", + "minimum": 0.0 +} diff --git a/contracts/coconut-dkg/src/contract.rs b/contracts/coconut-dkg/src/contract.rs index a1d84337af..acc1917b8d 100644 --- a/contracts/coconut-dkg/src/contract.rs +++ b/contracts/coconut-dkg/src/contract.rs @@ -13,8 +13,9 @@ use crate::dealings::queries::{ use crate::dealings::transactions::{try_commit_dealings_chunk, try_submit_dealings_metadata}; use crate::epoch_state::queries::{ query_can_advance_state, query_current_epoch, query_current_epoch_threshold, + query_epoch_threshold, }; -use crate::epoch_state::storage::CURRENT_EPOCH; +use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; use crate::epoch_state::transactions::{ try_advance_epoch_state, try_initiate_dkg, try_trigger_reset, try_trigger_resharing, }; @@ -134,6 +135,9 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { to_binary(&query_current_epoch_threshold(deps.storage)?)? } + QueryMsg::GetEpochThreshold { epoch_id } => { + to_binary(&query_epoch_threshold(deps.storage, epoch_id)?)? + } QueryMsg::GetRegisteredDealer { dealer_address, epoch_id, @@ -217,6 +221,13 @@ pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result // SPDX-License-Identifier: Apache-2.0 -use crate::epoch_state::storage::{CURRENT_EPOCH, THRESHOLD}; +use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; use crate::epoch_state::utils::check_state_completion; use crate::error::ContractError; use cosmwasm_std::{Env, Storage}; -use nym_coconut_dkg_common::types::{Epoch, EpochState, StateAdvanceResponse}; +use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState, StateAdvanceResponse}; pub(crate) fn query_can_advance_state( storage: &dyn Storage, @@ -45,6 +45,13 @@ pub(crate) fn query_current_epoch_threshold( Ok(THRESHOLD.may_load(storage)?) } +pub(crate) fn query_epoch_threshold( + storage: &dyn Storage, + epoch_id: EpochId, +) -> Result, ContractError> { + Ok(EPOCH_THRESHOLDS.may_load(storage, epoch_id)?) +} + #[cfg(test)] pub(crate) mod test { use super::*; diff --git a/contracts/coconut-dkg/src/epoch_state/storage.rs b/contracts/coconut-dkg/src/epoch_state/storage.rs index 56c80fac2d..0ab1d9ab9c 100644 --- a/contracts/coconut-dkg/src/epoch_state/storage.rs +++ b/contracts/coconut-dkg/src/epoch_state/storage.rs @@ -1,8 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cw_storage_plus::Item; -use nym_coconut_dkg_common::types::Epoch; +use cw_storage_plus::{Item, Map}; +use nym_coconut_dkg_common::types::{Epoch, EpochId}; pub(crate) const CURRENT_EPOCH: Item<'_, Epoch> = Item::new("current_epoch"); + pub const THRESHOLD: Item = Item::new("threshold"); + +pub const EPOCH_THRESHOLDS: Map = Map::new("epoch_thresholds"); diff --git a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs index 396a7d1408..7ca350120a 100644 --- a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs +++ b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::epoch_state::storage::{CURRENT_EPOCH, THRESHOLD}; +use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; use crate::epoch_state::transactions::reset_dkg_state; use crate::epoch_state::utils::check_state_completion; use crate::error::ContractError; @@ -61,7 +61,10 @@ pub fn try_advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::NymEcashContract; +use crate::helpers::{create_batch_redemption_proposal, create_blacklist_proposal, ProposalId}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{to_binary, Addr, Coin, Deps, SubMsg}; +use cw3::ProposalResponse; +use nym_ecash_contract_common::EcashContractError; +use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg; +use sylvia::types::ExecCtx; + +#[cw_serde] +pub(crate) struct Invariants { + pub(crate) ticket_book_value: Coin, + pub(crate) ticket_value: Coin, +} + +impl NymEcashContract<'_> { + fn must_get_multisig_addr(&self, deps: Deps) -> Result { + // SAFETY: multisig admin MUST always be set on initialisation, + // if the call fails, we're in some weird UB land + #[allow(clippy::expect_used)] + Ok(self + .multisig + .get(deps)? + .expect("multisig admin must always be set on initialisation")) + } + + pub(crate) fn create_redemption_proposal( + &self, + ctx: ExecCtx, + commitment_bs58: String, + number_of_tickets: u16, + ) -> Result { + let multisig_addr = self.must_get_multisig_addr(ctx.deps.as_ref())?; + + create_batch_redemption_proposal( + commitment_bs58, + ctx.info.sender.into_string(), + number_of_tickets, + ctx.env.contract.address.into_string(), + multisig_addr.into_string(), + ) + .map_err(Into::into) + } + + pub(crate) fn create_blacklist_proposal( + &self, + ctx: ExecCtx, + public_key: String, + ) -> Result { + let multisig_addr = self.must_get_multisig_addr(ctx.deps.as_ref())?; + + create_blacklist_proposal( + public_key, + ctx.env.contract.address.into_string(), + multisig_addr.into_string(), + ) + .map_err(Into::into) + } + + pub(crate) fn query_multisig_proposal( + &self, + deps: Deps, + proposal_id: ProposalId, + ) -> Result { + let msg = MultisigQueryMsg::Proposal { proposal_id }; + let multisig_addr = self.must_get_multisig_addr(deps)?; + + let proposal_response: ProposalResponse = deps.querier.query( + &cosmwasm_std::QueryRequest::Wasm(cosmwasm_std::WasmQuery::Smart { + contract_addr: multisig_addr.to_string(), + msg: to_binary(&msg)?, + }), + )?; + Ok(proposal_response) + } +} diff --git a/contracts/ecash/src/contract/mod.rs b/contracts/ecash/src/contract/mod.rs new file mode 100644 index 0000000000..66c5a60873 --- /dev/null +++ b/contracts/ecash/src/contract/mod.rs @@ -0,0 +1,411 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::helpers::Invariants; +use crate::deposit::DepositStorage; +use crate::helpers::{ + BlacklistKey, Config, MultisigReply, BLACKLIST_PAGE_DEFAULT_LIMIT, BLACKLIST_PAGE_MAX_LIMIT, + CONTRACT_NAME, CONTRACT_VERSION, DEPOSITS_PAGE_DEFAULT_LIMIT, DEPOSITS_PAGE_MAX_LIMIT, +}; +use cosmwasm_std::{BankMsg, Coin, Decimal, Event, Order, Reply, Response, StdResult, Uint128}; +use cw4::Cw4Contract; +use cw_controllers::Admin; +use cw_storage_plus::{Bound, Item, Map}; +use nym_contracts_common::set_build_information; +use nym_ecash_contract_common::blacklist::{ + BlacklistedAccount, BlacklistedAccountResponse, Blacklisting, PagedBlacklistedAccountResponse, +}; +use nym_ecash_contract_common::deposit::{DepositData, DepositResponse, PagedDepositsResponse}; +use nym_ecash_contract_common::events::{ + BLACKLIST_PROPOSAL_REPLY_ID, DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ID, + PROPOSAL_ID_ATTRIBUTE_NAME, REDEMPTION_PROPOSAL_REPLY_ID, TICKET_BOOK_VALUE, TICKET_VALUE, +}; +use nym_ecash_contract_common::EcashContractError; +use sylvia::types::{ExecCtx, InstantiateCtx, MigrateCtx, QueryCtx, ReplyCtx}; +use sylvia::{contract, entry_points}; + +mod helpers; + +#[cfg(test)] +mod test; + +pub struct NymEcashContract<'a> { + pub(crate) multisig: Admin<'a>, + pub(crate) config: Item<'a, Config>, + pub(crate) expected_invariants: Item<'a, Invariants>, + + pub(crate) blacklist: Map<'a, BlacklistKey, Blacklisting>, + + pub(crate) deposits: DepositStorage<'a>, +} + +#[entry_points] +#[contract] +#[error(EcashContractError)] +impl NymEcashContract<'_> { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + Self { + multisig: Admin::new("multisig"), + config: Item::new("config"), + expected_invariants: Item::new("expected_invariants"), + blacklist: Map::new("blacklist"), + deposits: DepositStorage::new(), + } + } + + #[msg(instantiate)] + pub fn instantiate( + &self, + mut ctx: InstantiateCtx, + holding_account: String, + multisig_addr: String, + group_addr: String, + mix_denom: String, + ) -> Result { + let multisig_addr = ctx.deps.api.addr_validate(&multisig_addr)?; + let holding_account = ctx.deps.api.addr_validate(&holding_account)?; + let group_addr = Cw4Contract(ctx.deps.api.addr_validate(&group_addr).map_err(|_| { + EcashContractError::InvalidGroup { + addr: group_addr.clone(), + } + })?); + + self.multisig + .set(ctx.deps.branch(), Some(multisig_addr.clone()))?; + + self.expected_invariants.save( + ctx.deps.storage, + &Invariants { + ticket_book_value: Coin::new(TICKET_BOOK_VALUE, &mix_denom), + ticket_value: Coin::new(TICKET_VALUE, &mix_denom), + }, + )?; + + let cfg = Config { + group_addr, + mix_denom, + holding_account, + + redemption_gateway_share: Decimal::percent(5), + }; + self.config.save(ctx.deps.storage, &cfg)?; + + cw2::set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + set_build_information!(ctx.deps.storage)?; + + Ok(Response::default()) + } + + /*================== + ======QUERIES======= + ==================*/ + #[msg(query)] + pub fn get_blacklist_paged( + &self, + ctx: QueryCtx, + limit: Option, + start_after: Option, + ) -> StdResult { + let limit = limit + .unwrap_or(BLACKLIST_PAGE_DEFAULT_LIMIT) + .min(BLACKLIST_PAGE_MAX_LIMIT) as usize; + + let start = start_after.as_deref().map(Bound::exclusive); + + let nodes = self + .blacklist + .range(ctx.deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(Into::into)) + .collect::>>()?; + + let start_next_after = nodes + .last() + .map(|account: &BlacklistedAccount| account.public_key().to_string()); + + Ok(PagedBlacklistedAccountResponse::new( + nodes, + limit, + start_next_after, + )) + } + + #[msg(query)] + pub fn get_blacklisted_account( + &self, + ctx: QueryCtx, + public_key: String, + ) -> StdResult { + let account = self.blacklist.may_load(ctx.deps.storage, public_key)?; + Ok(BlacklistedAccountResponse::new(account)) + } + + #[msg(query)] + pub fn get_required_deposit_amount(&self, ctx: QueryCtx) -> Result { + let mix_denom = self.config.load(ctx.deps.storage)?.mix_denom; + let expected_deposit = self + .expected_invariants + .load(ctx.deps.storage)? + .ticket_book_value; + let current = Coin::new(TICKET_BOOK_VALUE, mix_denom); + if expected_deposit != current { + return Err(EcashContractError::DepositAmountChanged { + at_init: expected_deposit, + current, + }); + } + + Ok(current) + } + + #[msg(query)] + pub fn get_deposit( + &self, + ctx: QueryCtx, + deposit_id: u32, + ) -> Result { + Ok(DepositResponse { + id: deposit_id, + deposit: self.deposits.try_load_by_id(ctx.deps.storage, deposit_id)?, + }) + } + + #[msg(query)] + pub fn get_deposits_paged( + &self, + ctx: QueryCtx, + limit: Option, + start_after: Option, + ) -> StdResult { + let limit = limit + .unwrap_or(DEPOSITS_PAGE_DEFAULT_LIMIT) + .min(DEPOSITS_PAGE_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let deposits = self + .deposits + .range(ctx.deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(Into::into)) + .collect::>>()?; + + let start_next_after = deposits.last().map(|deposit| deposit.id); + + Ok(PagedDepositsResponse { + deposits, + start_next_after, + }) + } + + /*===================== + ======EXECUTIONS======= + =====================*/ + + #[msg(exec)] + pub fn deposit_ticket_book_funds( + &self, + ctx: ExecCtx, + identity_key: String, + ) -> Result { + let mix_denom = self.config.load(ctx.deps.storage)?.mix_denom; + let voucher_value = cw_utils::must_pay(&ctx.info, &mix_denom)?; + let amount = voucher_value.u128(); + + let expected_deposit = self + .expected_invariants + .load(ctx.deps.storage)? + .ticket_book_value; + if expected_deposit.amount.u128() != TICKET_BOOK_VALUE { + return Err(EcashContractError::DepositAmountChanged { + at_init: expected_deposit, + current: Coin::new(TICKET_BOOK_VALUE, mix_denom), + }); + } + + if amount != TICKET_BOOK_VALUE { + return Err(EcashContractError::WrongAmount { + received: amount, + amount: TICKET_BOOK_VALUE, + }); + } + + let deposit_id = self.deposits.save_deposit(ctx.deps.storage, identity_key)?; + + Ok(Response::new() + .add_event( + Event::new(DEPOSITED_FUNDS_EVENT_TYPE) + .add_attribute(DEPOSIT_ID, deposit_id.to_string()), + ) + .set_data(deposit_id.to_be_bytes())) + } + + #[msg(exec)] + pub fn request_redemption( + &self, + ctx: ExecCtx, + commitment_bs58: String, + number_of_tickets: u16, + ) -> Result { + // basic validation of commitment to make sure it's a valid sha256 digest + let Ok(digest) = bs58::decode(&commitment_bs58).into_vec() else { + return Err(EcashContractError::MalformedRedemptionCommitment); + }; + if digest.len() != 32 { + return Err(EcashContractError::MalformedRedemptionCommitment); + } + + let msg = self.create_redemption_proposal(ctx, commitment_bs58, number_of_tickets)?; + Ok(Response::new().add_submessage(msg)) + } + + #[msg(exec)] + pub fn redeem_tickets( + &self, + ctx: ExecCtx, + n: u16, + gw: String, + ) -> Result { + // only a mutlisig proposal can do that + self.multisig + .assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?; + + let config = self.config.load(ctx.deps.storage)?; + let denom = &config.mix_denom; + + let expected_ticket = self + .expected_invariants + .load(ctx.deps.storage)? + .ticket_value; + let current = Coin::new(TICKET_VALUE, denom); + if expected_ticket != current { + return Err(EcashContractError::TicketValueChanged { + at_init: expected_ticket, + current, + }); + } + + // TODO: we need unit tests for this + let return_amount = Uint128::new(TICKET_VALUE * n as u128); + let gw_share = config.redemption_gateway_share * return_amount; + let holding_share = return_amount - gw_share; + + Ok(Response::new() + .add_message(BankMsg::Send { + to_address: gw, + amount: vec![Coin { + denom: denom.to_owned(), + amount: gw_share, + }], + }) + .add_message(BankMsg::Send { + to_address: config.holding_account.to_string(), + amount: vec![Coin { + denom: denom.to_owned(), + amount: holding_share, + }], + })) + } + + #[msg(exec)] + pub fn propose_to_blacklist( + &self, + ctx: ExecCtx, + public_key: String, + ) -> Result { + let cfg = self.config.load(ctx.deps.storage)?; + cfg.group_addr + .is_voting_member(&ctx.deps.querier, &ctx.info.sender, ctx.env.block.height)? + .ok_or(EcashContractError::Unauthorized)?; + + if let Some(blacklisted) = self + .blacklist + .may_load(ctx.deps.storage, public_key.clone())? + { + // return existing proposal id + Ok(Response::new().add_attribute( + PROPOSAL_ID_ATTRIBUTE_NAME, + blacklisted.proposal_id.to_string(), + )) + } else { + let msg = self.create_blacklist_proposal(ctx, public_key)?; + Ok(Response::new().add_submessage(msg)) + } + } + + #[msg(exec)] + pub fn add_to_blacklist( + &self, + ctx: ExecCtx, + public_key: String, + ) -> Result { + //Only by multisig contract, actually add public key to blacklist + self.multisig + .assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?; + + let mut blacklisting = self.blacklist.load(ctx.deps.storage, public_key.clone())?; + blacklisting.finalized_at_height = Some(ctx.env.block.height); + self.blacklist + .save(ctx.deps.storage, public_key.clone(), &blacklisting)?; + + Ok(Response::new()) + } + + /*===================== + =========REPLY========= + =====================*/ + #[msg(reply)] + pub fn reply(&self, ctx: ReplyCtx, msg: Reply) -> Result { + match msg.id { + n if n == BLACKLIST_PROPOSAL_REPLY_ID => self.handle_blacklist_proposal_reply(ctx, msg), + n if n == REDEMPTION_PROPOSAL_REPLY_ID => { + self.handle_redemption_proposal_reply(ctx, msg) + } + other => Err(EcashContractError::InvalidReplyId { id: other }), + } + } + + fn handle_blacklist_proposal_reply( + &self, + ctx: ReplyCtx, + msg: Reply, + ) -> Result { + let proposal_id = msg.multisig_proposal_id()?; + + let proposal = self.query_multisig_proposal(ctx.deps.as_ref(), proposal_id)?; + let public_key = proposal.description; + self.blacklist.save( + ctx.deps.storage, + public_key, + &Blacklisting::new(proposal_id), + )?; + + // TODO: that `BLACKLIST_PROPOSAL_ID` might be redundant since it should be available from cw3 event + Ok(Response::new().add_attribute(PROPOSAL_ID_ATTRIBUTE_NAME, proposal_id.to_string())) + } + + fn handle_redemption_proposal_reply( + &self, + _ctx: ReplyCtx, + msg: Reply, + ) -> Result { + let proposal_id = msg.multisig_proposal_id()?; + + // emit the proposal_id in the response data for easy client access and to make sure it can't be tampered with + // (since it's included as part of block hash) + + Ok(Response::new().set_data(proposal_id.to_be_bytes())) + } + + /*===================== + =======MIGRATION======= + =====================*/ + #[msg(migrate)] + pub fn migrate(&self, ctx: MigrateCtx) -> Result { + set_build_information!(ctx.deps.storage)?; + cw2::ensure_from_older_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new()) + } +} diff --git a/contracts/ecash/src/contract/test.rs b/contracts/ecash/src/contract/test.rs new file mode 100644 index 0000000000..6ea55e8f2e --- /dev/null +++ b/contracts/ecash/src/contract/test.rs @@ -0,0 +1,89 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::NymEcashContract; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; +use cosmwasm_std::{Addr, Empty, Env, MemoryStorage, OwnedDeps}; +use sylvia::types::{InstantiateCtx, QueryCtx}; + +pub const TEST_DENOM: &str = "unym"; + +#[allow(dead_code)] +pub struct TestSetup { + pub contract: NymEcashContract<'static>, + pub deps: OwnedDeps>, + pub env: Env, + + pub holding_account: Addr, + pub multisig_contract: Addr, + pub group_contract: Addr, +} + +impl TestSetup { + pub fn init() -> TestSetup { + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = mock_info("admin", &[]); + let init_ctx = InstantiateCtx::from((deps.as_mut(), env.clone(), admin)); + + let multisig_contract = "multisig"; + let group_contract = "group"; + let holding = "holding"; + + let contract = NymEcashContract::new(); + contract + .instantiate( + init_ctx, + holding.to_string(), + multisig_contract.to_string(), + group_contract.to_string(), + TEST_DENOM.to_string(), + ) + .unwrap(); + + TestSetup { + contract, + deps, + env, + holding_account: Addr::unchecked(holding), + multisig_contract: Addr::unchecked(multisig_contract), + group_contract: Addr::unchecked(group_contract), + } + } + + pub fn query_ctx(&self) -> QueryCtx { + QueryCtx::from((self.deps.as_ref(), self.env.clone())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_ecash_contract_common::deposit::Deposit; + use sylvia::anyhow; + + #[test] + fn deposit_queries() -> anyhow::Result<()> { + let mut test = TestSetup::init(); + + // no deposit + let res = test.contract.get_deposit(test.query_ctx(), 42)?; + assert!(res.deposit.is_none()); + + let deps = test.deps.as_mut(); + let deposit_id = test.contract.deposits.save_deposit( + deps.storage, + "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(), + )?; + + // deposit exists + let res = test.contract.get_deposit(test.query_ctx(), deposit_id)?; + let expected = Deposit { + bs58_encoded_ed25519_pubkey: "GLdR2NRVZBiCoCbv4fNqt9wUJZAnNjGXHkx3TjVAUzrK".to_string(), + }; + + assert_eq!(expected, res.deposit.unwrap()); + + Ok(()) + } +} diff --git a/contracts/ecash/src/deposit.rs b/contracts/ecash/src/deposit.rs new file mode 100644 index 0000000000..310b4a0a26 --- /dev/null +++ b/contracts/ecash/src/deposit.rs @@ -0,0 +1,230 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Order, StdResult, Storage}; +use cw_storage_plus::{Bound, Item, Key, Path, Prefix, PrimaryKey}; +use nym_ecash_contract_common::deposit::DepositId; +use nym_ecash_contract_common::{deposit::Deposit, EcashContractError}; + +pub(crate) struct DepositStorage<'a> { + pub(crate) deposit_id_counter: Item<'a, DepositId>, + pub(crate) deposits: StoredDeposits, +} + +impl<'a> DepositStorage<'a> { + pub const fn new() -> Self { + DepositStorage { + deposit_id_counter: Item::new("deposit_ids"), + deposits: StoredDeposits, + } + } + + fn next_id(&self, store: &mut dyn Storage) -> Result { + let id: DepositId = self.deposit_id_counter.may_load(store)?.unwrap_or_default(); + let next_id = id + 1; + self.deposit_id_counter.save(store, &next_id)?; + Ok(id) + } + + pub fn save_deposit( + &self, + storage: &mut dyn Storage, + bs58_encoded_ed25519: String, + ) -> Result { + let id = self.next_id(storage)?; + + // ed25519 key MUST be represented with valid bs58 representation + // and after decoding it's always exactly 32 bytes which takes less + // space than its string representation (~44 bytes) + let bytes = Deposit::new(bs58_encoded_ed25519).to_bytes()?; + + let storage_key = StoredDeposits::storage_key(id); + storage.set(&storage_key, &bytes); + Ok(id) + } + + pub fn try_load_by_id( + &self, + storage: &dyn Storage, + id: DepositId, + ) -> Result, EcashContractError> { + let storage_key = StoredDeposits::storage_key(id); + + let Some(deposit_bytes) = storage.get(&storage_key) else { + return Ok(None); + }; + + Ok(Some(Deposit::try_from_bytes(&deposit_bytes)?)) + } + pub fn range( + &'a self, + store: &'a dyn Storage, + min: Option>, + max: Option>, + order: Order, + ) -> impl Iterator> + 'a { + self.deposits.no_prefix().range(store, min, max, order) + } +} + +// a helper structure for storing deposits to bypass json serialisation and use more efficient and compact representation +pub(crate) struct StoredDeposits; + +impl StoredDeposits { + const NAMESPACE: &'static [u8] = b"deposit"; + + fn deserialize_deposit_record(kv: cosmwasm_std::Record) -> StdResult<(DepositId, Deposit)> { + let (k, deposit_bytes) = kv; + let id = ::from_vec(k)?; + + Ok((id, Deposit::try_from_bytes(&deposit_bytes)?)) + } + + fn no_prefix(&self) -> Prefix { + cw_storage_plus::Prefix::with_deserialization_functions( + Self::NAMESPACE, + &[], + &[], + // explicitly panic to make sure we're never attempting to call an unexpected deserializer on our data + |_, _, kv| Self::deserialize_deposit_record(kv), + |_, _, _| panic!("attempted to call custom de_fn_v"), + ) + } + + fn storage_key(deposit_id: u32) -> Path> { + let key = deposit_id; + Path::new( + Self::NAMESPACE, + &key.key().iter().map(Key::as_ref).collect::>(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_rng; + use cosmwasm_std::testing::mock_dependencies; + use nym_crypto::asymmetric::ed25519; + + #[test] + fn iterating_over_deposits() { + let mut deps = mock_dependencies(); + let mut rng = test_rng(); + + let storage = DepositStorage::new(); + + let count = 10; + let mut expected = Vec::new(); + for _ in 0..count { + let ed25519_keypair = ed25519::KeyPair::new(&mut rng); + + let bs58_encoded_ed25519 = ed25519_keypair.public_key().to_base58_string(); + expected.push(Deposit { + bs58_encoded_ed25519_pubkey: bs58_encoded_ed25519.clone(), + }); + + storage + .save_deposit(deps.as_mut().storage, bs58_encoded_ed25519) + .unwrap(); + } + + // just first entry + let res = storage + .range( + deps.as_ref().storage, + None, + Some(Bound::inclusive(1u32)), + Order::Ascending, + ) + .collect::>(); + let (id, deposit) = res[0].as_ref().unwrap(); + assert_eq!(0, *id); + assert_eq!(deposit, &expected[0]); + + // first three entries + let res = storage + .range( + deps.as_ref().storage, + None, + Some(Bound::exclusive(4u32)), + Order::Ascending, + ) + .collect::>(); + for i in 0..3 { + let (id, deposit) = res[i].as_ref().unwrap(); + assert_eq!(i as u32, *id); + assert_eq!(deposit, &expected[i]); + } + + // two entries in the middle + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(5u32)), + Some(Bound::inclusive(6u32)), + Order::Ascending, + ) + .collect::>(); + let (id1, deposit1) = res[0].as_ref().unwrap(); + let (id2, deposit2) = res[1].as_ref().unwrap(); + assert_eq!(5, *id1); + assert_eq!(deposit1, &expected[5]); + assert_eq!(6, *id2); + assert_eq!(deposit2, &expected[6]); + + // last 2 entries + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(8u32)), + Some(Bound::inclusive(9u32)), + Order::Ascending, + ) + .collect::>(); + let (id1, deposit1) = res[0].as_ref().unwrap(); + let (id2, deposit2) = res[1].as_ref().unwrap(); + assert_eq!(8, *id1); + assert_eq!(deposit1, &expected[8]); + assert_eq!(9, *id2); + assert_eq!(deposit2, &expected[9]); + + // last 2 entries but with iterator going beyond + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(8u32)), + Some(Bound::inclusive(42u32)), + Order::Ascending, + ) + .collect::>(); + let (id1, deposit1) = res[0].as_ref().unwrap(); + let (id2, deposit2) = res[1].as_ref().unwrap(); + assert_eq!(8, *id1); + assert_eq!(deposit1, &expected[8]); + assert_eq!(9, *id2); + assert_eq!(deposit2, &expected[9]); + + // outside the saved range + let res = storage + .range( + deps.as_ref().storage, + Some(Bound::inclusive(42u32)), + Some(Bound::inclusive(666u32)), + Order::Ascending, + ) + .collect::>(); + assert!(res.is_empty()); + + // all entries + let res = storage + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>(); + assert_eq!(res.len(), count as usize); + for (i, val) in res.into_iter().enumerate() { + let (id, deposit) = val.unwrap(); + assert_eq!(id, i as u32); + assert_eq!(&deposit, &expected[i]) + } + } +} diff --git a/contracts/ecash/src/helpers.rs b/contracts/ecash/src/helpers.rs new file mode 100644 index 0000000000..8a1b298dd5 --- /dev/null +++ b/contracts/ecash/src/helpers.rs @@ -0,0 +1,121 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{ + to_binary, Addr, CosmosMsg, Decimal, Reply, StdError, StdResult, SubMsg, SubMsgResult, WasmMsg, +}; +use cw4::Cw4Contract; +use nym_contracts_common::events::try_find_attribute; +use nym_ecash_contract_common::events::{ + PROPOSAL_ID_ATTRIBUTE_NAME, REDEMPTION_PROPOSAL_REPLY_ID, WASM_EVENT_NAME, +}; +use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; +use nym_ecash_contract_common::{ + events::BLACKLIST_PROPOSAL_REPLY_ID, msg::ExecuteMsg, EcashContractError, +}; +use nym_multisig_contract_common::msg::ExecuteMsg as MultisigExecuteMsg; +use serde::{Deserialize, Serialize}; + +// version info for migration info +pub(crate) const CONTRACT_NAME: &str = "crate:nym-ecash"; +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Config { + pub group_addr: Cw4Contract, + pub mix_denom: String, + pub holding_account: Addr, + + pub redemption_gateway_share: Decimal, +} + +//type aliases for easier reasoning +pub(crate) type BlacklistKey = String; +pub(crate) type ProposalId = u64; + +// paged retrieval limits for all blacklist queries and transactions +pub(crate) const BLACKLIST_PAGE_MAX_LIMIT: u32 = 75; +pub(crate) const BLACKLIST_PAGE_DEFAULT_LIMIT: u32 = 50; + +// paged retrieval limits for all deposit queries and transactions +pub(crate) const DEPOSITS_PAGE_MAX_LIMIT: u32 = 100; +pub(crate) const DEPOSITS_PAGE_DEFAULT_LIMIT: u32 = 50; + +pub(crate) fn create_batch_redemption_proposal( + tickets_digest: String, + gw: String, + n: u16, + ecash_bandwidth_address: String, + multisig_addr: String, +) -> StdResult { + let release_funds_req = ExecuteMsg::RedeemTickets { n, gw }; + let release_funds_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: ecash_bandwidth_address, + msg: to_binary(&release_funds_req)?, + funds: vec![], + }); + let req = MultisigExecuteMsg::Propose { + title: BATCH_REDEMPTION_PROPOSAL_TITLE.to_string(), + description: tickets_digest, + msgs: vec![release_funds_msg], + latest: None, + }; + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: multisig_addr, + msg: to_binary(&req)?, + funds: vec![], + }); + + let submsg = SubMsg::reply_always(msg, REDEMPTION_PROPOSAL_REPLY_ID); + + Ok(submsg) +} + +pub(crate) fn create_blacklist_proposal( + public_key: String, + ecash_bandwidth_address: String, + multisig_addr: String, +) -> StdResult { + let blacklist_req = ExecuteMsg::AddToBlacklist { + public_key: public_key.clone(), + }; + let blacklist_req_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: ecash_bandwidth_address, + msg: to_binary(&blacklist_req)?, + funds: vec![], + }); + let req = MultisigExecuteMsg::Propose { + title: String::from("Add to blacklist, as ordered by Ecash Bandwidth Contract"), + description: public_key, + msgs: vec![blacklist_req_msg], + latest: None, + }; + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: multisig_addr, + msg: to_binary(&req)?, + funds: vec![], + }); + + let submsg = SubMsg::reply_always(msg, BLACKLIST_PROPOSAL_REPLY_ID); + + Ok(submsg) +} + +pub(crate) trait MultisigReply { + fn multisig_proposal_id(&self) -> Result; +} + +impl MultisigReply for Reply { + fn multisig_proposal_id(&self) -> Result { + match &self.result { + SubMsgResult::Ok(res) => { + let proposal_id: u64 = + try_find_attribute(&res.events, WASM_EVENT_NAME, PROPOSAL_ID_ATTRIBUTE_NAME) + .ok_or(EcashContractError::MissingProposalId)? + .map_err(|_| EcashContractError::MalformedProposalId)?; + Ok(proposal_id) + } + SubMsgResult::Err(reply_err) => Err(StdError::generic_err(reply_err).into()), + } + } +} diff --git a/contracts/ecash/src/lib.rs b/contracts/ecash/src/lib.rs new file mode 100644 index 0000000000..e71f86f8cc --- /dev/null +++ b/contracts/ecash/src/lib.rs @@ -0,0 +1,14 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +pub mod contract; +mod deposit; +mod helpers; +#[cfg(test)] +pub mod multitest; +mod support; diff --git a/contracts/ecash/src/multitest.rs b/contracts/ecash/src/multitest.rs new file mode 100644 index 0000000000..5baf8032db --- /dev/null +++ b/contracts/ecash/src/multitest.rs @@ -0,0 +1,73 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Addr, Coin}; +use cw_utils::PaymentError; +use nym_ecash_contract_common::EcashContractError; +use sylvia::{cw_multi_test::App as MtApp, multitest::App}; + +use crate::contract::multitest_utils::CodeId; + +#[test] +fn invalid_deposit() { + let owner = "owner"; + let denom = "unym"; + + let mtapp = MtApp::new(|router, _, storage| { + router + .bank + .init_balance( + storage, + &Addr::unchecked(owner), + vec![ + Coin::new(10000000, denom), + Coin::new(10000000, "some_denom"), + ], + ) + .unwrap() + }); + let app = App::new(mtapp); + + let code_id = CodeId::store_code(&app); + + let contract = code_id + .instantiate( + "holding_acount".to_string(), + "multisig_addr".to_string(), + "group_addr".to_string(), + denom.to_string(), + ) + .call(owner) + .unwrap(); + + let verification_key = "Verification key"; + + assert_eq!( + contract + .deposit_ticket_book_funds(verification_key.to_string(),) + .call(owner) + .unwrap_err(), + EcashContractError::InvalidDeposit(PaymentError::NoFunds {}) + ); + + let coin = Coin::new(1000000, denom.to_string()); + let second_coin = Coin::new(1000000, "some_denom"); + + assert_eq!( + contract + .deposit_ticket_book_funds(verification_key.to_string(),) + .with_funds(&[coin, second_coin.clone()]) + .call(owner) + .unwrap_err(), + EcashContractError::InvalidDeposit(PaymentError::MultipleDenoms {}) + ); + + assert_eq!( + contract + .deposit_ticket_book_funds(verification_key.to_string(),) + .with_funds(&[second_coin]) + .call(owner) + .unwrap_err(), + EcashContractError::InvalidDeposit(PaymentError::MissingDenom(denom.to_string())) + ); +} diff --git a/contracts/ecash/src/support/mod.rs b/contracts/ecash/src/support/mod.rs new file mode 100644 index 0000000000..f217570509 --- /dev/null +++ b/contracts/ecash/src/support/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub mod tests; diff --git a/contracts/ecash/src/support/tests.rs b/contracts/ecash/src/support/tests.rs new file mode 100644 index 0000000000..2f44544092 --- /dev/null +++ b/contracts/ecash/src/support/tests.rs @@ -0,0 +1,10 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha20Rng; + +pub fn test_rng() -> ChaCha20Rng { + let dummy_seed = [42u8; 32]; + ChaCha20Rng::from_seed(dummy_seed) +} diff --git a/deny.toml b/deny.toml index 812b1e6e1d..317455647c 100644 --- a/deny.toml +++ b/deny.toml @@ -74,9 +74,9 @@ notice = "warn" # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ - "RUSTSEC-2020-0159", - "RUSTSEC-2020-0071", - "RUSTSEC-2021-0145", + "RUSTSEC-2020-0159", + "RUSTSEC-2020-0071", + "RUSTSEC-2021-0145", ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score # lower than the range specified will be ignored. Note that ignored advisories @@ -115,6 +115,7 @@ allow = [ "Unicode-DFS-2016", "OpenSSL", "Zlib", + "Unicode-3.0", ] # List of explicitly disallowed licenses # See https://spdx.org/licenses/ for list of possible licenses @@ -217,7 +218,7 @@ deny = [ # Wrapper crates can optionally be specified to allow the crate when it # is a direct dependency of the otherwise banned crate #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, - { name = "openssl"}, + { name = "openssl" }, ] # List of features to allow/deny diff --git a/envs/local.env b/envs/local.env index 11338f12b4..09dd5ef227 100644 --- a/envs/local.env +++ b/envs/local.env @@ -13,7 +13,7 @@ STAKE_DENOM_DISPLAY=nyx DENOMS_EXPONENT=6 MIXNET_CONTRACT_ADDRESS=n1xsml6tm6pnx8nsuz3a54cznh55g6nuwu9ug9t92ucjkq5ewp5w9syxeqsv VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1lp5zex6685kd22agzskhqsylpnssxnweyuvsz4edr4p4ta92qf3q3dhmsx +ECASH_CONTRACT_ADDRESS=n1lp5zex6685kd22agzskhqsylpnssxnweyuvsz4edr4p4ta92qf3q3dhmsx GROUP_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m MULTISIG_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m COCONUT_DKG_CONTRACT_ADDRESS=n1vwtgazgpancsfel04y7syc95ausmat47cjtldqzkdmx6phyrwx2qvkv32p diff --git a/envs/qa.env b/envs/qa.env index f889c60b81..781d4ef19f 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -11,7 +11,7 @@ STAKE_DENOM_DISPLAY=nyx DENOMS_EXPONENT=6 MIXNET_CONTRACT_ADDRESS=n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296 -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n14y2x8a60knc5jjfeztt84kw8x8l5pwdgnqg256v0p9v4p7t2q6eswxyusw +ECASH_CONTRACT_ADDRESS=n14y2x8a60knc5jjfeztt84kw8x8l5pwdgnqg256v0p9v4p7t2q6eswxyusw GROUP_CONTRACT_ADDRESS=n1qp35fcj0v9u3trhaps5v9q0lc42t4m6aty2wryss75ee8zuqnsqqdcreyq MULTISIG_CONTRACT_ADDRESS=n1qa4hswlcjmttulj0q9qa46jf64f93pecl6tydcsjldfe0hy5ju0sdmwzya COCONUT_DKG_CONTRACT_ADDRESS=n1ayrk6wp6w5lf6njtnfjwljmtcc9vevv5sxwkz7uq24rp2pw67t0qhmmxdd diff --git a/envs/sandbox.env b/envs/sandbox.env index 90dc8d7e7c..6deaafdfaa 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -13,7 +13,7 @@ DENOMS_EXPONENT=6 REWARDING_VALIDATOR_ADDRESS=n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ljlwey4xdj0zs7zueepc48nkr033fca6fjgvurfvttqegm8dvsrswsul70 +ECASH_CONTRACT_ADDRESS=n1ljlwey4xdj0zs7zueepc48nkr033fca6fjgvurfvttqegm8dvsrswsul70 GROUP_CONTRACT_ADDRESS=n10v3rjnq4cjyccfykyams68ztce337gksuu6f0lvtl4meuwvkewaqru4uav MULTISIG_CONTRACT_ADDRESS=n1cemnu8as0ls45v3caunpesl8jlsfw2ff9rlwnltlecp7zrxct4dsqc2y42 COCONUT_DKG_CONTRACT_ADDRESS=n1zx96qgd88vqlzcxkpwzks7kqs5ctrx36xtzfc58p7q6c4ng9anlqzc4nh8 diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index cc2c6c37d1..330f0a9824 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -11,28 +11,38 @@ authors = [ ] description = "Implementation of the Nym Mixnet Gateway" edition = "2021" -rust-version = "1.70" +rust-version = "1.76" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +path = "src/lib.rs" + +[[bin]] +name = "nym-gateway" +required-features = ["bin-deps"] +path = "src/main.rs" + [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } bip39 = { workspace = true } bs58 = { workspace = true } -clap = { workspace = true, features = ["cargo", "derive"] } +clap = { workspace = true, features = ["cargo", "derive"], optional = true } colored = { workspace = true } +cosmwasm-std = { workspace = true } +cw-utils = { workspace = true } dashmap = { workspace = true } dirs = { workspace = true } dotenvy = { workspace = true } futures = { workspace = true } humantime-serde = { workspace = true } ipnetwork = { workspace = true } -log = { workspace = true } once_cell = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +si-scale = { workspace = true } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", "sqlite", @@ -52,19 +62,23 @@ tokio = { workspace = true, features = [ tokio-stream = { workspace = true, features = ["fs"] } tokio-tungstenite = { workspace = true } tokio-util = { workspace = true, features = ["codec"] } +tracing = { workspace = true } url = { workspace = true, features = ["serde"] } time = { workspace = true } zeroize = { workspace = true } +bloomfilter = { workspace = true } # internal nym-authenticator = { path = "../service-providers/authenticator" } nym-api-requests = { path = "../nym-api/nym-api-requests" } -nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } +nym-bin-common = { path = "../common/bin-common" } nym-config = { path = "../common/config" } nym-credentials = { path = "../common/credentials" } nym-credentials-interface = { path = "../common/credentials-interface" } nym-crypto = { path = "../common/crypto" } +nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" } +nym-ecash-double-spending = { path = "../common/ecash-double-spending" } nym-gateway-requests = { path = "gateway-requests" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } nym-mixnode-common = { path = "../common/mixnode-common" } @@ -95,6 +109,7 @@ sqlx = { workspace = true, features = [ [features] wireguard = ["nym-wireguard", "defguard_wireguard_rs"] +bin-deps = ["clap", 'nym-bin-common/output_format'] [package.metadata.deb] name = "nym-gateway" diff --git a/gateway/gateway-requests/Cargo.toml b/gateway/gateway-requests/Cargo.toml index 9802a6c590..365edc2a5c 100644 --- a/gateway/gateway-requests/Cargo.toml +++ b/gateway/gateway-requests/Cargo.toml @@ -14,14 +14,14 @@ license.workspace = true bs58 = { workspace = true } futures = { workspace = true } generic-array = { workspace = true, features = ["serde"] } -log = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true, features = ["log"] } zeroize = { workspace = true } -nym-crypto = { path = "../../common/crypto" } +nym-crypto = { path = "../../common/crypto" } nym-pemstore = { path = "../../common/pemstore" } nym-sphinx = { path = "../../common/nymsphinx" } @@ -40,4 +40,7 @@ features = ["tokio"] workspace = true default-features = false +[dev-dependencies] +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } # we need specific imports in tests + diff --git a/gateway/gateway-requests/src/models.rs b/gateway/gateway-requests/src/models.rs index 339715bbf7..d4e651c3c2 100644 --- a/gateway/gateway-requests/src/models.rs +++ b/gateway/gateway-requests/src/models.rs @@ -1,329 +1,132 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::GatewayRequestsError; -use nym_credentials::coconut::bandwidth::CredentialSpendingData; -use nym_credentials_interface::{CoconutError, VerifyCredentialRequest}; +use nym_credentials::ecash::bandwidth::CredentialSpendingData; +use nym_credentials_interface::{Base58, Bytable, CompactEcashError}; use serde::{Deserialize, Serialize}; -// reimplements old coconut-interface::Credential for backwards compatibility sake -// (so that 'new' gateways could still understand those requests) -#[derive(Debug, PartialEq, Eq)] -pub struct OldV1Credential { - pub n_params: u32, - - pub theta: VerifyCredentialRequest, - - pub voucher_value: u64, - - pub voucher_info: String, - - pub epoch_id: u64, -} - -// attempt to convert the old request type into the new variant -impl TryFrom for CredentialSpendingRequest { - type Error = GatewayRequestsError; - - fn try_from(value: OldV1Credential) -> Result { - if value.n_params <= 2 { - return Err(GatewayRequestsError::InvalidNumberOfEmbededParameters( - value.n_params, - )); - } - let embedded_private_attributes = value.n_params as usize - 2; - let typ = value.voucher_info.parse()?; - let public_attributes_plain = vec![value.voucher_value.to_string(), value.voucher_info]; - - Ok(CredentialSpendingRequest { - data: CredentialSpendingData { - embedded_private_attributes, - verify_credential_request: value.theta, - public_attributes_plain, - typ, - epoch_id: value.epoch_id, - }, - }) - } -} - -impl OldV1Credential { - pub fn as_bytes(&self) -> Vec { - let n_params_bytes = self.n_params.to_be_bytes(); - let theta_bytes = self.theta.to_bytes(); - let theta_bytes_len = theta_bytes.len(); - let voucher_value_bytes = self.voucher_value.to_be_bytes(); - let epoch_id_bytes = self.epoch_id.to_be_bytes(); - let voucher_info_bytes = self.voucher_info.as_bytes(); - let voucher_info_len = voucher_info_bytes.len(); - - let mut bytes = Vec::with_capacity(28 + theta_bytes_len + voucher_info_len); - bytes.extend_from_slice(&n_params_bytes); - bytes.extend_from_slice(&(theta_bytes_len as u64).to_be_bytes()); - bytes.extend_from_slice(&theta_bytes); - bytes.extend_from_slice(&voucher_value_bytes); - bytes.extend_from_slice(&epoch_id_bytes); - bytes.extend_from_slice(voucher_info_bytes); - - bytes - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() < 28 { - return Err(CoconutError::Deserialization(String::from( - "To few bytes in credential", - ))); - } - let mut four_byte = [0u8; 4]; - let mut eight_byte = [0u8; 8]; - - four_byte.copy_from_slice(&bytes[..4]); - let n_params = u32::from_be_bytes(four_byte); - eight_byte.copy_from_slice(&bytes[4..12]); - let theta_len = u64::from_be_bytes(eight_byte); - if bytes.len() < 28 + theta_len as usize { - return Err(CoconutError::Deserialization(String::from( - "To few bytes in credential", - ))); - } - let theta = VerifyCredentialRequest::from_bytes(&bytes[12..12 + theta_len as usize]) - .map_err(|e| CoconutError::Deserialization(e.to_string()))?; - eight_byte.copy_from_slice(&bytes[12 + theta_len as usize..20 + theta_len as usize]); - let voucher_value = u64::from_be_bytes(eight_byte); - eight_byte.copy_from_slice(&bytes[20 + theta_len as usize..28 + theta_len as usize]); - let epoch_id = u64::from_be_bytes(eight_byte); - let voucher_info = String::from_utf8(bytes[28 + theta_len as usize..].to_vec()) - .map_err(|e| CoconutError::Deserialization(e.to_string()))?; - - Ok(OldV1Credential { - n_params, - theta, - voucher_value, - voucher_info, - epoch_id, - }) - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct CredentialSpendingRequest { /// The cryptographic material required for spending the underlying credential. pub data: CredentialSpendingData, } -// just a helper macro for checking required length and advancing the buffer -macro_rules! ensure_len_and_advance { - ($b:expr, $n:expr) => {{ - if $b.len() < $n { - return Err(GatewayRequestsError::CredentialDeserializationFailureEOF); - } - // create binding to the desired range - let bytes = &$b[..$n]; - - // update the initial binding - - $b = &$b[$n..]; - - bytes - }}; -} - impl CredentialSpendingRequest { pub fn new(data: CredentialSpendingData) -> Self { CredentialSpendingRequest { data } } - pub fn matches_blinded_serial_number( - &self, - blinded_serial_number_bs58: &str, - ) -> Result { - self.data - .verify_credential_request - .has_blinded_serial_number(blinded_serial_number_bs58) - } - - pub fn unchecked_voucher_value(&self) -> u64 { - self.data - .get_bandwidth_attribute() - .expect("failed to extract bandwidth attribute") - .parse() - .expect("failed to parse voucher value") - } + // pub fn matches_serial_number( + // &self, + // serial_number_bs58: &str, + // ) -> Result { + // self.data.payment.has_serial_number(serial_number_bs58) + // } pub fn to_bytes(&self) -> Vec { - // simple length prefixed serialization - // TODO: change it to a standard format instead - let mut bytes = Vec::new(); - - let embedded_private = (self.data.embedded_private_attributes as u32).to_be_bytes(); - let theta = self.data.verify_credential_request.to_bytes(); - let theta_len = (theta.len() as u32).to_be_bytes(); - - let public = (self.data.public_attributes_plain.len() as u32).to_be_bytes(); - let typ = self.data.typ.to_string(); - let typ_bytes = typ.as_bytes(); - let typ_len = (typ_bytes.len() as u32).to_be_bytes(); - - bytes.extend_from_slice(&embedded_private); - bytes.extend_from_slice(&theta_len); - bytes.extend_from_slice(&theta); - bytes.extend_from_slice(&public); - - for pub_element in &self.data.public_attributes_plain { - let bytes_el = pub_element.as_bytes(); - let len = (bytes_el.len() as u32).to_be_bytes(); - - bytes.extend_from_slice(&len); - bytes.extend_from_slice(bytes_el); - } - - bytes.extend_from_slice(&typ_len); - bytes.extend_from_slice(typ_bytes); - bytes.extend_from_slice(&self.data.epoch_id.to_be_bytes()); - - bytes + self.data.to_bytes() } - pub fn try_from_bytes(raw: &[u8]) -> Result { - // initial binding - let mut b = raw; - let embedded_private_bytes = ensure_len_and_advance!(b, 4); - let embedded_private_attributes = - u32::from_be_bytes(embedded_private_bytes.try_into().unwrap()) as usize; - - let theta_len_bytes = ensure_len_and_advance!(b, 4); - let theta_len = u32::from_be_bytes(theta_len_bytes.try_into().unwrap()) as usize; - - let theta_bytes = ensure_len_and_advance!(b, theta_len); - let theta = VerifyCredentialRequest::from_bytes(theta_bytes) - .map_err(GatewayRequestsError::CredentialDeserializationFailureMalformedTheta)?; - - let public_bytes = ensure_len_and_advance!(b, 4); - let public = u32::from_be_bytes(public_bytes.try_into().unwrap()) as usize; - - let mut public_attributes_plain = Vec::with_capacity(public); - for _ in 0..public { - let element_len_bytes = ensure_len_and_advance!(b, 4); - let element_len = u32::from_be_bytes(element_len_bytes.try_into().unwrap()) as usize; - - let element_bytes = ensure_len_and_advance!(b, element_len); - let element = String::from_utf8(element_bytes.to_vec())?; - public_attributes_plain.push(element); - } - - let typ_len_bytes = ensure_len_and_advance!(b, 4); - let typ_len = u32::from_be_bytes(typ_len_bytes.try_into().unwrap()) as usize; - - let typ_bytes = ensure_len_and_advance!(b, typ_len); - let raw_typ = String::from_utf8(typ_bytes.to_vec())?; - let typ = raw_typ.parse()?; - - // tell the linter to chill out in for this last iteration - #[allow(unused_assignments)] - let epoch_id_bytes = ensure_len_and_advance!(b, 8); - let epoch_id = u64::from_be_bytes(epoch_id_bytes.try_into().unwrap()); - + pub fn try_from_bytes(raw: &[u8]) -> Result { Ok(CredentialSpendingRequest { - data: CredentialSpendingData { - embedded_private_attributes, - verify_credential_request: theta, - public_attributes_plain, - typ, - epoch_id, - }, + data: CredentialSpendingData::try_from_bytes(raw)?, }) } + + pub fn encoded_serial_number(&self) -> Vec { + self.data.encoded_serial_number() + } + + pub fn serial_number_bs58(&self) -> String { + self.data.serial_number_b58() + } } +impl Bytable for CredentialSpendingRequest { + fn to_byte_vec(&self) -> Vec { + self.to_bytes() + } + + fn try_from_byte_slice(slice: &[u8]) -> Result { + Self::try_from_bytes(slice) + } +} + +impl Base58 for CredentialSpendingRequest {} + #[cfg(test)] mod tests { use super::*; - use nym_credentials::coconut::bandwidth::bandwidth_credential_params; - use nym_credentials::IssuanceBandwidthCredential; - use nym_credentials_interface::{ - blind_sign, hash_to_scalar, prove_bandwidth_credential, Attribute, Base58, Parameters, - Signature, VerificationKey, + use nym_compact_ecash::{ + issue, + tests::helpers::{generate_coin_indices_signatures, generate_expiration_date_signatures}, + ttp_keygen, PayInfo, }; - - #[test] - fn old_v1_coconut_credential_roundtrip() { - let voucher_value = 1000000u64; - let voucher_info = String::from("BandwidthVoucher"); - let serial_number = - Attribute::try_from_bs58("7Rp3imcuNX3w9se9wm5th8gSvc2czsnMrGsdt5HsrycA").unwrap(); - let binding_number = - Attribute::try_from_bs58("Auf8yVEgyEAWNHaXUZmimS4n9g5YiYnNYqp6F9BtBe9E").unwrap(); - let signature = Signature::try_from_bs58( - "ta3pM9ffj5T6YGbwjSBp2W118rcwyP9PXStc\ - 7ssb91g5GQYMQHhuTNajbdZcjxUFBFL5rhED8EHpRzE8r432ss3qbPBfpNev4CdkfMkQ3wepyM7hy7q1W6Rn9WmFoZL\ - ZR9j", - ) - .unwrap(); - let params = Parameters::new(4).unwrap(); - let verification_key = VerificationKey::try_from_bs58("8CFtVVXdwLy4WHMQPE4\ - woe89q3DRHoNxBSchftrEjSBPWA4r4xZv4Y9qSvS5x5bMmFtp7BX6ikECAnuXr5EjXWSsgjirZJmpS5XDUynVfht1cD\ - FWGDvy2XFrRCuoCMotNXi3PoF6wYqdTR9Rqcfoj3i2H5Nid422WBaLtVoC9QNobvpvaqq6vX5PbsSyPayvU8HCXFxM6\ - JjScYpbRTxQtdwefWLrk3LmXyJQBWi7c2VAhSxu9msp7VTBycqdwQNgxHETStZuwXsozxaGQ2KssVUCaaoYPR4g2RqK\ - UAvtWwA7pMiAQNcbkXcbsjCgVjWaCpMWC37XA31cLcFf3zbjHD9e5tXjAcqa4M89fbFhuvvSXxowSAZ5NoWrN32kd5d\ - wxJm1JW3Tt2h6yDDBe84oMy71462dZn7N78DVk2mFNGwBCibrZWA7oUzRBMfYxiQrksoFcou7QfLLd58zoNYmPQPt84\ - 1VpQopEBfdQ7Nf9zoXxBt3zMy7g5NsFGvzh7KTbDUyeeXrdkKJPQBs6dqaizr9sS8CPPmR4uk96vDTRh8CJ5FbSsmb8\ - nP71dRvvwRZJHGzwYirMo6SXS3ZYxFuiA3mkxYuqDHCwkTWDuRCcAaztrDYRZg7VCMo4Q446AaEso5eqpeWpHZQt53E\ - ZRpqmNYKASGwMhTeEHPSLgSmtoAAUcaRWpGRzYfd6kzEma8tdGLwyP4rLXgvSvtDLP37dU7YgF3LEXbGAz57U9ATy46\ - 6sroLpHPdaCWB8RF11wvB6Tu196JnJd2KyQBP1iUWP3rtZs3GhAF1QVcxquh8BqDZzAcpQ6wCS1P9c5GxKgww77FVF5\ - Kp83XtoxSrw3GaYVyKTGxNh3vcKPR31txCjTxPaN2fg7TaPLhoQJX4YaAroFSXqrqbbRsisuHhhCeUP2YwDjHedes9y") - .unwrap(); - let theta = prove_bandwidth_credential( - ¶ms, - &verification_key, - &signature, - &serial_number, - &binding_number, - ) - .unwrap(); - - let credential = OldV1Credential { - n_params: 4, - theta, - voucher_value, - voucher_info, - epoch_id: 42, - }; - - let serialized_credential = credential.as_bytes(); - let deserialized_credential = OldV1Credential::from_bytes(&serialized_credential).unwrap(); - - assert_eq!(credential, deserialized_credential); - } + use nym_credentials::ecash::utils::EcashTime; + use nym_credentials::IssuanceTicketBook; + use nym_crypto::asymmetric::ed25519; + use rand::rngs::OsRng; #[test] fn credential_roundtrip() { // make valid request - let params = bandwidth_credential_params(); - let keypair = nym_credentials_interface::keygen(params); + let keypair = ttp_keygen(1, 1).unwrap().remove(0); - let issuance = IssuanceBandwidthCredential::new_freepass(None); + let mut rng = OsRng; + let signing_key = ed25519::PrivateKey::new(&mut rng); + + let issuance = IssuanceTicketBook::new(42, [], signing_key); + let expiration_date = issuance.expiration_date(); let sig_req = issuance.prepare_for_signing(); - let pub_attrs_hashed = sig_req - .public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect::>(); - let pub_attrs = pub_attrs_hashed.iter().collect::>(); - let blind_sig = blind_sign( - params, - keypair.secret_key(), - &sig_req.blind_sign_request, - &pub_attrs, + let exp_date_sigs = generate_expiration_date_signatures( + sig_req.expiration_date.ecash_unix_timestamp(), + &[keypair.secret_key()], + &vec![keypair.verification_key()], + &keypair.verification_key(), + &[keypair.index.unwrap()], + ) + .unwrap(); + let blind_sig = issue( + keypair.secret_key(), + sig_req.ecash_pub_key.clone(), + &sig_req.withdrawal_request, + expiration_date.ecash_unix_timestamp(), ) .unwrap(); - let sig = blind_sig.unblind( - keypair.verification_key(), - &sig_req.pedersen_commitments_openings, - ); - let issued = issuance.into_issued_credential(sig, 42); + let partial_wallet = issuance + .unblind_signature( + &keypair.verification_key(), + &sig_req, + blind_sig, + keypair.index.unwrap(), + ) + .unwrap(); + + let wallet = issuance + .aggregate_signature_shares(&keypair.verification_key(), &vec![partial_wallet], sig_req) + .unwrap(); + + let mut issued = issuance.into_issued_ticketbook(wallet, 1); + let coin_indices_signatures = generate_coin_indices_signatures( + nym_credentials_interface::ecash_parameters(), + &[keypair.secret_key()], + &vec![keypair.verification_key()], + &keypair.verification_key(), + &[keypair.index.unwrap()], + ) + .unwrap(); + let pay_info = PayInfo { + pay_info_bytes: [6u8; 72], + }; let spending = issued - .prepare_for_spending(keypair.verification_key()) + .prepare_for_spending( + &keypair.verification_key(), + pay_info, + &coin_indices_signatures, + &exp_date_sigs, + 1, + ) .unwrap(); let with_epoch = CredentialSpendingRequest { data: spending }; diff --git a/gateway/gateway-requests/src/registration/handshake/state.rs b/gateway/gateway-requests/src/registration/handshake/state.rs index 6212a919b5..328b5d59cb 100644 --- a/gateway/gateway-requests/src/registration/handshake/state.rs +++ b/gateway/gateway-requests/src/registration/handshake/state.rs @@ -6,7 +6,6 @@ use crate::registration::handshake::shared_key::{SharedKeySize, SharedKeys}; use crate::registration::handshake::WsItem; use crate::types; use futures::{Sink, SinkExt, Stream, StreamExt}; -use log::*; use nym_crypto::{ asymmetric::{encryption, identity}, generic_array::typenum::Unsigned, @@ -15,6 +14,7 @@ use nym_crypto::{ }; use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewaySharedKeyHkdfAlgorithm}; use rand::{CryptoRng, RngCore}; +use tracing::log::*; use std::str::FromStr; use std::time::Duration; diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index 6c564d4ae8..6e48d0441e 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::authentication::encrypted_address::EncryptedAddressBytes; -use crate::iv::IV; -use crate::models::{CredentialSpendingRequest, OldV1Credential}; +use crate::iv::{IVConversionError, IV}; +use crate::models::CredentialSpendingRequest; use crate::registration::handshake::SharedKeys; use crate::{GatewayMacSize, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION}; -use log::error; -use nym_credentials::coconut::bandwidth::CredentialSpendingData; -use nym_credentials_interface::{CoconutError, UnknownCredentialType}; +use nym_credentials::ecash::bandwidth::CredentialSpendingData; +use nym_credentials_interface::CompactEcashError; use nym_crypto::generic_array::typenum::Unsigned; use nym_crypto::hmac::recompute_keyed_hmac_and_verify_tag; use nym_crypto::symmetric::stream_cipher; @@ -18,6 +17,7 @@ use nym_sphinx::params::packet_sizes::PacketSize; use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewayIntegrityHmacAlgorithm}; use nym_sphinx::DestinationAddressBytes; use serde::{Deserialize, Serialize}; +use tracing::log::error; use std::str::FromStr; use std::string::FromUtf8Error; @@ -92,6 +92,9 @@ pub enum GatewayRequestsError { #[error("provided MAC is invalid")] InvalidMac, + #[error("Provided bandwidth IV is malformed: {0}")] + MalformedIV(#[from] IVConversionError), + #[error("address field was incorrectly encoded: {source}")] IncorrectlyEncodedAddress { #[from] @@ -122,18 +125,15 @@ pub enum GatewayRequestsError { source: MixPacketFormattingError, }, + #[error("failed to deserialize provided credential: {0}")] + EcashCredentialDeserializationFailure(#[from] CompactEcashError), + #[error("failed to deserialize provided credential: EOF")] CredentialDeserializationFailureEOF, #[error("failed to deserialize provided credential: malformed string: {0}")] CredentialDeserializationFailureMalformedString(#[from] FromUtf8Error), - #[error("failed to deserialize provided credential: {0}")] - CredentialDeserializationFailureUnknownType(#[from] UnknownCredentialType), - - #[error("failed to deserialize provided credential: malformed verify request: {0}")] - CredentialDeserializationFailureMalformedTheta(CoconutError), - #[error("the provided [v1] credential has invalid number of parameters - {0}")] InvalidNumberOfEmbededParameters(u32), } @@ -164,6 +164,10 @@ pub enum ClientControlRequest { enc_credential: Vec, iv: Vec, }, + EcashCredential { + enc_credential: Vec, + iv: Vec, + }, ClaimFreeTestnetBandwidth, } @@ -200,37 +204,14 @@ impl ClientControlRequest { ClientControlRequest::BandwidthCredentialV2 { .. } => { "BandwidthCredentialV2".to_string() } + ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(), ClientControlRequest::ClaimFreeTestnetBandwidth => { "ClaimFreeTestnetBandwidth".to_string() } } } - pub fn new_enc_coconut_bandwidth_credential_v1( - credential: &OldV1Credential, - shared_key: &SharedKeys, - iv: IV, - ) -> Self { - let serialized_credential = credential.as_bytes(); - let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner())); - - ClientControlRequest::BandwidthCredential { - enc_credential, - iv: iv.to_bytes(), - } - } - - pub fn try_from_enc_coconut_bandwidth_credential_v1( - enc_credential: Vec, - shared_key: &SharedKeys, - iv: IV, - ) -> Result { - let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?; - OldV1Credential::from_bytes(&credential_bytes) - .map_err(|_| GatewayRequestsError::MalformedEncryption) - } - - pub fn new_enc_coconut_bandwidth_credential_v2( + pub fn new_enc_ecash_credential( credential: CredentialSpendingData, shared_key: &SharedKeys, iv: IV, @@ -239,19 +220,20 @@ impl ClientControlRequest { let serialized_credential = cred.to_bytes(); let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner())); - ClientControlRequest::BandwidthCredentialV2 { + ClientControlRequest::EcashCredential { enc_credential, iv: iv.to_bytes(), } } - pub fn try_from_enc_coconut_bandwidth_credential_v2( + pub fn try_from_enc_ecash_credential( enc_credential: Vec, shared_key: &SharedKeys, - iv: IV, + iv: Vec, ) -> Result { + let iv = IV::try_from_bytes(&iv)?; let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?; - CredentialSpendingRequest::try_from_bytes(&credential_bytes) + CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice()) .map_err(|_| GatewayRequestsError::MalformedEncryption) } } diff --git a/gateway/migrations/20240624120000_ecash_changes.sql b/gateway/migrations/20240624120000_ecash_changes.sql new file mode 100644 index 0000000000..502de5c568 --- /dev/null +++ b/gateway/migrations/20240624120000_ecash_changes.sql @@ -0,0 +1,93 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +ALTER TABLE available_bandwidth +RENAME COLUMN freepass_expiration TO expiration; + +DROP TABLE spent_credential; + +-- we need the id field to prevent data duplication +CREATE TABLE shared_keys_tmp ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + client_address_bs58 TEXT NOT NULL UNIQUE, + derived_aes128_ctr_blake3_hmac_keys_bs58 TEXT NOT NULL +); + +INSERT INTO shared_keys_tmp (client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58) +SELECT * FROM shared_keys; + +-- ideally this table would be called "clients" but I don't want to cause too many breaking changes +DROP TABLE shared_keys; +ALTER TABLE shared_keys_tmp RENAME TO shared_keys; + +CREATE TABLE available_bandwidth_tmp ( + client_id INTEGER NOT NULL PRIMARY KEY REFERENCES shared_keys(id), + available INTEGER NOT NULL, + expiration TIMESTAMP WITHOUT TIME ZONE +); + +INSERT INTO available_bandwidth_tmp (client_id, available, expiration) +SELECT t1.id as client_id, t2.available, t2.expiration + FROM shared_keys as t1 + JOIN available_bandwidth as t2 + ON t1.client_address_bs58 = t2.client_address_bs58; + +DROP TABLE available_bandwidth; +ALTER TABLE available_bandwidth_tmp RENAME TO available_bandwidth; + +CREATE TABLE ecash_signer ( + epoch_id INTEGER NOT NULL, + +-- unique id assigned by the DKG contract. it does not change between epochs + signer_id INTEGER NOT NULL +); + +CREATE TABLE received_ticket ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + client_id INTEGER NOT NULL REFERENCES shared_keys(id), + received_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + rejected BOOLEAN +); + +CREATE INDEX received_ticket_index ON received_ticket (received_at); + +-- received tickets that are in the process of verifying +CREATE TABLE ticket_data ( + ticket_id INTEGER NOT NULL PRIMARY KEY REFERENCES received_ticket(id), + + -- serial_number, alongside the entire row, will get purged after redemption is complete + serial_number BLOB NOT NULL UNIQUE, + + -- data will get purged after 80% of signers verifies it + data BLOB +); + + +-- result of a verification from a single signer (API) +CREATE TABLE ticket_verification ( + ticket_id INTEGER NOT NULL REFERENCES received_ticket(id), + signer_id INTEGER NOT NULL, + verified_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + accepted BOOLEAN NOT NULL, + + PRIMARY KEY (ticket_id, signer_id) +); + +CREATE INDEX ticket_verification_index ON ticket_verification (ticket_id); + +-- verified tickets that are yet to be redeemed +CREATE TABLE verified_tickets ( + ticket_id INTEGER NOT NULL PRIMARY KEY REFERENCES received_ticket(id), + proposal_id INTEGER REFERENCES redemption_proposals(proposal_id) +); + +CREATE INDEX verified_tickets_index ON verified_tickets (proposal_id); + +CREATE TABLE redemption_proposals ( + proposal_id INTEGER NOT NULL PRIMARY KEY, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + resolved_at TIMESTAMP WITHOUT TIME ZONE, -- either got executed or got rejected + rejected BOOLEAN +); \ No newline at end of file diff --git a/gateway/src/commands/helpers.rs b/gateway/src/commands/helpers.rs index c8551cc445..9711ab1583 100644 --- a/gateway/src/commands/helpers.rs +++ b/gateway/src/commands/helpers.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::commands::upgrade_helpers; use log::{error, info}; use nym_config::{save_formatted_config_to_file, OptionalSet}; use nym_crypto::asymmetric::identity; @@ -17,6 +18,7 @@ use nym_gateway::helpers::{ use nym_network_defaults::mainnet; use nym_network_defaults::var_names::NYXD; use nym_network_defaults::var_names::{BECH32_PREFIX, NYM_API}; +use tracing::{error, info}; use nym_network_requester::{ generate_new_client_keys, set_active_gateway, setup_fs_gateways_storage, setup_gateway, diff --git a/gateway/src/commands/mod.rs b/gateway/src/commands/mod.rs index d2100efab6..1a08e249a0 100644 --- a/gateway/src/commands/mod.rs +++ b/gateway/src/commands/mod.rs @@ -5,10 +5,10 @@ use crate::Cli; use anyhow::bail; use clap::CommandFactory; use clap::Subcommand; -use log::{error, warn}; use nym_bin_common::completions::{fig_generate, ArgShell}; use std::io::IsTerminal; use std::time::Duration; +use tracing::{error, warn}; pub(crate) mod build_info; pub(crate) mod helpers; diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs index ceb30fcb4c..e9b4fb8d1b 100644 --- a/gateway/src/commands/run.rs +++ b/gateway/src/commands/run.rs @@ -4,13 +4,13 @@ use crate::commands::helpers::{try_load_current_config, try_override_config, OverrideConfig}; use anyhow::bail; use clap::Args; -use log::warn; use nym_bin_common::output_format::OutputFormat; use nym_config::helpers::SPECIAL_ADDRESSES; use nym_gateway::helpers::{OverrideIpPacketRouterConfig, OverrideNetworkRequesterConfig}; use nym_gateway::GatewayError; use std::net::IpAddr; use std::path::PathBuf; +use tracing::warn; #[derive(Args, Clone)] pub struct Run { diff --git a/gateway/src/commands/setup_network_requester.rs b/gateway/src/commands/setup_network_requester.rs index c489983424..3611392a56 100644 --- a/gateway/src/commands/setup_network_requester.rs +++ b/gateway/src/commands/setup_network_requester.rs @@ -3,12 +3,12 @@ use crate::commands::helpers::{initialise_local_network_requester, try_load_current_config}; use clap::Args; -use log::warn; use nym_bin_common::output_format::OutputFormat; use nym_gateway::helpers::{load_public_key, OverrideNetworkRequesterConfig}; use std::io::IsTerminal; use std::path::PathBuf; use std::time::Duration; +use tracing::warn; #[derive(Args, Clone)] pub struct CmdArgs { diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index 91d1e3b92e..0750b3bf0d 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::template::CONFIG_TEMPLATE; -use log::{debug, warn}; use nym_bin_common::logging::LoggingSettings; use nym_config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; use nym_config::helpers::inaddr_any; @@ -17,6 +16,7 @@ use std::io; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::time::Duration; +use tracing::{debug, warn}; use url::Url; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -67,7 +67,7 @@ pub fn default_data_directory>(id: P) -> PathBuf { .join(DEFAULT_DATA_DIR) } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { // additional metadata holding on-disk location of this config file @@ -220,7 +220,6 @@ impl Config { self.gateway.only_coconut_credentials = only_coconut_credentials; self } - pub fn with_custom_nym_apis(mut self, nym_api_urls: Vec) -> Self { self.gateway.nym_api_urls = nym_api_urls; self @@ -415,7 +414,7 @@ impl Default for IpPacketRouter { } } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(default)] pub struct Debug { /// Initial value of an exponential backoff to reconnect to dropped TCP connection when @@ -459,6 +458,9 @@ pub struct Debug { // existing nodes whilst everyone else is upgrading and getting the code for handling the new field. // It shall be disabled in the subsequent releases. pub use_legacy_framed_packet_version: bool, + + #[serde(default)] + pub zk_nym_tickets: ZkNymTicketHandlerDebug, } impl Default for Debug { @@ -475,6 +477,51 @@ impl Default for Debug { client_bandwidth_max_delta_flushing_amount: DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT, use_legacy_framed_packet_version: false, + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ZkNymTicketHandlerDebug { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + #[serde(with = "humantime_serde")] + pub pending_poller: Duration, + + pub minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than 30 days. + #[serde(with = "humantime_serde")] + pub maximum_time_between_redemption: Duration, +} + +impl ZkNymTicketHandlerDebug { + pub const DEFAULT_REVOCATION_BANDWIDTH_PENALTY: f32 = 10.0; + pub const DEFAULT_PENDING_POLLER: Duration = Duration::from_secs(300); + pub const DEFAULT_MINIMUM_API_QUORUM: f32 = 0.8; + pub const DEFAULT_MINIMUM_REDEMPTION_TICKETS: usize = 100; + pub const DEFAULT_MAXIMUM_TIME_BETWEEN_REDEMPTION: Duration = Duration::from_secs(86400 * 25); +} + +impl Default for ZkNymTicketHandlerDebug { + fn default() -> Self { + ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: Self::DEFAULT_REVOCATION_BANDWIDTH_PENALTY, + pending_poller: Self::DEFAULT_PENDING_POLLER, + minimum_api_quorum: Self::DEFAULT_MINIMUM_API_QUORUM, + minimum_redemption_tickets: Self::DEFAULT_MINIMUM_REDEMPTION_TICKETS, + maximum_time_between_redemption: Self::DEFAULT_MAXIMUM_TIME_BETWEEN_REDEMPTION, } } } diff --git a/gateway/src/error.rs b/gateway/src/error.rs index edf34b3a95..b666a1e4b5 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -14,6 +14,7 @@ use std::path::PathBuf; use thiserror::Error; pub use crate::node::client_handling::websocket::connection_handler::authenticated::RequestHandlingError; +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; #[derive(Debug, Error)] pub enum GatewayError { @@ -169,6 +170,12 @@ pub enum GatewayError { source: RequestHandlingError, }, + #[error("ecash related failure: {source}")] + EcashFailure { + #[from] + source: EcashTicketError, + }, + #[error("failed to catch an interrupt: {source}")] ShutdownFailure { source: Box, @@ -189,6 +196,9 @@ pub enum GatewayError { source: ipnetwork::IpNetworkError, }, + #[error("the current multisig contract is not using 'AbsolutePercentage' threshold!")] + InvalidMultisigThreshold, + #[cfg(all(feature = "wireguard", target_os = "linux"))] #[error("failed to remove wireguard interface: {0}")] WireguardInterfaceError(#[from] defguard_wireguard_rs::error::WireguardInterfaceError), diff --git a/gateway/src/helpers.rs b/gateway/src/helpers.rs index 7a00000a2f..7b17da7170 100644 --- a/gateway/src/helpers.rs +++ b/gateway/src/helpers.rs @@ -13,6 +13,7 @@ use nym_types::gateway::{ GatewayIpPacketRouterDetails, GatewayNetworkRequesterDetails, GatewayNodeDetailsResponse, }; use std::path::Path; +use tracing::info; pub use crate::node::helpers::{load_ip_packet_router_config, load_network_requester_config}; @@ -72,7 +73,7 @@ pub fn override_ip_packet_router_config( // disable poisson rate in the BASE client if the IPR option is enabled if cfg.ip_packet_router.disable_poisson_rate { - log::info!("Disabling poisson rate for ip packet router"); + info!("Disabling poisson rate for ip packet router"); cfg.set_no_poisson_process(); } diff --git a/gateway/src/http/mod.rs b/gateway/src/http/mod.rs index 23bc8d8693..1a7c8f29b5 100644 --- a/gateway/src/http/mod.rs +++ b/gateway/src/http/mod.rs @@ -4,7 +4,6 @@ use crate::config::Config; use crate::error::GatewayError; use crate::helpers::load_public_key; -use log::{debug, error, warn}; use nym_bin_common::bin_info_owned; use nym_crypto::asymmetric::{encryption, identity}; use nym_network_requester::RequestFilter; @@ -15,6 +14,7 @@ use nym_node_http_api::NymNodeHttpError; use nym_sphinx::addressing::clients::Recipient; use nym_task::TaskClient; use std::sync::Arc; +use tracing::{debug, error, warn}; fn load_gateway_details( config: &Config, diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 3c5109817b..6759180420 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -6,12 +6,12 @@ use clap::{crate_name, crate_version, Parser}; use colored::Colorize; -use log::error; use nym_bin_common::bin_info; use nym_bin_common::logging::{maybe_print_banner, setup_logging}; use nym_network_defaults::setup_env; use std::io::IsTerminal; use std::sync::OnceLock; +use tracing::error; mod commands; diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index df88d16213..962765067e 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -4,9 +4,9 @@ use super::websocket::message_receiver::{IsActiveRequestSender, MixMessageSender}; use crate::node::client_handling::embedded_clients::LocalEmbeddedClientHandle; use dashmap::DashMap; -use log::warn; use nym_sphinx::DestinationAddressBytes; use std::sync::Arc; +use tracing::warn; enum ActiveClient { /// Handle to a remote client connected via a network socket. diff --git a/gateway/src/node/client_handling/bandwidth.rs b/gateway/src/node/client_handling/bandwidth.rs index d4ce709c96..3f15451576 100644 --- a/gateway/src/node/client_handling/bandwidth.rs +++ b/gateway/src/node/client_handling/bandwidth.rs @@ -1,21 +1,16 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use log::{error, warn}; -use nym_credentials::coconut::bandwidth::CredentialType; use std::num::ParseIntError; use thiserror::Error; use time::error::ComponentRange; -use time::OffsetDateTime; +use tracing::error; #[derive(Debug, Error)] pub enum BandwidthError { #[error("Provided bandwidth credential asks for more bandwidth than it is supported to add at once (credential value: {0}, supported: {}). Try to split it before attempting again", i64::MAX)] UnsupportedBandwidthValue(u64), - #[error("the provided free pass has already expired (expiry was on {expiry_date})")] - ExpiredFreePass { expiry_date: OffsetDateTime }, - #[error("failed to parse the bandwidth voucher value: {source}")] VoucherValueParsingFailure { #[source] @@ -46,51 +41,10 @@ impl Bandwidth { Bandwidth { value } } - pub fn new(bandwidth_value: u64) -> Result { - if bandwidth_value > i64::MAX as u64 { - // note that this would have represented more than 1 exabyte, - // which is like 125,000 worth of hard drives, so I don't think we have - // to worry about it for now... - warn!("Somehow we received bandwidth value higher than 9223372036854775807. We don't really want to deal with this now"); - return Err(BandwidthError::UnsupportedBandwidthValue(bandwidth_value)); + pub fn ticket_amount() -> Self { + Bandwidth { + value: nym_network_defaults::TICKET_BANDWIDTH_VALUE, } - - Ok(Bandwidth { - value: bandwidth_value, - }) - } - - pub(crate) fn parse_raw_bandwidth( - value: &str, - typ: CredentialType, - ) -> Result<(u64, Option), BandwidthError> { - let (bandwidth_value, freepass_expiration) = - match typ { - CredentialType::Voucher => { - let token_value: u64 = value - .parse() - .map_err(|source| BandwidthError::VoucherValueParsingFailure { source })?; - (token_value * nym_network_defaults::BYTES_PER_UTOKEN, None) - } - CredentialType::FreePass => { - let expiry_timestamp: i64 = value - .parse() - .map_err(|source| BandwidthError::ExpiryDateParsingFailure { source })?; - - let expiry_date = OffsetDateTime::from_unix_timestamp(expiry_timestamp) - .map_err(|source| BandwidthError::InvalidExpiryDate { - unix_timestamp: expiry_timestamp, - source, - })?; - let now = OffsetDateTime::now_utc(); - - if expiry_date < now { - return Err(BandwidthError::ExpiredFreePass { expiry_date }); - } - (nym_network_defaults::BYTES_PER_FREEPASS, Some(expiry_date)) - } - }; - Ok((bandwidth_value, freepass_expiration)) } pub fn value(&self) -> u64 { diff --git a/gateway/src/node/client_handling/embedded_clients/mod.rs b/gateway/src/node/client_handling/embedded_clients/mod.rs index a7264f0985..7974bb9c41 100644 --- a/gateway/src/node/client_handling/embedded_clients/mod.rs +++ b/gateway/src/node/client_handling/embedded_clients/mod.rs @@ -5,11 +5,11 @@ use crate::node::client_handling::websocket::message_receiver::{ MixMessageReceiver, MixMessageSender, }; use futures::StreamExt; -use log::{debug, error}; use nym_network_requester::{GatewayPacketRouter, PacketRouter}; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::DestinationAddressBytes; use nym_task::TaskClient; +use tracing::{debug, error, trace}; #[derive(Debug)] pub(crate) struct LocalEmbeddedClientHandle { @@ -71,12 +71,12 @@ impl MessageRouter { messages = self.mix_receiver.next() => match messages { Some(messages) => self.handle_received_messages(messages), None => { - log::trace!("embedded_clients::MessageRouter: Stopping since channel closed"); + trace!("embedded_clients::MessageRouter: Stopping since channel closed"); break; } }, _ = shutdown.recv_with_delay() => { - log::trace!("embedded_clients::MessageRouter: Received shutdown"); + trace!("embedded_clients::MessageRouter: Received shutdown"); debug_assert!(shutdown.is_shutdown()); break } diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index e112997e57..3a2a206368 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -1,15 +1,16 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; +use crate::node::client_handling::websocket::connection_handler::ecash::EcashManager; use crate::node::client_handling::websocket::connection_handler::BandwidthFlushingBehaviourConfig; use nym_crypto::asymmetric::identity; use std::sync::Arc; // I can see this being possible expanded with say storage or client store #[derive(Clone)] -pub(crate) struct CommonHandlerState { - pub(crate) coconut_verifier: Arc, +pub(crate) struct CommonHandlerState { + pub(crate) ecash_verifier: Arc>, + pub(crate) storage: S, pub(crate) local_identity: Arc, pub(crate) only_coconut_credentials: bool, pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 249f60997f..fa31cc6960 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -1,11 +1,12 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::client_handling::bandwidth::BandwidthError; +use crate::node::client_handling::bandwidth::{Bandwidth, BandwidthError}; +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +use crate::node::client_handling::websocket::connection_handler::ecash::ClientTicket; use crate::node::client_handling::websocket::connection_handler::ClientBandwidth; use crate::node::{ client_handling::{ - bandwidth::Bandwidth, websocket::{ connection_handler::{ClientDetails, FreshHandler}, message_receiver::{ @@ -20,24 +21,24 @@ use futures::{ future::{FusedFuture, OptionFuture}, FutureExt, StreamExt, }; -use log::*; -use nym_credentials::coconut::bandwidth::{bandwidth_credential_params, CredentialType}; -use nym_credentials_interface::{Base58, CoconutError}; +use nym_credentials::ecash::utils::{ecash_today, EcashTime}; +use nym_credentials_interface::CredentialSpendingData; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_requests::{ - iv::{IVConversionError, IV}, types::{BinaryRequest, ServerResponse}, ClientControlRequest, GatewayRequestsError, }; use nym_sphinx::forwarding::packet::MixPacket; use nym_task::TaskClient; -use nym_validator_client::coconut::CoconutApiError; +use nym_validator_client::coconut::EcashApiError; use rand::{CryptoRng, Rng}; +use si_scale::helpers::bibytes2; use std::{process, time::Duration}; use thiserror::Error; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; +use tracing::*; #[derive(Debug, Error)] pub enum RequestHandlingError { @@ -49,9 +50,6 @@ pub enum RequestHandlingError { )] MissingClientBandwidthEntry { client_address: String }, - #[error("Provided bandwidth IV is malformed - {0}")] - MalformedIV(#[from] IVConversionError), - #[error("Provided binary request was malformed - {0}")] InvalidBinaryRequest(#[from] GatewayRequestsError), @@ -61,8 +59,13 @@ pub enum RequestHandlingError { #[error("The received request is not valid in the current context: {additional_context}")] IllegalRequest { additional_context: String }, - #[error("Provided bandwidth credential did not verify correctly on {0}")] - InvalidBandwidthCredential(String), + #[error("credential has been rejected by the validators")] + RejectedProposal, + + #[error( + "the provided credential has an invalid spending date. got {got} but expected {expected}" + )] + InvalidCredentialSpendingDate { got: Date, expected: Date }, #[error("the provided bandwidth credential has already been spent before at this gateway")] BandwidthCredentialAlreadySpent, @@ -76,41 +79,29 @@ pub enum RequestHandlingError { #[error("Validator API error - {0}")] APIError(#[from] nym_validator_client::ValidatorClientError), - #[error("Not enough nym API endpoints provided. Needed {needed}, received {received}")] - NotEnoughNymAPIs { received: usize, needed: usize }, - #[error("There was a problem with the proposal id: {reason}")] ProposalIdError { reason: String }, - #[error("coconut failure: {0}")] - CoconutError(#[from] CoconutError), + #[error("compact ecash error: {0}")] + CompactEcashError(#[from] nym_credentials_interface::CompactEcashError), #[error("coconut api query failure: {0}")] - CoconutApiError(#[from] CoconutApiError), + CoconutApiError(#[from] EcashApiError), #[error("Credential error - {0}")] CredentialError(#[from] nym_credentials::error::Error), + #[error("Internal error")] + InternalError, + #[error("failed to recover bandwidth value: {0}")] BandwidthRecoveryFailure(#[from] BandwidthError), - #[error("the provided credential did not contain a valid type attribute")] - InvalidTypeAttribute, - #[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")] OutOfBandwidth { required: i64, available: i64 }, - #[error("the provided credential did not have a bandwidth attribute")] - MissingBandwidthAttribute, - - #[error("attempted to claim a bandwidth voucher for an account using a free pass (it expires on {expiration})")] - BandwidthVoucherForFreePassAccount { expiration: OffsetDateTime }, - - #[error("attempted to claim another free pass for the account while another free pass is still active (it expires on {expiration})")] - PreexistingFreePass { expiration: OffsetDateTime }, - - #[error("the DKG contract is unavailable")] - UnavailableDkgContract, + #[error(transparent)] + EcashFailure(EcashTicketError), } impl RequestHandlingError { @@ -119,6 +110,17 @@ impl RequestHandlingError { } } +impl From for RequestHandlingError { + fn from(err: EcashTicketError) -> Self { + // don't expose storage issue details to the user + if let EcashTicketError::InternalStorageFailure { source } = err { + RequestHandlingError::StorageError(source) + } else { + RequestHandlingError::EcashFailure(err) + } + } +} + /// Helper trait that allows converting result of handling client request into a websocket message // Note: I couldn't have implemented a normal "From" trait as both `Message` and `Result` are foreign types trait IntoWSMessage { @@ -159,7 +161,7 @@ impl AuthenticatedHandler where // TODO: those trait bounds here don't really make sense.... R: Rng + CryptoRng, - St: Storage, + St: Storage + Clone + 'static, { /// Upgrades `FreshHandler` into the Authenticated variant implying the client is now authenticated /// and thus allowed to perform more actions with the gateway, such as redeeming bandwidth or @@ -181,8 +183,9 @@ where // so in theory we could just unwrap the value here, but since we're returning a Result anyway, // we might as well return a failure response instead let bandwidth = fresh + .shared_state .storage - .get_available_bandwidth(client.address) + .get_available_bandwidth(client.id) .await? .ok_or(RequestHandlingError::MissingClientBandwidthEntry { client_address: client.address.as_base58_string(), @@ -205,10 +208,11 @@ where .disconnect(self.client.address) } - async fn expire_freepass(&mut self) -> Result<(), RequestHandlingError> { + async fn expire_bandwidth(&mut self) -> Result<(), RequestHandlingError> { + self.inner.expire_bandwidth(self.client.id).await?; self.client_bandwidth.bandwidth = Default::default(); - self.client_bandwidth.update_flush_data(); - Ok(self.inner.expire_freepass(self.client.address).await?) + self.client_bandwidth.update_sync_data(); + Ok(()) } /// Increases the amount of available bandwidth of the connected client by the specified value. @@ -216,28 +220,20 @@ where /// # Arguments /// /// * `amount`: amount to increase the available bandwidth by. + /// * `expiration` : the expiration date of that bandwidth async fn increase_bandwidth( &mut self, bandwidth: Bandwidth, + expiration: OffsetDateTime, ) -> Result<(), RequestHandlingError> { self.client_bandwidth.bandwidth.bytes += bandwidth.value() as i64; + self.client_bandwidth.bytes_delta_since_sync += bandwidth.value() as i64; + self.client_bandwidth.bandwidth.expiration = expiration; // any increases to bandwidth should get flushed immediately // (we don't want to accidentally miss somebody claiming a gigabyte voucher) - self.flush_bandwidth().await - } - - async fn set_freepass_expiration( - &mut self, - expiration: OffsetDateTime, - ) -> Result<(), RequestHandlingError> { - self.client_bandwidth.bandwidth.freepass_expiration = Some(expiration); - self.inner - .storage - .set_freepass_expiration(self.client.address, expiration) - .await?; - self.client_bandwidth.update_flush_data(); - Ok(()) + self.sync_expiration().await?; + self.sync_bandwidth().await } /// Decreases the amount of available bandwidth of the connected client by the specified value. @@ -247,14 +243,15 @@ where /// * `amount`: amount to decrease the available bandwidth by. async fn consume_bandwidth(&mut self, amount: i64) -> Result<(), RequestHandlingError> { self.client_bandwidth.bandwidth.bytes -= amount; + self.client_bandwidth.bytes_delta_since_sync -= amount; // since we're going to be operating on a fair use policy anyway, even if we crash and let extra few packets // through, that's completely fine if self .client_bandwidth - .should_flush(self.inner.shared_state.bandwidth_cfg) + .should_sync(self.inner.shared_state.bandwidth_cfg) { - self.flush_bandwidth().await?; + self.sync_bandwidth().await?; } Ok(()) @@ -272,148 +269,107 @@ where } } - async fn handle_bandwidth_request( - &mut self, - credential: CredentialSpendingRequest, - ) -> Result { - // check if the credential hasn't been spent before - let serial_number = credential.data.blinded_serial_number(); - trace!("processing credential {}", serial_number.to_bs58()); + async fn check_local_db_for_double_spending( + &self, + serial_number: &[u8], + ) -> Result<(), RequestHandlingError> { + trace!("checking local db for double spending..."); - // if we already have had received a free pass (that's not expired, don't accept any additional bandwidth) - if self.client_bandwidth.bandwidth.freepass_expired() { - // the free pass we used before has expired -> reset our state and handle the request as normal - self.expire_freepass().await?; - } else if let Some(expiration) = self.client_bandwidth.bandwidth.freepass_expiration { - // the free pass is still valid -> return error - return match credential.data.typ { - CredentialType::Voucher => { - Err(RequestHandlingError::BandwidthVoucherForFreePassAccount { expiration }) - } - CredentialType::FreePass => { - Err(RequestHandlingError::PreexistingFreePass { expiration }) - } - }; - } - - let already_spent = self + let spent = self .inner + .shared_state .storage - .contains_credential(&serial_number) + .contains_ticket(serial_number) .await?; - if already_spent { - trace!("the credential has already been spent before"); + if spent { + trace!("the credential has already been spent before at this gateway"); return Err(RequestHandlingError::BandwidthCredentialAlreadySpent); } - - trace!( - "attempting to obtain aggregate verification key for epoch {}", - credential.data.epoch_id - ); - - if !credential.data.validate_type_attribute() { - trace!("mismatch in the type attribute"); - return Err(RequestHandlingError::InvalidTypeAttribute); - } - - let Some(bandwidth_attribute) = credential.data.get_bandwidth_attribute() else { - trace!("missing bandwidth attribute"); - return Err(RequestHandlingError::MissingBandwidthAttribute); - }; - - // this will extract token amounts out of bandwidth vouchers and validate expiry of free passes - let (raw_bandwidth, freepass_expiration) = - Bandwidth::parse_raw_bandwidth(bandwidth_attribute, credential.data.typ)?; - - let bandwidth = Bandwidth::new(raw_bandwidth)?; - - trace!("embedded bandwidth: {bandwidth:?}"); - - // locally verify the credential - { - let aggregated_verification_key = self - .inner - .shared_state - .coconut_verifier - .verification_key(credential.data.epoch_id) - .await?; - - let params = bandwidth_credential_params(); - if !credential.data.verify(params, &aggregated_verification_key) { - trace!("the credential did not verify correctly"); - return Err(RequestHandlingError::InvalidBandwidthCredential( - String::from("local credential verification has failed"), - )); - } - } - - match credential.data.typ { - CredentialType::Voucher => { - trace!("the credential is a bandwidth voucher. attempting to release the funds"); - let api_clients = self - .inner - .shared_state - .coconut_verifier - .api_clients(credential.data.epoch_id) - .await?; - - self.inner - .shared_state - .coconut_verifier - .release_bandwidth_voucher_funds(&api_clients, credential) - .await?; - } - CredentialType::FreePass => { - // no need to do anything special here, we already extracted the bandwidth amount and checked expiry - info!("received a free pass credential"); - } - } - - // technically this is not atomic, i.e. checking for the spending and then marking as spent, - // but because we have the `UNIQUE` constraint on the database table - // if somebody attempts to spend the same credential in another, parallel request, - // one of them will fail - // - // mark the credential as spent - // TODO: technically this should be done under a storage transaction so that if we experience any - // failures later on, it'd get reverted - trace!("storing serial number information"); - self.inner - .storage - .insert_spent_credential( - serial_number, - freepass_expiration.is_some(), - self.client.address, - ) - .await?; - - trace!("increasing client bandwidth"); - self.increase_bandwidth(bandwidth).await?; - // set free pass expiration - if let Some(expiration) = freepass_expiration { - self.set_freepass_expiration(expiration).await?; - } - - let available_total = self.client_bandwidth.bandwidth.bytes; - - Ok(ServerResponse::Bandwidth { available_total }) + Ok(()) } - async fn handle_bandwidth_v1( - &mut self, - enc_credential: Vec, - iv: Vec, - ) -> Result { - debug!("handling v1 bandwidth request"); + async fn check_bloomfilter(&self, serial_number: &Vec) -> Result<(), RequestHandlingError> { + trace!("checking the bloomfilter..."); - let iv = IV::try_from_bytes(&iv)?; - let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential_v1( - enc_credential, - &self.client.shared_keys, - iv, - )?; + let spent = self + .inner + .shared_state + .ecash_verifier + .check_double_spend(serial_number) + .await; - self.handle_bandwidth_request(credential.try_into()?).await + if spent { + trace!("the credential has already been spent before at some gateway before (bloomfilter failure)"); + return Err(RequestHandlingError::BandwidthCredentialAlreadySpent); + } + Ok(()) + } + + fn check_credential_spending_date( + &self, + proposed: Date, + today: Date, + ) -> Result<(), RequestHandlingError> { + trace!("checking ticket spending date..."); + + if today != proposed { + trace!("invalid credential spending date. received {proposed}"); + return Err(RequestHandlingError::InvalidCredentialSpendingDate { + got: proposed, + expected: today, + }); + } + Ok(()) + } + + async fn cryptographically_verify_ticket( + &self, + credential: &CredentialSpendingRequest, + ) -> Result<(), RequestHandlingError> { + trace!("attempting to perform ticket verification..."); + + let aggregated_verification_key = self + .inner + .shared_state + .ecash_verifier + .verification_key(credential.data.epoch_id) + .await?; + + self.inner + .shared_state + .ecash_verifier + .check_payment(&credential.data, &aggregated_verification_key) + .await?; + Ok(()) + } + + fn async_verify_ticket(&self, ticket: CredentialSpendingData, ticket_id: i64) { + let client_ticket = ClientTicket::new(ticket, ticket_id); + + self.inner + .shared_state + .ecash_verifier + .async_verify(client_ticket); + } + + async fn store_received_ticket( + &self, + ticket_data: &CredentialSpendingRequest, + received_at: OffsetDateTime, + ) -> Result { + trace!("storing received ticket"); + let ticket_id = self + .inner + .shared_state + .storage + .insert_received_ticket( + self.client.id, + received_at, + ticket_data.encoded_serial_number(), + ticket_data.to_bytes(), + ) + .await?; + Ok(ticket_id) } /// Tries to handle the received bandwidth request by checking correctness of the received data @@ -421,23 +377,51 @@ where /// /// # Arguments /// - /// * `enc_credential`: raw encrypted bandwidth credential to verify. + /// * `enc_credential`: raw encrypted credential to verify. /// * `iv`: fresh iv used for the credential. - async fn handle_bandwidth_v2( + async fn handle_ecash_bandwidth( &mut self, enc_credential: Vec, iv: Vec, ) -> Result { - debug!("handling v2 bandwidth request"); + let received_at = OffsetDateTime::now_utc(); + // TODO: change it into a span field instead once we move to tracing + debug!( + "handling e-cash bandwidth request from {}", + self.client.address + ); - let iv = IV::try_from_bytes(&iv)?; - let credential = ClientControlRequest::try_from_enc_coconut_bandwidth_credential_v2( + let credential = ClientControlRequest::try_from_enc_ecash_credential( enc_credential, &self.client.shared_keys, iv, )?; + let spend_date = ecash_today(); - self.handle_bandwidth_request(credential).await + // check if the credential hasn't been spent before + let serial_number = credential.data.encoded_serial_number(); + + self.check_credential_spending_date(credential.data.spend_date, spend_date.ecash_date())?; + self.check_bloomfilter(&serial_number).await?; + self.check_local_db_for_double_spending(&serial_number) + .await?; + + // TODO: do we HAVE TO do it? + self.cryptographically_verify_ticket(&credential).await?; + + let ticket_id = self.store_received_ticket(&credential, received_at).await?; + self.async_verify_ticket(credential.data, ticket_id); + + // TODO: double storing? + // self.store_spent_credential(serial_number_bs58).await?; + + let bandwidth = Bandwidth::ticket_amount(); + + self.increase_bandwidth(bandwidth, spend_date).await?; + + let available_total = self.client_bandwidth.bandwidth.bytes; + + Ok(ServerResponse::Bandwidth { available_total }) } async fn handle_claim_testnet_bandwidth( @@ -449,29 +433,47 @@ where return Err(RequestHandlingError::OnlyCoconutCredentials); } - self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE) + self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE, ecash_today()) .await?; let available_total = self.client_bandwidth.bandwidth.bytes; Ok(ServerResponse::Bandwidth { available_total }) } - async fn flush_bandwidth(&mut self) -> Result<(), RequestHandlingError> { - trace!("flushing client bandwidth to the underlying storage"); + async fn sync_expiration(&mut self) -> Result<(), RequestHandlingError> { self.inner + .shared_state .storage - .set_bandwidth(self.client.address, self.client_bandwidth.bandwidth.bytes) + .set_expiration(self.client.id, self.client_bandwidth.bandwidth.expiration) .await?; - self.client_bandwidth.update_flush_data(); Ok(()) } + #[instrument(level = "trace", skip_all)] + async fn sync_bandwidth(&mut self) -> Result<(), RequestHandlingError> { + trace!("syncing client bandwidth with the underlying storage"); + let updated = self + .inner + .shared_state + .storage + .increase_bandwidth(self.client.id, self.client_bandwidth.bytes_delta_since_sync) + .await?; + + trace!(updated); + + self.client_bandwidth.bandwidth.bytes = updated; + + self.client_bandwidth.update_sync_data(); + Ok(()) + } + + #[instrument(skip_all)] async fn try_use_bandwidth( &mut self, required_bandwidth: i64, ) -> Result { - if self.client_bandwidth.bandwidth.freepass_expired() { - self.expire_freepass().await?; + if self.client_bandwidth.bandwidth.expired() { + self.expire_bandwidth().await?; } let available_bandwidth = self.client_bandwidth.bandwidth.bytes; @@ -482,8 +484,12 @@ where }); } + let available_bi2 = bibytes2(available_bandwidth as f64); + let required_bi2 = bibytes2(required_bandwidth as f64); + debug!(available = available_bi2, required = required_bi2); + self.consume_bandwidth(required_bandwidth).await?; - Ok(self.client_bandwidth.bandwidth.bytes) + Ok(available_bandwidth) } /// Tries to handle request to forward sphinx packet into the network. The request can only succeed @@ -494,6 +500,7 @@ where /// # Arguments /// /// * `mix_packet`: packet received from the client that should get forwarded into the network. + #[instrument(skip_all)] async fn handle_forward_sphinx( &mut self, mix_packet: MixPacket, @@ -543,14 +550,22 @@ where match ClientControlRequest::try_from(raw_request) { Err(e) => RequestHandlingError::InvalidTextRequest(e).into_error_message(), Ok(request) => match request { - ClientControlRequest::BandwidthCredential { enc_credential, iv } => self - .handle_bandwidth_v1(enc_credential, iv) - .await - .into_ws_message(), - ClientControlRequest::BandwidthCredentialV2 { enc_credential, iv } => self - .handle_bandwidth_v2(enc_credential, iv) + ClientControlRequest::EcashCredential { enc_credential, iv } => self + .handle_ecash_bandwidth(enc_credential, iv) .await .into_ws_message(), + ClientControlRequest::BandwidthCredential { .. } => { + RequestHandlingError::IllegalRequest { + additional_context: "coconut credential are not longer supported".into(), + } + .into_error_message() + } + ClientControlRequest::BandwidthCredentialV2 { .. } => { + RequestHandlingError::IllegalRequest { + additional_context: "coconut credential are not longer supported".into(), + } + .into_error_message() + } ClientControlRequest::ClaimFreeTestnetBandwidth => self .handle_claim_testnet_bandwidth() .await diff --git a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs deleted file mode 100644 index 26a7e76640..0000000000 --- a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2022-2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use super::authenticated::RequestHandlingError; -use log::*; -use nym_credentials_interface::VerificationKey; -use nym_gateway_requests::models::CredentialSpendingRequest; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nym_api::EpochId; -use nym_validator_client::nyxd::contract_traits::{MultisigQueryClient, NymContractsProvider}; -use nym_validator_client::nyxd::AccountId; -use nym_validator_client::{ - nyxd::{ - contract_traits::{CoconutBandwidthSigningClient, DkgQueryClient, MultisigSigningClient}, - cosmwasm_client::logs::{find_attribute, BANDWIDTH_PROPOSAL_ID}, - Coin, - }, - CoconutApiClient, DirectSigningHttpRpcNyxdClient, -}; -use std::collections::HashMap; -use std::ops::Deref; -use tokio::sync::{RwLock, RwLockReadGuard}; - -pub(crate) struct CoconutVerifier { - address: AccountId, - nyxd_client: RwLock, - - // **CURRENTLY** api client addresses don't change during the epochs - api_clients: RwLock>>, - - // keys never change during epochs - master_keys: RwLock>, - mix_denom_base: String, -} - -impl CoconutVerifier { - pub async fn new( - nyxd_client: DirectSigningHttpRpcNyxdClient, - only_coconut_credentials: bool, - ) -> Result { - let mix_denom_base = nyxd_client.current_chain_details().mix_denom.base.clone(); - let address = nyxd_client.address(); - - let mut master_keys = HashMap::new(); - let mut api_clients = HashMap::new(); - - // don't make it a hard failure in case we're running on mainnet (where DKG hasn't been deployed yet) - if nyxd_client.dkg_contract_address().is_none() { - if !only_coconut_credentials { - warn!( - "the DKG contract address is not available - \ - no coconut credentials will be redeemable \ - (if the DKG ceremony hasn't been run yet this warning is expected)" - ); - } else { - // if we require coconut credentials, we MUST have DKG contract available - return Err(RequestHandlingError::UnavailableDkgContract); - } - - return Ok(CoconutVerifier { - address, - nyxd_client: RwLock::new(nyxd_client), - api_clients: Default::default(), - master_keys: Default::default(), - mix_denom_base, - }); - } - - let Ok(current_epoch) = nyxd_client.get_current_epoch().await else { - // another case of somebody putting a placeholder address that doesn't exist - error!("the specified DKG contract address is invalid - no coconut credentials will be redeemable"); - if only_coconut_credentials { - // if we require coconut credentials, we MUST have DKG contract available - return Err(RequestHandlingError::UnavailableDkgContract); - } - return Ok(CoconutVerifier { - address, - nyxd_client: RwLock::new(nyxd_client), - api_clients: Default::default(), - master_keys: Default::default(), - mix_denom_base, - }); - }; - - // might as well obtain the key for the current epoch, if applicable - if current_epoch.state.is_in_progress() { - // note: even though we're constructing clients here, we will NOT be making any network requests - let epoch_api_clients = - all_coconut_api_clients(&nyxd_client, current_epoch.epoch_id).await?; - let threshold = nyxd_client.get_current_epoch_threshold().await?; - - // SAFETY: - // if epoch state is in the 'in progress' state, it means the threshold value MUST HAVE - // been established. if it wasn't, there's an underlying issue with the DKG contract in which - // case we shouldn't continue anyway because here be dragons - #[allow(clippy::expect_used)] - let threshold = threshold.expect("unavailable threshold value") as usize; - if epoch_api_clients.len() < threshold { - return Err(RequestHandlingError::NotEnoughNymAPIs { - received: epoch_api_clients.len(), - needed: threshold, - }); - } - let aggregated_verification_key = - nym_credentials::obtain_aggregate_verification_key(&epoch_api_clients)?; - - api_clients.insert(current_epoch.epoch_id, epoch_api_clients); - master_keys.insert(current_epoch.epoch_id, aggregated_verification_key); - } - - Ok(CoconutVerifier { - address, - nyxd_client: RwLock::new(nyxd_client), - api_clients: RwLock::new(api_clients), - master_keys: RwLock::new(master_keys), - mix_denom_base, - }) - } - - pub async fn api_clients( - &self, - epoch_id: EpochId, - ) -> Result>, RequestHandlingError> { - let guard = self.api_clients.read().await; - - // the key was already in the map - if let Ok(mapped) = RwLockReadGuard::try_map(guard, |clients| clients.get(&epoch_id)) { - trace!("we already had cached api clients for epoch {epoch_id}"); - return Ok(mapped); - } - - let api_clients = self.query_api_clients(epoch_id).await?; - trace!( - "obtained {} api clients for epoch {epoch_id} from the contract", - api_clients.len() - ); - - // EDGE CASE: - // if this epoch is from the past, we can't query for its threshold - // we can only hope that enough valid keys were submitted - // the best we can do is check if we have at least a api - if api_clients.is_empty() { - return Err(RequestHandlingError::NotEnoughNymAPIs { - received: 0, - needed: 1, - }); - } - - let mut guard = self.api_clients.write().await; - guard.insert(epoch_id, api_clients); - let guard = guard.downgrade(); - trace!("stored api clients for epoch {epoch_id}"); - - // SAFETY: - // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) - // so it MUST exist and thus the unwrap is fine - #[allow(clippy::unwrap_used)] - Ok(RwLockReadGuard::map(guard, |clients| { - clients.get(&epoch_id).unwrap() - })) - } - - pub async fn verification_key( - &self, - epoch_id: EpochId, - ) -> Result, RequestHandlingError> { - let guard = self.master_keys.read().await; - - // the key was already in the map - if let Ok(mapped) = RwLockReadGuard::try_map(guard, |keys| keys.get(&epoch_id)) { - trace!("we already had cached verification key for epoch {epoch_id}"); - return Ok(mapped); - } - - let api_clients = self.api_clients(epoch_id).await?; - trace!( - "attempting to obtain verification key from {} api clients", - api_clients.len() - ); - - let aggregated_verification_key = - nym_credentials::obtain_aggregate_verification_key(&api_clients)?; - - let mut guard = self.master_keys.write().await; - guard.insert(epoch_id, aggregated_verification_key); - let guard = guard.downgrade(); - trace!("stored aggregated verification key for epoch {epoch_id}"); - - // SAFETY: - // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) - // so it MUST exist and thus the unwrap is fine - #[allow(clippy::unwrap_used)] - Ok(RwLockReadGuard::map(guard, |keys| { - keys.get(&epoch_id).unwrap() - })) - } - - pub async fn query_api_clients( - &self, - epoch_id: u64, - ) -> Result, RequestHandlingError> { - Ok(all_coconut_api_clients(self.nyxd_client.read().await.deref(), epoch_id).await?) - } - - pub async fn release_bandwidth_voucher_funds( - &self, - api_clients: &[CoconutApiClient], - credential: CredentialSpendingRequest, - ) -> Result<(), RequestHandlingError> { - if !credential.data.typ.is_voucher() { - unimplemented!() - } - - // safety: the voucher funds are released after the credential has already been verified locally - // and the underlying bandwidth value has been extracted, so the below MUST succeed - let voucher_amount = credential.unchecked_voucher_value() as u128; - - let blinded_serial_number = credential - .data - .verify_credential_request - .blinded_serial_number_bs58(); - - let res = self - .nyxd_client - .write() - .await - .spend_credential( - Coin::new(voucher_amount, &self.mix_denom_base), - blinded_serial_number, - self.address.to_string(), - None, - ) - .await?; - let proposal_id = find_attribute(&res.logs, "wasm", BANDWIDTH_PROPOSAL_ID) - .ok_or(RequestHandlingError::ProposalIdError { - reason: String::from("proposal id not found"), - })? - .value - .parse::() - .map_err(|_| RequestHandlingError::ProposalIdError { - reason: String::from("proposal id could not be parsed to u64"), - })?; - - let proposal = self - .nyxd_client - .read() - .await - .query_proposal(proposal_id) - .await?; - if !credential.matches_blinded_serial_number(&proposal.description)? { - return Err(RequestHandlingError::ProposalIdError { - reason: String::from("proposal has different serial number"), - }); - } - - let req = nym_api_requests::coconut::VerifyCredentialBody::new( - credential.data, - proposal_id, - self.address.clone(), - ); - for client in api_clients { - let ret = client.api_client.verify_bandwidth_credential(&req).await; - let client_url = client.api_client.nym_api.current_url(); - match ret { - Ok(res) => { - if !res.verification_result { - warn!("Validator at {client_url} didn't accept the credential. It will probably vote No on the spending proposal"); - } - } - Err(err) => { - warn!("Validator at {client_url} could not be reached. There might be a problem with the coconut endpoint: {err}"); - } - } - } - - self.nyxd_client - .write() - .await - .execute_proposal(proposal_id, None) - .await?; - - Ok(()) - } -} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/credential_sender.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/credential_sender.rs new file mode 100644 index 0000000000..66eda62269 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/credential_sender.rs @@ -0,0 +1,963 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::bandwidth::Bandwidth; +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +use crate::node::client_handling::websocket::connection_handler::ecash::helpers::for_each_api_concurrent; +use crate::node::client_handling::websocket::connection_handler::ecash::state::SharedState; +use crate::node::storage::Storage; +use crate::GatewayError; +use cosmwasm_std::Fraction; +use cw_utils::ThresholdResponse; +use futures::channel::mpsc::UnboundedReceiver; +use futures::{Stream, StreamExt}; +use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; +use nym_api_requests::ecash::models::{BatchRedeemTicketsBody, VerifyEcashTicketBody}; +use nym_credentials_interface::CredentialSpendingData; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::{ + EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient, +}; +use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::nyxd::cw3::Status; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::EcashApiClient; +use si_scale::helpers::bibytes2; +use std::collections::{HashMap, HashSet}; +use std::ops::Deref; +use std::sync::atomic::{AtomicUsize, Ordering}; +use time::OffsetDateTime; +use tokio::sync::{Mutex, RwLockReadGuard}; +use tokio::time::{interval_at, Duration, Instant}; +use tracing::{debug, error, info, instrument, trace, warn}; + +enum ProposalResult { + Executed, + Rejected, + Pending, +} + +impl ProposalResult { + fn is_pending(&self) -> bool { + matches!(self, ProposalResult::Pending) + } + + fn is_rejected(&self) -> bool { + matches!(self, ProposalResult::Rejected) + } +} + +#[derive(Clone)] +pub struct ClientTicket { + pub spending_data: CredentialSpendingData, + pub ticket_id: i64, +} + +impl ClientTicket { + pub fn new(spending_data: CredentialSpendingData, ticket_id: i64) -> Self { + ClientTicket { + spending_data, + ticket_id, + } + } +} + +struct PendingVerification { + ticket: ClientTicket, + + // vec of node ids of apis that haven't sent a valid response + pending: Vec, +} + +impl PendingVerification { + fn new(ticket: ClientTicket, pending: Vec) -> Self { + PendingVerification { ticket, pending } + } + + fn to_request_body(&self, gateway_cosmos_addr: AccountId) -> VerifyEcashTicketBody { + VerifyEcashTicketBody { + // TODO: redundant clone + credential: self.ticket.spending_data.clone(), + gateway_cosmos_addr, + } + } +} + +struct PendingRedemptionVote { + proposal_id: u64, + digest: Vec, + included_serial_numbers: Vec>, + epoch_id: EpochId, + + // vec of node ids of apis that haven't sent a valid response + pending: Vec, +} + +impl PendingRedemptionVote { + fn new( + proposal_id: u64, + digest: Vec, + included_serial_numbers: Vec>, + epoch_id: EpochId, + pending: Vec, + ) -> Self { + PendingRedemptionVote { + proposal_id, + digest, + included_serial_numbers, + epoch_id, + pending, + } + } + + fn to_request_body(&self, gateway_cosmos_addr: AccountId) -> BatchRedeemTicketsBody { + BatchRedeemTicketsBody::new( + self.digest.clone(), + self.proposal_id, + self.included_serial_numbers.clone(), + gateway_cosmos_addr, + ) + } +} + +pub(crate) struct CredentialHandlerConfig { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub(crate) revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + pub(crate) pending_poller: Duration, + + pub(crate) minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub(crate) minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than 30 days. + pub(crate) maximum_time_between_redemption: Duration, +} + +pub(crate) struct CredentialHandler { + config: CredentialHandlerConfig, + multisig_threshold: f32, + ticket_receiver: UnboundedReceiver, + shared_state: SharedState, + pending_tickets: Vec, + pending_redemptions: Vec, +} + +impl CredentialHandler +where + St: Storage + Clone + 'static, +{ + async fn rebuild_pending_tickets( + shared_state: &SharedState, + ) -> Result, EcashTicketError> { + // 1. get all tickets that were not fully verified + let unverified = shared_state.storage.get_all_unverified_tickets().await?; + let mut pending = Vec::with_capacity(unverified.len()); + + // a lookup of ids for signers for given epoch + let mut apis_lookup = HashMap::new(); + + // 2. for each of them, reconstruct missing votes + for ticket in unverified { + let epoch = ticket.spending_data.epoch_id; + assert!(epoch <= i64::MAX as u64); + let signers = match apis_lookup.get(&epoch) { + Some(signers) => signers, + None => { + // get all signers for given epoch + let signers = shared_state.storage.get_signers(epoch as i64).await?; + apis_lookup.insert(epoch, signers); + + // safety: we just inserted that entry + #[allow(clippy::unwrap_used)] + apis_lookup.get(&epoch).unwrap() + } + }; + // get all votes the ticket received + let votes = shared_state + .storage + .get_votes(ticket.ticket_id) + .await? + .into_iter() + .collect::>(); + let mut missing_votes = Vec::new(); + for signer in signers { + // for each signer, check if they have actually voted; if not, that's the missing guy + if !votes.contains(signer) { + missing_votes.push(*signer as u64) + } + } + pending.push(PendingVerification { + ticket, + pending: missing_votes, + }) + } + Ok(pending) + } + + async fn rebuild_pending_votes( + shared_state: &SharedState, + ) -> Result, EcashTicketError> { + // 1. get all tickets that were not fully verified + let unverified = shared_state.storage.get_all_unresolved_proposals().await?; + let mut pending = Vec::with_capacity(unverified.len()); + + let epoch_id = shared_state.current_epoch_id().await?; + let apis = shared_state + .api_clients(epoch_id) + .await? + .iter() + .map(|s| (s.cosmos_address.to_string(), s.node_id)) + .collect::>(); + + for proposal_id in unverified { + // get all of the votes + let votes = shared_state + .start_query() + .await + .get_all_votes(proposal_id as u64) + .await + .map_err(EcashTicketError::chain_query_failure)? + .into_iter() + .map(|v| v.voter) + .collect::>(); + + let mut missing_votes = Vec::new(); + + // see who hasn't voted + for (api_address, api_id) in &apis { + // for each signer, check if they have actually voted; if not, that's the missing guy + if !votes.contains(api_address) { + missing_votes.push(*api_id) + } + } + + // attempt to rebuild SN and digest from the proposal info + storage data + let proposal_info = shared_state + .start_query() + .await + .query_proposal(proposal_id as u64) + .await + .map_err(EcashTicketError::chain_query_failure)?; + + let tickets = shared_state + .storage + .get_all_proposed_tickets_with_sn(proposal_id as u32) + .await?; + let digest = + BatchRedeemTicketsBody::make_digest(tickets.iter().map(|t| &t.serial_number)); + let encoded_digest = bs58::encode(&digest).into_string(); + if encoded_digest != proposal_info.description { + error!("the lost proposal {proposal_id} does not have a matching digest!"); + continue; + } + + pending.push(PendingRedemptionVote { + proposal_id: proposal_id as u64, + digest, + included_serial_numbers: tickets.into_iter().map(|t| t.serial_number).collect(), + epoch_id, + pending: missing_votes, + }) + } + + Ok(pending) + } + + pub(crate) async fn new( + config: CredentialHandlerConfig, + ticket_receiver: UnboundedReceiver, + shared_state: SharedState, + ) -> Result { + let multisig_threshold = shared_state + .nyxd_client + .read() + .await + .query_threshold() + .await?; + + let ThresholdResponse::AbsolutePercentage { percentage, .. } = multisig_threshold else { + return Err(GatewayError::InvalidMultisigThreshold); + }; + + // that's a nasty conversion, but it works : ) + let multisig_threshold = + percentage.numerator().u128() as f32 / percentage.denominator().u128() as f32; + + // on startup read pending credentials and api responses from the storage + let pending_tickets = Self::rebuild_pending_tickets(&shared_state).await?; + + // on startup read pending proposals from the storage + // then reconstruct the votes by querying the multisig contract for votes on those proposals + // digest from the description and count from the message + let pending_redemptions = Self::rebuild_pending_votes(&shared_state).await?; + + Ok(CredentialHandler { + config, + multisig_threshold, + ticket_receiver, + shared_state, + pending_tickets, + pending_redemptions, + }) + } + + // the argument is temporary as we'll be reading from the storage + async fn create_redemption_proposal( + &self, + commitment: &[u8], + number_of_tickets: u16, + ) -> Result { + let res = self + .shared_state + .start_tx() + .await + .request_ticket_redemption( + bs58::encode(commitment).into_string(), + number_of_tickets, + None, + ) + .await + .map_err(|source| EcashTicketError::RedemptionProposalCreationFailure { source })?; + + // that one is quite tricky because proposal exists on chain, but we didn't get the id... + // but it should be quite impossible to ever reach this unless we make breaking changes + let proposal_id = res + .parse_singleton_u64_contract_data() + .inspect_err(|err| error!("reached seemingly impossible error! could not recover the redemption proposal id: {err}")) + .map_err(|source| EcashTicketError::ProposalIdParsingFailure { source })?; + + info!("created redemption proposal {proposal_id} to redeem {number_of_tickets} tickets"); + + Ok(proposal_id) + } + + /// Attempt to send ticket verification request to the provided ecash verifier. + async fn verify_ticket( + &self, + ticket_id: i64, + request: &VerifyEcashTicketBody, + client: &EcashApiClient, + ) -> Result { + match client.api_client.verify_ecash_ticket(request).await { + Ok(res) => { + let accepted = match res.verified { + Ok(_) => { + trace!("{client} has accepted ticket {ticket_id}"); + true + } + Err(rejection) => { + warn!("{client} has rejected ticket {ticket_id}: {rejection}"); + false + } + }; + self.shared_state + .storage + .insert_ticket_verification( + ticket_id, + client.node_id as i64, + OffsetDateTime::now_utc(), + accepted, + ) + .await?; + Ok(accepted) + } + Err(err) => { + error!("failed to send ticket {ticket_id} for verification to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later"); + Ok(false) + } + } + } + + #[instrument(skip(self))] + async fn revoke_ticket_bandwidth(&self, ticket_id: i64) -> Result<(), EcashTicketError> { + warn!("revoking bandwidth associated with ticket {ticket_id} since it failed verification"); + + let bytes_to_revoke = + Bandwidth::ticket_amount().value() as f32 * self.config.revocation_bandwidth_penalty; + let to_revoke_bi2 = bibytes2(bytes_to_revoke as f64); + + info!(to_revoke_bi2); + + self.shared_state + .storage + .revoke_ticket_bandwidth(ticket_id, bytes_to_revoke as i64) + .await?; + Ok(()) + } + + /// Attempt to send the pending ticket to all ecash verifiers that haven't yet returned valid response. + async fn send_pending_ticket_for_verification( + &self, + pending: &mut PendingVerification, + api_clients: Option>>, + ) -> Result { + let ticket_id = pending.ticket.ticket_id; + let api_clients = match api_clients { + Some(clients) => clients, + None => { + self.shared_state + .api_clients(pending.ticket.spending_data.epoch_id) + .await? + } + }; + + let verification_request = pending.to_request_body(self.shared_state.address.clone()); + + let total = api_clients.len(); + let api_failures = Mutex::new(Vec::new()); + let rejected = AtomicUsize::new(0); + + // this vector will never contain more than ~30 entries so linear lookup is fine. + // it's probably even faster than hashset due to overhead + futures::stream::iter( + api_clients + .deref() + .iter() + .filter(|client| pending.pending.contains(&client.node_id)), + ) + .for_each_concurrent(32, |ecash_client| async { + // errors are only returned on hard, storage, failures + match self + .verify_ticket( + pending.ticket.ticket_id, + &verification_request, + ecash_client, + ) + .await + { + Err(err) => { + error!("internal failure. could not proceed with ticket verification: {err}"); + api_failures.lock().await.push(ecash_client.node_id); + } + Ok(false) => { + rejected.fetch_add(1, Ordering::SeqCst); + } + _ => {} + } + }) + .await; + + let api_failures = api_failures.into_inner(); + let num_failures = api_failures.len(); + pending.pending = api_failures; + + let rejected = rejected.into_inner(); + let rejected_ratio = rejected as f32 / total as f32; + let rejected_perc = rejected_ratio * 100.; + if rejected_ratio >= (1. - self.config.minimum_api_quorum) { + error!("{rejected_perc:.2}% of signers rejected ticket {ticket_id}. we won't be able to redeem it"); + + self.shared_state + .storage + .update_rejected_ticket(pending.ticket.ticket_id) + .await?; + self.revoke_ticket_bandwidth(pending.ticket.ticket_id) + .await?; + } + + let accepted_ratio = (total - rejected - num_failures) as f32 / total as f32; + let accepted_perc = accepted_ratio * 100.; + match accepted_ratio { + n if n < self.multisig_threshold => error!("less than 2/3 of signers ({accepted_perc:.2}%) accepted ticket {ticket_id}. we won't be able to spend it"), + n if n < self.config.minimum_api_quorum => warn!("less than 80%, but more than 67% of signers ({accepted_perc:.2}%) accepted ticket {ticket_id}. technically we could redeem it, but we'll wait for the bigger quorum"), + _ => { + trace!("{accepted_perc:.2}% of signers accepted ticket {ticket_id}"); + self.shared_state.storage.update_verified_ticket(pending.ticket.ticket_id).await?; + return Ok(true) + } + } + + Ok(false) + } + + async fn send_ticket_for_verification( + &mut self, + ticket: ClientTicket, + ) -> Result<(), EcashTicketError> { + let api_clients = self + .shared_state + .api_clients(ticket.spending_data.epoch_id) + .await?; + + let ids = api_clients.iter().map(|c| c.node_id).collect(); + let mut pending = PendingVerification::new(ticket, ids); + + let got_quorum = self + .send_pending_ticket_for_verification(&mut pending, Some(api_clients)) + .await?; + if !got_quorum { + debug!("failed to reach quorum for ticket {}. apis: {:?} haven't responded. we'll retry later", pending.ticket.ticket_id, pending.pending); + self.pending_tickets.push(pending); + } else { + // since we reached the quorum we no longer need to hold the ticket's binary data + self.shared_state + .storage + .remove_verified_ticket_binary_data(pending.ticket.ticket_id) + .await?; + } + + Ok(()) + } + + async fn handle_client_ticket(&mut self, ticket: ClientTicket) { + // attempt to send for verification + let ticket_id = ticket.ticket_id; + if let Err(err) = self.send_ticket_for_verification(ticket).await { + error!("failed to verify ticket {ticket_id}: {err}") + } + } + + async fn resolve_pending(&mut self) -> Result<(), EcashTicketError> { + let mut still_failing = Vec::new(); + + // 1. attempt to resolve all pending proposals + while let Some(mut pending) = self.pending_redemptions.pop() { + match self.try_resolve_pending_proposal(&mut pending, None).await { + Ok(resolution) => { + if resolution.is_pending() { + warn!("still failed to reach quorum for proposal {}. apis: {:?} haven't responded. we'll retry later", pending.proposal_id, pending.pending); + still_failing.push(pending); + } else { + self.shared_state + .storage + .clear_post_proposal_data( + pending.proposal_id as u32, + OffsetDateTime::now_utc(), + resolution.is_rejected(), + ) + .await?; + } + } + Err(err) => { + error!("experienced internal error when attempting to resolve pending proposal: {err}"); + // make sure to update internal state to not lose any data + self.pending_redemptions.push(pending); + self.pending_redemptions.append(&mut still_failing); + return Err(err); + } + } + } + + let mut still_failing = Vec::new(); + + // 2. attempt to verify the remaining tickets + while let Some(mut pending) = self.pending_tickets.pop() { + // possible optimisation: if there's a lot of pending tickets, pre-emptively grab locks for api_clients + match self + .send_pending_ticket_for_verification(&mut pending, None) + .await + { + Ok(got_quorum) => { + if !got_quorum { + warn!("still failed to reach quorum for ticket {}. apis: {:?} haven't responded. we'll retry later", pending.ticket.ticket_id, pending.pending); + still_failing.push(pending); + } else { + // since we reached the quorum we no longer need to hold the ticket's binary data + self.shared_state + .storage + .remove_verified_ticket_binary_data(pending.ticket.ticket_id) + .await?; + } + } + Err(err) => { + error!("experienced internal error when attempting to resolve pending ticket: {err}"); + // make sure to update internal state to not lose any data + self.pending_tickets.push(pending); + self.pending_tickets.append(&mut still_failing); + return Err(err); + } + } + } + // at this point self.pending_tickets is empty + self.pending_tickets = still_failing; + Ok(()) + } + + /// Attempt to send batch redemption request to the provided ecash verifier. + async fn redeem_tickets( + &self, + proposal_id: u64, + request: &BatchRedeemTicketsBody, + client: &EcashApiClient, + ) -> Result { + match client.api_client.batch_redeem_ecash_tickets(request).await { + Ok(res) => { + let accepted = if res.proposal_accepted { + trace!("{client} has accepted proposal {proposal_id}"); + true + } else { + warn!("{client} has rejected proposal {proposal_id}"); + false + }; + + Ok(accepted) + } + Err(err) => { + error!("failed to send proposal {proposal_id} for redemption vote to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later"); + Ok(false) + } + } + } + + async fn try_execute_proposal(&self, proposal_id: u64) -> Result<(), EcashTicketError> { + self.shared_state + .start_tx() + .await + .execute_proposal(proposal_id, None) + .await + .map_err( + |source| EcashTicketError::RedemptionProposalExecutionFailure { + proposal_id, + source, + }, + )?; + Ok(()) + } + + async fn get_proposal_status(&self, proposal_id: u64) -> Result { + Ok(self + .shared_state + .start_query() + .await + .query_proposal(proposal_id) + .await + .map_err(EcashTicketError::chain_query_failure)? + .status) + } + + async fn try_finalize_proposal( + &self, + proposal_id: u64, + ) -> Result { + match self.get_proposal_status(proposal_id).await? { + Status::Pending => { + // the voting hasn't even begun! + error!("impossible case! the proposal {proposal_id} is still pending"); + Ok(ProposalResult::Pending) + } + Status::Open => { + debug!("proposal {proposal_id} is still open and needs more votes"); + Ok(ProposalResult::Pending) + } + Status::Rejected => { + warn!("proposal {proposal_id} has been rejected"); + Ok(ProposalResult::Rejected) + } + Status::Passed => { + info!( + "proposal {proposal_id} has already been passed - we just need to execute it" + ); + self.try_execute_proposal(proposal_id).await?; + info!("executed proposal {proposal_id}"); + Ok(ProposalResult::Executed) + } + Status::Executed => { + info!("proposal {proposal_id} has already been executed - nothing to do!"); + Ok(ProposalResult::Executed) + } + } + } + + async fn try_resolve_pending_proposal( + &self, + pending: &mut PendingRedemptionVote, + api_clients: Option>>, + ) -> Result { + let proposal_id = pending.proposal_id; + + info!( + "attempting to resolve pending redemption proposal {proposal_id} to redeem {} tickets", + pending.included_serial_numbers.len() + ); + + // check if the proposal still needs more votes from the apis + let result = self.try_finalize_proposal(proposal_id).await?; + if !result.is_pending() { + return Ok(result); + } + + let api_clients = match api_clients { + Some(clients) => clients, + None => self.shared_state.api_clients(pending.epoch_id).await?, + }; + + let redemption_request = pending.to_request_body(self.shared_state.address.clone()); + + // TODO: optimisation: tell other apis they can purge our tickets even if they haven't voted + + let total = api_clients.len(); + let api_failures = Mutex::new(Vec::new()); + let rejected = AtomicUsize::new(0); + + for_each_api_concurrent(&api_clients, &pending.pending, |ecash_client| async { + // errors are only returned on hard, storage, failures + match self + .redeem_tickets(pending.proposal_id, &redemption_request, ecash_client) + .await + { + Err(err) => { + error!("internal failure. could not proceed with ticket redemption: {err}"); + api_failures.lock().await.push(ecash_client.node_id); + } + Ok(false) => { + rejected.fetch_add(1, Ordering::SeqCst); + } + _ => {} + } + }) + .await; + + let api_failures = api_failures.into_inner(); + let num_failures = api_failures.len(); + pending.pending = api_failures; + + let rejected = rejected.into_inner(); + let rejected_ratio = rejected as f32 / total as f32; + let rejected_perc = rejected_ratio * 100.; + if rejected_ratio >= (1. - self.multisig_threshold) { + error!("{rejected_perc:.2}% of signers rejected proposal {proposal_id}. we won't be able to execute it"); + // no need to query the chain as with so many rejections it's impossible it has passed. + return Ok(ProposalResult::Rejected); + } + + let accepted_ratio = (total - rejected - num_failures) as f32 / total as f32; + let accepted_perc = accepted_ratio * 100.; + match accepted_ratio { + n if n < self.multisig_threshold => { + error!("less than 2/3 of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}. we're not yet be able to execute it to get funds out"); + return Ok(ProposalResult::Pending); + } + n if n < self.config.minimum_api_quorum => { + warn!("the system seems to be a bit unstable: less than 80%, but more than 67% of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}"); + } + _ => { + trace!("{accepted_perc:.2}% of signers accepted proposal {proposal_id}"); + } + } + + // attempt to execute the proposal if it reached the required threshold + self.try_finalize_proposal(proposal_id).await + } + + async fn maybe_redeem_tickets(&mut self) -> Result<(), EcashTicketError> { + if !self.pending_tickets.is_empty() { + return Err(EcashTicketError::PendingTickets); + } + + let latest_stored = self.shared_state.storage.latest_proposal().await?; + + // check if we have already created the proposal but crashed before persisting it in the db + // + // if we have some persisted proposals in storage, try to see if there's anything more recent on chain + // (i.e. the missing proposal) + // if not (i.e. this would have been our first) check the latest page of proposals. + // while this is not ideal, realistically speaking we probably crashed few minutes ago + // and worst case scenario we'll just recreate the proposal instead + // + // LIMITATION: if MULTIPLE proposals got created in between, well. though luck. + let latest_on_chain = if let Some(latest_stored) = &latest_stored { + // those are sorted in ASCENDING way + self.shared_state + .proposals_since(latest_stored.proposal_id as u64) + .await? + .pop() + } else { + // but those are DESCENDING + self.shared_state + .last_proposal_page() + .await? + .first() + .cloned() + }; + + let now = OffsetDateTime::now_utc(); + + let prior_proposal = match (&latest_stored, latest_on_chain) { + (None, None) => { + // we haven't created any proposals before + trace!("this could be our first redemption proposal"); + None + } + (Some(stored), None) => { + if stored.created_at + MIN_BATCH_REDEMPTION_DELAY > now { + trace!("too soon to create new redemption proposal"); + return Ok(()); + } + None + } + (_, Some(on_chain)) => { + warn!("we seem to have crashed after creating proposal, but before persisting it onto disk!"); + + Some(on_chain) + } + }; + + // technically we could have been just caching all of those serial numbers as we verify tickets, + // but given how infrequently we call this, there's no point in wasting this memory + let verified_tickets = self + .shared_state + .storage + .get_all_verified_tickets_with_sn() + .await?; + + // TODO: somehow simplify that nasty nested if + if verified_tickets.len() < self.config.minimum_redemption_tickets { + // bypass the number of tickets check if we're about to lose our rewards due to expiration + if let Some(latest_stored) = latest_stored { + if latest_stored.created_at + self.config.maximum_time_between_redemption < now { + {} + } else { + info!("we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))", verified_tickets.len(), self.config.minimum_redemption_tickets); + return Ok(()); + } + } else { + // first proposal + info!("we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))", verified_tickets.len(), self.config.minimum_redemption_tickets); + return Ok(()); + } + } + + // this should have been ensured when querying + assert!(verified_tickets.len() <= u16::MAX as usize); + + let digest = + BatchRedeemTicketsBody::make_digest(verified_tickets.iter().map(|t| &t.serial_number)); + let encoded_digest = bs58::encode(&digest).into_string(); + + let prior_proposal_id = if let Some(prior_proposal) = prior_proposal { + if prior_proposal.description == encoded_digest { + info!("we have already created proposal for those tickets"); + Some(prior_proposal.id) + } else { + warn!( + "our missed proposal seem to have been for different tickets - abandoning it" + ); + None + } + } else { + None + }; + + // if the proposal has already existed on chain, do use it. otherwise create a new one + let proposal_id = if let Some(prior) = prior_proposal_id { + prior + } else { + self.create_redemption_proposal(&digest, verified_tickets.len() as u16) + .await? + }; + + if proposal_id > u32::MAX as u64 { + // realistically will we ever reach it? no. + panic!( + "we have created more than {} proposals. we can't handle that.", + u32::MAX + ) + } + + self.shared_state + .storage + .insert_redemption_proposal( + &verified_tickets, + proposal_id as u32, + OffsetDateTime::now_utc(), + ) + .await?; + + let current_epoch = self.shared_state.current_epoch_id().await?; + let api_clients = self.shared_state.api_clients(current_epoch).await?; + let ids = api_clients.iter().map(|c| c.node_id).collect(); + let mut pending = PendingRedemptionVote::new( + proposal_id, + digest, + verified_tickets + .into_iter() + .map(|t| t.serial_number) + .collect(), + current_epoch, + ids, + ); + + let resolution = self + .try_resolve_pending_proposal(&mut pending, Some(api_clients)) + .await?; + if resolution.is_pending() { + warn!("failed to reach quorum for proposal {proposal_id}. apis: {:?} haven't responded. we'll retry later", pending.pending); + self.pending_redemptions.push(pending); + } else { + self.shared_state + .storage + .clear_post_proposal_data( + proposal_id as u32, + OffsetDateTime::now_utc(), + resolution.is_rejected(), + ) + .await?; + } + + Ok(()) + } + + async fn periodic_operations(&mut self) -> Result<(), EcashTicketError> { + trace!("attempting to resolve all pending operations -> tickets that are waiting for verification and possibly redemption"); + + // 1. retry all operations that have failed in the past: verification requests and pending redemption + self.resolve_pending().await?; + + // 2. if applicable, attempt to redeem all newly verified tickets + self.maybe_redeem_tickets().await?; + + Ok(()) + } + + async fn run(mut self, mut shutdown: nym_task::TaskClient) { + info!("Starting Ecash CredentialSender"); + + // attempt to clear any pending operations + info!("attempting to resolve any pending operations"); + if let Err(err) = self.periodic_operations().await { + error!("failed to resolve pending operations on startup: {err}") + } + + let start = Instant::now() + self.config.pending_poller; + let mut resolver_interval = interval_at(start, self.config.pending_poller); + + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + trace!("client_handling::credentialSender : received shutdown"); + }, + Some(ticket) = self.ticket_receiver.next() => { + let (queued_up, _) = self.ticket_receiver.size_hint(); + + // this will help us determine if we need to parallelize it + match queued_up { + n if n < 5 => debug!("there are {n} tickets queued up that need processing"), + n if (5..20).contains(&n) => info!("there are {n} tickets queued up that need processing"), + n if (20..50).contains(&n) => warn!("there are {n} tickets queued up that need processing!"), + n => error!("there are {n} tickets queued up that need processing!"), + } + + self.handle_client_ticket(ticket).await + }, + _ = resolver_interval.tick() => { + if let Err(err) = self.periodic_operations().await { + error!("failed to deal with periodic operations: {err}") + } + } + } + } + } + + pub(crate) fn start(self, shutdown: nym_task::TaskClient) { + tokio::spawn(async move { self.run(shutdown).await }); + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/double_spending.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/double_spending.rs new file mode 100644 index 0000000000..a15966bbab --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/double_spending.rs @@ -0,0 +1,92 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +use crate::node::client_handling::websocket::connection_handler::ecash::state::SharedState; +use crate::node::Storage; +use nym_ecash_double_spending::DoubleSpendingFilter; +use nym_task::TaskClient; +use nym_validator_client::EcashApiClient; +use std::ops::Deref; +use std::sync::Arc; +use tokio::sync::{RwLock, RwLockReadGuard}; +use tokio::time::{interval, Duration}; +use tracing::{info, trace, warn}; + +#[derive(Clone)] +pub(crate) struct DoubleSpendingDetector { + spent_serial_numbers: Arc>, + shared_state: SharedState, +} + +impl DoubleSpendingDetector +where + S: Storage + Clone + Send + Sync + 'static, +{ + pub(crate) fn new(shared_state: SharedState) -> Self { + DoubleSpendingDetector { + spent_serial_numbers: Arc::new(RwLock::new(DoubleSpendingFilter::new_empty_ecash())), + shared_state, + } + } + + pub(crate) async fn check(&self, serial_number: &Vec) -> bool { + self.spent_serial_numbers.read().await.check(serial_number) + } + + async fn latest_api_endpoints( + &self, + ) -> Result>, EcashTicketError> { + let epoch_id = self.shared_state.current_epoch_id().await?; + self.shared_state.api_clients(epoch_id).await + } + + async fn refresh_bloomfilter(&self) { + //here be api query and union of different results + let mut filter_builder = self.spent_serial_numbers.read().await.rebuild(); + + let api_clients = match self.latest_api_endpoints().await { + Ok(clients) => clients, + Err(err) => { + warn!("failed to obtain current api clients: {err}"); + return; + } + }; + + for ecash_client in api_clients.deref().iter() { + match ecash_client.api_client.spent_credentials_filter().await { + Ok(response) => { + let added = filter_builder.add_bytes(&response.bitmap); + if !added { + warn!("Validator {ecash_client} gave us an incompatible bitmap for the double spending detector, we're gonna ignore it"); + } + } + Err(err) => { + warn!("Validator {ecash_client} could not be reached. There might be a problem with the coconut endpoint: {err}"); + } + } + } + + *self.spent_serial_numbers.write().await = filter_builder.build(); + } + + async fn run(&self, mut shutdown: TaskClient) { + info!("Starting Ecash DoubleSpendingDetector"); + let mut interval = interval(Duration::from_secs(300)); + + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + trace!("ecash_verifier::DoubleSpendingDetector : received shutdown"); + }, + _ = interval.tick() => self.refresh_bloomfilter().await, + + } + } + } + + pub(crate) fn start(self, shutdown: nym_task::TaskClient) { + tokio::spawn(async move { self.run(shutdown).await }); + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/error.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/error.rs new file mode 100644 index 0000000000..1494598a55 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/error.rs @@ -0,0 +1,83 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node::storage::error::StorageError; +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::error::NyxdError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum EcashTicketError { + // TODO: this should be more granual + #[error(transparent)] + ApiFailure(#[from] EcashApiError), + + #[error(transparent)] + CredentialError(#[from] nym_credentials::error::Error), + + #[error("the provided ticket failed to get verified")] + MalformedTicket, + + #[error("failed to verify provided ticket due to invalid expiration date signatures")] + MalformedTicketInvalidDateSignatures, + + #[error("provided payinfo's public key does not match provider's")] + InvalidPayInfoPublicKey, + + #[error("provided payinfo's timestamp is invalid")] + InvalidPayInfoTimestamp, + + #[error("received payinfo is a duplicate")] + DuplicatePayInfo, + + #[error("could not handle the ecash ticket due to internal storage failure: {source}")] + InternalStorageFailure { + #[from] + source: StorageError, + }, + + #[error("failed to create ticket redemption proposal: {source}")] + RedemptionProposalCreationFailure { + #[source] + source: NyxdError, + }, + + #[error("failed to execute ticket redemption proposal {proposal_id}: {source}")] + RedemptionProposalExecutionFailure { + proposal_id: u64, + + #[source] + source: NyxdError, + }, + + #[error("failed to parse out the redemption proposal id: {source}")] + ProposalIdParsingFailure { + #[source] + source: NyxdError, + }, + + #[error("failed to query the nyx chain: {source}")] + ChainQueryFailure { + #[source] + source: NyxdError, + }, + + #[error("Not enough nym API endpoints provided. Needed {needed}, received {received}")] + NotEnoughNymAPIs { received: usize, needed: usize }, + + #[error("the DKG contract is unavailable")] + UnavailableDkgContract, + + #[error("the DKG threshold value for epoch {epoch_id} is currently unavailable. we're probably mid-epoch transition")] + DKGThresholdUnavailable { epoch_id: EpochId }, + + #[error("could not create redemption proposal as we have tickets pending full verification")] + PendingTickets, +} + +impl EcashTicketError { + pub fn chain_query_failure(source: NyxdError) -> EcashTicketError { + EcashTicketError::ChainQueryFailure { source } + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/helpers.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/helpers.rs new file mode 100644 index 0000000000..9d767fba6c --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/helpers.rs @@ -0,0 +1,36 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use futures::{Stream, StreamExt}; +use nym_validator_client::EcashApiClient; +use std::future::Future; +use std::ops::Deref; +use tokio::sync::RwLockReadGuard; + +pub(crate) fn apis_stream<'a>( + // if needed we could make this argument more generic to accept either locks or iterators, etc. + all_clients: &'a RwLockReadGuard<'a, Vec>, + filter_by_id: &'a [u64], +) -> impl Stream + 'a { + // this vector will never contain more than ~30 entries so linear lookup is fine. + // it's probably even faster than hashset due to overhead + futures::stream::iter( + all_clients + .deref() + .iter() + .filter(|client| filter_by_id.contains(&client.node_id)), + ) +} + +pub(crate) async fn for_each_api_concurrent<'a, F, Fut>( + all_clients: &'a RwLockReadGuard<'a, Vec>, + filter_by_id: &'a [u64], + f: F, +) where + F: FnMut(&'a EcashApiClient) -> Fut, + Fut: Future, +{ + apis_stream(all_clients, filter_by_id) + .for_each_concurrent(32, f) + .await +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/mod.rs new file mode 100644 index 0000000000..40d82abd91 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/mod.rs @@ -0,0 +1,175 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::websocket::connection_handler::ecash::state::SharedState; +use crate::node::storage::Storage; +use crate::GatewayError; +use credential_sender::CredentialHandler; +use double_spending::DoubleSpendingDetector; +use futures::channel::mpsc::{self, UnboundedSender}; +use nym_credentials::CredentialSpendingData; +use nym_credentials_interface::{CompactEcashError, NymPayInfo, VerificationKeyAuth}; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use time::OffsetDateTime; +use tokio::sync::{Mutex, RwLockReadGuard}; +use tracing::error; + +use crate::node::client_handling::websocket::connection_handler::ecash::credential_sender::CredentialHandlerConfig; +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +pub use credential_sender::ClientTicket; + +pub(crate) mod credential_sender; +pub(crate) mod double_spending; +pub(crate) mod error; +mod helpers; +mod state; + +const TIME_RANGE_SEC: i64 = 30; + +pub struct EcashManager { + shared_state: SharedState, + + pk_bytes: [u8; 32], // bytes representation of a pub key representing the verifier + pay_infos: Mutex>, + cred_sender: UnboundedSender, + double_spend_detector: DoubleSpendingDetector, +} + +impl EcashManager +where + S: Storage + Clone + 'static, +{ + pub async fn new( + credential_handler_cfg: CredentialHandlerConfig, + nyxd_client: DirectSigningHttpRpcNyxdClient, + pk_bytes: [u8; 32], + shutdown: nym_task::TaskClient, + storage: S, + ) -> Result { + let shared_state = SharedState::new(nyxd_client, storage).await?; + + let double_spend_detector = DoubleSpendingDetector::new(shared_state.clone()); + double_spend_detector.clone().start(shutdown.clone()); + + let (cred_sender, cred_receiver) = mpsc::unbounded(); + + let cs = + CredentialHandler::new(credential_handler_cfg, cred_receiver, shared_state.clone()) + .await?; + cs.start(shutdown); + + Ok(EcashManager { + shared_state, + pk_bytes, + pay_infos: Default::default(), + cred_sender, + double_spend_detector, + }) + } + + pub async fn verification_key( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + self.shared_state.verification_key(epoch_id).await + } + + //Check for duplicate pay_info, then check the payment, then insert pay_info if everything succeeded + pub async fn check_payment( + &self, + credential: &CredentialSpendingData, + aggregated_verification_key: &VerificationKeyAuth, + ) -> Result<(), EcashTicketError> { + let insert_index = self.verify_pay_info(credential.pay_info.into()).await?; + + credential + .verify(aggregated_verification_key) + .map_err(|err| match err { + CompactEcashError::ExpirationDateSignatureValidity => { + EcashTicketError::MalformedTicketInvalidDateSignatures + } + _ => EcashTicketError::MalformedTicket, + })?; + + self.insert_pay_info(credential.pay_info.into(), insert_index) + .await + } + + pub async fn verify_pay_info(&self, pay_info: NymPayInfo) -> Result { + //Public key check + if pay_info.pk() != self.pk_bytes { + return Err(EcashTicketError::InvalidPayInfoPublicKey); + } + + //Timestamp range check + let timestamp = OffsetDateTime::now_utc().unix_timestamp(); + let tmin = timestamp - TIME_RANGE_SEC; + let tmax = timestamp + TIME_RANGE_SEC; + if pay_info.timestamp() > tmax || pay_info.timestamp() < tmin { + return Err(EcashTicketError::InvalidPayInfoTimestamp); + } + + let mut inner = self.pay_infos.lock().await; + + //Cleanup inner + let low = inner.partition_point(|x| x.timestamp() < tmin); + let high = inner.partition_point(|x| x.timestamp() < tmax); + inner.truncate(high); + drop(inner.drain(..low)); + + //Duplicate check + match inner.binary_search_by(|info| info.timestamp().cmp(&pay_info.timestamp())) { + Result::Err(index) => Ok(index), + Result::Ok(index) => { + if inner[index] == pay_info { + return Err(EcashTicketError::DuplicatePayInfo); + } + //tbh, I don't expect ending up here if all parties are honest + //binary search returns an arbitrary match, so we have to check for potential multiple matches + let mut i = index as i64; + while i >= 0 && inner[i as usize].timestamp() == pay_info.timestamp() { + if inner[i as usize] == pay_info { + return Err(EcashTicketError::DuplicatePayInfo); + } + i -= 1; + } + + let mut i = index + 1; + while i < inner.len() && inner[i].timestamp() == pay_info.timestamp() { + if inner[i] == pay_info { + return Err(EcashTicketError::DuplicatePayInfo); + } + i += 1; + } + Ok(index) + } + } + } + + async fn insert_pay_info( + &self, + pay_info: NymPayInfo, + index: usize, + ) -> Result<(), EcashTicketError> { + let mut inner = self.pay_infos.lock().await; + if index > inner.len() { + inner.push(pay_info); + return Ok(()); + } + inner.insert(index, pay_info); + Ok(()) + } + + pub async fn check_double_spend(&self, serial_number: &Vec) -> bool { + self.double_spend_detector.check(serial_number).await + } + + pub fn async_verify(&self, ticket: ClientTicket) { + // TODO: I guess do something for shutdowns + let _ = self + .cred_sender + .unbounded_send(ticket) + .inspect_err(|_| error!("failed to send the client ticket for verification task")); + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/ecash/state.rs b/gateway/src/node/client_handling/websocket/connection_handler/ecash/state.rs new file mode 100644 index 0000000000..57f39e95fd --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/ecash/state.rs @@ -0,0 +1,257 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node::client_handling::websocket::connection_handler::ecash::error::EcashTicketError; +use crate::node::Storage; +use crate::GatewayError; +use cosmwasm_std::{from_binary, CosmosMsg, WasmMsg}; +use nym_credentials_interface::VerificationKeyAuth; +use nym_ecash_contract_common::msg::ExecuteMsg; +use nym_validator_client::coconut::all_ecash_api_clients; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, MultisigQueryClient, NymContractsProvider, +}; +use nym_validator_client::nyxd::cw3::ProposalResponse; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, EcashApiClient}; +use std::collections::BTreeMap; +use std::ops::Deref; +use std::sync::Arc; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use tracing::{error, trace, warn}; + +// state shared by different subtasks dealing with credentials +#[derive(Clone)] +pub(crate) struct SharedState { + pub(crate) nyxd_client: Arc>, + pub(crate) address: AccountId, + pub(crate) epoch_data: Arc>>, + pub(crate) storage: S, +} + +impl SharedState +where + S: Storage + Clone, +{ + pub(crate) async fn new( + nyxd_client: DirectSigningHttpRpcNyxdClient, + storage: S, + ) -> Result { + let address = nyxd_client.address(); + + if nyxd_client.dkg_contract_address().is_none() { + error!("the DKG contract address is not available"); + return Err(EcashTicketError::UnavailableDkgContract.into()); + } + + let Ok(current_epoch) = nyxd_client.get_current_epoch().await else { + error!("the specified DKG contract address is invalid - no coconut credentials will be redeemable"); + // if we require coconut credentials, we MUST have DKG contract available + return Err(EcashTicketError::UnavailableDkgContract.into()); + }; + + let this = SharedState { + nyxd_client: Arc::new(RwLock::new(nyxd_client)), + address, + epoch_data: Arc::new(RwLock::new(BTreeMap::new())), + storage, + }; + + // might as well obtain the data for the current epoch, if applicable + if current_epoch.state.is_in_progress() { + if let Err(err) = this.set_epoch_data(current_epoch.epoch_id).await { + warn!("failed to set initial epoch data: {err}") + } + } + + Ok(this) + } + + fn created_redemption_proposal(&self, proposal: &ProposalResponse) -> bool { + let Some(msg) = proposal.msgs.first() else { + return false; + }; + let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = msg else { + return false; + }; + let Ok(ExecuteMsg::RedeemTickets { gw, .. }) = from_binary(msg) else { + return false; + }; + + gw == self.address.as_ref() + } + + /// retrieve all redemption proposals made by this gateway since, but excluding, the provided id + pub(crate) async fn proposals_since( + &self, + proposal_id: u64, + ) -> Result, EcashTicketError> { + Ok(self + .start_query() + .await + .list_proposals(Some(proposal_id), None) + .await + .map_err(EcashTicketError::chain_query_failure)? + .proposals + .into_iter() + .filter(|p| self.created_redemption_proposal(p)) + .collect()) + } + + /// retrieve all redemption proposals made by this gateway that are available on the last page of the query + pub(crate) async fn last_proposal_page( + &self, + ) -> Result, EcashTicketError> { + Ok(self + .start_query() + .await + .reverse_proposals(None, None) + .await + .map_err(EcashTicketError::chain_query_failure)? + .proposals + .into_iter() + .filter(|p| self.created_redemption_proposal(p)) + .collect()) + } + + async fn set_epoch_data( + &self, + epoch_id: EpochId, + ) -> Result>, EcashTicketError> { + let Some(threshold) = self.threshold(epoch_id).await? else { + return Err(EcashTicketError::DKGThresholdUnavailable { epoch_id }); + }; + + // TODO: optimise: query nym-apis for aggregate key instead + // (when the below code was originally written, that query didn't exist) + let api_clients = self.query_api_clients(epoch_id).await?; + + if api_clients.len() < threshold as usize { + return Err(EcashTicketError::NotEnoughNymAPIs { + received: api_clients.len(), + needed: threshold as usize, + }); + } + + let aggregated_verification_key = + nym_credentials::aggregate_verification_keys(&api_clients)?; + + let mut guard = self.epoch_data.write().await; + + self.storage + .insert_epoch_signers( + epoch_id as i64, + api_clients.iter().map(|c| c.node_id as i64).collect(), + ) + .await?; + + guard.insert( + epoch_id, + EpochState { + api_clients, + master_key: aggregated_verification_key, + threshold, + }, + ); + Ok(guard) + } + + async fn query_api_clients( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + Ok(all_ecash_api_clients(self.nyxd_client.read().await.deref(), epoch_id).await?) + } + + pub(crate) async fn threshold( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + self.nyxd_client + .read() + .await + .get_epoch_threshold(epoch_id) + .await + .map_err(EcashTicketError::chain_query_failure) + } + + pub(crate) async fn api_clients( + &self, + epoch_id: EpochId, + ) -> Result>, EcashTicketError> { + let guard = self.epoch_data.read().await; + + // the key was already in the map + if let Ok(mapped) = + RwLockReadGuard::try_map(guard, |data| data.get(&epoch_id).map(|d| &d.api_clients)) + { + trace!("we already had cached api clients for epoch {epoch_id}"); + return Ok(mapped); + } + + let write_guard = self.set_epoch_data(epoch_id).await?; + let guard = write_guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |data| { + data.get(&epoch_id).map(|d| &d.api_clients).unwrap() + })) + } + + pub(crate) async fn verification_key( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError> { + let guard = self.epoch_data.read().await; + + // the key was already in the map + if let Ok(mapped) = + RwLockReadGuard::try_map(guard, |data| data.get(&epoch_id).map(|d| &d.master_key)) + { + trace!("we already had cached api clients for epoch {epoch_id}"); + return Ok(mapped); + } + + let write_guard = self.set_epoch_data(epoch_id).await?; + let guard = write_guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |data| { + data.get(&epoch_id).map(|d| &d.master_key).unwrap() + })) + } + + pub(crate) async fn start_tx(&self) -> RwLockWriteGuard { + self.nyxd_client.write().await + } + + pub(crate) async fn start_query(&self) -> RwLockReadGuard { + self.nyxd_client.read().await + } + + pub(crate) async fn current_epoch_id(&self) -> Result { + Ok(self + .start_query() + .await + .get_current_epoch() + .await + .map_err(EcashTicketError::chain_query_failure)? + .epoch_id) + } +} + +pub(crate) struct EpochState { + // note: **CURRENTLY** api client addresses don't change during the epochs + pub(crate) api_clients: Vec, + pub(crate) master_key: VerificationKeyAuth, + + #[allow(unused)] + pub(crate) threshold: u64, +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 4f00ebcf90..611f25c22a 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -6,7 +6,6 @@ use futures::{ channel::{mpsc, oneshot}, SinkExt, StreamExt, }; -use log::*; use nym_crypto::asymmetric::identity; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, @@ -21,10 +20,12 @@ use nym_gateway_requests::{ use nym_mixnet_client::forwarder::MixForwardingSender; use nym_sphinx::DestinationAddressBytes; use rand::{CryptoRng, Rng}; +use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; +use tracing::*; use crate::node::client_handling::websocket::common_state::CommonHandlerState; use crate::node::client_handling::websocket::connection_handler::AvailableBandwidth; @@ -123,11 +124,11 @@ impl InitialAuthenticationError { pub(crate) struct FreshHandler { rng: R, - pub(crate) shared_state: CommonHandlerState, + pub(crate) shared_state: CommonHandlerState, pub(crate) active_clients_store: ActiveClientsStore, pub(crate) outbound_mix_sender: MixForwardingSender, pub(crate) socket_connection: SocketStream, - pub(crate) storage: St, + pub(crate) peer_address: SocketAddr, // currently unused (but populated) pub(crate) negotiated_protocol: Option, @@ -136,7 +137,7 @@ pub(crate) struct FreshHandler { impl FreshHandler where R: Rng + CryptoRng, - St: Storage, + St: Storage + Clone + 'static, { // for time being we assume handle is always constructed from raw socket. // if we decide we want to change it, that's not too difficult @@ -147,16 +148,16 @@ where rng: R, conn: S, outbound_mix_sender: MixForwardingSender, - storage: St, active_clients_store: ActiveClientsStore, - shared_state: CommonHandlerState, + shared_state: CommonHandlerState, + peer_address: SocketAddr, ) -> Self { FreshHandler { rng, active_clients_store, outbound_mix_sender, socket_connection: SocketStream::RawTcp(conn), - storage, + peer_address, negotiated_protocol: None, shared_state, } @@ -311,6 +312,7 @@ where loop { // retrieve some messages let (messages, new_start_next_after) = self + .shared_state .storage .retrieve_messages(client_address, start_next_after) .await?; @@ -326,7 +328,7 @@ where return Err(InitialAuthenticationError::ConnectionError(err)); } else { // if it was successful - remove them from the store - self.storage.remove_messages(ids).await?; + self.shared_state.storage.remove_messages(ids).await?; } // no more messages to grab @@ -356,7 +358,11 @@ where encrypted_address: EncryptedAddressBytes, iv: IV, ) -> Result, InitialAuthenticationError> { - let shared_keys = self.storage.get_shared_keys(client_address).await?; + let shared_keys = self + .shared_state + .storage + .get_shared_keys(client_address) + .await?; if let Some(shared_keys) = shared_keys { // this should never fail as we only ever construct persisted shared keys ourselves when inserting @@ -543,39 +549,42 @@ where .await?; } - let shared_keys = self + let Some(shared_keys) = self .authenticate_client(address, encrypted_address, iv) - .await?; - let status = shared_keys.is_some(); + .await? + else { + // it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour + return Ok(InitialAuthResult::new_failed(Some(negotiated_protocol))); + }; - let available_bandwidth: AvailableBandwidth = - self.storage.get_available_bandwidth(address).await?.into(); + let client_id = self.shared_state.storage.get_client_id(address).await?; - let bandwidth_remaining = if available_bandwidth.freepass_expired() { - self.expire_freepass(address).await?; + let available_bandwidth: AvailableBandwidth = self + .shared_state + .storage + .get_available_bandwidth(client_id) + .await? + .into(); + + let bandwidth_remaining = if available_bandwidth.expired() { + self.expire_bandwidth(client_id).await?; 0 } else { available_bandwidth.bytes }; - let client_details = - shared_keys.map(|shared_keys| ClientDetails::new(address, shared_keys)); - Ok(InitialAuthResult::new( - client_details, + Some(ClientDetails::new(client_id, address, shared_keys)), ServerResponse::Authenticate { protocol_version: Some(negotiated_protocol), - status, + status: true, bandwidth_remaining, }, )) } - pub(crate) async fn expire_freepass( - &self, - client: DestinationAddressBytes, - ) -> Result<(), StorageError> { - self.storage.reset_freepass_bandwidth(client).await + pub(crate) async fn expire_bandwidth(&self, client_id: i64) -> Result<(), StorageError> { + self.shared_state.storage.reset_bandwidth(client_id).await } /// Attempts to finalize registration of the client by storing the derived shared keys in the @@ -588,34 +597,41 @@ where /// * `client`: details (i.e. address and shared keys) of the registered client async fn register_client( &mut self, - client: &ClientDetails, - ) -> Result + client_address: DestinationAddressBytes, + client_shared_keys: &SharedKeys, + ) -> Result where S: AsyncRead + AsyncWrite + Unpin, { debug!( "Processing register client request for: {}", - client.address.as_base58_string() + client_address.as_base58_string() ); - self.storage - .insert_shared_keys(client.address, &client.shared_keys) + let client_id = self + .shared_state + .storage + .insert_shared_keys(client_address, client_shared_keys) .await?; // see if we have bandwidth entry for the client already, if not, create one with zero value if self + .shared_state .storage - .get_available_bandwidth(client.address) + .get_available_bandwidth(client_id) .await? .is_none() { - self.storage.create_bandwidth_entry(client.address).await?; + self.shared_state + .storage + .create_bandwidth_entry(client_id) + .await?; } - self.push_stored_messages_to_client(client.address, &client.shared_keys) + self.push_stored_messages_to_client(client_address, client_shared_keys) .await?; - Ok(true) + Ok(client_id) } /// Tries to handle the received register request by checking attempting to complete registration @@ -644,15 +660,15 @@ where } let shared_keys = self.perform_registration_handshake(init_data).await?; - let client_details = ClientDetails::new(remote_address, shared_keys); + let client_id = self.register_client(remote_address, &shared_keys).await?; - let status = self.register_client(&client_details).await?; + let client_details = ClientDetails::new(client_id, remote_address, shared_keys); Ok(InitialAuthResult::new( Some(client_details), ServerResponse::Register { protocol_version: Some(negotiated_protocol), - status, + status: true, }, )) } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index ee880d4832..94ff8398f7 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -3,7 +3,7 @@ use crate::config::Config; use crate::node::storage::Storage; -use log::{trace, warn}; +use nym_credentials::ecash::utils::ecash_today; use nym_gateway_requests::registration::handshake::SharedKeys; use nym_gateway_requests::ServerResponse; use nym_sphinx::DestinationAddressBytes; @@ -13,13 +13,14 @@ use std::time::Duration; use time::OffsetDateTime; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::WebSocketStream; +use tracing::{instrument, trace, warn}; use zeroize::{Zeroize, ZeroizeOnDrop}; pub(crate) use self::authenticated::AuthenticatedHandler; pub(crate) use self::fresh::FreshHandler; pub(crate) mod authenticated; -pub(crate) mod coconut; +pub(crate) mod ecash; mod fresh; // TODO: note for my future self to consider the following idea: @@ -44,13 +45,15 @@ impl SocketStream { pub(crate) struct ClientDetails { #[zeroize(skip)] pub(crate) address: DestinationAddressBytes, + pub(crate) id: i64, pub(crate) shared_keys: SharedKeys, } impl ClientDetails { - pub(crate) fn new(address: DestinationAddressBytes, shared_keys: SharedKeys) -> Self { + pub(crate) fn new(id: i64, address: DestinationAddressBytes, shared_keys: SharedKeys) -> Self { ClientDetails { address, + id, shared_keys, } } @@ -68,15 +71,28 @@ impl InitialAuthResult { server_response, } } + + fn new_failed(protocol_version: Option) -> Self { + InitialAuthResult { + client_details: None, + server_response: ServerResponse::Authenticate { + protocol_version, + status: false, + bandwidth_remaining: 0, + }, + } + } } +// imo there's no point in including the peer address in anything higher than debug +#[instrument(level = "debug", skip_all, fields(peer = %handle.peer_address))] pub(crate) async fn handle_connection( mut handle: FreshHandler, mut shutdown: TaskClient, ) where R: Rng + CryptoRng, S: AsyncRead + AsyncWrite + Unpin + Send, - St: Storage, + St: Storage + Clone + 'static, { // If the connection handler abruptly stops, we shouldn't signal global shutdown shutdown.mark_as_success(); @@ -136,27 +152,36 @@ impl<'a> From<&'a Config> for BandwidthFlushingBehaviourConfig { } } -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub(crate) struct AvailableBandwidth { pub(crate) bytes: i64, - pub(crate) freepass_expiration: Option, + pub(crate) expiration: OffsetDateTime, } impl AvailableBandwidth { - pub(crate) fn freepass_expired(&self) -> bool { - if let Some(expiration) = self.freepass_expiration { - if expiration < OffsetDateTime::now_utc() { - return true; - } + pub(crate) fn expired(&self) -> bool { + self.expiration < ecash_today() + } +} + +impl Default for AvailableBandwidth { + fn default() -> Self { + Self { + bytes: 0, + expiration: OffsetDateTime::UNIX_EPOCH, } - false } } pub(crate) struct ClientBandwidth { pub(crate) bandwidth: AvailableBandwidth, pub(crate) last_flushed: OffsetDateTime, - pub(crate) bytes_at_last_flush: i64, + + /// the number of bytes the client had during the last sync. + /// it is used to determine whether the current value should be synced with the storage + /// by checking the delta with the known amount + pub(crate) bytes_at_last_sync: i64, + pub(crate) bytes_delta_since_sync: i64, } impl ClientBandwidth { @@ -164,14 +189,13 @@ impl ClientBandwidth { ClientBandwidth { bandwidth, last_flushed: OffsetDateTime::now_utc(), - bytes_at_last_flush: bandwidth.bytes, + bytes_at_last_sync: bandwidth.bytes, + bytes_delta_since_sync: 0, } } - pub(crate) fn should_flush(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool { - if (self.bytes_at_last_flush - self.bandwidth.bytes).abs() - >= cfg.client_bandwidth_max_delta_flushing_amount - { + pub(crate) fn should_sync(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool { + if self.bytes_delta_since_sync.abs() >= cfg.client_bandwidth_max_delta_flushing_amount { return true; } @@ -182,8 +206,9 @@ impl ClientBandwidth { false } - pub(crate) fn update_flush_data(&mut self) { + pub(crate) fn update_sync_data(&mut self) { self.last_flushed = OffsetDateTime::now_utc(); - self.bytes_at_last_flush = self.bandwidth.bytes; + self.bytes_at_last_sync = self.bandwidth.bytes; + self.bytes_delta_since_sync = 0; } } diff --git a/gateway/src/node/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs index a33f6a0fb5..383e1b4b7d 100644 --- a/gateway/src/node/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -5,20 +5,23 @@ use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::websocket::common_state::CommonHandlerState; use crate::node::client_handling::websocket::connection_handler::FreshHandler; use crate::node::storage::Storage; -use log::*; use nym_mixnet_client::forwarder::MixForwardingSender; use rand::rngs::OsRng; use std::net::SocketAddr; use std::process; use tokio::task::JoinHandle; +use tracing::*; -pub(crate) struct Listener { +pub(crate) struct Listener { address: SocketAddr, - shared_state: CommonHandlerState, + shared_state: CommonHandlerState, } -impl Listener { - pub(crate) fn new(address: SocketAddr, shared_state: CommonHandlerState) -> Self { +impl Listener +where + S: Storage + Send + Sync + Clone + 'static, +{ + pub(crate) fn new(address: SocketAddr, shared_state: CommonHandlerState) -> Self { Listener { address, shared_state, @@ -27,15 +30,12 @@ impl Listener { // TODO: change the signature to pub(crate) async fn run(&self, handler: Handler) - pub(crate) async fn run( + pub(crate) async fn run( &mut self, outbound_mix_sender: MixForwardingSender, - storage: St, active_clients_store: ActiveClientsStore, mut shutdown: nym_task::TaskClient, - ) where - St: Storage + Clone + 'static, - { + ) { info!("Starting websocket listener at {}", self.address); let tcp_listener = match tokio::net::TcpListener::bind(self.address).await { Ok(listener) => listener, @@ -61,9 +61,9 @@ impl Listener { OsRng, socket, outbound_mix_sender.clone(), - storage.clone(), active_clients_store.clone(), self.shared_state.clone(), + remote_addr, ); let shutdown = shutdown.clone().named(format!("ClientConnectionHandler_{remote_addr}")); tokio::spawn(async move { handle.start_handling(shutdown).await }); @@ -76,18 +76,14 @@ impl Listener { } } - pub(crate) fn start( + pub(crate) fn start( mut self, outbound_mix_sender: MixForwardingSender, - storage: St, active_clients_store: ActiveClientsStore, shutdown: nym_task::TaskClient, - ) -> JoinHandle<()> - where - St: Storage + Clone + 'static, - { + ) -> JoinHandle<()> { tokio::spawn(async move { - self.run(outbound_mix_sender, storage, active_clients_store, shutdown) + self.run(outbound_mix_sender, active_clients_store, shutdown) .await }) } diff --git a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs index d07dc6489e..4cd1f20160 100644 --- a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs +++ b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs @@ -8,7 +8,6 @@ use crate::node::storage::error::StorageError; use crate::node::storage::Storage; use futures::channel::mpsc::SendError; use futures::StreamExt; -use log::*; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_mixnode_common::packet_processor::processor::ProcessedFinalHop; use nym_sphinx::forwarding::packet::MixPacket; @@ -21,6 +20,7 @@ use std::net::SocketAddr; use thiserror::Error; use tokio::net::TcpStream; use tokio_util::codec::Framed; +use tracing::*; // defines errors that warrant a panic if not thrown in the context of a shutdown #[derive(Debug, Error)] diff --git a/gateway/src/node/mixnet_handling/receiver/listener.rs b/gateway/src/node/mixnet_handling/receiver/listener.rs index 9ca22c77af..39d21cd814 100644 --- a/gateway/src/node/mixnet_handling/receiver/listener.rs +++ b/gateway/src/node/mixnet_handling/receiver/listener.rs @@ -3,11 +3,11 @@ use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use crate::node::storage::Storage; -use log::*; use nym_task::TaskClient; use std::net::SocketAddr; use std::process; use tokio::task::JoinHandle; +use tracing::*; pub(crate) struct Listener { address: SocketAddr, diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index c17c9dd4a8..a306a82779 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -12,11 +12,10 @@ use crate::http::HttpApiBuilder; use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, MessageRouter}; use crate::node::client_handling::websocket; -use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; +use crate::node::client_handling::websocket::connection_handler::ecash::EcashManager; use crate::node::helpers::{initialise_main_storage, load_network_requester_config}; use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use futures::channel::{mpsc, oneshot}; -use log::*; use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; use nym_network_defaults::NymNetworkDetails; @@ -30,13 +29,15 @@ use rand::thread_rng; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; +use tracing::*; pub(crate) mod client_handling; pub(crate) mod helpers; pub(crate) mod mixnet_handling; pub(crate) mod storage; -pub use storage::{InMemStorage, PersistentStorage, Storage}; +use crate::node::client_handling::websocket::connection_handler::ecash::credential_sender::CredentialHandlerConfig; +pub use storage::{PersistentStorage, Storage}; // TODO: should this struct live here? struct StartedNetworkRequester { @@ -327,9 +328,9 @@ impl Gateway { forwarding_channel: MixForwardingSender, active_clients_store: ActiveClientsStore, shutdown: TaskClient, - coconut_verifier: Arc, + ecash_verifier: Arc>, ) where - St: Storage + Clone + 'static, + St: Storage + Send + Sync + Clone + 'static, { info!("Starting client [web]socket listener..."); @@ -339,7 +340,8 @@ impl Gateway { ); let shared_state = websocket::CommonHandlerState { - coconut_verifier, + ecash_verifier, + storage: self.storage.clone(), local_identity: Arc::clone(&self.identity_keypair), only_coconut_credentials: self.config.gateway.only_coconut_credentials, bandwidth_cfg: (&self.config).into(), @@ -347,7 +349,6 @@ impl Gateway { websocket::Listener::new(listening_address, shared_state).start( forwarding_channel, - self.storage.clone(), active_clients_store, shutdown, ); @@ -576,8 +577,32 @@ impl Gateway { } } - let coconut_verifier = - CoconutVerifier::new(nyxd_client, self.config.gateway.only_coconut_credentials).await?; + let handler_config = CredentialHandlerConfig { + revocation_bandwidth_penalty: self + .config + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: self.config.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: self.config.debug.zk_nym_tickets.minimum_api_quorum, + minimum_redemption_tickets: self.config.debug.zk_nym_tickets.minimum_redemption_tickets, + maximum_time_between_redemption: self + .config + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }; + + let ecash_manager = { + EcashManager::new( + handler_config, + nyxd_client, + self.identity_keypair.public_key().to_bytes(), + shutdown.fork("EcashVerifier"), + self.storage.clone(), + ) + .await + }?; let mix_forwarding_channel = self.start_packet_forwarder(shutdown.fork("PacketForwarder")); @@ -592,7 +617,7 @@ impl Gateway { mix_forwarding_channel.clone(), active_clients_store.clone(), shutdown.fork("websocket::Listener"), - Arc::new(coconut_verifier), + Arc::new(ecash_manager), ); let nr_request_filter = if self.config.network_requester.enabled { diff --git a/gateway/src/node/storage/bandwidth.rs b/gateway/src/node/storage/bandwidth.rs index 1ebcccea85..991c77d82e 100644 --- a/gateway/src/node/storage/bandwidth.rs +++ b/gateway/src/node/storage/bandwidth.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::storage::models::{PersistedBandwidth, SpentCredential}; +use crate::node::storage::models::PersistedBandwidth; use time::OffsetDateTime; #[derive(Clone)] @@ -20,42 +20,32 @@ impl BandwidthManager { } /// Creates a new bandwidth entry for the particular client. - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. - pub(crate) async fn insert_new_client( - &self, - client_address_bs58: &str, - ) -> Result<(), sqlx::Error> { + pub(crate) async fn insert_new_client(&self, client_id: i64) -> Result<(), sqlx::Error> { + // FIXME: hack; we need to change api slightly sqlx::query!( - "INSERT INTO available_bandwidth(client_address_bs58, available) VALUES (?, 0)", - client_address_bs58 + "INSERT INTO available_bandwidth(client_id, available, expiration) VALUES (?, 0, ?)", + client_id, + OffsetDateTime::UNIX_EPOCH, ) .execute(&self.connection_pool) .await?; Ok(()) } - /// Set the freepass expiration date of the particular client to the provided date. - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. - /// * `freepass_expiration`: the expiration date of the associated free pass. - pub(crate) async fn set_freepass_expiration( + /// Set the expiration date of the particular client to the provided date. + pub(crate) async fn set_expiration( &self, - client_address_bs58: &str, - freepass_expiration: OffsetDateTime, + client_id: i64, + expiration: OffsetDateTime, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" UPDATE available_bandwidth - SET freepass_expiration = ? - WHERE client_address_bs58 = ? + SET expiration = ? + WHERE client_id = ? "#, - freepass_expiration, - client_address_bs58 + expiration, + client_id ) .execute(&self.connection_pool) .await?; @@ -63,21 +53,15 @@ impl BandwidthManager { } /// Reset all the bandwidth associated with the freepass and reset its expiration date - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. - pub(crate) async fn reset_freepass_bandwidth( - &self, - client_address_bs58: &str, - ) -> Result<(), sqlx::Error> { + pub(crate) async fn reset_bandwidth(&self, client_id: i64) -> Result<(), sqlx::Error> { sqlx::query!( r#" UPDATE available_bandwidth - SET available = 0, freepass_expiration = NULL - WHERE client_address_bs58 = ? + SET available = 0, expiration = ? + WHERE client_id = ? "#, - client_address_bs58 + OffsetDateTime::UNIX_EPOCH, + client_id ) .execute(&self.connection_pool) .await?; @@ -85,92 +69,92 @@ impl BandwidthManager { } /// Tries to retrieve available bandwidth for the particular client. - /// - /// # Arguments - /// - /// * `client_address_bs58`: base58-encoded address of the client. pub(crate) async fn get_available_bandwidth( &self, - client_address_bs58: &str, + client_id: i64, ) -> Result, sqlx::Error> { - sqlx::query_as("SELECT * FROM available_bandwidth WHERE client_address_bs58 = ?") - .bind(client_address_bs58) + sqlx::query_as("SELECT * FROM available_bandwidth WHERE client_id = ?") + .bind(client_id) .fetch_optional(&self.connection_pool) .await } - /// Sets available bandwidth of the particular client to the provided amount; - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `amount`: the updated client bandwidth amount. - pub(crate) async fn set_available_bandwidth( + pub(crate) async fn increase_bandwidth( &self, - client_address_bs58: &str, + client_id: i64, + amount: i64, + ) -> Result { + let mut tx = self.connection_pool.begin().await?; + sqlx::query!( + r#" + UPDATE available_bandwidth + SET available = available + ? + WHERE client_id = ? + "#, + amount, + client_id + ) + .execute(&mut tx) + .await?; + + let remaining = sqlx::query!( + "SELECT available FROM available_bandwidth WHERE client_id = ?", + client_id + ) + .fetch_one(&mut tx) + .await? + .available; + + tx.commit().await?; + Ok(remaining) + } + + pub(crate) async fn revoke_ticket_bandwidth( + &self, + ticket_id: i64, amount: i64, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" UPDATE available_bandwidth - SET available = ? - WHERE client_address_bs58 = ? + SET available = available - ? + WHERE client_id = (SELECT client_id FROM received_ticket WHERE id = ?) "#, amount, - client_address_bs58 + ticket_id, ) .execute(&self.connection_pool) .await?; Ok(()) } - /// Mark received credential as spent and insert it into the storage. - /// - /// # Arguments - /// - /// * `blinded_serial_number_bs58`: the unique blinded serial number embedded in the credential - /// * `was_freepass`: indicates whether the spent credential was a freepass - /// * `client_address_bs58`: address of the client that spent the credential - pub(crate) async fn insert_spent_credential( + pub(crate) async fn decrease_bandwidth( &self, - blinded_serial_number_bs58: &str, - was_freepass: bool, - client_address_bs58: &str, - ) -> Result<(), sqlx::Error> { + client_id: i64, + amount: i64, + ) -> Result { + let mut tx = self.connection_pool.begin().await?; sqlx::query!( r#" - INSERT INTO spent_credential - (blinded_serial_number_bs58, was_freepass, client_address_bs58) - VALUES (?, ?, ?) + UPDATE available_bandwidth + SET available = available - ? + WHERE client_id = ? "#, - blinded_serial_number_bs58, - was_freepass, - client_address_bs58 + amount, + client_id ) - .execute(&self.connection_pool) + .execute(&mut tx) .await?; - Ok(()) - } - /// Retrieve the spent credential with the provided blinded serial number from the storage. - /// - /// # Arguments - /// - /// * `blinded_serial_number_bs58`: the unique blinded serial number embedded in the credential - pub(crate) async fn retrieve_spent_credential( - &self, - blinded_serial_number_bs58: &str, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - SpentCredential, - r#" - SELECT * FROM spent_credential - WHERE blinded_serial_number_bs58 = ? - LIMIT 1 - "#, - blinded_serial_number_bs58, + let remaining = sqlx::query!( + "SELECT available FROM available_bandwidth WHERE client_id = ?", + client_id ) - .fetch_optional(&self.connection_pool) - .await + .fetch_one(&mut tx) + .await? + .available; + + tx.commit().await?; + Ok(remaining) } } diff --git a/gateway/src/node/storage/error.rs b/gateway/src/node/storage/error.rs index f908eac083..4cf292ea57 100644 --- a/gateway/src/node/storage/error.rs +++ b/gateway/src/node/storage/error.rs @@ -10,4 +10,10 @@ pub enum StorageError { #[error("Failed to perform database migration: {0}")] MigrationError(#[from] sqlx::migrate::MigrateError), + + #[error("Somehow stored data is incorrect: {0}")] + DataCorruption(String), + + #[error("the stored data associated with ticket {ticket_id} is malformed!")] + MalformedStoredTicketData { ticket_id: i64 }, } diff --git a/gateway/src/node/storage/mod.rs b/gateway/src/node/storage/mod.rs index e7123a44a6..d339fc779f 100644 --- a/gateway/src/node/storage/mod.rs +++ b/gateway/src/node/storage/mod.rs @@ -1,28 +1,37 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node::client_handling::websocket::connection_handler::ecash::ClientTicket; use crate::node::storage::bandwidth::BandwidthManager; use crate::node::storage::error::StorageError; use crate::node::storage::inboxes::InboxManager; -use crate::node::storage::models::{PersistedBandwidth, PersistedSharedKeys, StoredMessage}; +use crate::node::storage::models::{ + PersistedBandwidth, PersistedSharedKeys, RedemptionProposal, StoredMessage, VerifiedTicket, +}; use crate::node::storage::shared_keys::SharedKeysManager; +use crate::node::storage::tickets::TicketStorageManager; use async_trait::async_trait; -use log::{debug, error}; -use nym_credentials_interface::{Base58, BlindedSerialNumber}; use nym_gateway_requests::registration::handshake::SharedKeys; use nym_sphinx::DestinationAddressBytes; use sqlx::ConnectOptions; use std::path::Path; use time::OffsetDateTime; +use tracing::{debug, error}; mod bandwidth; pub(crate) mod error; mod inboxes; -mod models; +pub(crate) mod models; mod shared_keys; +mod tickets; #[async_trait] pub trait Storage: Send + Sync { + async fn get_client_id( + &self, + client_address: DestinationAddressBytes, + ) -> Result; + /// Inserts provided derived shared keys into the database. /// If keys previously existed for the provided client, they are overwritten with the new data. /// @@ -34,7 +43,7 @@ pub trait Storage: Send + Sync { &self, client_address: DestinationAddressBytes, shared_keys: &SharedKeys, - ) -> Result<(), StorageError>; + ) -> Result; /// Tries to retrieve shared keys stored for the particular client. /// @@ -94,81 +103,110 @@ pub trait Storage: Send + Sync { async fn remove_messages(&self, ids: Vec) -> Result<(), StorageError>; /// Creates a new bandwidth entry for the particular client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - async fn create_bandwidth_entry( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError>; + async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), StorageError>; /// Set the freepass expiration date of the particular client to the provided date. /// /// # Arguments /// /// * `client_address`: address of the client - /// * `freepass_expiration`: the expiration date of the associated free pass. - async fn set_freepass_expiration( + /// * `expiration`: the expiration date of the associated free pass. + async fn set_expiration( &self, - client_address: DestinationAddressBytes, - freepass_expiration: OffsetDateTime, + client_id: i64, + expiration: OffsetDateTime, ) -> Result<(), StorageError>; - /// Reset all the bandwidth associated with the freepass and reset its expiration date + /// Reset all the bandwidth /// /// # Arguments /// /// * `client_address`: address of the client - async fn reset_freepass_bandwidth( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError>; + async fn reset_bandwidth(&self, client_id: i64) -> Result<(), StorageError>; /// Tries to retrieve available bandwidth for the particular client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client async fn get_available_bandwidth( &self, - client_address: DestinationAddressBytes, + client_id: i64, ) -> Result, StorageError>; - /// Sets available bandwidth of the particular client to the provided amount; - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `amount`: the updated client bandwidth amount. - async fn set_bandwidth( + /// Increases specified client's bandwidth by the provided amount and returns the current value. + async fn increase_bandwidth(&self, client_id: i64, amount: i64) -> Result; + + async fn revoke_ticket_bandwidth( &self, - client_address: DestinationAddressBytes, + ticket_id: i64, amount: i64, ) -> Result<(), StorageError>; - /// Mark received credential as spent and insert it into the storage. - /// - /// # Arguments - /// - /// * `blinded_serial_number`: the unique blinded serial number embedded in the credential - /// * `client_address`: address of the client that spent the credential - async fn insert_spent_credential( + #[allow(dead_code)] + /// Decreases specified client's bandwidth by the provided amount and returns the current value. + async fn decrease_bandwidth(&self, client_id: i64, amount: i64) -> Result; + + async fn insert_epoch_signers( &self, - blinded_serial_number: BlindedSerialNumber, - was_freepass: bool, - client_address: DestinationAddressBytes, + epoch_id: i64, + signer_ids: Vec, ) -> Result<(), StorageError>; - /// Check if the credential with the provided blinded serial number if already present in the storage. + async fn insert_received_ticket( + &self, + client_id: i64, + received_at: OffsetDateTime, + serial_number: Vec, + data: Vec, + ) -> Result; + + // note: this only checks very recent tickets that haven't yet been redeemed + // (but it's better than nothing) + /// Check if the ticket with the provided serial number if already present in the storage. /// /// # Arguments /// - /// * `blinded_serial_number`: the unique blinded serial number embedded in the credential - async fn contains_credential( + /// * `serial_number`: the unique serial number embedded in the ticket + async fn contains_ticket(&self, serial_number: &[u8]) -> Result; + + async fn insert_ticket_verification( &self, - blinded_serial_number: &BlindedSerialNumber, - ) -> Result; + ticket_id: i64, + signer_id: i64, + verified_at: OffsetDateTime, + accepted: bool, + ) -> Result<(), StorageError>; + + async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), StorageError>; + + async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), StorageError>; + + async fn remove_verified_ticket_binary_data(&self, ticket_id: i64) -> Result<(), StorageError>; + + async fn get_all_verified_tickets_with_sn(&self) -> Result, StorageError>; + async fn get_all_proposed_tickets_with_sn( + &self, + proposal_id: u32, + ) -> Result, StorageError>; + + async fn insert_redemption_proposal( + &self, + tickets: &[VerifiedTicket], + proposal_id: u32, + created_at: OffsetDateTime, + ) -> Result<(), StorageError>; + + async fn clear_post_proposal_data( + &self, + proposal_id: u32, + resolved_at: OffsetDateTime, + rejected: bool, + ) -> Result<(), StorageError>; + + async fn latest_proposal(&self) -> Result, StorageError>; + + async fn get_all_unverified_tickets(&self) -> Result, StorageError>; + async fn get_all_unresolved_proposals(&self) -> Result, StorageError>; + async fn get_votes(&self, ticket_id: i64) -> Result, StorageError>; + + async fn get_signers(&self, epoch_id: i64) -> Result, StorageError>; } // note that clone here is fine as upon cloning the same underlying pool will be used @@ -177,6 +215,7 @@ pub struct PersistentStorage { shared_key_manager: SharedKeysManager, inbox_manager: InboxManager, bandwidth_manager: BandwidthManager, + ticket_manager: TicketStorageManager, } impl PersistentStorage { @@ -222,26 +261,37 @@ impl PersistentStorage { Ok(PersistentStorage { shared_key_manager: SharedKeysManager::new(connection_pool.clone()), inbox_manager: InboxManager::new(connection_pool.clone(), message_retrieval_limit), - bandwidth_manager: BandwidthManager::new(connection_pool), + bandwidth_manager: BandwidthManager::new(connection_pool.clone()), + ticket_manager: TicketStorageManager::new(connection_pool), }) } } #[async_trait] impl Storage for PersistentStorage { + async fn get_client_id( + &self, + client_address: DestinationAddressBytes, + ) -> Result { + Ok(self + .shared_key_manager + .client_id(&client_address.as_base58_string()) + .await?) + } + async fn insert_shared_keys( &self, client_address: DestinationAddressBytes, shared_keys: &SharedKeys, - ) -> Result<(), StorageError> { - let persisted_shared_keys = PersistedSharedKeys { - client_address_bs58: client_address.as_base58_string(), - derived_aes128_ctr_blake3_hmac_keys_bs58: shared_keys.to_base58_string(), - }; - self.shared_key_manager - .insert_shared_keys(persisted_shared_keys) + ) -> Result { + let client_id = self + .shared_key_manager + .insert_shared_keys( + client_address.as_base58_string(), + shared_keys.to_base58_string(), + ) .await?; - Ok(()) + Ok(client_id) } async fn get_shared_keys( @@ -296,194 +346,237 @@ impl Storage for PersistentStorage { Ok(()) } - async fn create_bandwidth_entry( + async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), StorageError> { + self.bandwidth_manager.insert_new_client(client_id).await?; + Ok(()) + } + + async fn set_expiration( &self, - client_address: DestinationAddressBytes, + client_id: i64, + expiration: OffsetDateTime, ) -> Result<(), StorageError> { self.bandwidth_manager - .insert_new_client(&client_address.as_base58_string()) + .set_expiration(client_id, expiration) .await?; Ok(()) } - async fn set_freepass_expiration( - &self, - client_address: DestinationAddressBytes, - freepass_expiration: OffsetDateTime, - ) -> Result<(), StorageError> { - self.bandwidth_manager - .set_freepass_expiration(&client_address.as_base58_string(), freepass_expiration) - .await?; - Ok(()) - } - - async fn reset_freepass_bandwidth( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - self.bandwidth_manager - .reset_freepass_bandwidth(&client_address.as_base58_string()) - .await?; + async fn reset_bandwidth(&self, client_id: i64) -> Result<(), StorageError> { + self.bandwidth_manager.reset_bandwidth(client_id).await?; Ok(()) } async fn get_available_bandwidth( &self, - client_address: DestinationAddressBytes, + client_id: i64, ) -> Result, StorageError> { Ok(self .bandwidth_manager - .get_available_bandwidth(&client_address.as_base58_string()) + .get_available_bandwidth(client_id) .await?) } - async fn set_bandwidth( + async fn increase_bandwidth(&self, client_id: i64, amount: i64) -> Result { + Ok(self + .bandwidth_manager + .increase_bandwidth(client_id, amount) + .await?) + } + + async fn revoke_ticket_bandwidth( &self, - client_address: DestinationAddressBytes, + ticket_id: i64, amount: i64, ) -> Result<(), StorageError> { - self.bandwidth_manager - .set_available_bandwidth(&client_address.as_base58_string(), amount) + Ok(self + .bandwidth_manager + .revoke_ticket_bandwidth(ticket_id, amount) + .await?) + } + + async fn decrease_bandwidth(&self, client_id: i64, amount: i64) -> Result { + Ok(self + .bandwidth_manager + .decrease_bandwidth(client_id, amount) + .await?) + } + + async fn insert_epoch_signers( + &self, + epoch_id: i64, + signer_ids: Vec, + ) -> Result<(), StorageError> { + self.ticket_manager + .insert_ecash_signers(epoch_id, signer_ids) .await?; Ok(()) } - async fn insert_spent_credential( + async fn insert_received_ticket( &self, - blinded_serial_number: BlindedSerialNumber, - was_freepass: bool, - client_address: DestinationAddressBytes, + client_id: i64, + received_at: OffsetDateTime, + serial_number: Vec, + data: Vec, + ) -> Result { + // technically if we crash between those 2 calls we'll have a bit of data inconsistency, + // but nothing too tragic. we just won't get paid for a single ticket + let ticket_id = self + .ticket_manager + .insert_new_ticket(client_id, received_at) + .await?; + self.ticket_manager + .insert_ticket_data(ticket_id, &serial_number, &data) + .await?; + + Ok(ticket_id) + } + + async fn contains_ticket(&self, serial_number: &[u8]) -> Result { + Ok(self.ticket_manager.has_ticket_data(serial_number).await?) + } + + async fn insert_ticket_verification( + &self, + ticket_id: i64, + signer_id: i64, + verified_at: OffsetDateTime, + accepted: bool, ) -> Result<(), StorageError> { - self.bandwidth_manager - .insert_spent_credential( - &blinded_serial_number.to_bs58(), - was_freepass, - &client_address.as_base58_string(), + self.ticket_manager + .insert_ticket_verification(ticket_id, signer_id, verified_at, accepted) + .await?; + Ok(()) + } + + async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), StorageError> { + // TODO: + error!("unimplemented: decrease clients bandwidth"); + + // set the ticket as rejected + self.ticket_manager.set_rejected_ticket(ticket_id).await?; + + // drop all ticket_data - we no longer need it + // TODO: or maybe we do as a proof of receiving bad data? + self.ticket_manager.remove_ticket_data(ticket_id).await?; + + Ok(()) + } + + async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), StorageError> { + // 1. insert into verified table + self.ticket_manager + .insert_verified_ticket(ticket_id) + .await?; + + // TODO: maybe we want to leave that be until ticket gets fully redeemed instead? + // 2. remove individual verifications + self.ticket_manager + .remove_ticket_verification(ticket_id) + .await?; + Ok(()) + } + + async fn remove_verified_ticket_binary_data(&self, ticket_id: i64) -> Result<(), StorageError> { + self.ticket_manager + .remove_binary_ticket_data(ticket_id) + .await?; + Ok(()) + } + + async fn get_all_verified_tickets_with_sn(&self) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_all_verified_tickets_with_sn() + .await?) + } + + async fn get_all_proposed_tickets_with_sn( + &self, + proposal_id: u32, + ) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_all_proposed_tickets_with_sn(proposal_id as i64) + .await?) + } + + async fn insert_redemption_proposal( + &self, + tickets: &[VerifiedTicket], + proposal_id: u32, + created_at: OffsetDateTime, + ) -> Result<(), StorageError> { + // if we crash between those, there might a bit of an issue. we should revisit it later + + // 1. insert the actual proposal + self.ticket_manager + .insert_redemption_proposal(proposal_id as i64, created_at) + .await?; + + // 2. update all the associated tickets + self.ticket_manager + .insert_verified_tickets_proposal_id( + tickets.iter().map(|t| t.ticket_id), + proposal_id as i64, ) .await?; Ok(()) } - async fn contains_credential( + async fn clear_post_proposal_data( &self, - blinded_serial_number: &BlindedSerialNumber, - ) -> Result { - let cred = self - .bandwidth_manager - .retrieve_spent_credential(&blinded_serial_number.to_bs58()) + proposal_id: u32, + resolved_at: OffsetDateTime, + rejected: bool, + ) -> Result<(), StorageError> { + // 1. update proposal metadata + self.ticket_manager + .update_redemption_proposal(proposal_id as i64, resolved_at, rejected) .await?; - Ok(cred.is_some()) - } -} - -/// In-memory implementation of `Storage`. The intention is primarily in testing environments. -#[derive(Clone)] -pub struct InMemStorage; - -//#[cfg(test)] -//impl InMemStorage { -// #[allow(unused)] -// async fn init + Send>() -> Result { -// todo!() -// } -//} - -#[cfg(test)] -#[async_trait] -impl Storage for InMemStorage { - async fn insert_shared_keys( - &self, - _client_address: DestinationAddressBytes, - _shared_keys: &SharedKeys, - ) -> Result<(), StorageError> { - todo!() - } - - async fn get_shared_keys( - &self, - _client_address: DestinationAddressBytes, - ) -> Result, StorageError> { - todo!() - } - - async fn remove_shared_keys( - &self, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn store_message( - &self, - _client_address: DestinationAddressBytes, - _message: Vec, - ) -> Result<(), StorageError> { - todo!() - } - - async fn retrieve_messages( - &self, - _client_address: DestinationAddressBytes, - _start_after: Option, - ) -> Result<(Vec, Option), StorageError> { - todo!() - } - - async fn remove_messages(&self, _ids: Vec) -> Result<(), StorageError> { - todo!() - } - - async fn create_bandwidth_entry( - &self, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn set_freepass_expiration( - &self, - _client_address: DestinationAddressBytes, - _freepass_expiration: OffsetDateTime, - ) -> Result<(), StorageError> { - todo!() - } - - async fn reset_freepass_bandwidth( - &self, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn get_available_bandwidth( - &self, - _client_address: DestinationAddressBytes, - ) -> Result, StorageError> { - todo!() - } - - async fn set_bandwidth( - &self, - _client_address: DestinationAddressBytes, - _amount: i64, - ) -> Result<(), StorageError> { - todo!() - } - - async fn insert_spent_credential( - &self, - _blinded_serial_number: BlindedSerialNumber, - _was_freepass: bool, - _client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { - todo!() - } - - async fn contains_credential( - &self, - _blinded_serial_number: &BlindedSerialNumber, - ) -> Result { - todo!() + // 2. remove ticket data rows (we can drop serial numbers) + self.ticket_manager + .remove_redeemed_tickets_data(proposal_id as i64) + .await?; + + // 3. remove verified tickets rows + self.ticket_manager + .remove_verified_tickets(proposal_id as i64) + .await?; + + Ok(()) + } + + async fn latest_proposal(&self) -> Result, StorageError> { + Ok(self.ticket_manager.get_latest_redemption_proposal().await?) + } + + async fn get_all_unverified_tickets(&self) -> Result, StorageError> { + self.ticket_manager + .get_unverified_tickets() + .await? + .into_iter() + .map(TryInto::try_into) + .collect() + } + + async fn get_all_unresolved_proposals(&self) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_all_unresolved_redemption_proposal_ids() + .await?) + } + + async fn get_votes(&self, ticket_id: i64) -> Result, StorageError> { + Ok(self + .ticket_manager + .get_verification_votes(ticket_id) + .await?) + } + + async fn get_signers(&self, epoch_id: i64) -> Result, StorageError> { + Ok(self.ticket_manager.get_epoch_signers(epoch_id).await?) } } diff --git a/gateway/src/node/storage/models.rs b/gateway/src/node/storage/models.rs index 999c9f827f..9a9f0145c0 100644 --- a/gateway/src/node/storage/models.rs +++ b/gateway/src/node/storage/models.rs @@ -1,11 +1,18 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node::client_handling::websocket::connection_handler::ecash::ClientTicket; use crate::node::client_handling::websocket::connection_handler::AvailableBandwidth; +use crate::node::storage::error::StorageError; +use nym_credentials_interface::CredentialSpendingData; use sqlx::FromRow; use time::OffsetDateTime; pub struct PersistedSharedKeys { + #[allow(dead_code)] + pub(crate) id: i64, + + #[allow(dead_code)] pub(crate) client_address_bs58: String, pub(crate) derived_aes128_ctr_blake3_hmac_keys_bs58: String, } @@ -22,14 +29,14 @@ pub struct PersistedBandwidth { #[allow(dead_code)] pub(crate) client_address_bs58: String, pub(crate) available: i64, - pub(crate) freepass_expiration: Option, + pub(crate) expiration: Option, } impl From for AvailableBandwidth { fn from(value: PersistedBandwidth) -> Self { AvailableBandwidth { bytes: value.available, - freepass_expiration: value.freepass_expiration, + expiration: value.expiration.unwrap_or(OffsetDateTime::UNIX_EPOCH), } } } @@ -43,12 +50,35 @@ impl From> for AvailableBandwidth { } } -#[derive(Debug, Clone, FromRow)] -pub struct SpentCredential { - #[allow(dead_code)] - pub(crate) blinded_serial_number_bs58: String, - #[allow(dead_code)] - pub(crate) was_freepass: bool, - #[allow(dead_code)] - pub(crate) client_address_bs58: String, +#[derive(FromRow)] +pub struct VerifiedTicket { + pub(crate) serial_number: Vec, + pub(crate) ticket_id: i64, +} + +#[derive(FromRow)] +pub struct RedemptionProposal { + pub(crate) proposal_id: i64, + pub(crate) created_at: OffsetDateTime, +} + +#[derive(FromRow)] +pub struct UnverifiedTicketData { + pub(crate) data: Vec, + pub(crate) ticket_id: i64, +} + +impl TryFrom for ClientTicket { + type Error = StorageError; + + fn try_from(value: UnverifiedTicketData) -> Result { + Ok(ClientTicket { + spending_data: CredentialSpendingData::try_from_bytes(&value.data).map_err(|_| { + StorageError::MalformedStoredTicketData { + ticket_id: value.ticket_id, + } + })?, + ticket_id: value.ticket_id, + }) + } } diff --git a/gateway/src/node/storage/shared_keys.rs b/gateway/src/node/storage/shared_keys.rs index d04ab12492..de1ce0abf7 100644 --- a/gateway/src/node/storage/shared_keys.rs +++ b/gateway/src/node/storage/shared_keys.rs @@ -18,6 +18,17 @@ impl SharedKeysManager { SharedKeysManager { connection_pool } } + pub(crate) async fn client_id(&self, client_address_bs58: &str) -> Result { + let client_id = sqlx::query!( + "SELECT id FROM shared_keys WHERE client_address_bs58 = ?", + client_address_bs58 + ) + .fetch_one(&self.connection_pool) + .await? + .id; + Ok(client_id) + } + /// Inserts provided derived shared keys into the database. /// If keys previously existed for the provided client, they are overwritten with the new data. /// @@ -26,13 +37,15 @@ impl SharedKeysManager { /// * `shared_keys`: shared encryption (AES128CTR) and mac (hmac-blake3) derived shared keys to store. pub(crate) async fn insert_shared_keys( &self, - shared_keys: PersistedSharedKeys, - ) -> Result<(), sqlx::Error> { + client_address_bs58: String, + derived_aes128_ctr_blake3_hmac_keys_bs58: String, + ) -> Result { sqlx::query!("INSERT OR REPLACE INTO shared_keys(client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58) VALUES (?, ?)", - shared_keys.client_address_bs58, - shared_keys.derived_aes128_ctr_blake3_hmac_keys_bs58, + client_address_bs58, + derived_aes128_ctr_blake3_hmac_keys_bs58, ).execute(&self.connection_pool).await?; - Ok(()) + + self.client_id(&client_address_bs58).await } /// Tries to retrieve shared keys stored for the particular client. diff --git a/gateway/src/node/storage/tickets.rs b/gateway/src/node/storage/tickets.rs new file mode 100644 index 0000000000..19a4287801 --- /dev/null +++ b/gateway/src/node/storage/tickets.rs @@ -0,0 +1,358 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::storage::models::{RedemptionProposal, UnverifiedTicketData, VerifiedTicket}; +use time::OffsetDateTime; + +#[derive(Clone)] +pub(crate) struct TicketStorageManager { + connection_pool: sqlx::SqlitePool, +} + +impl TicketStorageManager { + /// Creates new instance of the `CredentialManager` with the provided sqlite connection pool. + /// + /// # Arguments + /// + /// * `connection_pool`: database connection pool to use. + pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self { + TicketStorageManager { connection_pool } + } + + pub(crate) async fn insert_ecash_signers( + &self, + epoch_id: i64, + signer_ids: Vec, + ) -> Result<(), sqlx::Error> { + let mut query_builder = + sqlx::QueryBuilder::new("INSERT INTO ecash_signer (epoch_id, signer_id) "); + + query_builder.push_values(signer_ids, |mut b, signer_id| { + b.push_bind(epoch_id).push_bind(signer_id); + }); + + query_builder.build().execute(&self.connection_pool).await?; + Ok(()) + } + + pub(crate) async fn insert_new_ticket( + &self, + client_id: i64, + received_at: OffsetDateTime, + ) -> Result { + Ok(sqlx::query!( + "INSERT INTO received_ticket (client_id, received_at) VALUES (?, ?)", + client_id, + received_at + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid()) + } + + pub(crate) async fn set_rejected_ticket(&self, ticket_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE received_ticket SET rejected = true WHERE id = ?", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn insert_ticket_data( + &self, + ticket_id: i64, + serial_number: &[u8], + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO ticket_data(ticket_id, serial_number, data) VALUES (?, ?, ?)", + ticket_id, + serial_number, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_ticket_data(&self, ticket_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM ticket_data WHERE ticket_id = ?", ticket_id) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn has_ticket_data(&self, serial_number: &[u8]) -> Result { + sqlx::query!( + "SELECT EXISTS (SELECT 1 FROM ticket_data WHERE serial_number = ?) AS 'exists'", + serial_number + ) + .fetch_one(&self.connection_pool) + .await + .map(|result| result.exists == 1) + } + + pub(crate) async fn remove_binary_ticket_data( + &self, + ticket_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE ticket_data SET data = NULL WHERE ticket_id = ?", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_redeemed_tickets_data( + &self, + proposal_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM ticket_data + WHERE ticket_id IN ( + SELECT ticket_id + FROM verified_tickets + WHERE proposal_id = ? + ) + "#, + proposal_id + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_ticket_verification( + &self, + ticket_id: i64, + signer_id: i64, + verified_at: OffsetDateTime, + accepted: bool, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO ticket_verification (ticket_id, signer_id, verified_at, accepted) + VALUES (?, ?, ?, ?) + "#, + ticket_id, + signer_id, + verified_at, + accepted + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn remove_ticket_verification( + &self, + ticket_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM ticket_verification WHERE ticket_id = ?", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn insert_verified_ticket(&self, ticket_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO verified_tickets (ticket_id) VALUES (?)", + ticket_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + /// Attempts to retrieve ticket data for all tickets that that are **NOT** present in `verified_tickets` table + pub(crate) async fn get_unverified_tickets( + &self, + ) -> Result, sqlx::Error> { + // force not nullable `data` field as we explicitly ensured this via the query + sqlx::query_as!( + UnverifiedTicketData, + r#" + SELECT t1.ticket_id, t1.data as "data!" + FROM ticket_data as t1 + LEFT JOIN verified_tickets as t2 + ON t1.ticket_id = t2.ticket_id + WHERE + t2.ticket_id IS NULL + AND + t1.data IS NOT NULL + "# + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn get_epoch_signers(&self, epoch_id: i64) -> Result, sqlx::Error> { + sqlx::query!( + "SELECT signer_id FROM ecash_signer WHERE epoch_id = ?", + epoch_id + ) + .fetch_all(&self.connection_pool) + .await + .map(|records| records.into_iter().map(|r| r.signer_id).collect()) + } + + pub(crate) async fn get_verification_votes( + &self, + ticket_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query!( + "SELECT signer_id FROM ticket_verification WHERE ticket_id = ?", + ticket_id + ) + .fetch_all(&self.connection_pool) + .await + .map(|records| records.into_iter().map(|r| r.signer_id).collect()) + } + + pub(crate) async fn get_all_verified_tickets_with_sn( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + VerifiedTicket, + r#" + SELECT t1.ticket_id, t2.serial_number + FROM verified_tickets as t1 + JOIN ticket_data as t2 + ON t1.ticket_id = t2.ticket_id + JOIN received_ticket as t3 + ON t1.ticket_id = t3.id + + ORDER BY t3.received_at ASC + LIMIT 65535 + "# + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn get_all_proposed_tickets_with_sn( + &self, + proposal_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + VerifiedTicket, + r#" + SELECT t1.ticket_id, t2.serial_number + FROM verified_tickets as t1 + JOIN ticket_data as t2 + ON t1.ticket_id = t2.ticket_id + WHERE t1.proposal_id = ? + "#, + proposal_id + ) + .fetch_all(&self.connection_pool) + .await + } + + /// for each ticket in `verified_tickets` where the `ticket_id` is present in the provided iterator, + /// set the associated `proposal_id` to the provided value. + pub(crate) async fn insert_verified_tickets_proposal_id( + &self, + ticket_ids: I, + proposal_id: i64, + ) -> Result<(), sqlx::Error> + where + I: Iterator, + { + // UPDATE verified_tickets SET proposal_id = ... WHERE ticket_id IN (1,2,3,...) + let mut query_builder = + sqlx::QueryBuilder::new("UPDATE verified_tickets SET proposal_id = "); + query_builder + .push_bind(proposal_id) + .push("WHERE ticket_id IN ("); + + let mut separated = query_builder.separated(", "); + for ticket_id in ticket_ids { + separated.push_bind(ticket_id); + } + separated.push_unseparated(") "); + + query_builder.build().execute(&self.connection_pool).await?; + Ok(()) + } + + pub(crate) async fn remove_verified_tickets( + &self, + proposal_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM verified_tickets WHERE proposal_id = ?", + proposal_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn insert_redemption_proposal( + &self, + proposal_id: i64, + created_at: OffsetDateTime, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO redemption_proposals (proposal_id, created_at) VALUES (?, ?)", + proposal_id, + created_at + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn update_redemption_proposal( + &self, + proposal_id: i64, + resolved_at: OffsetDateTime, + rejected: bool, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE redemption_proposals SET resolved_at = ?, rejected = ? WHERE proposal_id = ?", + resolved_at, + rejected, + proposal_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_latest_redemption_proposal( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as( + r#" + SELECT proposal_id, created_at + FROM redemption_proposals + ORDER BY created_at DESC + LIMIT 1 + "#, + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn get_all_unresolved_redemption_proposal_ids( + &self, + ) -> Result, sqlx::Error> { + sqlx::query!("SELECT proposal_id FROM redemption_proposals WHERE resolved_at IS NULL") + .fetch_all(&self.connection_pool) + .await + .map(|records| records.into_iter().map(|r| r.proposal_id).collect()) + } +} diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index d75715be1a..2688275e7f 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -19,6 +19,8 @@ rust-version = "1.76.0" async-trait = { workspace = true } bs58 = { workspace = true } bip39 = { workspace = true } +bincode.workspace = true +bloomfilter = "1.0.13" cfg-if = { workspace = true } clap = { workspace = true, features = ["cargo", "derive"] } console-subscriber = { workspace = true, optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" @@ -52,6 +54,7 @@ sqlx = { workspace = true, features = [ "sqlite", "macros", "migrate", + "time", ] } okapi = { workspace = true, features = ["impl_json_schema"] } @@ -70,12 +73,16 @@ zeroize = { workspace = true } ## internal #ephemera = { path = "../ephemera" } nym-bandwidth-controller = { path = "../common/bandwidth-controller" } -nym-coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } +nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" } +nym-ecash-double-spending = { path = "../common/ecash-double-spending" } +nym-ecash-time = { path = "../common/ecash-time", features = ["expiration"] } nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" } +nym-compact-ecash = { path = "../common/nym_offline_compact_ecash" } +nym-credentials-interface = { path = "../common/credentials-interface" } #nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" } nym-config = { path = "../common/config" } cosmwasm-std = { workspace = true } -nym-credential-storage = { path = "../common/credential-storage" } +nym-credential-storage = { path = "../common/credential-storage", features = ["persistent-storage"] } nym-credentials = { path = "../common/credentials" } nym-crypto = { path = "../common/crypto" } cw2 = { workspace = true } @@ -93,7 +100,7 @@ nym-sphinx = { path = "../common/nymsphinx" } nym-pemstore = { path = "../common/pemstore" } nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } -nym-api-requests = { path = "nym-api-requests", features = ["request-parsing"] } +nym-api-requests = { path = "nym-api-requests", features = ["rocket-traits"] } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } nym-node-tester-utils = { path = "../common/node-tester-utils" } diff --git a/nym-api/migrations/20240708120000_ecash_tables.sql b/nym-api/migrations/20240708120000_ecash_tables.sql new file mode 100644 index 0000000000..68037fe83d --- /dev/null +++ b/nym-api/migrations/20240708120000_ecash_tables.sql @@ -0,0 +1,101 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + + +CREATE TABLE bloomfilter_parameters ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + num_hashes INTEGER NOT NULL, + bitmap_size INTEGER NOT NULL, + + sip0_key0 BLOB NOT NULL, + sip0_key1 BLOB NOT NULL, + + sip1_key0 BLOB NOT NULL, + sip1_key1 BLOB NOT NULL +); + +-- table containing partial bloomfilters produced from tickets spent on particular date +-- the 'current' bloomfilter is always OR(last_30) +CREATE TABLE partial_bloomfilter ( + date DATE NOT NULL UNIQUE PRIMARY KEY, + parameters INTEGER NOT NULL REFERENCES bloomfilter_parameters(id), + bitmap BLOB NOT NULL +); + +CREATE TABLE master_verification_key ( + epoch_id INTEGER PRIMARY KEY NOT NULL, + serialised_key BLOB NOT NULL +); + +CREATE TABLE global_coin_index_signatures ( + -- we can only have a single entry + epoch_id INTEGER PRIMARY KEY NOT NULL, + + -- combined signatures for all indices + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE partial_coin_index_signatures ( + -- we can only have a single entry + epoch_id INTEGER PRIMARY KEY NOT NULL, + + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE global_expiration_date_signatures ( + expiration_date DATE NOT NULL UNIQUE PRIMARY KEY, + + epoch_id INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE partial_expiration_date_signatures ( + expiration_date DATE NOT NULL UNIQUE PRIMARY KEY, + + epoch_id INTEGER NOT NULL, + + serialised_signatures BLOB NOT NULL +); + + +DROP TABLE issued_credential; + +-- particular **partial** ecash credential issued in this epoch +CREATE TABLE issued_credential +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + epoch_id INTEGER NOT NULL, + deposit_id INTEGER NOT NULL UNIQUE, +-- at some point those should be blobified + bs58_partial_credential VARCHAR NOT NULL, + bs58_signature VARCHAR NOT NULL, + joined_private_commitments VARCHAR NOT NULL, + expiration_date DATE NOT NULL +); + +CREATE TABLE ticket_providers +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + gateway_address TEXT NOT NULL UNIQUE, + last_batch_verification TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE verified_tickets +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ticket_data BLOB NOT NULL, + serial_number BLOB NOT NULL UNIQUE, + spending_date DATE NOT NULL, + verified_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + gateway_id INTEGER NOT NULL REFERENCES ticket_providers(id) +); + +-- helper index for getting tickets verified by particular gateway +CREATE INDEX verified_tickets_index ON verified_tickets (gateway_id, verified_at desc); + +-- helper index for getting all tickets with particular spending date for rebuilding the bloomfilters +CREATE INDEX verified_tickets_spending_index ON verified_tickets (spending_date); \ No newline at end of file diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 7ed5d2ad18..d908694c5d 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -16,14 +16,20 @@ serde = { workspace = true, features = ["derive"] } ts-rs = { workspace = true, optional = true } tendermint = { workspace = true } time = { workspace = true, features = ["serde", "parsing", "formatting"] } +thiserror.workspace = true rocket = { workspace = true, optional = true } +sha2 = "0.10.8" + # for serde on secp256k1 signatures ecdsa = { workspace = true, features = ["serde"] } +serde-helpers = { path = "../../common/serde-helpers", features = ["bs58", "base64"] } nym-credentials-interface = { path = "../../common/credentials-interface" } nym-crypto = { path = "../../common/crypto", features = ["serde", "asymmetric"] } +nym-ecash-time = { path = "../../common/ecash-time" } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false } @@ -33,5 +39,5 @@ serde_json.workspace = true [features] default = [] -request-parsing = ["rocket"] +rocket-traits = ["rocket"] generate-ts = ["ts-rs", "nym-mixnet-contract-common/generate-ts"] diff --git a/nym-api/nym-api-requests/src/coconut/models.rs b/nym-api/nym-api-requests/src/coconut/models.rs deleted file mode 100644 index 50df750a82..0000000000 --- a/nym-api/nym-api-requests/src/coconut/models.rs +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright 2023-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::coconut::helpers::issued_credential_plaintext; -use cosmrs::AccountId; -use nym_credentials_interface::{ - hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, Bytable, CoconutError, - CredentialSpendingData, VerificationKey, -}; -use nym_crypto::asymmetric::identity; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use tendermint::hash::Hash; - -#[derive(Serialize, Deserialize)] -pub struct VerifyCredentialBody { - /// The cryptographic material required for spending the underlying credential. - pub credential_data: CredentialSpendingData, - - /// Multisig proposal for releasing funds for the provided bandwidth credential - pub proposal_id: u64, - - /// Cosmos address of the spender of the credential - pub gateway_cosmos_addr: AccountId, -} - -impl VerifyCredentialBody { - pub fn new( - credential_data: CredentialSpendingData, - proposal_id: u64, - gateway_cosmos_addr: AccountId, - ) -> VerifyCredentialBody { - VerifyCredentialBody { - credential_data, - proposal_id, - gateway_cosmos_addr, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct VerifyCredentialResponse { - pub verification_result: bool, -} - -impl VerifyCredentialResponse { - pub fn new(verification_result: bool) -> Self { - VerifyCredentialResponse { - verification_result, - } - } -} - -// All strings are base58 encoded representations of structs -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct BlindSignRequestBody { - pub inner_sign_request: BlindSignRequest, - - /// Hash of the deposit transaction - pub tx_hash: Hash, - - /// Signature on the inner sign request and the tx hash - pub signature: identity::Signature, - - pub public_attributes_plain: Vec, -} - -impl BlindSignRequestBody { - pub fn new( - inner_sign_request: BlindSignRequest, - tx_hash: Hash, - signature: identity::Signature, - public_attributes_plain: Vec, - ) -> BlindSignRequestBody { - BlindSignRequestBody { - inner_sign_request, - tx_hash, - signature, - public_attributes_plain, - } - } - - pub fn attributes(&self) -> u32 { - (self.public_attributes_plain.len() + self.inner_sign_request.num_private_attributes()) - as u32 - } - - pub fn public_attributes_hashed(&self) -> Vec { - self.public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect() - } - - pub fn encode_commitments(&self) -> Vec { - use nym_credentials_interface::Base58; - - self.inner_sign_request - .get_private_attributes_pedersen_commitments() - .iter() - .map(|c| c.to_bs58()) - .collect() - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FreePassNonceResponse { - pub current_nonce: [u8; 16], -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct BlindedSignatureResponse { - pub blinded_signature: BlindedSignature, -} - -impl BlindedSignatureResponse { - pub fn new(blinded_signature: BlindedSignature) -> BlindedSignatureResponse { - BlindedSignatureResponse { blinded_signature } - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(&self.to_bytes()).into_string() - } - - pub fn from_base58_string>(val: I) -> Result { - let bytes = bs58::decode(val).into_vec()?; - Self::from_bytes(&bytes) - } - - pub fn to_bytes(&self) -> Vec { - self.blinded_signature.to_byte_vec() - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - Ok(BlindedSignatureResponse { - blinded_signature: BlindedSignature::from_bytes(bytes)?, - }) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FreePassRequest { - // secp256k1 key associated with the admin account - pub cosmos_pubkey: cosmrs::crypto::PublicKey, - - pub inner_sign_request: BlindSignRequest, - - // we need to include a nonce here to prevent replay attacks - // (and not making the nym-api store the serial numbers of all issued credential) - pub used_nonce: [u8; 16], - - /// Signature on the nonce - /// to prove the possession of the cosmos key/address - pub nonce_signature: cosmrs::crypto::secp256k1::Signature, - - pub public_attributes_plain: Vec, -} - -impl FreePassRequest { - pub fn new( - cosmos_pubkey: cosmrs::crypto::PublicKey, - inner_sign_request: BlindSignRequest, - used_nonce: [u8; 16], - nonce_signature: cosmrs::crypto::secp256k1::Signature, - public_attributes_plain: Vec, - ) -> Self { - FreePassRequest { - cosmos_pubkey, - inner_sign_request, - used_nonce, - nonce_signature, - public_attributes_plain, - } - } - - pub fn tendermint_pubkey(&self) -> tendermint::PublicKey { - self.cosmos_pubkey.into() - } - - pub fn public_attributes_hashed(&self) -> Vec { - self.public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect() - } -} - -#[derive(Serialize, Deserialize)] -pub struct VerificationKeyResponse { - pub key: VerificationKey, -} - -impl VerificationKeyResponse { - pub fn new(key: VerificationKey) -> VerificationKeyResponse { - VerificationKeyResponse { key } - } -} - -#[derive(Serialize, Deserialize)] -pub struct CosmosAddressResponse { - pub addr: AccountId, -} - -impl CosmosAddressResponse { - pub fn new(addr: AccountId) -> CosmosAddressResponse { - CosmosAddressResponse { addr } - } -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Pagination { - /// last_key is the last value returned in the previous query. - /// it's used to indicate the start of the next (this) page. - /// the value itself is not included in the response. - pub last_key: Option, - - /// limit is the total number of results to be returned in the result page. - /// If left empty it will default to a value to be set by each app. - pub limit: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct CredentialsRequestBody { - /// Explicit ids of the credentials to retrieve. Note: it can't be set alongside pagination. - pub credential_ids: Vec, - - /// Pagination settings for retrieving credentials. Note: it can't be set alongside explicit ids. - pub pagination: Option>, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct EpochCredentialsResponse { - pub epoch_id: u64, - pub first_epoch_credential_id: Option, - pub total_issued: u32, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredentialsResponse { - // note: BTreeMap returns ordered results so it's fine to use it with pagination - pub credentials: BTreeMap, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredentialResponse { - pub credential: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredentialBody { - pub credential: IssuedCredential, - - pub signature: identity::Signature, -} - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct IssuedCredential { - pub id: i64, - pub epoch_id: u32, - pub tx_hash: Hash, - - // NOTE: if we find creation of this guy takes too long, - // change `BlindedSignature` to `BlindedSignatureBytes` - // so that nym-api wouldn't need to parse the value out of its storage - pub blinded_partial_credential: BlindedSignature, - pub bs58_encoded_private_attributes_commitments: Vec, - pub public_attributes: Vec, -} - -impl IssuedCredential { - // this method doesn't have to be reversible so just naively concatenate everything - pub fn signable_plaintext(&self) -> Vec { - issued_credential_plaintext( - self.epoch_id, - self.tx_hash, - &self.blinded_partial_credential, - &self.bs58_encoded_private_attributes_commitments, - &self.public_attributes, - ) - } -} diff --git a/nym-api/nym-api-requests/src/constants.rs b/nym-api/nym-api-requests/src/constants.rs new file mode 100644 index 0000000000..f3bee4522c --- /dev/null +++ b/nym-api/nym-api-requests/src/constants.rs @@ -0,0 +1,7 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use time::Duration; + +// we should probably monitor this constant and adjust it when/if required +pub const MIN_BATCH_REDEMPTION_DELAY: Duration = Duration::DAY; diff --git a/nym-api/nym-api-requests/src/coconut/helpers.rs b/nym-api/nym-api-requests/src/ecash/helpers.rs similarity index 69% rename from nym-api/nym-api-requests/src/coconut/helpers.rs rename to nym-api/nym-api-requests/src/ecash/helpers.rs index 73d1a7f42f..154b733f9f 100644 --- a/nym-api/nym-api-requests/src/coconut/helpers.rs +++ b/nym-api/nym-api-requests/src/ecash/helpers.rs @@ -1,33 +1,30 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_credentials_interface::BlindedSignature; -use tendermint::hash::Hash; +use nym_compact_ecash::BlindedSignature; +use nym_ecash_time::EcashTime; +use time::Date; // recomputes plaintext on the credential nym-api has used for signing // // note: this method doesn't have to be reversible so just naively concatenate everything pub fn issued_credential_plaintext( epoch_id: u32, - tx_hash: Hash, + deposit_id: u32, blinded_partial_credential: &BlindedSignature, bs58_encoded_private_attributes_commitments: &[String], - public_attributes: &[String], + expiration_date: Date, ) -> Vec { epoch_id .to_be_bytes() .into_iter() - .chain(tx_hash.as_bytes().iter().copied()) + .chain(deposit_id.to_be_bytes()) .chain(blinded_partial_credential.to_bytes()) .chain( bs58_encoded_private_attributes_commitments .iter() .flat_map(|attr| attr.as_bytes().iter().copied()), ) - .chain( - public_attributes - .iter() - .flat_map(|attr| attr.as_bytes().iter().copied()), - ) + .chain(expiration_date.ecash_unix_timestamp().to_be_bytes()) .collect() } diff --git a/nym-api/nym-api-requests/src/coconut/mod.rs b/nym-api/nym-api-requests/src/ecash/mod.rs similarity index 59% rename from nym-api/nym-api-requests/src/coconut/mod.rs rename to nym-api/nym-api-requests/src/ecash/mod.rs index 1971f0b8f8..60f3c1c41c 100644 --- a/nym-api/nym-api-requests/src/coconut/mod.rs +++ b/nym-api/nym-api-requests/src/ecash/mod.rs @@ -5,6 +5,7 @@ pub mod helpers; pub mod models; pub use models::{ - BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, FreePassRequest, - VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, + PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, + VerificationKeyResponse, VerifyEcashCredentialBody, }; diff --git a/nym-api/nym-api-requests/src/ecash/models.rs b/nym-api/nym-api-requests/src/ecash/models.rs new file mode 100644 index 0000000000..b22fcd7e1d --- /dev/null +++ b/nym-api/nym-api-requests/src/ecash/models.rs @@ -0,0 +1,447 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::helpers::issued_credential_plaintext; +use crate::helpers::PlaceholderJsonSchemaImpl; +use cosmrs::AccountId; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_credentials_interface::{ + BlindedSignature, CompactEcashError, CredentialSpendingData, PublicKeyUser, + VerificationKeyAuth, WithdrawalRequest, +}; +use nym_crypto::asymmetric::identity; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use sha2::Digest; +use std::collections::BTreeMap; +use std::ops::Deref; +use thiserror::Error; +use time::Date; + +#[derive(Serialize, Deserialize, Clone, JsonSchema)] +pub struct VerifyEcashTicketBody { + /// The cryptographic material required for spending the underlying credential. + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub credential: CredentialSpendingData, + + /// Cosmos address of the sender of the credential + #[schemars(with = "String")] + pub gateway_cosmos_addr: AccountId, +} + +#[derive(Serialize, Deserialize, Clone, JsonSchema)] +pub struct VerifyEcashCredentialBody { + /// The cryptographic material required for spending the underlying credential. + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub credential: CredentialSpendingData, + + /// Cosmos address of the sender of the credential + #[schemars(with = "String")] + pub gateway_cosmos_addr: AccountId, + + /// Multisig proposal for releasing funds for the provided bandwidth credential + pub proposal_id: u64, +} + +impl VerifyEcashCredentialBody { + pub fn new( + credential: CredentialSpendingData, + gateway_cosmos_addr: AccountId, + proposal_id: u64, + ) -> VerifyEcashCredentialBody { + VerifyEcashCredentialBody { + credential, + gateway_cosmos_addr, + proposal_id, + } + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct EcashTicketVerificationResponse { + pub verified: Result<(), EcashTicketVerificationRejection>, +} + +impl EcashTicketVerificationResponse { + pub fn reject(reason: EcashTicketVerificationRejection) -> Self { + EcashTicketVerificationResponse { + verified: Err(reason), + } + } +} + +#[derive(Debug, Error, Serialize, Deserialize, JsonSchema)] +pub enum EcashTicketVerificationRejection { + #[error("invalid ticket spent date. expected either today's ({today}) or yesterday's* ({yesterday}) date but got {received} instead\n*assuming it's before 1AM UTC")] + InvalidSpentDate { + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + today: Date, + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + yesterday: Date, + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + received: Date, + }, + + #[error("this ticket has already been received before")] + ReplayedTicket, + + #[error("this ticket has already been spent before")] + DoubleSpend, + + #[error("failed to verify the provided ticket")] + InvalidTicket, +} + +// All strings are base58 encoded representations of structs +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] +pub struct BlindSignRequestBody { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub inner_sign_request: WithdrawalRequest, + + /// the id of the associated deposit + pub deposit_id: u32, + + /// Signature on the inner sign request and the tx hash + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signature: identity::Signature, + + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub ecash_pubkey: PublicKeyUser, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, +} + +impl BlindSignRequestBody { + pub fn new( + inner_sign_request: WithdrawalRequest, + deposit_id: u32, + signature: identity::Signature, + ecash_pubkey: PublicKeyUser, + expiration_date: Date, + ) -> BlindSignRequestBody { + BlindSignRequestBody { + inner_sign_request, + deposit_id, + signature, + ecash_pubkey, + expiration_date, + } + } + + pub fn encode_commitments(&self) -> Vec { + use nym_compact_ecash::Base58; + + self.inner_sign_request + .get_private_attributes_commitments() + .iter() + .map(|c| c.to_bs58()) + .collect() + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct BlindedSignatureResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub blinded_signature: BlindedSignature, +} + +impl BlindedSignatureResponse { + pub fn new(blinded_signature: BlindedSignature) -> BlindedSignatureResponse { + BlindedSignatureResponse { blinded_signature } + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: I) -> Result { + let bytes = bs58::decode(val).into_vec()?; + Self::from_bytes(&bytes) + } + + pub fn to_bytes(&self) -> Vec { + self.blinded_signature.to_bytes().to_vec() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(BlindedSignatureResponse { + blinded_signature: BlindedSignature::from_bytes(bytes)?, + }) + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct MasterVerificationKeyResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub key: VerificationKeyAuth, +} + +impl MasterVerificationKeyResponse { + pub fn new(key: VerificationKeyAuth) -> MasterVerificationKeyResponse { + MasterVerificationKeyResponse { key } + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct VerificationKeyResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub key: VerificationKeyAuth, +} + +impl VerificationKeyResponse { + pub fn new(key: VerificationKeyAuth) -> VerificationKeyResponse { + VerificationKeyResponse { key } + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct CosmosAddressResponse { + #[schemars(with = "String")] + pub addr: AccountId, +} + +impl CosmosAddressResponse { + pub fn new(addr: AccountId) -> CosmosAddressResponse { + CosmosAddressResponse { addr } + } +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct PartialExpirationDateSignatureResponse { + pub epoch_id: u64, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct PartialCoinIndicesSignatureResponse { + pub epoch_id: u64, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct AggregatedExpirationDateSignatureResponse { + pub epoch_id: u64, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, + + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct AggregatedCoinIndicesSignatureResponse { + pub epoch_id: u64, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signatures: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +pub struct Pagination { + /// last_key is the last value returned in the previous query. + /// it's used to indicate the start of the next (this) page. + /// the value itself is not included in the response. + pub last_key: Option, + + /// limit is the total number of results to be returned in the result page. + /// If left empty it will default to a value to be set by each app. + pub limit: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct CredentialsRequestBody { + /// Explicit ids of the credentials to retrieve. Note: it can't be set alongside pagination. + pub credential_ids: Vec, + + /// Pagination settings for retrieving credentials. Note: it can't be set alongside explicit ids. + pub pagination: Option>, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, PartialEq)] +pub struct SerialNumberWrapper( + #[serde(with = "serde_helpers::bs58")] + #[schemars(with = "String")] + Vec, +); + +impl Deref for SerialNumberWrapper { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for SerialNumberWrapper { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From> for SerialNumberWrapper { + fn from(value: Vec) -> Self { + SerialNumberWrapper(value) + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, PartialEq)] +pub struct BatchRedeemTicketsBody { + #[serde(with = "serde_helpers::bs58")] + #[schemars(with = "String")] + pub digest: Vec, + pub included_serial_numbers: Vec, + pub proposal_id: u64, + #[schemars(with = "String")] + pub gateway_cosmos_addr: AccountId, +} + +impl BatchRedeemTicketsBody { + pub fn make_digest(serial_numbers: I) -> Vec + where + I: Iterator, + T: AsRef<[u8]>, + { + let mut hasher = sha2::Sha256::new(); + for sn in serial_numbers { + hasher.update(sn) + } + hasher.finalize().to_vec() + } + + pub fn new( + digest: Vec, + proposal_id: u64, + serial_numbers: Vec>, + redeemer: AccountId, + ) -> Self { + BatchRedeemTicketsBody { + digest, + included_serial_numbers: serial_numbers.into_iter().map(Into::into).collect(), + proposal_id, + gateway_cosmos_addr: redeemer, + } + } + + pub fn verify_digest(&self) -> bool { + Self::make_digest(self.included_serial_numbers.iter()) == self.digest + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct EcashBatchTicketRedemptionResponse { + pub proposal_accepted: bool, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct SpentCredentialsResponse { + #[serde(with = "serde_helpers::base64")] + #[schemars(with = "String")] + pub bitmap: Vec, +} + +impl SpentCredentialsResponse { + pub fn new(bitmap: Vec) -> SpentCredentialsResponse { + SpentCredentialsResponse { bitmap } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EpochCredentialsResponse { + pub epoch_id: u64, + pub first_epoch_credential_id: Option, + pub total_issued: u32, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredentialsResponse { + // note: BTreeMap returns ordered results so it's fine to use it with pagination + pub credentials: BTreeMap, +} + +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredentialResponse { + pub credential: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredentialBody { + pub credential: IssuedCredential, + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signature: identity::Signature, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IssuedCredential { + pub id: i64, + pub epoch_id: u32, + pub deposit_id: u32, + + // NOTE: if we find creation of this guy takes too long, + // change `BlindedSignature` to `BlindedSignatureBytes` + // so that nym-api wouldn't need to parse the value out of its storage + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub blinded_partial_credential: BlindedSignature, + pub bs58_encoded_private_attributes_commitments: Vec, + + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, +} + +impl IssuedCredential { + // this method doesn't have to be reversible so just naively concatenate everything + pub fn signable_plaintext(&self) -> Vec { + issued_credential_plaintext( + self.epoch_id, + self.deposit_id, + &self.blinded_partial_credential, + &self.bs58_encoded_private_attributes_commitments, + self.expiration_date, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // had some issues with `Date` and serde... + // so might as well leave this unit test in case we do something to the helper + #[test] + fn aggregated_expiration_date_signature_responses_deserialises_correctly() { + let raw = r#"{"epoch_id":0,"expiration_date":"2024-08-03","signatures":[{"signature":{"h":"9379384af5236bffd7d6c533782d74863f892cf6e0fc8b4b64647e8bd5bccfe63df639a4a556d21826ab7ef54e4adafe","s":"83124c907935eba10c791cbc2cedf8714b68c6a266efead7bc4897c3b9652680bd151493689577ac790b9ccef1112f9b"},"expiration_timestamp":1722643200,"spending_timestamp":1720137600},{"signature":{"h":"817b9fa0eef617ce751963d9c853df56fdf38a643b9ef8649b658d7ec3da61d49d9279f22509c66dd07051132a418c62","s":"af9fd22c2fde4cb36a0ee021c5a756f97f871ea404d639af1be845b277bed583f715d228d3556e79d15eba23a5b0d5e0"},"expiration_timestamp":1722643200,"spending_timestamp":1720224000},{"signature":{"h":"89f04f5ae1a1532551c2e17166775eac3c5ebba0198eff2919073efd733d6e17e0a6496e31061b0797f51b69d0fa17fc","s":"b752cdb2f4d8a4254d24d5773a8490de1ad84207cbe693638d1fdcbffd8426be96fae3586f19c570beb005bb6bf27b9b"},"expiration_timestamp":1722643200,"spending_timestamp":1720310400},{"signature":{"h":"b1072da2612a6ebc0c02c7f673898dfea88eeeeb076a9dd2ab981d47a8176b87804ca6090cfb5b0c128f06adc6fdf0de","s":"8746a62f6333c3948f1fbb05543f0095cd44f610926c71f339511bf3bb97d98fd019d44c4a4dfc6710a9d5c718c5bfc2"},"expiration_timestamp":1722643200,"spending_timestamp":1720396800},{"signature":{"h":"8d266e7e56af7364d79c4190b24d5f0a601d7771c359815f642115123f5418d0304a46226d70e873b10a051f15178630","s":"a11d67c3293b224abb1256050031b32236484dc0f0fe66a9363d322e6b8c9b24719f4d4da0584d6106eb0f59fa6bb3c8"},"expiration_timestamp":1722643200,"spending_timestamp":1720483200},{"signature":{"h":"8ee31ef821949be316528b7b75b81baa6d0cf2846b615ff6d34e478f25746a07287ae6944279550879735740803c7922","s":"aa26b0df4445d8a10a226f1c70908451d211f787458c0743ba891c46bbbbde3bc59777f4edb9cabdd5fd181d772cfa43"},"expiration_timestamp":1722643200,"spending_timestamp":1720569600},{"signature":{"h":"b8fbacde3f9d507fb5bf6d5f96a33dd5c9c40cc9a062e92f9cdc6a2bddf8cbd873ffcc5112de816f98fbf47b74330abb","s":"a791aee82bdcf3cc4203ce657fbd2c3e9c2f0decd032c4ab91713d3cfd0ac57ebb9bb20e133d9a8a4ba1b8692a3b2706"},"expiration_timestamp":1722643200,"spending_timestamp":1720656000},{"signature":{"h":"b9ed48b47c1a1e6a1ae847ffaad01e0b7d22b2c9f9075beedf4924958ca044579f1e9ca18e268e2a0ef743c214934692","s":"a3e9191d0c3651c656a376e99f3d8c121a742ba55522765479ce866df8aaecc0281ae08003439debf7977f23e450a45f"},"expiration_timestamp":1722643200,"spending_timestamp":1720742400},{"signature":{"h":"a6f1fbdcc28953f318f067cde6e3d6ad0362fd46a501a74492be0884308cf306a7f30006181710f8883a380a8d4885f7","s":"9260d035ea97fc1b93d2db3802678005c95a4acdb0ce0c947cb0b9866ba4028a3b833d742f02ed0227ed33cee4ed17b4"},"expiration_timestamp":1722643200,"spending_timestamp":1720828800},{"signature":{"h":"95109fd35f7808084f224aa1fde645728c0c163751fe24d97e0002a7c38dc5a3a592dcb770893ddd25fc67f84e29e0a1","s":"b85279c4566aa365d208b57817ce96f1a1245895fbe25363202da89559fc454a2cfb09972fc3d1c468b96e8bc11b424c"},"expiration_timestamp":1722643200,"spending_timestamp":1720915200},{"signature":{"h":"8f4b7f93871d8f995a248d31664b02e1fe7107d67512f248a8f550d477846ca9f9862a5d44f5da458c6b83f4d8e62494","s":"b87e46a5d25de574eb4e16cf24199b2e0a658e0c1ef965b632165ed2d695637df76d373484dc93cec30e2f0a29ac91f8"},"expiration_timestamp":1722643200,"spending_timestamp":1721001600},{"signature":{"h":"b740491d3527f8fa0b8bed76d234d0737c09a049f653bb1e8a552b29f568aabd077604b08e7337d44f551fbda565e52e","s":"a08634a2560b3a79c8b56ff5387ef76c397c277810d78cc6dfac2161673df761d0ba61e6075e8a4781ec1b2ba7715b93"},"expiration_timestamp":1722643200,"spending_timestamp":1721088000},{"signature":{"h":"8d4b92fe48257d2574223fbe12afcdabd00db976e8d20faaddb5fb570373edb2a15af4ad9a7c10ceda3bb1304b0210aa","s":"8f8592c45a1cca753e5d0b04ffc2947fd27c7716f2751e632f2bc1bf0e73b179ce8080325951beb0886c5cf0fca79372"},"expiration_timestamp":1722643200,"spending_timestamp":1721174400},{"signature":{"h":"891d412e631041d07115cacf847575462d2389402a0e243a2f106b5e1c8f1431fa8e1aedc55cf943aef9fa8125d105a1","s":"ad2038ab95b823a8119bcc473be22101c4b58fd44ad375235c3664341fc617fe129702aec8765d8dc32ac845aec58a6a"},"expiration_timestamp":1722643200,"spending_timestamp":1721260800},{"signature":{"h":"84b97558e7889af167ba01ed608418ddabfc7fa71ba8f9cb045027f994fe9b5f9bb0ccc15822d1c73a37a48b041cd759","s":"adc370bc182ff9c7057591a3d4afcb21c714093e480374a07608c9f9d2baeff8c14fc5de3f0f457ea340e445ecc28487"},"expiration_timestamp":1722643200,"spending_timestamp":1721347200},{"signature":{"h":"b29b245acf66082c0f4e52cbf9db00ecb9cee070ddb1f5b56a9e85e2563dae04186ed03b7b590e0a9f4b4304ae1cd370","s":"8196445495ed1ee098bbe6083ece3522d4669e23dc4f3e2226e0e63294f1bc791e66380deedd21e5679e3019e93c7727"},"expiration_timestamp":1722643200,"spending_timestamp":1721433600},{"signature":{"h":"9750b9bdf1e86e8e4e6de44392f2160059da64838357a16e64155a5b7989fe7958ae95501e7c1f4039cf8b372ac3b214","s":"80707aa94af054fedcb4947809dab7a9317e303a12b7fb30858115c95bf8e46fec7ea9b0ce5097b75d8f7905c205f757"},"expiration_timestamp":1722643200,"spending_timestamp":1721520000},{"signature":{"h":"828be64b5cd147b5c43e68a2084cb77a762967d5621769136a0894c3d1ee50890412120d7c765cb3c835435d57ac4438","s":"956ca67f8f8591e72b8feeb53cbe52a0f69202313aa729c09760fb2a19b346fda1b7afd326822889fb228ca77a503072"},"expiration_timestamp":1722643200,"spending_timestamp":1721606400},{"signature":{"h":"83ac2c778f0b847af8a4bcc37bd5c2734544fe4427ee7ec5c4690738005cc805864fd5944266417ba2795d7333bd8c4a","s":"88936b7f61a0febfbedc8c0c40dad8f9c4eacb41d276ba00b7d03602ca69614faacca0d945713a272e9748dd6dd71f81"},"expiration_timestamp":1722643200,"spending_timestamp":1721692800},{"signature":{"h":"911c126da663478023efd1157586681937ac7f6c5a33b34a102b3611a63780b244ecdf05ecad09701e65ad20c5246f6a","s":"a1188f3b6ced776890b038e2d07b6af67f1a2c08f54a32d48dbfce61be3295e8ea910b6197b111a6ff16dd668eeda9f5"},"expiration_timestamp":1722643200,"spending_timestamp":1721779200},{"signature":{"h":"8c03c749b424e8fb3f26d51e45d21a7e9f59e6836f89f4a1f27b952e2bc8c038f3372f593dfb3bd284feb36918c2f194","s":"b20d3fd194f73142f0dfc68981519b76aa013e6743b2bdf63d41f3aa1e754d1f74c46012adf0332723f861e5aaa69dba"},"expiration_timestamp":1722643200,"spending_timestamp":1721865600},{"signature":{"h":"872de60a19f3ece61a70519b1f7a63476d13a20721ff00ae923a984bd710703af55112092e209970f074a8950b9e246e","s":"854906bbce5a0c2e47fa829441a03f5c3ac9357dc6d107547198f17505cd17852806e6d69b78264921a8c13c4a9fe50c"},"expiration_timestamp":1722643200,"spending_timestamp":1721952000},{"signature":{"h":"a2a57fd2b675fccccae34f9ff465f36638ee3809291c9b37b5830015a1a7c4d873528105f030a05efd16c6411672fe88","s":"b42f31e4ed05d3b026a62c5df97e04568c0c92d3fafaf95fa732ce69fbe7ee091ac025d04e3ce526b3474edaaf278398"},"expiration_timestamp":1722643200,"spending_timestamp":1722038400},{"signature":{"h":"b9be127e812602ac7a612897fd95e5869b360046993139590c82c2844654b6a5a72e9d74ed38eea6ee99ba0978382a3b","s":"b027db49bb9b0ec3931fc1cb4a6a739c968c45a59698d199056ebfcbeca7ddb363a123e55d05fbf5635d915be2a16ffb"},"expiration_timestamp":1722643200,"spending_timestamp":1722124800},{"signature":{"h":"8c40b2a5e17e1f9cdbe85eaaa419126374d35a255b8338474d442d0d40a4e10c3f2668f579155dfdad3fd403523683df","s":"a40cec4b00c9a975db6c307f25425f3a4d0e49eca2c05d5122630c54bd5d114f74e4abb78a7261752a9dfd4c020f8b91"},"expiration_timestamp":1722643200,"spending_timestamp":1722211200},{"signature":{"h":"b72821bc38167e7f422dcc669c19e0aeffb8247192c4bd50da1503b6aa28041bb5605598639bdb8b945dbc804fc852aa","s":"9443cad7c9d6194b601daf21cbc5fe3019d3350dd725f6d9e3ae366162f93e3b738779617f77ec146f0a05f9a40b602b"},"expiration_timestamp":1722643200,"spending_timestamp":1722297600},{"signature":{"h":"8fe36994ac7e92b5ad0869f2b1bfa3247aac27599459cce9d94cf84eeead8bba1ca4b294d16db3588a0eb9d9b28d4051","s":"8eae0ef0e966f9f473bd829f18e28cf9730447452040aaef114701f409ff237965f203f7394a7d61ca745a4b2f058d9b"},"expiration_timestamp":1722643200,"spending_timestamp":1722384000},{"signature":{"h":"a9f91a83a1db0c5609fcacd1290f1650e4e5ab98c3dd9f0b8fa1c54664c12ed17478ae6e598f0e900ace30addf1b0559","s":"801658058f8de9ba1c25518905196d7e4e0bc5e26a7a810c9dce3ac141f01c538c62808b4728ff683dee78670b2846f9"},"expiration_timestamp":1722643200,"spending_timestamp":1722470400},{"signature":{"h":"94ecd95c1d5ae03270079476aad6334dafdcf615157a6a34b05bb755a047bc2515e33a7e099f9fddc4eec3def8904cdf","s":"b59ff6ded76926a9ae97447ba00eac53d9aa0e1168218d990c5b97cc128a4f5a0c42f961f84cad8ad344630b2a15f3a9"},"expiration_timestamp":1722643200,"spending_timestamp":1722556800},{"signature":{"h":"ace778e70775d349c1e679e5ccef634a533738e7af8b51daa45ccbd920f976d15c4ba744d6ae8fdf711b84b20703cef6","s":"80581faceda2bc35b81240017e1ce8f121476c5ba4d75acddfc58ba6aaa2f12d626eaaab185548e6dd716b6e582740b8"},"expiration_timestamp":1722643200,"spending_timestamp":1722643200}]}"#; + let _: AggregatedExpirationDateSignatureResponse = serde_json::from_str(raw).unwrap(); + } + + #[test] + fn batch_redemption_body_roundtrip() { + let sn = vec![b"foomp".to_vec(), b"bar".to_vec()]; + let gateway: AccountId = "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf".parse().unwrap(); + let digest = [42u8; 32].to_vec(); + + let req = BatchRedeemTicketsBody::new(digest, 69, sn, gateway); + let bytes = serde_json::to_vec(&req).unwrap(); + + let de: BatchRedeemTicketsBody = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(req, de); + } +} diff --git a/nym-api/nym-api-requests/src/helpers.rs b/nym-api/nym-api-requests/src/helpers.rs new file mode 100644 index 0000000000..dc2a28c1f5 --- /dev/null +++ b/nym-api/nym-api-requests/src/helpers.rs @@ -0,0 +1,141 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use time::format_description::{modifier, BorrowedFormatItem, Component}; +use time::OffsetDateTime; + +// just to have something, even if not accurate to generate the swagger docs +#[derive(JsonSchema)] +pub struct PlaceholderJsonSchemaImpl {} + +const DATE_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::Year(modifier::Year::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Month(modifier::Month::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Day(modifier::Day::default())), +]; + +pub(crate) const fn unix_epoch() -> OffsetDateTime { + OffsetDateTime::UNIX_EPOCH +} + +pub(crate) mod overengineered_offset_date_time_serde { + use crate::helpers::{unix_epoch, DATE_FORMAT}; + use serde::de::Visitor; + use serde::ser::Error; + use serde::{Deserializer, Serialize, Serializer}; + use std::fmt::Formatter; + use time::format_description::well_known::Rfc3339; + use time::format_description::{modifier, BorrowedFormatItem, Component}; + use time::OffsetDateTime; + + struct OffsetDateTimeVisitor; + + // copied from time library because they keep it private -.- + const DEFAULT_OFFSET_DATE_TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Compound(DATE_FORMAT), + BorrowedFormatItem::Literal(b" "), + BorrowedFormatItem::Compound(TIME_FORMAT), + BorrowedFormatItem::Literal(b" "), + BorrowedFormatItem::Compound(UTC_OFFSET_FORMAT), + ]; + + const TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::Hour(modifier::Hour::default())), + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::Minute(modifier::Minute::default())), + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::Second(modifier::Second::default())), + BorrowedFormatItem::Literal(b"."), + BorrowedFormatItem::Component(Component::Subsecond(modifier::Subsecond::default())), + ]; + + const UTC_OFFSET_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::OffsetHour({ + let mut m = modifier::OffsetHour::default(); + m.sign_is_mandatory = true; + m + })), + BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::OffsetMinute( + modifier::OffsetMinute::default(), + )), + BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::OffsetSecond( + modifier::OffsetSecond::default(), + )), + ])), + ])), + ]; + + impl<'de> Visitor<'de> for OffsetDateTimeVisitor { + type Value = OffsetDateTime; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("an rfc3339 or human-readable `OffsetDateTime`") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + // first try rfc3339, if that fails use default human-readable impl from time, + // finally fallback to default unix epoch + Ok(OffsetDateTime::parse(v, &Rfc3339).unwrap_or_else(|_| { + OffsetDateTime::parse(v, &DEFAULT_OFFSET_DATE_TIME_FORMAT) + .unwrap_or_else(|_| unix_epoch()) + })) + } + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(OffsetDateTimeVisitor) + } + + pub(crate) fn serialize(datetime: &OffsetDateTime, serializer: S) -> Result + where + S: Serializer, + { + // serialize it with human-readable format for compatibility with eclipse and nutella clients + // in the future change it back to rfc3339 + datetime + .format(&DEFAULT_OFFSET_DATE_TIME_FORMAT) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} + +pub(crate) mod date_serde { + use crate::helpers::DATE_FORMAT; + use serde::ser::Error; + use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + use time::Date; + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + Date::parse(&s, DATE_FORMAT).map_err(de::Error::custom) + } + + pub(crate) fn serialize(datetime: &Date, serializer: S) -> Result + where + S: Serializer, + { + // serialize it with human-readable format for compatibility with eclipse and nutella clients + // in the future change it back to rfc3339 + datetime + .format(&DATE_FORMAT) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} diff --git a/nym-api/nym-api-requests/src/lib.rs b/nym-api/nym-api-requests/src/lib.rs index 5074e16146..7ea5db1f6d 100644 --- a/nym-api/nym-api-requests/src/lib.rs +++ b/nym-api/nym-api-requests/src/lib.rs @@ -4,7 +4,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -pub mod coconut; +pub mod constants; +pub mod ecash; +mod helpers; pub mod models; pub mod nym_nodes; pub mod pagination; diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 05cf9f702e..8245d26d70 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::helpers::unix_epoch; use crate::nym_nodes::NodeRole; use crate::pagination::PaginatedResponse; use cosmwasm_std::{Addr, Coin, Decimal}; @@ -16,7 +17,7 @@ use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display, Formatter}; use std::net::IpAddr; use std::ops::{Deref, DerefMut}; use std::{fmt, time::Duration}; @@ -41,7 +42,7 @@ impl RequestError { impl Display for RequestError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - self.message.fmt(f) + Display::fmt(&self.message, f) } } @@ -417,10 +418,6 @@ impl From for WebSocket } } -const fn unix_epoch() -> OffsetDateTime { - OffsetDateTime::UNIX_EPOCH -} - pub fn de_rfc3339_or_default<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -428,110 +425,14 @@ where Ok(time::serde::rfc3339::deserialize(deserializer).unwrap_or_else(|_| unix_epoch())) } -pub(crate) mod overengineered_offset_date_time_serde { - use crate::models::unix_epoch; - use serde::de::Visitor; - use serde::ser::Error; - use serde::{Deserializer, Serialize, Serializer}; - use std::fmt::Formatter; - use time::format_description::well_known::Rfc3339; - use time::format_description::{modifier, BorrowedFormatItem, Component}; - use time::OffsetDateTime; - - struct OffsetDateTimeVisitor; - - // copied from time library because they keep it private -.- - const DEFAULT_OFFSET_DATE_TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Compound(DATE_FORMAT), - BorrowedFormatItem::Literal(b" "), - BorrowedFormatItem::Compound(TIME_FORMAT), - BorrowedFormatItem::Literal(b" "), - BorrowedFormatItem::Compound(UTC_OFFSET_FORMAT), - ]; - - const DATE_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Component(Component::Year(modifier::Year::default())), - BorrowedFormatItem::Literal(b"-"), - BorrowedFormatItem::Component(Component::Month(modifier::Month::default())), - BorrowedFormatItem::Literal(b"-"), - BorrowedFormatItem::Component(Component::Day(modifier::Day::default())), - ]; - - const TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Component(Component::Hour(modifier::Hour::default())), - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::Minute(modifier::Minute::default())), - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::Second(modifier::Second::default())), - BorrowedFormatItem::Literal(b"."), - BorrowedFormatItem::Component(Component::Subsecond(modifier::Subsecond::default())), - ]; - - const UTC_OFFSET_FORMAT: &[BorrowedFormatItem<'_>] = &[ - BorrowedFormatItem::Component(Component::OffsetHour({ - let mut m = modifier::OffsetHour::default(); - m.sign_is_mandatory = true; - m - })), - BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::OffsetMinute( - modifier::OffsetMinute::default(), - )), - BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ - BorrowedFormatItem::Literal(b":"), - BorrowedFormatItem::Component(Component::OffsetSecond( - modifier::OffsetSecond::default(), - )), - ])), - ])), - ]; - - impl<'de> Visitor<'de> for OffsetDateTimeVisitor { - type Value = OffsetDateTime; - - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("an rfc3339 or human-readable `OffsetDateTime`") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - // first try rfc3339, if that fails use default human-readable impl from time, - // finally fallback to default unix epoch - Ok(OffsetDateTime::parse(v, &Rfc3339).unwrap_or_else(|_| { - OffsetDateTime::parse(v, &DEFAULT_OFFSET_DATE_TIME_FORMAT) - .unwrap_or_else(|_| unix_epoch()) - })) - } - } - - pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_str(OffsetDateTimeVisitor) - } - - pub(crate) fn serialize(datetime: &OffsetDateTime, serializer: S) -> Result - where - S: Serializer, - { - // serialize it with human-readable format for compatibility with eclipse and nutella clients - // in the future change it back to rfc3339 - datetime - .format(&DEFAULT_OFFSET_DATE_TIME_FORMAT) - .map_err(S::Error::custom)? - .serialize(serializer) - } -} - // for all intents and purposes it's just OffsetDateTime, but we need JsonSchema... #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] pub struct OffsetDateTimeJsonSchemaWrapper( - #[serde(default = "unix_epoch", with = "overengineered_offset_date_time_serde")] - pub OffsetDateTime, + #[serde( + default = "unix_epoch", + with = "crate::helpers::overengineered_offset_date_time_serde" + )] + pub OffsetDateTime, ); impl Default for OffsetDateTimeJsonSchemaWrapper { @@ -540,6 +441,12 @@ impl Default for OffsetDateTimeJsonSchemaWrapper { } } +impl Display for OffsetDateTimeJsonSchemaWrapper { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + impl From for OffsetDateTime { fn from(value: OffsetDateTimeJsonSchemaWrapper) -> Self { value.0 diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index ea874b90e7..50b527070a 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -17,7 +17,7 @@ pub struct CachedNodesResponse { #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] #[serde(rename_all = "kebab-case")] -#[cfg_attr(feature = "request-parsing", derive(rocket::form::FromFormField))] +#[cfg_attr(feature = "rocket-traits", derive(rocket::form::FromFormField))] pub enum NodeRoleQueryParam { ActiveMixnode, diff --git a/nym-api/src/coconut/api_routes/mod.rs b/nym-api/src/coconut/api_routes/mod.rs deleted file mode 100644 index d56202f067..0000000000 --- a/nym-api/src/coconut/api_routes/mod.rs +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright 2023-2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use std::ops::Deref; - -use k256::ecdsa::signature::Verifier; -use rand::rngs::OsRng; -use rand::RngCore; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use time::OffsetDateTime; - -use nym_api_requests::coconut::models::{ - CredentialsRequestBody, EpochCredentialsResponse, FreePassNonceResponse, FreePassRequest, - IssuedCredentialResponse, IssuedCredentialsResponse, -}; -use nym_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, -}; -use nym_coconut_bandwidth_contract_common::spend_credential::{ - funds_from_cosmos_msgs, SpendCredentialStatus, -}; -use nym_coconut_dkg_common::types::EpochId; -use nym_credentials::coconut::bandwidth::freepass::MAX_FREE_PASS_VALIDITY; -use nym_credentials::coconut::bandwidth::{ - bandwidth_credential_params, CredentialType, IssuanceBandwidthCredential, -}; -use nym_validator_client::nyxd::Coin; - -use crate::coconut::api_routes::helpers::build_credentials_response; -use crate::coconut::error::{CoconutError, Result}; -use crate::coconut::helpers::{accepted_vote_err, blind_sign}; -use crate::coconut::state::State; -use crate::coconut::storage::CoconutStorageExt; - -mod helpers; - -fn validate_freepass_public_attributes(res: &FreePassRequest) -> Result<()> { - let public_attributes = &res.public_attributes_plain; - - if public_attributes.len() != IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize { - return Err(CoconutError::InvalidFreePassAttributes { - got: public_attributes.len(), - expected: IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize, - }); - } - - // SAFETY: we just ensured correct number of attributes - let expiry_raw = public_attributes.first().unwrap(); - let type_raw = public_attributes.get(1).unwrap(); - - let parsed_type = type_raw.parse::()?; - if parsed_type != CredentialType::FreePass { - return Err(CoconutError::InvalidFreePassTypeAttribute { got: parsed_type }); - } - - let expiry_timestamp: i64 = expiry_raw - .parse() - .map_err(|source| CoconutError::ExpiryDateParsingFailure { source })?; - - let expiry_date = OffsetDateTime::from_unix_timestamp(expiry_timestamp).map_err(|source| { - CoconutError::InvalidExpiryDate { - unix_timestamp: expiry_timestamp, - source, - } - })?; - let now = OffsetDateTime::now_utc(); - - if expiry_date > now + MAX_FREE_PASS_VALIDITY { - return Err(CoconutError::TooLongFreePass { expiry_date }); - } - - if expiry_date < now { - return Err(CoconutError::FreePassExpiryInThePast { expiry_date }); - } - - Ok(()) -} - -#[get("/free-pass-nonce")] -pub async fn get_current_free_pass_nonce( - state: &RocketState, -) -> Result> { - debug!("Received free pass nonce request"); - - let current_nonce = state.freepass_nonce.read().await; - debug!("the current expected nonce is {current_nonce:?}"); - - Ok(Json(FreePassNonceResponse { - current_nonce: *current_nonce, - })) -} - -#[post("/free-pass", data = "")] -pub async fn post_free_pass( - freepass_request_body: Json, - state: &RocketState, -) -> Result> { - debug!("Received free pass sign request"); - trace!("body: {:?}", freepass_request_body); - - validate_freepass_public_attributes(&freepass_request_body)?; - - // check for explicit admin - let explicit_admin = state.get_authorised_freepass_requester().await; - - // otherwise fallback to bandwidth contract admin - let bandwidth_contract_admin = state - .get_bandwidth_contract_admin() - .await - .cloned() - .inspect_err(|_| error!("our bandwidth contract does not have an admin set! We won't be able to migrate the contract! We should redeploy it ASAP")) - .ok() - .flatten(); - - // extract account prefix - let prefix = match (&explicit_admin, &bandwidth_contract_admin) { - (None, None) => { - error!("neither explicit admin nor bandwidth contract admin has been set!"); - return Err(CoconutError::MissingBandwidthContractAddress); - } - (Some(addr), _) => addr.prefix(), - (None, Some(addr)) => addr.prefix(), - }; - - // derive the address out of the provided pubkey - let requester = match freepass_request_body.cosmos_pubkey.account_id(prefix) { - Ok(address) => address, - Err(err) => { - return Err(CoconutError::AdminAccountDerivationFailure { - formatted_source: err.to_string(), - }) - } - }; - debug!("derived the following address out of the provided public key: {requester}. Going to check it against the authorised admin ({explicit_admin:?}) and fallback to bandwidth contract admin: {bandwidth_contract_admin:?}"); - - // check if request matches any address - if Some(&requester) != explicit_admin.as_ref() - && Some(&requester) != bandwidth_contract_admin.as_ref() - { - return Err(CoconutError::UnauthorisedFreePassAccount { - requester, - explicit_admin, - bandwidth_contract_admin, - }); - } - - // get the write lock on the nonce to block other requests (since we don't need concurrency and nym is the only one getting them) - let mut current_nonce = state.freepass_nonce.write().await; - debug!("the current expected nonce is {current_nonce:?}"); - - if *current_nonce != freepass_request_body.used_nonce { - return Err(CoconutError::InvalidNonce { - current: *current_nonce, - received: freepass_request_body.used_nonce, - }); - } - - // check if we have the signing key available - debug!("checking if we actually have coconut keys derived..."); - let maybe_keypair_guard = state.coconut_keypair.get().await; - let Some(keypair_guard) = maybe_keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - let Some(signing_key) = keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - - let tm_pubkey = freepass_request_body.tendermint_pubkey(); - - // currently accounts (excluding validators) don't use ed25519 and are secp256k1-based - let Some(secp256k1_pubkey) = tm_pubkey.secp256k1() else { - return Err(CoconutError::UnsupportedNonSecp256k1Key); - }; - - // make sure the signature actually verifies - secp256k1_pubkey - .verify( - &freepass_request_body.used_nonce, - &freepass_request_body.nonce_signature, - ) - .map_err(|_| CoconutError::FreePassSignatureVerificationFailure)?; - - // produce the partial signature - debug!("producing the partial credential"); - let blinded_signature = - blind_sign(freepass_request_body.deref(), signing_key.keys.secret_key())?; - - // update the number of issued free passes - state.storage.increment_issued_freepasses().await?; - - // update the nonce - OsRng.fill_bytes(current_nonce.as_mut_slice()); - - // finally return the credential to the client - Ok(Json(BlindedSignatureResponse { blinded_signature })) -} - -#[post("/blind-sign", data = "")] -// Until we have serialization and deserialization traits we'll be using a crutch -pub async fn post_blind_sign( - blind_sign_request_body: Json, - state: &RocketState, -) -> Result> { - debug!("Received blind sign request"); - trace!("body: {:?}", blind_sign_request_body); - - // early check: does the request have the expected number of public attributes? - debug!("performing basic request validation"); - if blind_sign_request_body.public_attributes_plain.len() - != IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize - { - return Err(CoconutError::InconsistentPublicAttributes); - } - - // check if we already issued a credential for this tx hash - debug!( - "checking if we have already issued credential for this tx_hash (hash: {})", - blind_sign_request_body.tx_hash - ); - if let Some(blinded_signature) = state - .already_issued(blind_sign_request_body.tx_hash) - .await? - { - return Ok(Json(BlindedSignatureResponse { blinded_signature })); - } - - // check if we have the signing key available - debug!("checking if we actually have coconut keys derived..."); - let maybe_keypair_guard = state.coconut_keypair.get().await; - let Some(keypair_guard) = maybe_keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - let Some(signing_key) = keypair_guard.as_ref() else { - return Err(CoconutError::KeyPairNotDerivedYet); - }; - - // get the transaction details of the claimed deposit - debug!("getting transaction details from the chain"); - let tx = state - .get_transaction(blind_sign_request_body.tx_hash) - .await?; - - // check validity of the request - debug!("fully validating received request"); - state.validate_request(&blind_sign_request_body, tx).await?; - - // produce the partial signature - debug!("producing the partial credential"); - let blinded_signature = blind_sign( - blind_sign_request_body.deref(), - signing_key.keys.secret_key(), - )?; - - // store the information locally - debug!("storing the issued credential in the database"); - state - .store_issued_credential(blind_sign_request_body.into_inner(), &blinded_signature) - .await?; - - // finally return the credential to the client - Ok(Json(BlindedSignatureResponse { blinded_signature })) -} - -#[post("/verify-bandwidth-credential", data = "")] -pub async fn verify_bandwidth_credential( - verify_credential_body: Json, - state: &RocketState, -) -> Result> { - let proposal_id = verify_credential_body.proposal_id; - let credential_data = &verify_credential_body.credential_data; - let epoch_id = credential_data.epoch_id; - let theta = &credential_data.verify_credential_request; - - let voucher_value: u64 = if credential_data.typ.is_voucher() { - credential_data - .get_bandwidth_attribute() - .ok_or(CoconutError::MissingBandwidthValue)? - .parse() - .map_err(|source| CoconutError::VoucherValueParsingFailure { source })? - } else { - return Err(CoconutError::NotABandwidthVoucher { - typ: credential_data.typ, - }); - }; - - // TODO: introduce a check to make sure we haven't already voted for this proposal to prevent DDOS - - let proposal = state.client.get_proposal(proposal_id).await?; - - // Proposal description is the blinded serial number - if !theta.has_blinded_serial_number(&proposal.description)? { - return Err(CoconutError::IncorrectProposal { - reason: String::from("incorrect blinded serial number in description"), - }); - } - let proposed_release_funds = - funds_from_cosmos_msgs(proposal.msgs).ok_or(CoconutError::IncorrectProposal { - reason: String::from("action is not to release funds"), - })?; - // Credential has not been spent before, and is on its way of being spent - let credential_status = state - .client - .get_spent_credential(theta.blinded_serial_number_bs58()) - .await? - .spend_credential - .ok_or(CoconutError::InvalidCredentialStatus { - status: String::from("Inexistent"), - })? - .status(); - if credential_status != SpendCredentialStatus::InProgress { - return Err(CoconutError::InvalidCredentialStatus { - status: format!("{:?}", credential_status), - }); - } - let verification_key = state.verification_key(epoch_id).await?; - let params = bandwidth_credential_params(); - let mut vote_yes = credential_data.verify(params, &verification_key); - - vote_yes &= Coin::from(proposed_release_funds) - == Coin::new(voucher_value as u128, state.mix_denom.clone()); - - // Vote yes or no on the proposal based on the verification result - let ret = state - .client - .vote_proposal(proposal_id, vote_yes, None) - .await; - accepted_vote_err(ret)?; - - Ok(Json(VerifyCredentialResponse::new(vote_yes))) -} - -#[get("/epoch-credentials/")] -pub async fn epoch_credentials( - epoch: EpochId, - state: &RocketState, -) -> Result> { - let issued = state.storage.get_epoch_credentials(epoch).await?; - - let response = if let Some(issued) = issued { - issued.into() - } else { - EpochCredentialsResponse { - epoch_id: epoch, - first_epoch_credential_id: None, - total_issued: 0, - } - }; - - Ok(Json(response)) -} - -#[get("/issued-credential/")] -pub async fn issued_credential( - id: i64, - state: &RocketState, -) -> Result> { - let issued = state.storage.get_issued_credential(id).await?; - - let credential = if let Some(issued) = issued { - Some(issued.try_into()?) - } else { - None - }; - - Ok(Json(IssuedCredentialResponse { credential })) -} - -#[post("/issued-credentials", data = "")] -pub async fn issued_credentials( - params: Json, - state: &RocketState, -) -> Result> { - let params = params.into_inner(); - - if params.pagination.is_some() && !params.credential_ids.is_empty() { - return Err(CoconutError::InvalidQueryArguments); - } - - let credentials = if let Some(pagination) = params.pagination { - state - .storage - .get_issued_credentials_paged(pagination) - .await? - } else { - state - .storage - .get_issued_credentials(params.credential_ids) - .await? - }; - - build_credentials_response(credentials).map(Json) -} diff --git a/nym-api/src/coconut/comm.rs b/nym-api/src/coconut/comm.rs deleted file mode 100644 index 48e7823fce..0000000000 --- a/nym-api/src/coconut/comm.rs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::Result; -use crate::nyxd; -use crate::support::nyxd::ClientInner; -use nym_coconut::VerificationKey; -use nym_coconut_dkg_common::types::{Epoch, EpochId}; -use nym_credentials::coconut::utils::obtain_aggregate_verification_key; -use nym_validator_client::coconut::all_coconut_api_clients; -use nym_validator_client::nyxd::contract_traits::DkgQueryClient; -use std::cmp::min; -use std::collections::HashMap; -use std::ops::Deref; -use time::OffsetDateTime; -use tokio::sync::RwLock; - -#[async_trait] -pub trait APICommunicationChannel { - async fn current_epoch(&self) -> Result; - async fn aggregated_verification_key(&self, epoch_id: EpochId) -> Result; -} - -struct CachedEpoch { - valid_until: OffsetDateTime, - current_epoch_id: EpochId, -} - -impl Default for CachedEpoch { - fn default() -> Self { - CachedEpoch { - valid_until: OffsetDateTime::UNIX_EPOCH, - current_epoch_id: 0, - } - } -} - -impl CachedEpoch { - fn is_valid(&self) -> bool { - self.valid_until > OffsetDateTime::now_utc() - } - - async fn update(&mut self, epoch: Epoch) -> Result<()> { - let now = OffsetDateTime::now_utc(); - - let validity_duration = if let Some(epoch_finish) = epoch.deadline { - let state_end = - OffsetDateTime::from_unix_timestamp(epoch_finish.seconds() as i64).unwrap(); - let until_epoch_state_end = state_end - now; - // make it valid until the next epoch transition or next 5min, whichever is smaller - min(until_epoch_state_end, 5 * time::Duration::MINUTE) - } else { - 5 * time::Duration::MINUTE - }; - - self.valid_until = now + validity_duration; - self.current_epoch_id = epoch.epoch_id; - - Ok(()) - } -} - -pub(crate) struct QueryCommunicationChannel { - nyxd_client: nyxd::Client, - - epoch_keys: RwLock>, - cached_epoch: RwLock, -} - -impl QueryCommunicationChannel { - pub fn new(nyxd_client: nyxd::Client) -> Self { - QueryCommunicationChannel { - nyxd_client, - epoch_keys: Default::default(), - cached_epoch: Default::default(), - } - } -} - -#[async_trait] -impl APICommunicationChannel for QueryCommunicationChannel { - async fn current_epoch(&self) -> Result { - let guard = self.cached_epoch.read().await; - if guard.is_valid() { - return Ok(guard.current_epoch_id); - } - - // update cache - drop(guard); - let mut guard = self.cached_epoch.write().await; - - let epoch = match self.nyxd_client.read().await.deref() { - ClientInner::Query(client) => client.get_current_epoch().await?, - ClientInner::Signing(client) => client.get_current_epoch().await?, - }; - - guard.update(epoch).await?; - - return Ok(guard.current_epoch_id); - } - - async fn aggregated_verification_key(&self, epoch_id: EpochId) -> Result { - if let Some(vk) = self.epoch_keys.read().await.get(&epoch_id) { - return Ok(vk.clone()); - } - - let mut guard = self.epoch_keys.write().await; - let coconut_api_clients = match self.nyxd_client.read().await.deref() { - ClientInner::Query(client) => all_coconut_api_clients(client, epoch_id).await?, - ClientInner::Signing(client) => all_coconut_api_clients(client, epoch_id).await?, - }; - - let vk = obtain_aggregate_verification_key(&coconut_api_clients)?; - - guard.insert(epoch_id, vk.clone()); - - Ok(vk) - } -} diff --git a/nym-api/src/coconut/deposit.rs b/nym-api/src/coconut/deposit.rs deleted file mode 100644 index 985e9fadaa..0000000000 --- a/nym-api/src/coconut/deposit.rs +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::{CoconutError, Result}; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_coconut_bandwidth_contract_common::events::{ - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, - DEPOSIT_INFO, DEPOSIT_VALUE, -}; -use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData; -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::helpers::find_tx_attribute; -use nym_validator_client::nyxd::TxResponse; - -pub async fn validate_deposit_tx(request: &BlindSignRequestBody, tx: TxResponse) -> Result<()> { - if request.public_attributes_plain.len() - != IssuanceBandwidthCredential::PUBLIC_ATTRIBUTES as usize - { - return Err(CoconutError::InconsistentPublicAttributes); - } - - // extract actual public attributes + associated x25519 public key - let deposit_value = find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_VALUE) - .ok_or(CoconutError::DepositValueNotFound)?; - - let deposit_info = find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO) - .ok_or(CoconutError::DepositInfoNotFound)?; - - let x25519_raw = find_tx_attribute( - &tx, - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, - DEPOSIT_IDENTITY_KEY, - ) - .ok_or(CoconutError::DepositVerifKeyNotFound)?; - - // we're not using it anymore, but for legacy reasons it must be present - let _ed25519_raw = find_tx_attribute( - &tx, - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, - DEPOSIT_ENCRYPTION_KEY, - ) - .ok_or(CoconutError::DepositEncrKeyNotFound)?; - - // check public attributes against request data - // (thinking about it attaching that data might be redundant since we have the source of truth on the chain) - // safety: we won't read data out of bounds since we just checked we have BandwidthVoucher::PUBLIC_ATTRIBUTES values in the vec - if deposit_value != request.public_attributes_plain[0] { - return Err(CoconutError::InconsistentDepositValue { - request: request.public_attributes_plain[0].clone(), - on_chain: deposit_value, - }); - } - - if deposit_info != request.public_attributes_plain[1] { - return Err(CoconutError::InconsistentDepositInfo { - request: request.public_attributes_plain[1].clone(), - on_chain: deposit_info, - }); - } - - // verify signature - let x25519 = identity::PublicKey::from_base58_string(x25519_raw)?; - let plaintext = BandwidthVoucherIssuanceData::request_plaintext( - &request.inner_sign_request, - request.tx_hash, - ); - x25519.verify(plaintext, &request.signature)?; - - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::coconut::tests::{tx_entry_fixture, voucher_fixture}; - use cosmwasm_std::coin; - use nym_coconut::BlindSignRequest; - use nym_coconut_bandwidth_contract_common::events::DEPOSITED_FUNDS_EVENT_TYPE; - use nym_credentials::coconut::bandwidth::CredentialType; - use nym_validator_client::nyxd::{Event, EventAttribute}; - - #[tokio::test] - async fn validate_deposit_tx_test() { - let voucher = voucher_fixture(coin(1234, "unym"), None); - let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let correct_request = voucher_data.create_blind_sign_request_body(&signing_data); - - let mut tx_entry = tx_entry_fixture(correct_request.tx_hash); - let good_deposit_attribute = EventAttribute { - key: DEPOSIT_VALUE.to_string(), - value: correct_request.public_attributes_plain[0].clone(), - index: false, - }; - let good_info_attribute = EventAttribute { - key: DEPOSIT_INFO.to_string(), - value: correct_request.public_attributes_plain[1].clone(), - index: false, - }; - let good_identity_key_attribute = EventAttribute { - key: DEPOSIT_IDENTITY_KEY.to_string(), - value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He".to_string(), - index: false, - }; - let good_encryption_key_attribute = EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.to_string(), - value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He".to_string(), - index: false, - }; - - tx_entry.tx_result.events.push(Event { - kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE), - attributes: vec![], - }); - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositValueNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - EventAttribute { - key: DEPOSIT_VALUE.parse().unwrap(), - value: "10".parse().unwrap(), - index: false, - }, - good_info_attribute.clone(), - good_identity_key_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::InconsistentDepositValue { - request: "1234".to_string(), - on_chain: "10".to_string() - } - .to_string() - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_identity_key_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositInfoNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - EventAttribute { - key: DEPOSIT_INFO.parse().unwrap(), - value: "bandwidth deposit info".parse().unwrap(), - index: false, - }, - good_identity_key_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::InconsistentDepositInfo { - on_chain: "bandwidth deposit info".to_string(), - request: CredentialType::Voucher.to_string(), - } - .to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositVerifKeyNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "verification key".parse().unwrap(), - index: false, - }, - good_encryption_key_attribute.clone(), - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - - assert!(matches!( - err, - CoconutError::Ed25519ParseError( - nym_crypto::asymmetric::identity::Ed25519RecoveryError::MalformedPublicKeyString { .. } - ) - )); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "2eSxwquNJb2nZTEW5p4rbqjHfBaz9UaNhjHHiexPN4He" - .parse() - .unwrap(), - index: false, - }, - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::DepositEncrKeyNotFound.to_string(), - ); - - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "6EJGMdEq7t8Npz54uPkftGsdmj7DKntLVputAnDfVZB2" - .parse() - .unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: "encryption key".parse().unwrap(), - index: false, - }, - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - - assert!(matches!(err, CoconutError::SignatureVerificationError(..))); - - let expected_encryption_key = "HxnTpWTkgigSTAysVKLE8pEiUULHdTT1BxFfzfJvQRi6"; - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "6EJGMdEq7t8Npz54uPkftGsdmj7DKntLVputAnDfVZB2" - .parse() - .unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: expected_encryption_key.parse().unwrap(), - index: false, - }, - ]; - let err = validate_deposit_tx(&correct_request, tx_entry.clone()) - .await - .unwrap_err(); - assert_eq!( - err.to_string(), - CoconutError::SignatureVerificationError( - nym_crypto::asymmetric::identity::SignatureError::default(), - ) - .to_string(), - ); - - // hard-coded values, that generate a correct signature - let blind_sign_req = BlindSignRequest::from_bytes(&[ - 176, 113, 19, 237, 218, 252, 113, 20, 225, 238, 59, 88, 217, 45, 233, 178, 65, 28, 242, - 0, 222, 48, 110, 216, 26, 111, 51, 235, 61, 74, 200, 15, 130, 245, 45, 170, 155, 190, - 156, 77, 180, 142, 29, 63, 15, 224, 150, 31, 139, 24, 65, 175, 143, 153, 11, 203, 33, - 16, 152, 22, 221, 203, 99, 233, 208, 142, 161, 194, 46, 227, 177, 96, 119, 30, 175, 69, - 104, 14, 2, 191, 26, 94, 30, 165, 15, 28, 40, 176, 1, 78, 253, 79, 20, 137, 102, 74, 2, - 0, 0, 0, 0, 0, 0, 0, 131, 133, 112, 115, 53, 98, 58, 166, 240, 70, 185, 210, 203, 12, - 114, 66, 180, 38, 139, 12, 187, 45, 250, 201, 68, 102, 159, 172, 218, 124, 151, 23, - 172, 18, 216, 122, 246, 7, 185, 76, 20, 167, 123, 122, 152, 241, 175, 226, 176, 8, 170, - 70, 140, 252, 36, 130, 67, 204, 111, 116, 107, 92, 200, 77, 252, 31, 138, 18, 10, 215, - 165, 243, 95, 199, 193, 61, 200, 187, 22, 198, 109, 213, 145, 71, 171, 132, 174, 68, - 105, 248, 0, 115, 50, 55, 199, 84, 67, 16, 125, 216, 250, 154, 115, 174, 9, 206, 44, - 88, 63, 163, 124, 10, 239, 64, 158, 191, 27, 169, 177, 194, 223, 142, 202, 206, 189, - 122, 123, 91, 171, 15, 40, 192, 148, 75, 174, 24, 116, 229, 127, 170, 110, 183, 151, 2, - 118, 168, 22, 113, 87, 237, 91, 228, 249, 120, 114, 255, 53, 175, 245, 39, 2, 0, 0, 0, - 0, 0, 0, 0, 225, 45, 230, 25, 62, 202, 96, 166, 171, 241, 206, 137, 254, 51, 154, 255, - 122, 130, 107, 54, 5, 206, 207, 120, 193, 214, 64, 10, 111, 195, 86, 55, 201, 36, 10, - 18, 154, 158, 183, 87, 185, 59, 228, 89, 134, 193, 217, 188, 64, 164, 249, 21, 248, 20, - 207, 58, 31, 10, 19, 176, 246, 150, 45, 48, 2, 0, 0, 0, 0, 0, 0, 0, 173, 60, 65, 209, - 100, 114, 138, 186, 158, 150, 109, 230, 111, 86, 101, 72, 194, 237, 173, 195, 139, 175, - 238, 25, 169, 18, 188, 75, 77, 54, 111, 20, 115, 235, 195, 2, 123, 133, 164, 81, 15, - 45, 11, 84, 139, 38, 8, 224, 197, 181, 95, 147, 49, 77, 193, 207, 52, 141, 195, 195, - 66, 137, 17, 32, - ]) - .unwrap(); - - let correct_request = BlindSignRequestBody::new( - blind_sign_req, - "7C41AF8266D91DE55E1C8F4712E6A952A165ED3D8C27C7B00428CBD0DE00A52B" - .parse() - .unwrap(), - "3vUCc6MCN5AC2LNgDYjRB1QeErZSN1S8f6K14JHjpUcKWXbjGYFExA8DbwQQBki9gyUqrpBF94Drttb4eMcGQXkp".parse().unwrap(), - voucher.get_plain_public_attributes(), - ); - tx_entry.tx_result.events.get_mut(0).unwrap().attributes = vec![ - good_deposit_attribute.clone(), - good_info_attribute.clone(), - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.parse().unwrap(), - value: "3xoM5GmUSq7YW4YNQrax1fEFLw1GbZozxe6UUoJcrqLG" - .parse() - .unwrap(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: "HxnTpWTkgigSTAysVKLE8pEiUULHdTT1BxFfzfJvQRi6" - .parse() - .unwrap(), - index: false, - }, - ]; - let res = validate_deposit_tx(&correct_request, tx_entry.clone()).await; - assert!(res.is_ok()) - } -} diff --git a/nym-api/src/coconut/helpers.rs b/nym-api/src/coconut/helpers.rs deleted file mode 100644 index 699c9b8823..0000000000 --- a/nym-api/src/coconut/helpers.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::CoconutError; -use crate::coconut::state::bandwidth_credential_params; -use nym_api_requests::coconut::models::FreePassRequest; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_coconut::{Attribute, BlindSignRequest, BlindedSignature, SecretKey}; -use nym_validator_client::nyxd::error::NyxdError::AbciError; - -// If the result is already established, the vote might be redundant and -// thus the transaction might fail -pub(crate) fn accepted_vote_err(ret: Result<(), CoconutError>) -> Result<(), CoconutError> { - if let Err(CoconutError::NyxdError(AbciError { ref log, .. })) = ret { - let accepted_err = - nym_multisig_contract_common::error::ContractError::NotOpen {}.to_string(); - // If redundant voting is not the case, error out on all other error variants - if !log.contains(&accepted_err) { - ret?; - } - } - Ok(()) -} - -pub(crate) trait CredentialRequest { - fn blind_sign_request(&self) -> &BlindSignRequest; - - fn public_attributes(&self) -> Vec; -} - -impl CredentialRequest for BlindSignRequestBody { - fn blind_sign_request(&self) -> &BlindSignRequest { - &self.inner_sign_request - } - - fn public_attributes(&self) -> Vec { - self.public_attributes_hashed() - } -} - -impl CredentialRequest for FreePassRequest { - fn blind_sign_request(&self) -> &BlindSignRequest { - &self.inner_sign_request - } - - fn public_attributes(&self) -> Vec { - self.public_attributes_hashed() - } -} - -pub(crate) fn blind_sign( - request: &C, - signing_key: &SecretKey, -) -> Result { - let public_attributes = request.public_attributes(); - let attributes_ref = public_attributes.iter().collect::>(); - - Ok(nym_coconut::blind_sign( - bandwidth_credential_params(), - signing_key, - request.blind_sign_request(), - &attributes_ref, - )?) -} diff --git a/nym-api/src/coconut/keys/persistence.rs b/nym-api/src/coconut/keys/persistence.rs deleted file mode 100644 index 4a67876036..0000000000 --- a/nym-api/src/coconut/keys/persistence.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::keys::KeyPairWithEpoch; -use crate::coconut::state::bandwidth_credential_params; -use nym_coconut::{CoconutError, KeyPair, SecretKey}; -use nym_coconut_dkg_common::types::EpochId; -use nym_pemstore::traits::PemStorableKey; -use std::mem; - -impl PemStorableKey for KeyPairWithEpoch { - // that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose - type Error = CoconutError; - - fn pem_type() -> &'static str { - "COCONUT KEY WITH EPOCH" - } - - fn to_bytes(&self) -> Vec { - let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec(); - bytes.append(&mut self.keys.secret_key().to_bytes()); - bytes - } - - fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() <= mem::size_of::() { - return Err(CoconutError::Deserialization( - "insufficient number of bytes to decode secret key with epoch id".into(), - )); - } - let epoch_id = EpochId::from_be_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], - ]); - - let sk = SecretKey::from_bytes(&bytes[mem::size_of::()..])?; - let vk = sk.verification_key(bandwidth_credential_params()); - - Ok(KeyPairWithEpoch { - keys: KeyPair::from_keys(sk, vk), - issued_for_epoch: epoch_id, - }) - } -} diff --git a/nym-api/src/coconut/mod.rs b/nym-api/src/coconut/mod.rs deleted file mode 100644 index 5047982304..0000000000 --- a/nym-api/src/coconut/mod.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use self::comm::APICommunicationChannel; -use crate::coconut::client::Client as LocalClient; -use crate::coconut::state::State; -use crate::support::storage::NymApiStorage; -use keys::KeyPair; -use nym_config::defaults::NYM_API_VERSION; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nym_api::routes::{BANDWIDTH, COCONUT_ROUTES}; -use rocket::fairing::AdHoc; - -pub(crate) mod api_routes; -pub(crate) mod client; -pub(crate) mod comm; -mod deposit; -pub(crate) mod dkg; -pub(crate) mod error; -pub(crate) mod helpers; -pub(crate) mod keys; -pub(crate) mod state; -pub(crate) mod storage; -#[cfg(test)] -pub(crate) mod tests; - -// equivalent of 10nym -pub(crate) const MINIMUM_BALANCE: u128 = 10_000000; - -pub fn stage( - client: C, - mix_denom: String, - identity_keypair: identity::KeyPair, - key_pair: KeyPair, - comm_channel: D, - storage: NymApiStorage, -) -> AdHoc -where - C: LocalClient + Send + Sync + 'static, - D: APICommunicationChannel + Send + Sync + 'static, -{ - let state = State::new( - client, - mix_denom, - identity_keypair, - key_pair, - comm_channel, - storage, - ); - AdHoc::on_ignite("Internal Sign Request Stage", |rocket| async { - rocket.manage(state).mount( - // this format! is so ugly... - format!("/{NYM_API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}"), - routes![ - api_routes::get_current_free_pass_nonce, - api_routes::post_free_pass, - api_routes::post_blind_sign, - api_routes::verify_bandwidth_credential, - api_routes::epoch_credentials, - api_routes::issued_credential, - api_routes::issued_credentials, - ], - ) - }) -} diff --git a/nym-api/src/coconut/state.rs b/nym-api/src/coconut/state.rs deleted file mode 100644 index ac7c3632f8..0000000000 --- a/nym-api/src/coconut/state.rs +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::client::Client as LocalClient; -use crate::coconut::comm::APICommunicationChannel; -use crate::coconut::deposit::validate_deposit_tx; -use crate::coconut::error::{CoconutError, Result}; -use crate::coconut::keys::KeyPair; -use crate::coconut::storage::CoconutStorageExt; -use crate::support::storage::NymApiStorage; -use nym_api_requests::coconut::helpers::issued_credential_plaintext; -use nym_api_requests::coconut::BlindSignRequestBody; -use nym_coconut::{BlindedSignature, VerificationKey}; -use nym_coconut_dkg_common::types::EpochId; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::{AccountId, Hash, TxResponse}; -use rand::rngs::OsRng; -use rand::RngCore; -use std::sync::Arc; -use time::{Duration, OffsetDateTime}; -use tokio::sync::{OnceCell, RwLock}; - -pub use nym_credentials::coconut::bandwidth::bandwidth_credential_params; - -pub struct State { - pub(crate) client: Arc, - pub(crate) bandwidth_contract_admin: OnceCell>, - pub(crate) mix_denom: String, - pub(crate) coconut_keypair: KeyPair, - pub(crate) identity_keypair: identity::KeyPair, - pub(crate) comm_channel: Arc, - pub(crate) storage: NymApiStorage, - pub(crate) freepass_nonce: Arc>, - pub(crate) authorised_freepass_requester: Arc>, -} - -const FREEPASS_REQUESTER_TTL: Duration = Duration::hours(1); -const AUTHORISED_FREEPASS_REQUESTER_ENDPOINT: &str = - "https://nymtech.net/.wellknown/authorised-freepass-requester.txt"; - -pub struct AuthorisedFreepassRequester { - address: Option, - refreshed_at: OffsetDateTime, -} - -impl Default for AuthorisedFreepassRequester { - fn default() -> Self { - AuthorisedFreepassRequester { - address: None, - refreshed_at: OffsetDateTime::UNIX_EPOCH, - } - } -} - -impl State { - pub(crate) fn new( - client: C, - mix_denom: String, - identity_keypair: identity::KeyPair, - key_pair: KeyPair, - comm_channel: D, - storage: NymApiStorage, - ) -> Self - where - C: LocalClient + Send + Sync + 'static, - D: APICommunicationChannel + Send + Sync + 'static, - { - let client = Arc::new(client); - let comm_channel = Arc::new(comm_channel); - - let mut nonce = [0u8; 16]; - OsRng.fill_bytes(&mut nonce); - - Self { - client, - bandwidth_contract_admin: OnceCell::new(), - mix_denom, - coconut_keypair: key_pair, - identity_keypair, - comm_channel, - storage, - freepass_nonce: Arc::new(RwLock::new(nonce)), - authorised_freepass_requester: Arc::new(Default::default()), - } - } - - /// Check if this nym-api has already issued a credential for the provided deposit hash. - /// If so, return it. - pub async fn already_issued(&self, tx_hash: Hash) -> Result> { - self.storage - .get_issued_bandwidth_credential_by_hash(&tx_hash.to_string()) - .await? - .map(|cred| cred.try_into()) - .transpose() - } - - pub async fn get_transaction(&self, tx_hash: Hash) -> Result { - self.client.get_tx(tx_hash).await - } - - pub async fn get_bandwidth_contract_admin(&self) -> Result<&Option> { - self.bandwidth_contract_admin - .get_or_try_init(|| async { self.client.bandwidth_contract_admin().await }) - .await - } - - async fn try_get_authorised_freepass_requester(&self) -> Result { - let address = reqwest::Client::builder() - .user_agent(format!( - "nym-api / {} identity: {}", - env!("CARGO_PKG_VERSION"), - self.identity_keypair.public_key().to_base58_string() - )) - .build()? - .get(AUTHORISED_FREEPASS_REQUESTER_ENDPOINT) - .send() - .await? - .text() - .await?; - let trimmed = address.trim(); - - address.parse().map_err( - |_| CoconutError::MalformedAuthorisedFreepassRequesterAddress { - address: trimmed.to_string(), - }, - ) - } - - pub async fn get_authorised_freepass_requester(&self) -> Option { - { - let cached = self.authorised_freepass_requester.read().await; - - // the entry hasn't expired - if cached.refreshed_at + FREEPASS_REQUESTER_TTL >= OffsetDateTime::now_utc() { - if let Some(cached_address) = cached.address.as_ref() { - return Some(cached_address.clone()); - } - } - } - - // refresh cache - let mut cache = self.authorised_freepass_requester.write().await; - - // whatever happens, update refresh time - cache.refreshed_at = OffsetDateTime::now_utc(); - - let refreshed = match self.try_get_authorised_freepass_requester().await { - Ok(upstream) => upstream, - Err(err) => { - warn!("failed to obtain authorised freepass requester address: {err}"); - return None; - } - }; - - cache.address = Some(refreshed.clone()); - Some(refreshed) - } - - pub async fn validate_request( - &self, - request: &BlindSignRequestBody, - tx: TxResponse, - ) -> Result<()> { - validate_deposit_tx(request, tx).await - } - - pub(crate) async fn sign_and_store_credential( - &self, - current_epoch: EpochId, - request_body: BlindSignRequestBody, - blinded_signature: &BlindedSignature, - ) -> Result { - let encoded_commitments = request_body.encode_commitments(); - - let plaintext = issued_credential_plaintext( - current_epoch as u32, - request_body.tx_hash, - blinded_signature, - &encoded_commitments, - &request_body.public_attributes_plain, - ); - - let signature = self.identity_keypair.private_key().sign(plaintext); - - // note: we have a UNIQUE constraint on the tx_hash column of the credential - // and so if the api is processing request for the same hash at the same time, - // only one of them will be successfully inserted to the database - let credential_id = self - .storage - .store_issued_credential( - current_epoch as u32, - request_body.tx_hash, - blinded_signature, - signature, - encoded_commitments, - request_body.public_attributes_plain, - ) - .await?; - - Ok(credential_id) - } - - pub async fn store_issued_credential( - &self, - request_body: BlindSignRequestBody, - blinded_signature: &BlindedSignature, - ) -> Result<()> { - let current_epoch = self.comm_channel.current_epoch().await?; - - // note: we have a UNIQUE constraint on the tx_hash column of the credential - // and so if the api is processing request for the same hash at the same time, - // only one of them will be successfully inserted to the database - let credential_id = self - .sign_and_store_credential(current_epoch, request_body, blinded_signature) - .await?; - self.storage - .update_epoch_credentials_entry(current_epoch, credential_id) - .await?; - debug!("the stored credential has id {credential_id}"); - - Ok(()) - } - - pub async fn verification_key(&self, epoch_id: EpochId) -> Result { - self.comm_channel - .aggregated_verification_key(epoch_id) - .await - } -} diff --git a/nym-api/src/coconut/storage/manager.rs b/nym-api/src/coconut/storage/manager.rs deleted file mode 100644 index 8cea9ef898..0000000000 --- a/nym-api/src/coconut/storage/manager.rs +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::storage::models::{EpochCredentials, IssuedCredential}; -use crate::support::storage::manager::StorageManager; -use nym_coconut_dkg_common::types::EpochId; -use thiserror::Error; - -#[async_trait] -pub trait CoconutStorageManagerExt { - /// Gets the information about all issued partial credentials in this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, sqlx::Error>; - - /// Creates new entry for EpochCredentials for this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error>; - - /// Update the EpochCredentials by incrementing the total number of issued credentials, - /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) - /// - /// # Arguments - /// * `epoch_id`: Id of the (coconut) epoch in question. - /// * `credential_id`: (database) Id of the coconut credential that triggered the update. - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), sqlx::Error>; - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `credential_id`: (database) id of the issued credential - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, sqlx::Error>; - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `tx_hash`: transaction hash of the deposit used in the issued bandwidth credential - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, sqlx::Error>; - - /// Store the provided issued credential information and return its (database) id. - /// - /// # Arguments - /// - /// * `credential`: partial credential, alongside any data required for verification. - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: String, - bs58_partial_credential: String, - bs58_signature: String, - joined_private_commitments: String, - joined_public_attributes: String, - ) -> Result; - - /// Attempts to retrieve issued credentials from the data store using provided ids. - /// - /// # Arguments - /// - /// * `credential_ids`: (database) ids of the issued credentials - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, sqlx::Error>; - - /// Attempts to retrieve issued credentials from the data store using pagination specification. - /// - /// # Arguments - /// - /// * `start_after`: the value preceding the first retrieved result - /// * `limit`: the maximum number of entries to retrieve - async fn get_issued_credentials_paged( - &self, - start_after: i64, - limit: u32, - ) -> Result, sqlx::Error>; - - async fn increment_issued_freepasses(&self) -> Result<(), sqlx::Error>; -} - -#[async_trait] -impl CoconutStorageManagerExt for StorageManager { - /// Gets the information about all issued partial credentials in this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, sqlx::Error> { - // even if we were changing epochs every second, it's rather impossible to overflow here - // within any sane amount of time - assert!(epoch_id <= u32::MAX as u64); - let epoch_id_downcasted = epoch_id as u32; - - sqlx::query_as!( - EpochCredentials, - r#" - SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" - FROM epoch_credentials - WHERE epoch_id = ? - "#, - epoch_id_downcasted - ) - .fetch_optional(&self.connection_pool) - .await - } - - /// Creates new entry for EpochCredentials for this (coconut) epoch. - /// - /// # Arguments - /// - /// * `epoch_id`: Id of the (coconut) epoch in question. - async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error> { - // even if we were changing epochs every second, it's rather impossible to overflow here - // within any sane amount of time - assert!(epoch_id <= u32::MAX as u64); - let epoch_id_downcasted = epoch_id as u32; - - sqlx::query!( - r#" - INSERT INTO epoch_credentials - (epoch_id, start_id, total_issued) - VALUES (?, ?, ?); - "#, - epoch_id_downcasted, - -1, - 0 - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - // the logic in this function can be summarised with: - // 1. get the current EpochCredentials for this epoch - // 2. if it exists -> increment `total_issued` - // 3. it has invalid (i.e. -1) `start_id` set it to the provided value - // 4. if it didn't exist, create new entry - /// Update the EpochCredentials by incrementing the total number of issued credentials, - /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) - /// - /// # Arguments - /// * `epoch_id`: Id of the (coconut) epoch in question. - /// * `credential_id`: (database) Id of the coconut credential that triggered the update. - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), sqlx::Error> { - // even if we were changing epochs every second, it's rather impossible to overflow here - // within any sane amount of time - assert!(epoch_id <= u32::MAX as u64); - let epoch_id_downcasted = epoch_id as u32; - - // make the atomic transaction in case other tasks are attempting to use the pool - let mut tx = self.connection_pool.begin().await?; - - if let Some(existing) = sqlx::query_as!( - EpochCredentials, - r#" - SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" - FROM epoch_credentials - WHERE epoch_id = ? - "#, - epoch_id_downcasted - ) - .fetch_optional(&mut tx) - .await? - { - // the entry has existed before -> update it - if existing.total_issued == 0 { - // no credentials has been issued -> we have to set the `start_id` - sqlx::query!( - r#" - UPDATE epoch_credentials - SET total_issued = 1, start_id = ? - WHERE epoch_id = ? - "#, - credential_id, - epoch_id_downcasted - ) - .execute(&mut tx) - .await?; - } else { - // we have issued credentials in this epoch before -> just increment `total_issued` - sqlx::query!( - r#" - UPDATE epoch_credentials - SET total_issued = total_issued + 1 - WHERE epoch_id = ? - "#, - epoch_id_downcasted - ) - .execute(&mut tx) - .await?; - } - } else { - // the entry has never been created -> probably some race condition; create it instead - sqlx::query!( - r#" - INSERT INTO epoch_credentials - (epoch_id, start_id, total_issued) - VALUES (?, ?, ?); - "#, - epoch_id_downcasted, - credential_id, - 1 - ) - .execute(&mut tx) - .await?; - } - - // finally commit the transaction - tx.commit().await - } - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `credential_id`: (database) id of the issued credential - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - IssuedCredential, - r#" - SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes - FROM issued_credential - WHERE id = ? - "#, - credential_id - ) - .fetch_optional(&self.connection_pool) - .await - } - - /// Attempts to retrieve an issued credential from the data store. - /// - /// # Arguments - /// - /// * `tx_hash`: transaction hash of the deposit used in the issued bandwidth credential - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - IssuedCredential, - r#" - SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes - FROM issued_credential - WHERE tx_hash = ? - "#, - tx_hash - ) - .fetch_optional(&self.connection_pool) - .await - } - - /// Store the provided issued credential information and return its (database) id. - /// - /// # Arguments - /// - /// * `credential`: partial credential, alongside any data required for verification. - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: String, - bs58_partial_credential: String, - bs58_signature: String, - joined_private_commitments: String, - joined_public_attributes: String, - ) -> Result { - let row_id = sqlx::query!( - r#" - INSERT INTO issued_credential - (epoch_id, tx_hash, bs58_partial_credential, bs58_signature, joined_private_commitments, joined_public_attributes) - VALUES - (?, ?, ?, ?, ?, ?) - "#, - epoch_id, tx_hash, bs58_partial_credential, bs58_signature, joined_private_commitments, joined_public_attributes - ).execute(&self.connection_pool).await?.last_insert_rowid(); - - Ok(row_id) - } - - /// Attempts to retrieve issued credentials from the data store using provided ids. - /// - /// # Arguments - /// - /// * `credential_ids`: (database) ids of the issued credentials - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, sqlx::Error> { - // that sucks : ( - // https://stackoverflow.com/a/70032524 - let params = format!("?{}", ", ?".repeat(credential_ids.len() - 1)); - let query_str = format!("SELECT * FROM issued_credential WHERE id IN ( {params} )"); - let mut query = sqlx::query_as(&query_str); - for id in credential_ids { - query = query.bind(id) - } - - query.fetch_all(&self.connection_pool).await - } - - /// Attempts to retrieve issued credentials from the data store using pagination specification. - /// - /// # Arguments - /// - /// * `start_after`: the value preceding the first retrieved result - /// * `limit`: the maximum number of entries to retrieve - async fn get_issued_credentials_paged( - &self, - start_after: i64, - limit: u32, - ) -> Result, sqlx::Error> { - sqlx::query_as!( - IssuedCredential, - r#" - SELECT id, epoch_id as "epoch_id: u32", tx_hash, bs58_partial_credential, bs58_signature,joined_private_commitments, joined_public_attributes - FROM issued_credential - WHERE id > ? - ORDER BY id - LIMIT ? - "#, - start_after, - limit - ) - .fetch_all(&self.connection_pool) - .await - } - - async fn increment_issued_freepasses(&self) -> Result<(), sqlx::Error> { - sqlx::query!("UPDATE issued_freepass SET issued = issued + 1",) - .execute(&self.connection_pool) - .await?; - Ok(()) - } -} - -#[derive(Debug, Error)] -#[error("tried to store an invalid nonce. the received value is {got} while current is {current}. expected {current} + 1")] -pub struct UnexpectedNonce { - current: u32, - got: u32, -} diff --git a/nym-api/src/coconut/storage/mod.rs b/nym-api/src/coconut/storage/mod.rs deleted file mode 100644 index 1b29f51b43..0000000000 --- a/nym-api/src/coconut/storage/mod.rs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::storage::manager::CoconutStorageManagerExt; -use crate::coconut::storage::models::{join_attributes, EpochCredentials, IssuedCredential}; -use crate::node_status_api::models::NymApiStorageError; -use crate::support::storage::NymApiStorage; -use nym_api_requests::coconut::models::Pagination; -use nym_coconut::{Base58, BlindedSignature}; -use nym_coconut_dkg_common::types::EpochId; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::Hash; - -pub(crate) mod manager; -pub(crate) mod models; - -const DEFAULT_CREDENTIALS_PAGE_LIMIT: u32 = 100; - -#[async_trait] -pub trait CoconutStorageExt { - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, NymApiStorageError>; - - #[allow(dead_code)] - async fn create_epoch_credentials_entry( - &self, - epoch_id: EpochId, - ) -> Result<(), NymApiStorageError>; - - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), NymApiStorageError>; - - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, NymApiStorageError>; - - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, NymApiStorageError>; - - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: Hash, - partial_credential: &BlindedSignature, - signature: identity::Signature, - private_commitments: Vec, - public_attributes: Vec, - ) -> Result; - - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, NymApiStorageError>; - - async fn get_issued_credentials_paged( - &self, - pagination: Pagination, - ) -> Result, NymApiStorageError>; - - async fn increment_issued_freepasses(&self) -> Result<(), NymApiStorageError>; -} - -#[async_trait] -impl CoconutStorageExt for NymApiStorage { - async fn get_epoch_credentials( - &self, - epoch_id: EpochId, - ) -> Result, NymApiStorageError> { - Ok(self.manager.get_epoch_credentials(epoch_id).await?) - } - - async fn create_epoch_credentials_entry( - &self, - epoch_id: EpochId, - ) -> Result<(), NymApiStorageError> { - Ok(self - .manager - .create_epoch_credentials_entry(epoch_id) - .await?) - } - - async fn update_epoch_credentials_entry( - &self, - epoch_id: EpochId, - credential_id: i64, - ) -> Result<(), NymApiStorageError> { - Ok(self - .manager - .update_epoch_credentials_entry(epoch_id, credential_id) - .await?) - } - - async fn get_issued_credential( - &self, - credential_id: i64, - ) -> Result, NymApiStorageError> { - Ok(self.manager.get_issued_credential(credential_id).await?) - } - - async fn get_issued_bandwidth_credential_by_hash( - &self, - tx_hash: &str, - ) -> Result, NymApiStorageError> { - Ok(self - .manager - .get_issued_bandwidth_credential_by_hash(tx_hash) - .await?) - } - - async fn store_issued_credential( - &self, - epoch_id: u32, - tx_hash: Hash, - partial_credential: &BlindedSignature, - signature: identity::Signature, - private_commitments: Vec, - public_attributes: Vec, - ) -> Result { - Ok(self - .manager - .store_issued_credential( - epoch_id, - tx_hash.to_string(), - partial_credential.to_bs58(), - signature.to_base58_string(), - join_attributes(private_commitments), - join_attributes(public_attributes), - ) - .await?) - } - - async fn get_issued_credentials( - &self, - credential_ids: Vec, - ) -> Result, NymApiStorageError> { - Ok(self.manager.get_issued_credentials(credential_ids).await?) - } - - async fn get_issued_credentials_paged( - &self, - pagination: Pagination, - ) -> Result, NymApiStorageError> { - // rows start at 1 - let start_after = pagination.last_key.unwrap_or(0); - let limit = match pagination.limit { - Some(v) => { - if v == 0 || v > DEFAULT_CREDENTIALS_PAGE_LIMIT { - DEFAULT_CREDENTIALS_PAGE_LIMIT - } else { - v - } - } - None => DEFAULT_CREDENTIALS_PAGE_LIMIT, - }; - - Ok(self - .manager - .get_issued_credentials_paged(start_after, limit) - .await?) - } - - async fn increment_issued_freepasses(&self) -> Result<(), NymApiStorageError> { - Ok(self.manager.increment_issued_freepasses().await?) - } -} diff --git a/nym-api/src/coconut/storage/models.rs b/nym-api/src/coconut/storage/models.rs deleted file mode 100644 index 4d2935c057..0000000000 --- a/nym-api/src/coconut/storage/models.rs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::coconut::error::CoconutError; -use nym_api_requests::coconut::models::{ - EpochCredentialsResponse, IssuedCredential as ApiIssuedCredential, - IssuedCredentialBody as ApiIssuedCredentialInner, -}; -use nym_api_requests::coconut::BlindedSignatureResponse; -use nym_coconut::{Base58, BlindedSignature}; -use sqlx::FromRow; -use std::fmt::Display; - -pub struct EpochCredentials { - pub epoch_id: u32, - pub start_id: i64, - pub total_issued: u32, -} - -impl From for EpochCredentialsResponse { - fn from(value: EpochCredentials) -> Self { - let first_epoch_credential_id = if value.start_id == -1 { - None - } else { - Some(value.start_id) - }; - - EpochCredentialsResponse { - epoch_id: value.epoch_id as u64, - first_epoch_credential_id, - total_issued: value.total_issued, - } - } -} - -#[derive(FromRow)] -pub struct IssuedCredential { - pub id: i64, - pub epoch_id: u32, - pub tx_hash: String, - - /// base58-encoded issued credential - pub bs58_partial_credential: String, - - /// base58-encoded signature on the issued credential (and the attributes) - pub bs58_signature: String, - - // i.e. "'attr1','attr2',..." - pub joined_private_commitments: String, - - // i.e. "'attr1','attr2',..." - pub joined_public_attributes: String, -} - -impl TryFrom for ApiIssuedCredentialInner { - type Error = CoconutError; - - fn try_from(value: IssuedCredential) -> Result { - Ok(ApiIssuedCredentialInner { - credential: ApiIssuedCredential { - id: value.id, - epoch_id: value.epoch_id, - tx_hash: value - .tx_hash - .parse() - .map_err(|source| CoconutError::TxHashParseError { source })?, - blinded_partial_credential: BlindedSignature::try_from_bs58( - value.bs58_partial_credential, - )?, - bs58_encoded_private_attributes_commitments: split_attributes( - &value.joined_private_commitments, - ), - public_attributes: split_attributes(&value.joined_public_attributes), - }, - signature: value.bs58_signature.parse()?, - }) - } -} - -impl TryFrom for BlindedSignatureResponse { - type Error = CoconutError; - - fn try_from(value: IssuedCredential) -> Result { - Ok(BlindedSignatureResponse { - blinded_signature: BlindedSignature::try_from_bs58(value.bs58_partial_credential)?, - }) - } -} - -impl TryFrom for BlindedSignature { - type Error = CoconutError; - - fn try_from(value: IssuedCredential) -> Result { - Ok(BlindedSignature::try_from_bs58( - value.bs58_partial_credential, - )?) - } -} - -pub fn join_attributes(attrs: I) -> String -where - I: IntoIterator, - M: Display, -{ - // I could have used `attrs.into_iter().join(",")`, - // but my IDE didn't like it (compiler was fine) - itertools::Itertools::join(&mut attrs.into_iter(), ",") -} - -pub fn split_attributes(attrs: &str) -> Vec { - attrs.split(',').map(|s| s.to_string()).collect() -} diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs new file mode 100644 index 0000000000..f552698d86 --- /dev/null +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -0,0 +1,81 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::{EcashError, Result}; +use crate::ecash::state::EcashState; +use log::trace; +use nym_api_requests::ecash::models::{ + AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, +}; +use nym_api_requests::ecash::VerificationKeyResponse; +use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_validator_client::nym_api::rfc_3339_date; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; +use time::Date; + +// routes with globally aggregated keys, signatures, etc. + +#[openapi(tag = "Ecash Global Data")] +#[get("/master-verification-key?")] +pub async fn master_verification_key( + epoch_id: Option, + state: &RocketState, +) -> Result> { + trace!("aggregated_verification_key request"); + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let key = state.master_verification_key(epoch_id).await?; + + Ok(Json(VerificationKeyResponse::new(key.clone()))) +} + +#[openapi(tag = "Ecash Global Data")] +#[get("/aggregated-expiration-date-signatures?")] +pub async fn expiration_date_signatures( + expiration_date: Option, + state: &RocketState, +) -> Result> { + trace!("aggregated_expiration_date_signatures request"); + + let expiration_date = match expiration_date { + None => cred_exp_date().ecash_date(), + Some(raw) => Date::parse(&raw, &rfc_3339_date()) + .map_err(|_| EcashError::MalformedExpirationDate { raw })?, + }; + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let expiration_date_signatures = state + .master_expiration_date_signatures(expiration_date) + .await?; + + Ok(Json(AggregatedExpirationDateSignatureResponse { + epoch_id: expiration_date_signatures.epoch_id, + expiration_date, + signatures: expiration_date_signatures.signatures.clone(), + })) +} + +#[openapi(tag = "Ecash Global Data")] +#[get("/aggregated-coin-indices-signatures?")] +pub async fn coin_indices_signatures( + epoch_id: Option, + state: &RocketState, +) -> Result> { + trace!("aggregated_coin_indices_signatures request"); + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let coin_indices_signatures = state.master_coin_index_signatures(epoch_id).await?; + + Ok(Json(AggregatedCoinIndicesSignatureResponse { + epoch_id: coin_indices_signatures.epoch_id, + signatures: coin_indices_signatures.signatures.clone(), + })) +} diff --git a/nym-api/src/coconut/api_routes/helpers.rs b/nym-api/src/ecash/api_routes/helpers.rs similarity index 81% rename from nym-api/src/coconut/api_routes/helpers.rs rename to nym-api/src/ecash/api_routes/helpers.rs index 93446d0d15..b05272d472 100644 --- a/nym-api/src/coconut/api_routes/helpers.rs +++ b/nym-api/src/ecash/api_routes/helpers.rs @@ -1,10 +1,10 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::Result; -use crate::coconut::storage::models::IssuedCredential; -use nym_api_requests::coconut::models::IssuedCredentialBody; -use nym_api_requests::coconut::models::IssuedCredentialsResponse; +use crate::ecash::error::Result; +use crate::ecash::storage::models::IssuedCredential; +use nym_api_requests::ecash::models::IssuedCredentialBody; +use nym_api_requests::ecash::models::IssuedCredentialsResponse; use std::collections::BTreeMap; pub(crate) fn build_credentials_response( diff --git a/nym-api/src/ecash/api_routes/issued.rs b/nym-api/src/ecash/api_routes/issued.rs new file mode 100644 index 0000000000..5b85e6b0cf --- /dev/null +++ b/nym-api/src/ecash/api_routes/issued.rs @@ -0,0 +1,82 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::api_routes::helpers::build_credentials_response; +use crate::ecash::error::EcashError; +use crate::ecash::state::EcashState; +use crate::ecash::storage::EcashStorageExt; +use nym_api_requests::ecash::models::{ + EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, +}; +use nym_api_requests::ecash::CredentialsRequestBody; +use nym_coconut_dkg_common::types::EpochId; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; + +#[openapi(tag = "Ecash")] +#[get("/epoch-credentials/")] +pub async fn epoch_credentials( + epoch: EpochId, + state: &RocketState, +) -> crate::ecash::error::Result> { + let issued = state.aux.storage.get_epoch_credentials(epoch).await?; + + let response = if let Some(issued) = issued { + issued.into() + } else { + EpochCredentialsResponse { + epoch_id: epoch, + first_epoch_credential_id: None, + total_issued: 0, + } + }; + + Ok(Json(response)) +} + +#[openapi(tag = "Ecash")] +#[get("/issued-credential/")] +pub async fn issued_credential( + id: i64, + state: &RocketState, +) -> crate::ecash::error::Result> { + let issued = state.aux.storage.get_issued_credential(id).await?; + + let credential = if let Some(issued) = issued { + Some(issued.try_into()?) + } else { + None + }; + + Ok(Json(IssuedCredentialResponse { credential })) +} + +#[openapi(tag = "Ecash")] +#[post("/issued-credentials", data = "")] +pub async fn issued_credentials( + params: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + let params = params.into_inner(); + + if params.pagination.is_some() && !params.credential_ids.is_empty() { + return Err(EcashError::InvalidQueryArguments); + } + + let credentials = if let Some(pagination) = params.pagination { + state + .aux + .storage + .get_issued_credentials_paged(pagination) + .await? + } else { + state + .aux + .storage + .get_issued_credentials(params.credential_ids) + .await? + }; + + build_credentials_response(credentials).map(Json) +} diff --git a/nym-api/src/ecash/api_routes/mod.rs b/nym-api/src/ecash/api_routes/mod.rs new file mode 100644 index 0000000000..6037a8d18f --- /dev/null +++ b/nym-api/src/ecash/api_routes/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod aggregation; +mod helpers; +pub(crate) mod issued; +pub(crate) mod partial_signing; +pub(crate) mod spending; diff --git a/nym-api/src/ecash/api_routes/partial_signing.rs b/nym-api/src/ecash/api_routes/partial_signing.rs new file mode 100644 index 0000000000..a0958a2a6b --- /dev/null +++ b/nym-api/src/ecash/api_routes/partial_signing.rs @@ -0,0 +1,118 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::ecash::helpers::blind_sign; +use crate::ecash::state::EcashState; +use nym_api_requests::ecash::{ + BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, + PartialExpirationDateSignatureResponse, +}; +use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_validator_client::nym_api::rfc_3339_date; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; +use std::ops::Deref; +use time::Date; + +#[openapi(tag = "Ecash")] +#[post("/blind-sign", data = "")] +pub async fn post_blind_sign( + blind_sign_request_body: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + debug!("Received blind sign request"); + trace!("body: {:?}", blind_sign_request_body); + + // check if we have the signing key available + debug!("checking if we actually have ecash keys derived..."); + let signing_key = state.ecash_signing_key().await?; + + // basic check of expiration date validity + if blind_sign_request_body.expiration_date > cred_exp_date().ecash_date() { + return Err(EcashError::ExpirationDateTooLate); + } + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + // check if we already issued a credential for this deposit + let deposit_id = blind_sign_request_body.deposit_id; + debug!( + "checking if we have already issued credential for this deposit (deposit_id: {deposit_id})", + ); + if let Some(blinded_signature) = state.already_issued(deposit_id).await? { + return Ok(Json(BlindedSignatureResponse { blinded_signature })); + } + + //check if account was blacklisted + let pub_key_bs58 = blind_sign_request_body.ecash_pubkey.to_base58_string(); + state.aux.ensure_not_blacklisted(&pub_key_bs58).await?; + + // get the deposit details of the claimed id + debug!("getting deposit details from the chain"); + let deposit = state.get_deposit(deposit_id).await?; + + // check validity of the request + debug!("fully validating received request"); + state + .validate_request(&blind_sign_request_body, deposit) + .await?; + + // produce the partial signature + debug!("producing the partial credential"); + let blinded_signature = blind_sign(blind_sign_request_body.deref(), signing_key.deref())?; + + // store the information locally + debug!("storing the issued credential in the database"); + state + .store_issued_credential(blind_sign_request_body.into_inner(), &blinded_signature) + .await?; + + // finally return the credential to the client + Ok(Json(BlindedSignatureResponse { blinded_signature })) +} + +#[openapi(tag = "Ecash")] +#[get("/partial-expiration-date-signatures?")] +pub async fn partial_expiration_date_signatures( + expiration_date: Option, + state: &RocketState, +) -> crate::ecash::error::Result> { + let expiration_date = match expiration_date { + None => cred_exp_date().ecash_date(), + Some(raw) => Date::parse(&raw, &rfc_3339_date()) + .map_err(|_| EcashError::MalformedExpirationDate { raw })?, + }; + + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let expiration_date_signatures = state + .partial_expiration_date_signatures(expiration_date) + .await?; + + Ok(Json(PartialExpirationDateSignatureResponse { + epoch_id: expiration_date_signatures.epoch_id, + expiration_date, + signatures: expiration_date_signatures.signatures.clone(), + })) +} + +#[openapi(tag = "Ecash")] +#[get("/partial-coin-indices-signatures?")] +pub async fn partial_coin_indices_signatures( + epoch_id: Option, + state: &RocketState, +) -> crate::ecash::error::Result> { + // see if we're not in the middle of new dkg + state.ensure_dkg_not_in_progress().await?; + + let coin_indices_signatures = state.partial_coin_index_signatures(epoch_id).await?; + + Ok(Json(PartialCoinIndicesSignatureResponse { + epoch_id: coin_indices_signatures.epoch_id, + signatures: coin_indices_signatures.signatures.clone(), + })) +} diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs new file mode 100644 index 0000000000..d6bb91ea21 --- /dev/null +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -0,0 +1,196 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::ecash::state::EcashState; +use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; +use nym_api_requests::ecash::models::{ + BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationRejection, + EcashTicketVerificationResponse, SpentCredentialsResponse, VerifyEcashTicketBody, +}; +use nym_compact_ecash::identify::IdentifyResult; +use nym_ecash_time::EcashTime; +use rocket::serde::json::Json; +use rocket::State as RocketState; +use rocket_okapi::openapi; +use std::collections::HashSet; +use std::ops::Deref; +use time::macros::time; +use time::{OffsetDateTime, Time}; + +const ONE_AM: Time = time!(1:00); + +fn reject_ticket( + reason: EcashTicketVerificationRejection, +) -> crate::ecash::error::Result> { + Ok(Json(EcashTicketVerificationResponse::reject(reason))) +} + +// TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential` +// introduce bloomfilter checks without touching storage first, etc. +#[openapi(tag = "Ecash")] +#[post("/verify-ecash-ticket", data = "")] +pub async fn verify_ticket( + // TODO in the future: make it send binary data rather than json + verify_ticket_body: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + let credential_data = &verify_ticket_body.credential; + let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr; + let sn = &credential_data.encoded_serial_number(); + let spend_date = credential_data.spend_date; + let epoch_id = credential_data.epoch_id; + + let now = OffsetDateTime::now_utc(); + let today_ecash = now.ecash_date(); + + #[allow(clippy::unwrap_used)] + let yesterday_ecash = today_ecash.previous_day().unwrap(); + + // only accept yesterday date if we're near the day transition, i.e. before 1AM UTC + if spend_date != today_ecash && now.time() > ONE_AM && spend_date != yesterday_ecash { + return reject_ticket(EcashTicketVerificationRejection::InvalidSpentDate { + today: today_ecash, + yesterday: yesterday_ecash, + received: spend_date, + }); + } + + // check the bloomfilter for obvious double-spending so that we wouldn't need to waste time on crypto verification + // TODO: when blacklisting is implemented, this should get removed + if state.check_bloomfilter(sn).await { + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + + // actual double spend detection with storage + if let Some(previous_payment) = state.get_ticket_data_by_serial_number(sn).await? { + match nym_compact_ecash::identify::identify( + &credential_data.payment, + &previous_payment.payment, + credential_data.pay_info, + previous_payment.pay_info, + ) { + IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? + IdentifyResult::DuplicatePayInfo(_) => { + log::warn!("Identical payInfo"); + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { + //Actual double spending + log::warn!( + "Double spending attempt for key {}", + pub_key.to_base58_string() + ); + log::error!("UNIMPLEMENTED: blacklisting the double spend key"); + return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); + } + } + } + + let verification_key = state.master_verification_key(Some(epoch_id)).await?; + + // perform actual crypto verification + if credential_data.verify(&verification_key).is_err() { + return reject_ticket(EcashTicketVerificationRejection::InvalidTicket); + } + + // finally get EXCLUSIVE lock on the bloomfilter, check if for the final time and insert the SN + let was_present = state + .update_bloomfilter(sn, spend_date, today_ecash) + .await?; + if was_present { + return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + } + + //store credential + state + .store_verified_ticket(credential_data, gateway_cosmos_addr) + .await?; + + Ok(Json(EcashTicketVerificationResponse { verified: Ok(()) })) +} + +// // for particular SN returns what gateway has submitted it and whether it has been verified correctly +// pub async fn credential_status() -> ! { +// todo!() +// } + +#[openapi(tag = "Ecash")] +#[post( + "/batch-redeem-ecash-tickets", + data = "" +)] +pub async fn batch_redeem_tickets( + // TODO in the future: make it send binary data rather than json + batch_redeem_credentials_body: Json, + state: &RocketState, +) -> crate::ecash::error::Result> { + // 1. see if that gateway has even submitted any tickets + let Some(provider_info) = state + .get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref()) + .await? + else { + return Err(EcashError::NotTicketsProvided); + }; + + // 2. check if the gateway is not trying to spam the redemption requests + // (we have to protect our poor chain) + if let Some(last_redemption) = provider_info.last_batch_verification { + let now = OffsetDateTime::now_utc(); + let next_allowed = last_redemption + MIN_BATCH_REDEMPTION_DELAY; + + if next_allowed > now { + return Err(EcashError::TooFrequentRedemption { + last_redemption, + next_allowed, + }); + } + } + + // 3. verify the request digest + if !batch_redeem_credentials_body.verify_digest() { + return Err(EcashError::MismatchedRequestDigest); + } + + // 4. verify the associated on-chain proposal (whether it's made by correct sender, has valid messages, etc.) + state + .validate_redemption_proposal(&batch_redeem_credentials_body) + .await?; + + let proposal_id = batch_redeem_credentials_body.proposal_id; + let received = batch_redeem_credentials_body + .into_inner() + .included_serial_numbers; + + // 5. check if **every** serial number included in the request has been verified by us + // if we have more than requested, tough luck, they're going to lose them + let verified = state.get_redeemable_tickets(provider_info).await?; + let verified_tickets: HashSet<_> = verified.iter().map(|sn| sn.deref()).collect(); + + for sn in &received { + if !verified_tickets.contains(sn.deref()) { + return Err(EcashError::TicketNotVerified { + serial_number_bs58: bs58::encode(sn).into_string(), + }); + } + } + + // TODO: offload it to separate task with work queue and batching (of tx messages) to vote for multiple proposals in the same tx + state.accept_proposal(proposal_id).await?; + Ok(Json(EcashBatchTicketRedemptionResponse { + proposal_accepted: true, + })) +} + +// explicitly mark it as v1 in the URL because the response type WILL change; +// it will probably be compressed bincode or something +#[openapi(tag = "Ecash")] +#[get("/double-spending-filter-v1")] +pub async fn double_spending_filter_v1( + state: &RocketState, +) -> crate::ecash::error::Result> { + let spent_credentials_export = state.export_bloomfilter().await; + Ok(Json(SpentCredentialsResponse::new( + spent_credentials_export, + ))) +} diff --git a/nym-api/src/coconut/client.rs b/nym-api/src/ecash/client.rs similarity index 85% rename from nym-api/src/coconut/client.rs rename to nym-api/src/ecash/client.rs index d009989996..e0970f2155 100644 --- a/nym-api/src/coconut/client.rs +++ b/nym-api/src/ecash/client.rs @@ -1,10 +1,9 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::Result; +use crate::ecash::error::Result; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use nym_coconut_dkg_common::dealer::{ DealerDetails, DealerDetailsResponse, RegisteredDealerDetails, }; @@ -19,8 +18,11 @@ use nym_coconut_dkg_common::types::{ use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use nym_contracts_common::IdentityKey; use nym_dkg::Threshold; +use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; +use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; -use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse}; +use nym_validator_client::nyxd::{AccountId, Fee}; +use nym_validator_client::EcashApiClient; #[async_trait] pub trait Client { @@ -28,9 +30,7 @@ pub trait Client { async fn dkg_contract_address(&self) -> Result; - async fn bandwidth_contract_admin(&self) -> Result>; - - async fn get_tx(&self, tx_hash: Hash) -> Result; + async fn get_deposit(&self, deposit_id: DepositId) -> Result; async fn get_proposal(&self, proposal_id: u64) -> Result; @@ -38,10 +38,10 @@ pub trait Client { async fn get_vote(&self, proposal_id: u64, voter: String) -> Result; - async fn get_spent_credential( + async fn get_blacklisted_account( &self, - blinded_serial_number: String, - ) -> Result; + public_key: String, + ) -> Result; async fn contract_state(&self) -> Result; @@ -50,6 +50,7 @@ pub trait Client { async fn group_member(&self, addr: String) -> Result; async fn get_current_epoch_threshold(&self) -> Result>; + async fn get_epoch_threshold(&self, epoch_id: EpochId) -> Result>; async fn get_self_registered_dealer_details(&self) -> Result; @@ -99,6 +100,8 @@ pub trait Client { async fn get_verification_key_shares(&self, epoch_id: EpochId) -> Result>; + async fn get_registered_ecash_clients(&self, epoch_id: EpochId) -> Result>; + async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option) -> Result<()>; diff --git a/nym-api/src/ecash/comm.rs b/nym-api/src/ecash/comm.rs new file mode 100644 index 0000000000..20941c484c --- /dev/null +++ b/nym-api/src/ecash/comm.rs @@ -0,0 +1,147 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::client::Client; +use crate::ecash::error::{EcashError, Result}; +use crate::ecash::helpers::CachedImmutableEpochItem; +use crate::{ecash, nyxd}; +use nym_coconut_dkg_common::types::{Epoch, EpochId}; +use nym_dkg::Threshold; +use nym_validator_client::EcashApiClient; +use std::cmp::min; +use time::OffsetDateTime; +use tokio::sync::{RwLock, RwLockWriteGuard}; + +#[async_trait] +pub trait APICommunicationChannel { + async fn current_epoch(&self) -> Result; + + async fn ecash_clients(&self, epoch_id: EpochId) -> Result>; + + async fn ecash_threshold(&self, epoch_id: EpochId) -> Result; + + async fn dkg_in_progress(&self) -> Result; +} + +struct CachedEpoch { + valid_until: OffsetDateTime, + current_epoch: Epoch, +} + +impl Default for CachedEpoch { + fn default() -> Self { + CachedEpoch { + valid_until: OffsetDateTime::UNIX_EPOCH, + current_epoch: Epoch::default(), + } + } +} + +impl CachedEpoch { + fn is_valid(&self) -> bool { + self.valid_until > OffsetDateTime::now_utc() + } + + async fn update(&mut self, epoch: Epoch) -> Result<()> { + let now = OffsetDateTime::now_utc(); + + let validity_duration = if let Some(epoch_finish) = epoch.deadline { + let state_end = + OffsetDateTime::from_unix_timestamp(epoch_finish.seconds() as i64).unwrap(); + let until_epoch_state_end = state_end - now; + // make it valid until the next epoch transition or next 5min, whichever is smaller + min(until_epoch_state_end, 5 * time::Duration::MINUTE) + } else { + 5 * time::Duration::MINUTE + }; + + self.valid_until = now + validity_duration; + self.current_epoch = epoch; + + Ok(()) + } +} + +pub(crate) struct QueryCommunicationChannel { + nyxd_client: nyxd::Client, + + epoch_clients: CachedImmutableEpochItem>, + cached_epoch: RwLock, + threshold_values: CachedImmutableEpochItem, +} + +impl QueryCommunicationChannel { + pub fn new(nyxd_client: nyxd::Client) -> Self { + QueryCommunicationChannel { + nyxd_client, + epoch_clients: Default::default(), + cached_epoch: Default::default(), + threshold_values: Default::default(), + } + } + + async fn update_epoch_cache(&self) -> Result> { + let mut guard = self.cached_epoch.write().await; + + let epoch = ecash::client::Client::get_current_epoch(&self.nyxd_client).await?; + + guard.update(epoch).await?; + Ok(guard) + } +} + +#[async_trait] +impl APICommunicationChannel for QueryCommunicationChannel { + async fn current_epoch(&self) -> Result { + let guard = self.cached_epoch.read().await; + if guard.is_valid() { + return Ok(guard.current_epoch.epoch_id); + } + + // update cache + drop(guard); + let guard = self.update_epoch_cache().await?; + + return Ok(guard.current_epoch.epoch_id); + } + + // TODO: perhaps this should be returning a ReadGuard instead? + async fn ecash_clients(&self, epoch_id: EpochId) -> Result> { + self.epoch_clients + .get_or_init(epoch_id, || async { + self.nyxd_client + .get_registered_ecash_clients(epoch_id) + .await + }) + .await + .map(|guard| guard.clone()) + } + + async fn ecash_threshold(&self, epoch_id: EpochId) -> Result { + self.threshold_values + .get_or_init(epoch_id, || async { + if let Some(threshold) = + ecash::client::Client::get_epoch_threshold(&self.nyxd_client, epoch_id).await? + { + Ok(threshold) + } else { + Err(EcashError::UnavailableThreshold { epoch_id }) + } + }) + .await + .map(|t| *t) + } + + async fn dkg_in_progress(&self) -> Result { + let guard = self.cached_epoch.read().await; + if guard.is_valid() { + return Ok(!guard.current_epoch.state.is_in_progress()); + } + + // update cache + drop(guard); + let guard = self.update_epoch_cache().await?; + + return Ok(!guard.current_epoch.state.is_in_progress()); + } +} diff --git a/nym-api/src/ecash/deposit.rs b/nym-api/src/ecash/deposit.rs new file mode 100644 index 0000000000..cc14884895 --- /dev/null +++ b/nym-api/src/ecash/deposit.rs @@ -0,0 +1,107 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::Result; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_credentials::IssuanceTicketBook; +use nym_crypto::asymmetric::ed25519; +use nym_ecash_contract_common::deposit::Deposit; + +pub async fn validate_deposit(request: &BlindSignRequestBody, deposit: Deposit) -> Result<()> { + // verify signature with the pubkey used in deposit + let ed25519 = ed25519::PublicKey::from_base58_string(deposit.bs58_encoded_ed25519_pubkey)?; + let plaintext = + IssuanceTicketBook::request_plaintext(&request.inner_sign_request, request.deposit_id); + ed25519.verify(plaintext, &request.signature)?; + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::ecash::error::EcashError; + use crate::ecash::tests::voucher_fixture; + use nym_compact_ecash::{generate_keypair_user, scheme::withdrawal::WithdrawalRequest}; + use rand::rngs::OsRng; + use time::macros::date; + + #[tokio::test] + async fn validate_deposit_test() { + let mut rng = OsRng; + let deposit_id = 42; + let voucher = voucher_fixture(Some(deposit_id)); + let signing_data = voucher.prepare_for_signing(); + let correct_request = voucher.create_blind_sign_request_body(&signing_data); + + let valid_ed25519 = ed25519::KeyPair::new(&mut rng); + let bs58_encoded_ed25519 = valid_ed25519.public_key().to_base58_string(); + + let malformed_deposit = Deposit { + bs58_encoded_ed25519_pubkey: "invalided25519pubkey".to_string(), + }; + + let err = validate_deposit(&correct_request, malformed_deposit) + .await + .unwrap_err(); + + assert!(matches!( + err, + EcashError::Ed25519ParseError( + nym_crypto::asymmetric::identity::Ed25519RecoveryError::MalformedPublicKeyString { .. } + ) + )); + + let wrong_deposit = Deposit { + bs58_encoded_ed25519_pubkey: bs58_encoded_ed25519, + }; + + let err = validate_deposit(&correct_request, wrong_deposit) + .await + .unwrap_err(); + assert!(matches!(err, EcashError::SignatureVerificationError(..))); + + //hard-coded values, that generate a correct signature + let blind_sign_req = WithdrawalRequest::from_bytes(&[ + 48, 168, 84, 109, 206, 194, 237, 227, 205, 67, 60, 127, 85, 54, 246, 120, 88, 129, 121, + 50, 255, 133, 50, 54, 155, 172, 179, 52, 16, 250, 6, 209, 67, 54, 251, 20, 37, 124, + 115, 63, 182, 101, 188, 68, 149, 18, 149, 57, 167, 48, 149, 100, 41, 48, 143, 115, 93, + 90, 244, 164, 161, 224, 65, 160, 63, 141, 65, 86, 128, 136, 128, 194, 40, 106, 158, 40, + 235, 242, 51, 108, 3, 109, 120, 11, 100, 82, 188, 61, 41, 12, 232, 54, 162, 243, 43, + 222, 215, 216, 2, 48, 137, 243, 126, 118, 124, 83, 221, 53, 252, 163, 175, 215, 94, 90, + 249, 172, 3, 222, 13, 45, 166, 245, 126, 173, 199, 89, 206, 11, 22, 204, 47, 26, 40, + 191, 217, 139, 75, 101, 45, 5, 62, 251, 52, 36, 117, 101, 166, 63, 48, 152, 195, 163, + 179, 117, 148, 93, 223, 210, 119, 105, 59, 71, 88, 155, 17, 33, 4, 87, 203, 169, 40, + 93, 203, 153, 213, 105, 107, 181, 214, 2, 25, 19, 187, 217, 243, 246, 185, 152, 81, + 118, 11, 169, 100, 74, 88, 215, 37, 32, 31, 123, 5, 222, 103, 255, 236, 74, 37, 222, + 170, 136, 5, 49, 4, 183, 156, 223, 33, 112, 122, 81, 122, 221, 166, 27, 5, 44, 153, 37, + 229, 107, 32, 95, 45, 147, 187, 40, 141, 22, 9, 222, 232, 125, 34, 52, 152, 157, 14, + 228, 200, 183, 29, 62, 24, 201, 228, 103, 119, 89, 186, 79, 116, 75, 53, 2, 32, 219, + 72, 52, 255, 108, 74, 76, 126, 233, 46, 34, 70, 188, 47, 57, 66, 153, 14, 6, 242, 112, + 129, 108, 166, 188, 226, 183, 51, 45, 195, 190, 58, 32, 170, 54, 18, 64, 215, 82, 118, + 243, 66, 186, 137, 175, 230, 172, 174, 226, 104, 188, 123, 239, 77, 180, 32, 225, 73, + 208, 255, 27, 195, 181, 201, 21, 2, 32, 34, 231, 200, 93, 64, 117, 244, 169, 58, 64, + 39, 5, 228, 205, 119, 135, 221, 130, 241, 205, 184, 182, 34, 248, 85, 26, 241, 233, 52, + 244, 17, 15, 32, 157, 211, 145, 238, 16, 101, 55, 132, 233, 11, 249, 129, 41, 226, 250, + 146, 160, 155, 154, 81, 241, 129, 154, 24, 221, 196, 54, 210, 16, 24, 116, 31, + ]) + .unwrap(); + let expiration_date = date!(2024 - 02 - 19); + let ecash_keypair = generate_keypair_user(); + + let correct_request = BlindSignRequestBody::new( + blind_sign_req, + deposit_id, + "3MpHDLYMCmuMvZ9zkZXPkTK6nKArvQW3dJA1notoPPxnbBW2ommkR2dkpRWoeWSkUjQSLv1nRyiRzMWbobGLw1eh".parse().unwrap(), + ecash_keypair.public_key(), + expiration_date, + ); + + let good_deposit = Deposit { + bs58_encoded_ed25519_pubkey: "JDTnyotGw3TtbohEamWNjhvGpj3tJz2C4X2Au9PrSTEx".to_string(), + }; + + let res = validate_deposit(&correct_request, good_deposit).await; + assert!(res.is_ok()) + } +} diff --git a/nym-api/src/coconut/dkg/client.rs b/nym-api/src/ecash/dkg/client.rs similarity index 82% rename from nym-api/src/coconut/dkg/client.rs rename to nym-api/src/ecash/dkg/client.rs index f85902ba48..1471083060 100644 --- a/nym-api/src/coconut/dkg/client.rs +++ b/nym-api/src/ecash/dkg/client.rs @@ -1,8 +1,8 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::client::Client; -use crate::coconut::error::CoconutError; +use crate::ecash::client::Client; +use crate::ecash::error::EcashError; use cw3::{ProposalResponse, Status, VoteResponse}; use cw4::MemberResponse; use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse}; @@ -38,19 +38,19 @@ impl DkgClient { self.inner.address().await } - pub(crate) async fn dkg_contract_address(&self) -> Result { + pub(crate) async fn dkg_contract_address(&self) -> Result { self.inner.dkg_contract_address().await } - pub(crate) async fn get_current_epoch(&self) -> Result { + pub(crate) async fn get_current_epoch(&self) -> Result { self.inner.get_current_epoch().await } - pub(crate) async fn get_contract_state(&self) -> Result { + pub(crate) async fn get_contract_state(&self) -> Result { self.inner.contract_state().await } - pub(crate) async fn group_member(&self) -> Result { + pub(crate) async fn group_member(&self) -> Result { self.inner .group_member(self.get_address().await.to_string()) .await @@ -58,13 +58,13 @@ impl DkgClient { pub(crate) async fn get_current_epoch_threshold( &self, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { self.inner.get_current_epoch_threshold().await } pub(crate) async fn get_self_registered_dealer_details( &self, - ) -> Result { + ) -> Result { self.inner.get_self_registered_dealer_details().await } @@ -72,14 +72,14 @@ impl DkgClient { &self, epoch_id: EpochId, dealer: String, - ) -> Result { + ) -> Result { self.inner .get_registered_dealer_details(epoch_id, dealer) .await .map(|d| d.details.is_some()) } - pub(crate) async fn get_current_dealers(&self) -> Result, CoconutError> { + pub(crate) async fn get_current_dealers(&self) -> Result, EcashError> { self.inner.get_current_dealers().await } @@ -87,7 +87,7 @@ impl DkgClient { &self, epoch_id: EpochId, dealer: String, - ) -> Result { + ) -> Result { self.inner .get_dealer_dealings_status(epoch_id, dealer) .await @@ -99,11 +99,11 @@ impl DkgClient { dealer: &str, dealing_index: DealingIndex, chunk_index: ChunkIndex, - ) -> Result { + ) -> Result { self.inner .get_dealing_chunk(epoch_id, dealer, dealing_index, chunk_index) .await? - .ok_or(CoconutError::MissingDealingChunk { + .ok_or(EcashError::MissingDealingChunk { epoch_id, dealer: dealer.to_string(), dealing_index, @@ -115,7 +115,7 @@ impl DkgClient { &self, epoch_id: EpochId, address: S, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { self.inner .get_verification_key_share(epoch_id, address.into()) .await @@ -124,7 +124,7 @@ impl DkgClient { pub(crate) async fn get_verification_own_key_share( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { let address = self.inner.address().await; self.get_verification_key_share(epoch_id, address).await } @@ -132,31 +132,28 @@ impl DkgClient { pub(crate) async fn get_verification_key_shares( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { self.inner.get_verification_key_shares(epoch_id).await } - pub(crate) async fn get_vote(&self, proposal_id: u64) -> Result { + pub(crate) async fn get_vote(&self, proposal_id: u64) -> Result { let address = self.get_address().await.to_string(); self.inner.get_vote(proposal_id, address).await } - pub(crate) async fn list_proposals(&self) -> Result, CoconutError> { + pub(crate) async fn list_proposals(&self) -> Result, EcashError> { self.inner.list_proposals().await } - pub(crate) async fn get_proposal_status( - &self, - proposal_id: u64, - ) -> Result { + pub(crate) async fn get_proposal_status(&self, proposal_id: u64) -> Result { self.inner.get_proposal(proposal_id).await.map(|p| p.status) } - pub(crate) async fn advance_epoch_state(&self) -> Result<(), CoconutError> { + pub(crate) async fn advance_epoch_state(&self) -> Result<(), EcashError> { self.inner.advance_epoch_state().await } - pub(crate) async fn can_advance_epoch_state(&self) -> Result { + pub(crate) async fn can_advance_epoch_state(&self) -> Result { self.inner.can_advance_epoch_state().await } @@ -166,18 +163,18 @@ impl DkgClient { identity_key: IdentityKey, announce_address: String, resharing: bool, - ) -> Result { + ) -> Result { let res = self .inner .register_dealer(bte_key, identity_key, announce_address, resharing) .await?; let node_index = find_attribute(&res.logs, "wasm", NODE_INDEX) - .ok_or(CoconutError::NodeIndexRecoveryError { + .ok_or(EcashError::NodeIndexRecoveryError { reason: String::from("node index not found"), })? .value .parse::() - .map_err(|_| CoconutError::NodeIndexRecoveryError { + .map_err(|_| EcashError::NodeIndexRecoveryError { reason: String::from("node index could not be parsed"), })?; @@ -189,7 +186,7 @@ impl DkgClient { dealing_index: DealingIndex, chunks: Vec, resharing: bool, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner .submit_dealing_metadata(dealing_index, chunks, resharing) .await?; @@ -199,7 +196,7 @@ impl DkgClient { pub(crate) async fn submit_dealing_chunk( &self, chunk: PartialContractDealing, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner.submit_dealing_chunk(chunk).await?; Ok(()) } @@ -208,7 +205,7 @@ impl DkgClient { &self, share: VerificationKeyShare, resharing: bool, - ) -> Result { + ) -> Result { self.inner .submit_verification_key_share(share.clone(), resharing) .await @@ -218,14 +215,14 @@ impl DkgClient { &self, proposal_id: u64, vote_yes: bool, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner.vote_proposal(proposal_id, vote_yes, None).await } pub(crate) async fn execute_verification_key_share( &self, proposal_id: u64, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { self.inner.execute_proposal(proposal_id).await } } diff --git a/nym-api/src/coconut/dkg/controller/error.rs b/nym-api/src/ecash/dkg/controller/error.rs similarity index 79% rename from nym-api/src/coconut/dkg/controller/error.rs rename to nym-api/src/ecash/dkg/controller/error.rs index e634477db6..4311ff7db5 100644 --- a/nym-api/src/coconut/dkg/controller/error.rs +++ b/nym-api/src/ecash/dkg/controller/error.rs @@ -1,12 +1,12 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::dealing::DealingGenerationError; -use crate::coconut::dkg::key_derivation::KeyDerivationError; -use crate::coconut::dkg::key_finalization::KeyFinalizationError; -use crate::coconut::dkg::key_validation::KeyValidationError; -use crate::coconut::dkg::public_key::PublicKeySubmissionError; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::dealing::DealingGenerationError; +use crate::ecash::dkg::key_derivation::KeyDerivationError; +use crate::ecash::dkg::key_finalization::KeyFinalizationError; +use crate::ecash::dkg::key_validation::KeyValidationError; +use crate::ecash::dkg::public_key::PublicKeySubmissionError; +use crate::ecash::error::EcashError; use std::path::PathBuf; use thiserror::Error; @@ -16,25 +16,25 @@ pub enum DkgError { StatePersistenceFailure { path: PathBuf, #[source] - source: CoconutError, + source: EcashError, }, #[error("failed to query for the current DKG epoch state: {source}")] EpochQueryFailure { #[source] - source: CoconutError, + source: EcashError, }, #[error("failed to query for the epoch state status: {source}")] StateStatusQueryFailure { #[source] - source: CoconutError, + source: EcashError, }, #[error("failed to query the CW4 group contract for the membership status: {source}")] GroupQueryFailure { #[source] - source: CoconutError, + source: EcashError, }, #[error("this API is currently not member of the DKG group and thus can't participate in the process")] @@ -73,6 +73,6 @@ pub enum DkgError { #[error("failed to advance the DKG state: {source}")] StateAdvancementFailure { #[source] - source: CoconutError, + source: EcashError, }, } diff --git a/nym-api/src/coconut/dkg/controller/keys.rs b/nym-api/src/ecash/dkg/controller/keys.rs similarity index 79% rename from nym-api/src/coconut/dkg/controller/keys.rs rename to nym-api/src/ecash/dkg/controller/keys.rs index ce0d16930a..6b806bf792 100644 --- a/nym-api/src/coconut/dkg/controller/keys.rs +++ b/nym-api/src/ecash/dkg/controller/keys.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::client::Client; -use crate::coconut::keys::KeyPairWithEpoch; +use crate::ecash::client::Client; +use crate::ecash::keys::{KeyPairWithEpoch, LegacyCoconutKeyWithEpoch}; use crate::support::{config, nyxd}; use anyhow::{anyhow, bail, Context}; use nym_coconut_dkg_common::types::{EpochId, EpochState}; @@ -35,15 +35,32 @@ pub(crate) fn load_bte_keypair(config: &config::CoconutSigner) -> anyhow::Result .context("bte keypair load failure") } -pub(crate) fn load_coconut_keypair_if_exists( +pub(crate) fn load_ecash_keypair_if_exists( config: &config::CoconutSigner, ) -> anyhow::Result> { if !config.storage_paths.coconut_key_path.exists() { return Ok(None); } - nym_pemstore::load_key(&config.storage_paths.coconut_key_path) - .context("coconut key load failure") - .map(Some) + + // first attempt to load ecash keys directly, + // if that fails fallback to coconut keys and perform migration + if let Ok(ecash_key) = + nym_pemstore::load_key::(&config.storage_paths.coconut_key_path) + { + return Ok(Some(ecash_key)); + } + + if let Ok(legacy_coconut_key) = nym_pemstore::load_key::( + &config.storage_paths.coconut_key_path, + ) { + let migrated_key: KeyPairWithEpoch = legacy_coconut_key.into(); + nym_pemstore::store_key(&migrated_key, &config.storage_paths.coconut_key_path) + .context("migrated key storage failure")?; + + return Ok(Some(migrated_key)); + } + + bail!("ecash key load failure") } // the keys can be considered valid if they were generated for the current dkg epoch diff --git a/nym-api/src/coconut/dkg/controller/mod.rs b/nym-api/src/ecash/dkg/controller/mod.rs similarity index 97% rename from nym-api/src/coconut/dkg/controller/mod.rs rename to nym-api/src/ecash/dkg/controller/mod.rs index db9ce0a82c..6dd454f10d 100644 --- a/nym-api/src/coconut/dkg/controller/mod.rs +++ b/nym-api/src/ecash/dkg/controller/mod.rs @@ -1,10 +1,10 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::client::DkgClient; -use crate::coconut::dkg::controller::error::DkgError; -use crate::coconut::dkg::state::{PersistentState, State}; -use crate::coconut::keys::KeyPair as CoconutKeyPair; +use crate::ecash::dkg::client::DkgClient; +use crate::ecash::dkg::controller::error::DkgError; +use crate::ecash::dkg::state::{PersistentState, State}; +use crate::ecash::keys::KeyPair as CoconutKeyPair; use crate::nyxd; use crate::support::config; use anyhow::{bail, Result}; @@ -340,7 +340,7 @@ impl DkgController { dkg_client, coconut_key_path: Default::default(), state, - rng: crate::coconut::tests::fixtures::test_rng([1u8; 32]), + rng: crate::ecash::tests::fixtures::test_rng([1u8; 32]), polling_rate: Default::default(), } } diff --git a/nym-api/src/coconut/dkg/dealing.rs b/nym-api/src/ecash/dkg/dealing.rs similarity index 97% rename from nym-api/src/coconut/dkg/dealing.rs rename to nym-api/src/ecash/dkg/dealing.rs index 21b5803854..bb8c17d92b 100644 --- a/nym-api/src/coconut/dkg/dealing.rs +++ b/nym-api/src/ecash/dkg/dealing.rs @@ -1,11 +1,11 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg; -use crate::coconut::dkg::controller::keys::archive_coconut_keypair; -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; -use crate::coconut::keys::KeyPairWithEpoch; +use crate::ecash::dkg; +use crate::ecash::dkg::controller::keys::archive_coconut_keypair; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; +use crate::ecash::keys::KeyPairWithEpoch; use log::debug; use nym_coconut_dkg_common::dealing::{chunk_dealing, DealingChunkInfo, MAX_DEALING_CHUNK_SIZE}; use nym_coconut_dkg_common::types::{DealingIndex, EpochId}; @@ -39,7 +39,7 @@ impl Debug for DealingGeneration { #[derive(Debug, Error)] pub enum DealingGenerationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("can't complete dealing exchange without registering public keys")] IncompletePublicKeyRegistration, @@ -70,7 +70,7 @@ impl DkgController { spec: DealingGeneration, ) -> Result, DealingGenerationError> { let threshold = self.dkg_client.get_current_epoch_threshold().await?.ok_or( - CoconutError::UnrecoverableState { + EcashError::UnrecoverableState { reason: String::from("Threshold should have been set"), }, )?; @@ -79,7 +79,7 @@ impl DkgController { .state .registration_state(epoch_id)? .assigned_index - .ok_or(CoconutError::UnrecoverableState { + .ok_or(EcashError::UnrecoverableState { reason: String::from("Node index should have been set"), })?; @@ -469,14 +469,12 @@ impl DkgController { #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::coconut::dkg::state::registration::KeyRejectionReason; - use crate::coconut::keys::KeyPair; - use crate::coconut::tests::fixtures::{ - dealers_fixtures, test_rng, TestingDkgControllerBuilder, - }; - use crate::coconut::tests::helpers::unchecked_decode_bte_key; - use nym_coconut::{ttp_keygen, Parameters}; + use crate::ecash::dkg::state::registration::KeyRejectionReason; + use crate::ecash::keys::KeyPair; + use crate::ecash::tests::fixtures::{dealers_fixtures, test_rng, TestingDkgControllerBuilder}; + use crate::ecash::tests::helpers::unchecked_decode_bte_key; use nym_coconut_dkg_common::types::DealerRegistrationDetails; + use nym_compact_ecash::ttp_keygen; use nym_dkg::bte::PublicKeyWithProof; #[tokio::test] @@ -619,7 +617,7 @@ pub(crate) mod tests { let epoch = 1; - let mut keys = ttp_keygen(&Parameters::new(4).unwrap(), 3, 4).unwrap(); + let mut keys = ttp_keygen(3, 4).unwrap(); let coconut_keypair = KeyPair::new(); coconut_keypair .set(KeyPairWithEpoch::new(keys.pop().unwrap(), epoch)) @@ -676,7 +674,7 @@ pub(crate) mod tests { let epoch = 1; - let mut keys = ttp_keygen(&Parameters::new(4).unwrap(), 3, 4).unwrap(); + let mut keys = ttp_keygen(3, 4).unwrap(); let coconut_keypair = KeyPair::new(); coconut_keypair .set(KeyPairWithEpoch::new(keys.pop().unwrap(), epoch - 1)) diff --git a/nym-api/src/coconut/dkg/helpers.rs b/nym-api/src/ecash/dkg/helpers.rs similarity index 94% rename from nym-api/src/coconut/dkg/helpers.rs rename to nym-api/src/ecash/dkg/helpers.rs index e36c1c04fc..d57e7962b0 100644 --- a/nym-api/src/coconut/dkg/helpers.rs +++ b/nym-api/src/ecash/dkg/helpers.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use cw3::{ProposalResponse, Status}; use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs; use nym_validator_client::nyxd::AccountId; @@ -29,7 +29,7 @@ impl DkgController { pub(crate) async fn get_validation_proposals( &self, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { let dkg_contract = self.dkg_client.dkg_contract_address().await?; // FUTURE OPTIMIZATION: don't query for ALL proposals. say if we're in epoch 50, diff --git a/nym-api/src/coconut/dkg/key_derivation.rs b/nym-api/src/ecash/dkg/key_derivation.rs similarity index 96% rename from nym-api/src/coconut/dkg/key_derivation.rs rename to nym-api/src/ecash/dkg/key_derivation.rs index eb6c5b7214..e2ad2a7cb2 100644 --- a/nym-api/src/coconut/dkg/key_derivation.rs +++ b/nym-api/src/ecash/dkg/key_derivation.rs @@ -1,19 +1,19 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg; -use crate::coconut::dkg::controller::keys::persist_coconut_keypair; -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::dkg::state::key_derivation::{DealerRejectionReason, DerivationFailure}; -use crate::coconut::error::CoconutError; -use crate::coconut::keys::KeyPairWithEpoch; -use crate::coconut::state::bandwidth_credential_params; +use crate::ecash::dkg; +use crate::ecash::dkg::controller::keys::persist_coconut_keypair; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::dkg::state::key_derivation::{DealerRejectionReason, DerivationFailure}; +use crate::ecash::error::EcashError; +use crate::ecash::keys::KeyPairWithEpoch; use cosmwasm_std::Addr; use log::debug; -use nym_coconut::KeyPair as CoconutKeyPair; -use nym_coconut::{check_vk_pairing, Base58, SecretKey, VerificationKey}; use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID; use nym_coconut_dkg_common::types::{DealingIndex, EpochId, NodeIndex}; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::utils::check_vk_pairing; +use nym_compact_ecash::{ecash_group_parameters, Base58, KeyPairAuth, VerificationKeyAuth}; use nym_dkg::{ bte::{self, decrypt_share}, combine_shares, try_recover_verification_keys, Dealing, @@ -28,7 +28,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum KeyDerivationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("can't complete key derivation without dealing exchange")] IncompleteDealingExchange, @@ -75,7 +75,7 @@ impl DkgController { dealer: &Addr, epoch_receivers: &BTreeMap, raw_dealings: HashMap>, - prior_public_key: Option, + prior_public_key: Option, ) -> Result, DealerRejectionReason>, KeyDerivationError> { let threshold = self.state.threshold(epoch_id)?; @@ -155,7 +155,7 @@ impl DkgController { &self, epoch_id: EpochId, dealer: &Addr, - ) -> Result, KeyDerivationError> { + ) -> Result, KeyDerivationError> { let Some(previous_epoch) = epoch_id.checked_sub(1) else { return Err(KeyDerivationError::ZerothEpochResharing); }; @@ -176,7 +176,7 @@ impl DkgController { // since this share appears as 'verified' on the chain, it means the consensus of dealers confirmed its validity // and thus they must have been able to parse it, so the unwrap/expect here is fine Ok(Some( - VerificationKey::try_from_bs58(&share.share) + VerificationKeyAuth::try_from_bs58(&share.share) .expect("failed to deserialize VERIFIED key"), )) } @@ -424,8 +424,8 @@ impl DkgController { // SAFETY: // we know we had a non-empty map of dealings and thus, at the very least, we must have derived a single secret // (i.e. the x-element) - let sk = SecretKey::create_from_raw(derived_x.unwrap(), derived_secrets); - let derived_vk = sk.verification_key(bandwidth_credential_params()); + let sk = SecretKeyAuth::create_from_raw(derived_x.unwrap(), derived_secrets); + let derived_vk = sk.verification_key(); // make the key we derived out of the decrypted shares matches the partial key // (cryptographically there shouldn't be any reason for the mismatch, @@ -436,21 +436,21 @@ impl DkgController { .derived_partials_for(receiver_index) .ok_or(KeyDerivationError::NoSelfPartialKey { receiver_index })?; - if !check_vk_pairing(bandwidth_credential_params(), &derived_partial, &derived_vk) { + if !check_vk_pairing(ecash_group_parameters(), &derived_partial, &derived_vk) { // can't do anything, we got all dealings, we derived all keys, but somehow they don't match error!("our derived key does not match the expected partials!"); return Ok(Err(DerivationFailure::MismatchedPartialKey)); } Ok(Ok(KeyPairWithEpoch::new( - CoconutKeyPair::from_keys(sk, derived_vk), + KeyPairAuth::from_keys(sk, derived_vk), epoch_id, ))) } async fn submit_partial_verification_key( &self, - key: &VerificationKey, + key: &VerificationKeyAuth, resharing: bool, ) -> Result { fn extract_proposal_id_from_logs( @@ -558,7 +558,7 @@ impl DkgController { debug!("we have already generated keys for this epoch but failed to send them to the contract"); let proposal_id = self - .submit_partial_verification_key(keys.keys.verification_key(), resharing) + .submit_partial_verification_key(&keys.keys.verification_key(), resharing) .await?; Ok(Some(proposal_id)) } else { @@ -657,7 +657,7 @@ impl DkgController { } let proposal_id = self - .submit_partial_verification_key(coconut_keypair.keys.verification_key(), resharing) + .submit_partial_verification_key(&coconut_keypair.keys.verification_key(), resharing) .await?; self.state.set_coconut_keypair(coconut_keypair).await; @@ -669,8 +669,8 @@ impl DkgController { // I've (@JS) only updated old, existing, tests. nothing more #[cfg(test)] pub(crate) mod tests { - use crate::coconut::dkg::state::key_derivation::DealerRejectionReason; - use crate::coconut::tests::helpers::{ + use crate::ecash::dkg::state::key_derivation::DealerRejectionReason; + use crate::ecash::tests::helpers::{ exchange_dealings, initialise_controllers, initialise_dkg, submit_public_keys, }; diff --git a/nym-api/src/coconut/dkg/key_finalization.rs b/nym-api/src/ecash/dkg/key_finalization.rs similarity index 96% rename from nym-api/src/coconut/dkg/key_finalization.rs rename to nym-api/src/ecash/dkg/key_finalization.rs index 628f04daa3..9ba5ff958a 100644 --- a/nym-api/src/coconut/dkg/key_finalization.rs +++ b/nym-api/src/ecash/dkg/key_finalization.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use cw3::Status; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; @@ -11,7 +11,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum KeyFinalizationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("our proposal for key verification is still open (or is pending) (proposal id: {proposal_id}) ")] UnresolvedProposal { proposal_id: u64 }, @@ -90,7 +90,7 @@ impl DkgController { #[cfg(test)] mod tests { use super::*; - use crate::coconut::tests::helpers::{ + use crate::ecash::tests::helpers::{ derive_keypairs, exchange_dealings, initialise_controllers, initialise_dkg, submit_public_keys, validate_keys, }; diff --git a/nym-api/src/coconut/dkg/key_validation.rs b/nym-api/src/ecash/dkg/key_validation.rs similarity index 96% rename from nym-api/src/coconut/dkg/key_validation.rs rename to nym-api/src/ecash/dkg/key_validation.rs index ada4f72898..b99bacd962 100644 --- a/nym-api/src/coconut/dkg/key_validation.rs +++ b/nym-api/src/ecash/dkg/key_validation.rs @@ -1,14 +1,15 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; -use crate::coconut::state::bandwidth_credential_params; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use cosmwasm_std::Addr; use cw3::Vote; -use nym_coconut::{check_vk_pairing, Base58, VerificationKey}; use nym_coconut_dkg_common::types::EpochId; use nym_coconut_dkg_common::verification_key::ContractVKShare; +use nym_compact_ecash::{ + ecash_group_parameters, utils::check_vk_pairing, Base58, VerificationKeyAuth, +}; use rand::{CryptoRng, RngCore}; use std::collections::HashMap; use thiserror::Error; @@ -24,7 +25,7 @@ fn vote_matches(voted_yes: bool, chain_vote: Vote) -> bool { #[derive(Debug, Error)] pub enum KeyValidationError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), #[error("can't complete key validation without key derivation")] IncompleteKeyDerivation, @@ -45,7 +46,7 @@ pub enum ShareRejectionReason { epoch_id: EpochId, owner: Addr, #[source] - source: nym_coconut::CoconutError, + source: nym_compact_ecash::error::CompactEcashError, }, #[error("did not derive partial keys for {owner} at index {receiver_index} for epoch {epoch_id} during the dealings exchange")] @@ -95,7 +96,7 @@ impl DkgController { }; // attempt to recover the underlying key from its bs58 representation - let recovered_key = match VerificationKey::try_from_bs58(share.share) { + let recovered_key = match VerificationKeyAuth::try_from_bs58(share.share) { Ok(key) => key, Err(source) => { return reject(ShareRejectionReason::MalformedKeyEncoding { @@ -119,7 +120,7 @@ impl DkgController { }); }; - if !check_vk_pairing(bandwidth_credential_params(), &self_derived, &recovered_key) { + if !check_vk_pairing(ecash_group_parameters(), &self_derived, &recovered_key) { return reject(ShareRejectionReason::InconsistentKeys { epoch_id, owner, @@ -258,7 +259,7 @@ impl DkgController { // I've (@JS) only updated old, existing, tests. nothing more #[cfg(test)] mod tests { - use crate::coconut::tests::helpers::{ + use crate::ecash::tests::helpers::{ derive_keypairs, exchange_dealings, initialise_controllers, initialise_dkg, submit_public_keys, }; diff --git a/nym-api/src/coconut/dkg/mod.rs b/nym-api/src/ecash/dkg/mod.rs similarity index 97% rename from nym-api/src/coconut/dkg/mod.rs rename to nym-api/src/ecash/dkg/mod.rs index 46083a2d1a..f7272cd8d2 100644 --- a/nym-api/src/coconut/dkg/mod.rs +++ b/nym-api/src/ecash/dkg/mod.rs @@ -20,11 +20,11 @@ pub(crate) mod state; #[cfg(test)] mod tests { - use crate::coconut::tests::helpers::{ + use crate::ecash::tests::helpers::{ derive_keypairs, exchange_dealings, finalize, init_chain, initialise_controller, initialise_dkg, submit_public_keys, validate_keys, }; - use nym_coconut::aggregate_verification_keys; + use nym_compact_ecash::aggregate_verification_keys; #[tokio::test] #[ignore] // expensive test diff --git a/nym-api/src/coconut/dkg/public_key.rs b/nym-api/src/ecash/dkg/public_key.rs similarity index 96% rename from nym-api/src/coconut/dkg/public_key.rs rename to nym-api/src/ecash/dkg/public_key.rs index b7a899b6cb..21b263777b 100644 --- a/nym-api/src/coconut/dkg/public_key.rs +++ b/nym-api/src/ecash/dkg/public_key.rs @@ -1,8 +1,8 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::error::CoconutError; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::error::EcashError; use log::debug; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; @@ -11,7 +11,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum PublicKeySubmissionError { #[error(transparent)] - CoconutError(#[from] CoconutError), + CoconutError(#[from] EcashError), } impl DkgController { @@ -82,7 +82,7 @@ impl DkgController { // I've (@JS) only updated old, existing, tests. nothing more #[cfg(test)] pub(crate) mod tests { - use crate::coconut::tests::fixtures; + use crate::ecash::tests::fixtures; #[tokio::test] async fn submit_public_key() -> anyhow::Result<()> { diff --git a/nym-api/src/coconut/dkg/state/dealing_exchange.rs b/nym-api/src/ecash/dkg/state/dealing_exchange.rs similarity index 94% rename from nym-api/src/coconut/dkg/state/dealing_exchange.rs rename to nym-api/src/ecash/dkg/state/dealing_exchange.rs index 02a0ff1dd2..d75b7aa808 100644 --- a/nym-api/src/coconut/dkg/state/dealing_exchange.rs +++ b/nym-api/src/ecash/dkg/state/dealing_exchange.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use super::serde_helpers::generated_dealings; -use crate::coconut::dkg::state::DkgParticipant; +use crate::ecash::dkg::state::DkgParticipant; use nym_coconut_dkg_common::types::DealingIndex; use nym_dkg::{Dealing, NodeIndex}; use serde::{Deserialize, Serialize}; diff --git a/nym-api/src/coconut/dkg/state/in_progress.rs b/nym-api/src/ecash/dkg/state/in_progress.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/in_progress.rs rename to nym-api/src/ecash/dkg/state/in_progress.rs diff --git a/nym-api/src/coconut/dkg/state/key_derivation.rs b/nym-api/src/ecash/dkg/state/key_derivation.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/key_derivation.rs rename to nym-api/src/ecash/dkg/state/key_derivation.rs diff --git a/nym-api/src/coconut/dkg/state/key_finalization.rs b/nym-api/src/ecash/dkg/state/key_finalization.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/key_finalization.rs rename to nym-api/src/ecash/dkg/state/key_finalization.rs diff --git a/nym-api/src/coconut/dkg/state/key_validation.rs b/nym-api/src/ecash/dkg/state/key_validation.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/key_validation.rs rename to nym-api/src/ecash/dkg/state/key_validation.rs diff --git a/nym-api/src/coconut/dkg/state/mod.rs b/nym-api/src/ecash/dkg/state/mod.rs similarity index 76% rename from nym-api/src/coconut/dkg/state/mod.rs rename to nym-api/src/ecash/dkg/state/mod.rs index 5ef0fae275..eb1cca637e 100644 --- a/nym-api/src/coconut/dkg/state/mod.rs +++ b/nym-api/src/ecash/dkg/state/mod.rs @@ -1,16 +1,14 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::state::dealing_exchange::DealingExchangeState; -use crate::coconut::dkg::state::in_progress::InProgressState; -use crate::coconut::dkg::state::key_derivation::KeyDerivationState; -use crate::coconut::dkg::state::key_finalization::FinalizationState; -use crate::coconut::dkg::state::key_validation::ValidationState; -use crate::coconut::dkg::state::registration::{ - DkgParticipant, ParticipantState, RegistrationState, -}; -use crate::coconut::error::CoconutError; -use crate::coconut::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch}; +use crate::ecash::dkg::state::dealing_exchange::DealingExchangeState; +use crate::ecash::dkg::state::in_progress::InProgressState; +use crate::ecash::dkg::state::key_derivation::KeyDerivationState; +use crate::ecash::dkg::state::key_finalization::FinalizationState; +use crate::ecash::dkg::state::key_validation::ValidationState; +use crate::ecash::dkg::state::registration::{DkgParticipant, ParticipantState, RegistrationState}; +use crate::ecash::error::EcashError; +use crate::ecash::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch}; use cosmwasm_std::Addr; use log::debug; use nym_coconut_dkg_common::dealer::DealerDetails; @@ -60,13 +58,13 @@ impl From<&State> for PersistentState { } impl PersistentState { - pub fn save_to_file>(&self, path: P) -> Result<(), CoconutError> { + pub fn save_to_file>(&self, path: P) -> Result<(), EcashError> { debug!("persisting the dkg state"); std::fs::write(path, serde_json::to_string(self)?)?; Ok(()) } - pub fn load_from_file>(path: P) -> Result { + pub fn load_from_file>(path: P) -> Result { Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) } } @@ -136,7 +134,7 @@ impl State { } } - pub fn persist(&self) -> Result<(), CoconutError> { + pub fn persist(&self) -> Result<(), EcashError> { PersistentState::from(self).save_to_file(self.persistent_state_path()) } @@ -158,7 +156,7 @@ impl State { pub fn valid_epoch_receivers( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(self .dealing_exchange_state(epoch_id)? .dealers @@ -177,7 +175,7 @@ impl State { pub fn valid_epoch_receivers_keys( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(self .dealing_exchange_state(epoch_id)? .dealers @@ -186,157 +184,151 @@ impl State { .collect()) } - pub fn dkg_state(&self, epoch_id: EpochId) -> Result<&DkgState, CoconutError> { + pub fn dkg_state(&self, epoch_id: EpochId) -> Result<&DkgState, EcashError> { self.dkg_instances .get(&epoch_id) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn dkg_state_mut(&mut self, epoch_id: EpochId) -> Result<&mut DkgState, CoconutError> { + pub fn dkg_state_mut(&mut self, epoch_id: EpochId) -> Result<&mut DkgState, EcashError> { self.dkg_instances .get_mut(&epoch_id) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn registration_state( - &self, - epoch_id: EpochId, - ) -> Result<&RegistrationState, CoconutError> { + pub fn registration_state(&self, epoch_id: EpochId) -> Result<&RegistrationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.registration) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn registration_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut RegistrationState, CoconutError> { + ) -> Result<&mut RegistrationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.registration) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn in_progress_state(&self, epoch_id: EpochId) -> Result<&InProgressState, CoconutError> { + pub fn in_progress_state(&self, epoch_id: EpochId) -> Result<&InProgressState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.in_progress) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn in_progress_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut InProgressState, CoconutError> { + ) -> Result<&mut InProgressState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.in_progress) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn dealing_exchange_state( &self, epoch_id: EpochId, - ) -> Result<&DealingExchangeState, CoconutError> { + ) -> Result<&DealingExchangeState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.dealing_exchange) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn dealing_exchange_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut DealingExchangeState, CoconutError> { + ) -> Result<&mut DealingExchangeState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.dealing_exchange) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_derivation_state( &self, epoch_id: EpochId, - ) -> Result<&KeyDerivationState, CoconutError> { + ) -> Result<&KeyDerivationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.key_generation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_derivation_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut KeyDerivationState, CoconutError> { + ) -> Result<&mut KeyDerivationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.key_generation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn key_validation_state( - &self, - epoch_id: EpochId, - ) -> Result<&ValidationState, CoconutError> { + pub fn key_validation_state(&self, epoch_id: EpochId) -> Result<&ValidationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.key_validation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_validation_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut ValidationState, CoconutError> { + ) -> Result<&mut ValidationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.key_validation) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_finalization_state( &self, epoch_id: EpochId, - ) -> Result<&FinalizationState, CoconutError> { + ) -> Result<&FinalizationState, EcashError> { self.dkg_instances .get(&epoch_id) .map(|state| &state.key_finalization) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } pub fn key_finalization_state_mut( &mut self, epoch_id: EpochId, - ) -> Result<&mut FinalizationState, CoconutError> { + ) -> Result<&mut FinalizationState, EcashError> { self.dkg_instances .get_mut(&epoch_id) .map(|state| &mut state.key_finalization) - .ok_or(CoconutError::MissingDkgState { epoch_id }) + .ok_or(EcashError::MissingDkgState { epoch_id }) } - pub fn threshold(&self, epoch_id: EpochId) -> Result { + pub fn threshold(&self, epoch_id: EpochId) -> Result { self.key_derivation_state(epoch_id)? .expected_threshold - .ok_or(CoconutError::UnavailableThreshold { epoch_id }) + .ok_or(EcashError::UnavailableThreshold { epoch_id }) } - pub fn assigned_index(&self, epoch_id: EpochId) -> Result { + pub fn assigned_index(&self, epoch_id: EpochId) -> Result { self.registration_state(epoch_id)? .assigned_index - .ok_or(CoconutError::UnavailableAssignedIndex { epoch_id }) + .ok_or(EcashError::UnavailableAssignedIndex { epoch_id }) } - pub fn receiver_index(&self, epoch_id: EpochId) -> Result { + pub fn receiver_index(&self, epoch_id: EpochId) -> Result { self.dealing_exchange_state(epoch_id)? .receiver_index - .ok_or(CoconutError::UnavailableReceiverIndex { epoch_id }) + .ok_or(EcashError::UnavailableReceiverIndex { epoch_id }) } - pub fn proposal_id(&self, epoch_id: EpochId) -> Result { + pub fn proposal_id(&self, epoch_id: EpochId) -> Result { self.key_derivation_state(epoch_id)? .proposal_id - .ok_or(CoconutError::UnavailableProposalId { epoch_id }) + .ok_or(EcashError::UnavailableProposalId { epoch_id }) } pub fn persistent_state_path(&self) -> &Path { diff --git a/nym-api/src/coconut/dkg/state/registration.rs b/nym-api/src/ecash/dkg/state/registration.rs similarity index 98% rename from nym-api/src/coconut/dkg/state/registration.rs rename to nym-api/src/ecash/dkg/state/registration.rs index 8391cebeed..c5a7271aab 100644 --- a/nym-api/src/coconut/dkg/state/registration.rs +++ b/nym-api/src/ecash/dkg/state/registration.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::state::serde_helpers::bte_pk_serde; +use crate::ecash::dkg::state::serde_helpers::bte_pk_serde; use cosmwasm_std::Addr; use nym_coconut_dkg_common::dealer::DealerDetails; use nym_coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; diff --git a/nym-api/src/coconut/dkg/state/serde_helpers.rs b/nym-api/src/ecash/dkg/state/serde_helpers.rs similarity index 100% rename from nym-api/src/coconut/dkg/state/serde_helpers.rs rename to nym-api/src/ecash/dkg/state/serde_helpers.rs diff --git a/nym-api/src/coconut/error.rs b/nym-api/src/ecash/error.rs similarity index 50% rename from nym-api/src/coconut/error.rs rename to nym-api/src/ecash/error.rs index 3fa75c443f..0464c5baa4 100644 --- a/nym-api/src/coconut/error.rs +++ b/nym-api/src/ecash/error.rs @@ -3,28 +3,34 @@ use crate::node_status_api::models::NymApiStorageError; use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId}; -use nym_credentials::coconut::bandwidth::{CredentialType, UnknownCredentialType}; use nym_crypto::asymmetric::{ encryption::KeyRecoveryError, identity::{Ed25519RecoveryError, SignatureError}, }; use nym_dkg::error::DkgError; -use nym_validator_client::coconut::CoconutApiError; -use nym_validator_client::nyxd::error::{NyxdError, TendermintError}; +use nym_dkg::Threshold; +use nym_ecash_contract_common::deposit::DepositId; +use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; +use okapi::openapi3::Responses; use rocket::http::{ContentType, Status}; use rocket::response::Responder; use rocket::{response, Request, Response}; +use rocket_okapi::gen::OpenApiGenerator; +use rocket_okapi::response::OpenApiResponderInner; +use rocket_okapi::util::ensure_status_code_exists; use std::io::Cursor; use std::num::ParseIntError; use thiserror::Error; use time::error::ComponentRange; use time::OffsetDateTime; -pub type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Debug, Error)] -pub enum CoconutError { +pub enum EcashError { #[error(transparent)] IOError(#[from] std::io::Error), @@ -37,49 +43,24 @@ pub enum CoconutError { #[error("failed to derive the admin account from the provided public key: {formatted_source}")] AdminAccountDerivationFailure { formatted_source: String }, - #[error("failed to query for the authorised freepass requester address: {source}")] - FreepassAuthorisedFreepassRequesterQueryFailure { - #[from] - source: reqwest::Error, - }, - - #[error("the provided authorised freepass requester address ({address}) is not a valid cosmos address")] - MalformedAuthorisedFreepassRequesterAddress { address: String }, - - #[error("the requester of the free pass ({requester}) is not authorised. the only allowed account is {explicit_admin:?} or {bandwidth_contract_admin:?}.")] - UnauthorisedFreePassAccount { - requester: AccountId, - explicit_admin: Option, - bandwidth_contract_admin: Option, - }, - - #[error("failed to verify signature on the provided free pass request")] - FreePassSignatureVerificationFailure, - - #[error("the provided signing nonce is invalid. the current value is: {current:?}. got {received:?} instead")] - InvalidNonce { - current: [u8; 16], - received: [u8; 16], - }, - #[error("only secp256k1 keys are supported for free pass issuance")] UnsupportedNonSecp256k1Key, - #[error("received credential request for an unknown type: {0}")] - UnknownCredentialType(#[from] UnknownCredentialType), - - #[error("the provided free pass request had an unexpected number of public attributes. got {got} but expected {expected}")] - InvalidFreePassAttributes { got: usize, expected: usize }, - - #[error("the provided free pass request had an invalid type attribute (got: '{got}')")] - InvalidFreePassTypeAttribute { got: CredentialType }, - #[error("failed to parse the free pass expiry date: {source}")] ExpiryDateParsingFailure { #[source] source: ParseIntError, }, + #[error("the provided expiration date is too late")] + ExpirationDateTooLate, + + #[error("the provided expiration date is too early")] + ExpirationDateTooEarly, + + #[error("the provided expiration date is malformed")] + MalformedExpirationDate { raw: String }, + #[error("failed to parse expiry timestamp into proper datetime: {source}")] InvalidExpiryDate { unix_timestamp: i64, @@ -87,22 +68,9 @@ pub enum CoconutError { source: ComponentRange, }, - #[error( - "the provided free pass request has too long expiry (expiry is set to on {expiry_date})" - )] - TooLongFreePass { expiry_date: OffsetDateTime }, - - #[error("the provided free pass expiry is set in the past!")] - FreePassExpiryInThePast { expiry_date: OffsetDateTime }, - #[error("the received bandwidth voucher did not contain deposit value")] MissingBandwidthValue, - #[error( - "the received bandwidth credential is not a bandwidth voucher. the encoded type is: {typ}" - )] - NotABandwidthVoucher { typ: CredentialType }, - #[error("failed to parse the bandwidth voucher value: {source}")] VoucherValueParsingFailure { #[source] @@ -110,7 +78,7 @@ pub enum CoconutError { }, #[error("coconut api query failure: {0}")] - CoconutApiError(#[from] CoconutApiError), + CoconutApiError(#[from] EcashApiError), #[error(transparent)] SerdeJsonError(#[from] serde_json::Error), @@ -121,12 +89,6 @@ pub enum CoconutError { #[error("could not parse X25519 data: {0}")] X25519ParseError(#[from] KeyRecoveryError), - #[error("could not parse tx hash in request body: {source}")] - TxHashParseError { - #[source] - source: TendermintError, - }, - #[error("could not get transaction details for '{tx_hash}': {source}")] TxRetrievalFailure { tx_hash: String, @@ -140,39 +102,21 @@ pub enum CoconutError { #[error("validator client error: {0}")] ValidatorClientError(#[from] nym_validator_client::ValidatorClientError), - #[error("coconut internal error: {0}")] - CoconutInternalError(#[from] nym_coconut::CoconutError), + #[error("Compact ecash internal error - {0}")] + CompactEcashInternalError(#[from] nym_compact_ecash::error::CompactEcashError), + + #[error("Account linked to this public key has been blacklisted")] + BlacklistedAccount, #[error("could not find a deposit event in the transaction provided")] DepositEventNotFound, - #[error("could not find the deposit value in the event")] - DepositValueNotFound, - #[error("could not find the deposit info in the event")] DepositInfoNotFound, - #[error("could not find the verification key in the event")] - DepositVerifKeyNotFound, - - #[error("could not find the encryption key in the event")] - DepositEncrKeyNotFound, - #[error("signature didn't verify correctly")] SignatureVerificationError(#[from] SignatureError), - #[error("inconsistent public attributes")] - InconsistentPublicAttributes, - - #[error("the provided deposit value is inconsistent. got '{request}' while the value on chain is '{on_chain}'")] - InconsistentDepositValue { request: String, on_chain: String }, - - #[error("the provided deposit info is inconsistent. got '{request}' while the value on chain is '{on_chain}'")] - InconsistentDepositInfo { request: String, on_chain: String }, - - #[error("public attributes in request differ from the ones in deposit: Expected {0}, got {1}")] - DifferentPublicAttributes(String, String), - #[error("storage error: {0}")] StorageError(#[from] NymApiStorageError), @@ -182,9 +126,6 @@ pub enum CoconutError { #[error("incorrect credential proposal description: {reason}")] IncorrectProposal { reason: String }, - #[error("invalid status of credential: {status}")] - InvalidCredentialStatus { status: String }, - #[error("DKG error: {0}")] DkgError(#[from] DkgError), @@ -210,6 +151,9 @@ pub enum CoconutError { #[error("the internal dkg state for epoch {epoch_id} is missing - we might have joined mid exchange")] MissingDkgState { epoch_id: EpochId }, + #[error("a new iteration of DKG is currently in progress. all ticket issuance is halted until that's completed")] + DkgInProgress, + #[error( "the node index value for epoch {epoch_id} is not available - are you sure we are a dealer?" )] @@ -231,9 +175,47 @@ pub enum CoconutError { dealing_index: DealingIndex, chunk_index: ChunkIndex, }, + + #[error("could not find ecash deposit associated with id {deposit_id}")] + NonExistentDeposit { deposit_id: DepositId }, + + #[error("the provided request digest does not match the hash of attached serial numbers")] + MismatchedRequestDigest, + + #[error("the on chain proposal digest does not match the attached request digest")] + MismatchedOnChainDigest, + + #[error("one of the attached tickets {serial_number_bs58} has not been verified before")] + TicketNotVerified { serial_number_bs58: String }, + + #[error("the provided ticket(s) redemption proposal is invalid: {source}")] + RedemptionProposalFailure { + #[from] + source: RedemptionError, + }, + + #[error("this gateway hasn't submitted any tickets for verification")] + NotTicketsProvided, + + #[error("this gateway is attempting to redeem its tickets too often. last redemption happened on {last_redemption}. the earliest next permitted redemption will be on {next_allowed}")] + TooFrequentRedemption { + last_redemption: OffsetDateTime, + next_allowed: OffsetDateTime, + }, + + #[error( + "could not sign the data for epoch {requested}. our current key is for epoch {available}" + )] + InvalidSigningKeyEpoch { + requested: EpochId, + available: EpochId, + }, + + #[error("could not obtain enough shares for aggregation. got {shares} shares whilst the threshold is {threshold}")] + InsufficientNumberOfShares { threshold: Threshold, shares: usize }, } -impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { +impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { let err_msg = self.to_string(); Response::build() @@ -243,3 +225,74 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { .ok() } } + +impl OpenApiResponderInner for EcashError { + fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { + let mut responses = Responses::default(); + ensure_status_code_exists(&mut responses, 400); + Ok(responses) + } +} + +#[derive(Debug, Error)] +pub enum RedemptionError { + #[error("failed to retrieve proposal {proposal_id} from the chain")] + ProposalRetrievalFailure { proposal_id: u64 }, + + #[error( + "the proposal {proposal_id} has invalid title. got {received} but expected {}", + BATCH_REDEMPTION_PROPOSAL_TITLE + )] + InvalidProposalTitle { proposal_id: u64, received: String }, + + #[error("the proposal {proposal_id} has invalid description. got {received} but expected {expected}")] + InvalidProposalDescription { + proposal_id: u64, + received: String, + expected: String, + }, + + #[error("the proposal {proposal_id} is still pending")] + StillPending { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has already been executed")] + AlreadyExecuted { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has already been rejected")] + AlreadyRejected { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has already been passed")] + AlreadyPassed { proposal_id: u64 }, + + #[error("the proposal {proposal_id} was proposed by an unexpected address {received}. expected the ecash contract at {expected}")] + InvalidProposer { + proposal_id: u64, + received: String, + expected: AccountId, + }, + + #[error( + "the proposal {proposal_id} did not contain exactly a single contract execution message" + )] + TooManyMessages { proposal_id: u64 }, + + #[error("the proposal {proposal_id} did not contain the correct redemption execution message")] + InvalidMessage { proposal_id: u64 }, + + #[error("the proposal {proposal_id} has not been made against the expected e-cash contract")] + InvalidContract { proposal_id: u64 }, + + #[error("the proposal {proposal_id} proposes redemption of tickets for gateway {proposed}, but the request has been sent by {received}")] + InvalidRedemptionTarget { + proposal_id: u64, + proposed: String, + received: String, + }, + + #[error("the proposal {proposal_id} proposes redemption of {proposed} tickets, but the request has been sent for {received} instead")] + InvalidRedemptionTicketCount { + proposal_id: u64, + proposed: u16, + received: u16, + }, +} diff --git a/nym-api/src/ecash/helpers.rs b/nym-api/src/ecash/helpers.rs new file mode 100644 index 0000000000..6a047d63c2 --- /dev/null +++ b/nym-api/src/ecash/helpers.rs @@ -0,0 +1,134 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::scheme::keygen::SecretKeyAuth; +use nym_compact_ecash::BlindedSignature; +use nym_compact_ecash::{PublicKeyUser, WithdrawalRequest}; +use nym_ecash_time::EcashTime; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::future::Future; +use std::hash::Hash; +use std::ops::Deref; +use tokio::sync::{RwLock, RwLockReadGuard}; + +#[derive(Serialize, Deserialize)] +pub(crate) struct IssuedExpirationDateSignatures { + pub(crate) epoch_id: EpochId, + pub(crate) signatures: Vec, +} + +#[derive(Serialize, Deserialize)] +pub(crate) struct IssuedCoinIndicesSignatures { + pub(crate) epoch_id: EpochId, + pub(crate) signatures: Vec, +} + +pub(crate) trait CredentialRequest { + fn withdrawal_request(&self) -> &WithdrawalRequest; + fn expiration_date_timestamp(&self) -> u64; + fn ecash_pubkey(&self) -> PublicKeyUser; +} + +impl CredentialRequest for BlindSignRequestBody { + fn withdrawal_request(&self) -> &WithdrawalRequest { + &self.inner_sign_request + } + + fn expiration_date_timestamp(&self) -> u64 { + self.expiration_date.ecash_unix_timestamp() + } + + fn ecash_pubkey(&self) -> PublicKeyUser { + self.ecash_pubkey.clone() + } +} + +pub(crate) fn blind_sign( + request: &C, + signing_key: &SecretKeyAuth, +) -> Result { + Ok(nym_compact_ecash::scheme::withdrawal::issue( + signing_key, + request.ecash_pubkey().clone(), + request.withdrawal_request(), + request.expiration_date_timestamp(), + )?) +} + +// a map of items that never change for given key +pub(crate) struct CachedImmutableItems { + // I wonder if there's a more efficient structure with OnceLock or OnceCell or something + inner: RwLock>, +} + +// an item that stays constant throughout given epoch +pub(crate) type CachedImmutableEpochItem = CachedImmutableItems; + +impl Default for CachedImmutableItems { + fn default() -> Self { + CachedImmutableItems { + inner: RwLock::new(HashMap::new()), + } + } +} + +impl Deref for CachedImmutableItems { + type Target = RwLock>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl CachedImmutableItems +where + K: Eq + Hash, +{ + pub(crate) async fn get_or_init( + &self, + key: K, + f: F, + ) -> Result, EcashError> + where + F: FnOnce() -> U, + U: Future>, + K: Clone, + { + // 1. see if we already have the item cached + let guard = self.inner.read().await; + if let Ok(item) = RwLockReadGuard::try_map(guard, |map| map.get(&key)) { + return Ok(item); + } + + // 2. attempt to retrieve (and cache) it + let mut write_guard = self.inner.write().await; + + // see if another task has already set the item whilst we were waiting for the lock + if write_guard.get(&key).is_some() { + let read_guard = write_guard.downgrade(); + + // SAFETY: we just checked the entry exists and we never dropped the guard + #[allow(clippy::unwrap_used)] + return Ok(RwLockReadGuard::map(read_guard, |map| { + map.get(&key).unwrap() + })); + } + + let init = f().await?; + write_guard.insert(key.clone(), init); + + let guard = write_guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |map| map.get(&key).unwrap())) + } +} diff --git a/nym-api/src/coconut/keys/mod.rs b/nym-api/src/ecash/keys/mod.rs similarity index 60% rename from nym-api/src/coconut/keys/mod.rs rename to nym-api/src/ecash/keys/mod.rs index 1902d9e180..87d01865e2 100644 --- a/nym-api/src/coconut/keys/mod.rs +++ b/nym-api/src/ecash/keys/mod.rs @@ -1,8 +1,9 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_coconut::VerificationKey; +use crate::ecash::error::EcashError; use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::{SecretKeyAuth, VerificationKeyAuth}; use nym_dkg::Scalar; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -19,12 +20,29 @@ pub struct KeyPair { #[derive(Debug)] pub struct KeyPairWithEpoch { - pub(crate) keys: nym_coconut::KeyPair, + pub(crate) keys: nym_compact_ecash::KeyPairAuth, + pub(crate) issued_for_epoch: EpochId, +} + +impl From for KeyPairWithEpoch { + fn from(value: LegacyCoconutKeyWithEpoch) -> Self { + let (x, ys) = value.secret_key.hazmat_to_raw(); + let sk = nym_compact_ecash::SecretKeyAuth::create_from_raw(x, ys); + + KeyPairWithEpoch { + keys: sk.into(), + issued_for_epoch: value.issued_for_epoch, + } + } +} + +pub struct LegacyCoconutKeyWithEpoch { + pub(crate) secret_key: nym_coconut::SecretKey, pub(crate) issued_for_epoch: EpochId, } impl KeyPairWithEpoch { - pub(crate) fn new(keys: nym_coconut::KeyPair, issued_for_epoch: EpochId) -> Self { + pub(crate) fn new(keys: nym_compact_ecash::KeyPairAuth, issued_for_epoch: EpochId) -> Self { KeyPairWithEpoch { keys, issued_for_epoch, @@ -64,9 +82,24 @@ impl KeyPair { } } - pub async fn verification_key(&self) -> Option> { - RwLockReadGuard::try_map(self.get().await?, |maybe_keypair| { - maybe_keypair.as_ref().map(|k| k.keys.verification_key()) + pub async fn keys(&self) -> Result, EcashError> { + let keypair_guard = self.get().await.ok_or(EcashError::KeyPairNotDerivedYet)?; + RwLockReadGuard::try_map(keypair_guard, |keypair| keypair.as_ref()) + .map_err(|_| EcashError::KeyPairNotDerivedYet) + } + + pub async fn signing_key(&self) -> Result, EcashError> { + let keypair_guard = self.get().await.ok_or(EcashError::KeyPairNotDerivedYet)?; + + RwLockReadGuard::try_map(keypair_guard, |keypair| { + keypair.as_ref().map(|k| k.keys.secret_key()) + }) + .map_err(|_| EcashError::KeyPairNotDerivedYet) + } + + pub async fn verification_key(&self) -> Option> { + RwLockReadGuard::try_map(self.get().await?, |maybe_keys| { + maybe_keys.as_ref().map(|k| k.keys.verification_key_ref()) }) .ok() } diff --git a/nym-api/src/ecash/keys/persistence.rs b/nym-api/src/ecash/keys/persistence.rs new file mode 100644 index 0000000000..976be0dfd6 --- /dev/null +++ b/nym-api/src/ecash/keys/persistence.rs @@ -0,0 +1,77 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::keys::{KeyPairWithEpoch, LegacyCoconutKeyWithEpoch}; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::{error::CompactEcashError, scheme::keygen::SecretKeyAuth, KeyPairAuth}; +use nym_pemstore::traits::PemStorableKey; +use std::mem; + +impl PemStorableKey for KeyPairWithEpoch { + // that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose + type Error = CompactEcashError; + + fn pem_type() -> &'static str { + "ECASH KEY WITH EPOCH" + } + + fn to_bytes(&self) -> Vec { + let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec(); + bytes.append(&mut self.keys.secret_key().to_bytes()); + bytes + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() <= mem::size_of::() { + return Err(CompactEcashError::DeserializationMinLength { + min: mem::size_of::(), + actual: bytes.len(), + }); + } + let epoch_id = EpochId::from_be_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]); + + let sk = SecretKeyAuth::from_bytes(&bytes[mem::size_of::()..])?; + let vk = sk.verification_key(); + + Ok(KeyPairWithEpoch { + keys: KeyPairAuth::from_keys(sk, vk), + issued_for_epoch: epoch_id, + }) + } +} + +impl PemStorableKey for LegacyCoconutKeyWithEpoch { + // that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose + type Error = nym_coconut::CoconutError; + + fn pem_type() -> &'static str { + "COCONUT KEY WITH EPOCH" + } + + fn to_bytes(&self) -> Vec { + let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec(); + bytes.append(&mut self.secret_key.to_bytes()); + bytes + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() <= mem::size_of::() { + return Err(nym_coconut::CoconutError::DeserializationMinLength { + min: mem::size_of::(), + actual: bytes.len(), + }); + } + let epoch_id = EpochId::from_be_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]); + + let sk = nym_coconut::SecretKey::from_bytes(&bytes[mem::size_of::()..])?; + + Ok(LegacyCoconutKeyWithEpoch { + secret_key: sk, + issued_for_epoch: epoch_id, + }) + } +} diff --git a/nym-api/src/ecash/mod.rs b/nym-api/src/ecash/mod.rs new file mode 100644 index 0000000000..662822114f --- /dev/null +++ b/nym-api/src/ecash/mod.rs @@ -0,0 +1,47 @@ +// Copyright 2021-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use okapi::openapi3::OpenApi; +use rocket::Route; +use rocket_okapi::openapi_get_routes_spec; +use rocket_okapi::settings::OpenApiSettings; + +pub(crate) mod api_routes; +pub(crate) mod client; +pub(crate) mod comm; +mod deposit; +pub(crate) mod dkg; +pub(crate) mod error; +pub(crate) mod helpers; +pub(crate) mod keys; +pub(crate) mod state; +pub(crate) mod storage; +#[cfg(test)] +pub(crate) mod tests; + +// equivalent of 100nym +pub(crate) const MINIMUM_BALANCE: u128 = 100_000000; + +pub(crate) fn routes_open_api(settings: &OpenApiSettings, enabled: bool) -> (Vec, OpenApi) { + if enabled { + openapi_get_routes_spec![ + settings: + api_routes::partial_signing::post_blind_sign, + api_routes::partial_signing::partial_expiration_date_signatures, + api_routes::partial_signing::partial_coin_indices_signatures, + api_routes::spending::verify_ticket, + api_routes::spending::batch_redeem_tickets, + api_routes::spending::double_spending_filter_v1, + api_routes::issued::epoch_credentials, + api_routes::issued::issued_credential, + api_routes::issued::issued_credentials, + api_routes::aggregation::master_verification_key, + api_routes::aggregation::coin_indices_signatures, + api_routes::aggregation::expiration_date_signatures + ] + } else { + openapi_get_routes_spec![ + settings: + ] + } +} diff --git a/nym-api/src/ecash/state/auxiliary.rs b/nym-api/src/ecash/state/auxiliary.rs new file mode 100644 index 0000000000..d96713a336 --- /dev/null +++ b/nym-api/src/ecash/state/auxiliary.rs @@ -0,0 +1,45 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::client::Client as LocalClient; +use crate::ecash::comm::APICommunicationChannel; +use crate::ecash::error::{EcashError, Result}; +use crate::support::storage::NymApiStorage; +use nym_coconut_dkg_common::types::EpochId; + +pub(crate) struct AuxiliaryEcashState { + pub(crate) client: Box, + pub(crate) comm_channel: Box, + pub(crate) storage: NymApiStorage, +} + +impl AuxiliaryEcashState { + pub(crate) fn new(client: C, comm_channel: D, storage: NymApiStorage) -> Self + where + C: LocalClient + Send + Sync + 'static, + D: APICommunicationChannel + Send + Sync + 'static, + { + AuxiliaryEcashState { + client: Box::new(client), + comm_channel: Box::new(comm_channel), + storage, + } + } + + pub(crate) async fn current_epoch(&self) -> Result { + self.comm_channel.current_epoch().await + } + + pub(crate) async fn ensure_not_blacklisted(&self, encoded_pubkey_bs58: &str) -> Result<()> { + let res = self + .client + .get_blacklisted_account(encoded_pubkey_bs58.to_string()) + .await?; + + if res.account.is_some() { + return Err(EcashError::BlacklistedAccount); + } + + Ok(()) + } +} diff --git a/nym-api/src/ecash/state/bloom.rs b/nym-api/src/ecash/state/bloom.rs new file mode 100644 index 0000000000..939f19b3a9 --- /dev/null +++ b/nym-api/src/ecash/state/bloom.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only diff --git a/nym-api/src/ecash/state/global.rs b/nym-api/src/ecash/state/global.rs new file mode 100644 index 0000000000..24d1ff00ff --- /dev/null +++ b/nym-api/src/ecash/state/global.rs @@ -0,0 +1,33 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::helpers::{ + CachedImmutableEpochItem, CachedImmutableItems, IssuedCoinIndicesSignatures, + IssuedExpirationDateSignatures, +}; +use nym_compact_ecash::VerificationKeyAuth; +use nym_validator_client::nyxd::AccountId; +use time::Date; + +pub(crate) struct GlobalEcachState { + pub(crate) contract_address: AccountId, + + pub(crate) master_verification_key: CachedImmutableEpochItem, + + // maybe we should use arrays here instead? + pub(crate) coin_index_signatures: CachedImmutableEpochItem, + + pub(crate) expiration_date_signatures: + CachedImmutableItems, +} + +impl GlobalEcachState { + pub(crate) fn new(contract_address: AccountId) -> Self { + GlobalEcachState { + contract_address, + master_verification_key: Default::default(), + coin_index_signatures: Default::default(), + expiration_date_signatures: Default::default(), + } + } +} diff --git a/nym-api/src/ecash/state/helpers.rs b/nym-api/src/ecash/state/helpers.rs new file mode 100644 index 0000000000..ffcab88644 --- /dev/null +++ b/nym-api/src/ecash/state/helpers.rs @@ -0,0 +1,160 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::ecash::state::local::TicketDoubleSpendingFilter; +use crate::ecash::storage::EcashStorageExt; +use crate::support::storage::NymApiStorage; +use futures::{stream, StreamExt}; +use nym_compact_ecash::constants; +use nym_config::defaults::BloomfilterParameters; +use nym_dkg::Threshold; +use nym_ecash_double_spending::{DoubleSpendingFilter, DoubleSpendingFilterBuilder}; +use nym_ecash_time::{cred_exp_date, ecash_today}; +use nym_validator_client::EcashApiClient; +use std::future::Future; +use time::ext::NumericalDuration; +use time::Date; +use tokio::sync::Mutex; + +// attempt to completely rebuild the bloomfilter data for given day +async fn try_rebuild_today_bloomfilter( + today: Date, + params: BloomfilterParameters, + storage: &NymApiStorage, +) -> Result { + log::info!("rebuilding bloomfilter for {today}"); + + let tickets = storage.get_all_spent_tickets_on(today).await?; + log::debug!( + "there are {} tickets to insert into the filter", + tickets.len() + ); + + let mut filter = DoubleSpendingFilter::new_empty(params); + for ticket in tickets { + filter.set(&ticket.serial_number) + } + Ok(filter) +} + +pub(crate) async fn prepare_partial_bloomfilter_builder( + storage: &NymApiStorage, + params: BloomfilterParameters, + params_id: i64, + start: Date, + days: i64, +) -> Result { + log::info!( + "attempting to rebuild partial bloomfilter starting at {start} which includes {days} days" + ); + + let mut filter_builder = DoubleSpendingFilter::builder(params); + for i in 0..days { + let date = start - i.days(); + let Some(bitmap) = storage + .try_load_partial_bloomfilter_bitmap(date, params_id) + .await? + else { + log::warn!("missing double spending bloomfilter bitmap for {date} (if this API hasn't been running for at least 30 days since 'ecash'-based zk-nyms were introduced this is expected)"); + continue; + }; + if !filter_builder.add_bytes(&bitmap) { + log::error!( + "failed to add bitmap from {date} to the global bloomfilter. it may be malformed!" + ); + } + } + Ok(filter_builder) +} + +pub(super) async fn try_rebuild_bloomfilter( + storage: &NymApiStorage, +) -> Result { + log::info!("attempting to rebuild the double spending bloomfilter..."); + let today = ecash_today().date(); + + let (params_id, params) = storage.get_double_spending_filter_params().await?; + log::info!("will use the following parameters: {params:?}"); + + // we're never going to have persisted data for 'today'. we need to rebuild it from scratch + let today_filter = try_rebuild_today_bloomfilter(today, params, storage).await?; + + log::info!("attempting to rebuild the global filter"); + let mut global_filter = prepare_partial_bloomfilter_builder( + storage, + params, + params_id, + today.previous_day().unwrap(), + constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 1, + ) + .await?; + + if !global_filter.add_bytes(&today_filter.dump_bitmap()) { + log::error!( + "failed to add bitmap from {today} to the global bloomfilter. it may be malformed!" + ); + } + + Ok(TicketDoubleSpendingFilter::new( + today, + params_id, + global_filter.build(), + today_filter, + )) +} + +pub(crate) fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), EcashError> { + let today = ecash_today(); + + if expiration_date < today.date() { + // what's the point of signatures with expiration in the past? + return Err(EcashError::ExpirationDateTooEarly); + } + + // SAFETY: we're nowhere near MAX date + #[allow(clippy::unwrap_used)] + if expiration_date > cred_exp_date().date().next_day().unwrap() { + // don't allow issuing signatures too far in advance (1 day beyond current value is fine) + return Err(EcashError::ExpirationDateTooLate); + } + + Ok(()) +} + +pub(crate) async fn query_all_threshold_apis( + all_apis: Vec, + threshold: Threshold, + f: F, +) -> Result, EcashError> +where + F: Fn(EcashApiClient) -> U, + U: Future>, +{ + let shares = Mutex::new(Vec::with_capacity(all_apis.len())); + + stream::iter(all_apis) + .for_each_concurrent(8, |api| async { + // can't be bothered to restructure the code to appease the borrow checker properly, + // so just assign this to a variable + let disp = api.to_string(); + match f(api).await { + Ok(partial_share) => shares.lock().await.push(partial_share), + Err(err) => { + log::warn!("failed to obtain partial threshold data from API: {disp}: {err}") + } + } + }) + .await; + + let shares = shares.into_inner(); + + if shares.len() < threshold as usize { + return Err(EcashError::InsufficientNumberOfShares { + threshold, + shares: shares.len(), + }); + } + + Ok(shares) +} diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs new file mode 100644 index 0000000000..3f97c98a53 --- /dev/null +++ b/nym-api/src/ecash/state/local.rs @@ -0,0 +1,107 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::helpers::{ + CachedImmutableEpochItem, CachedImmutableItems, IssuedCoinIndicesSignatures, + IssuedExpirationDateSignatures, +}; +use crate::ecash::keys::KeyPair; +use nym_config::defaults::BloomfilterParameters; +use nym_crypto::asymmetric::identity; +use nym_ecash_double_spending::DoubleSpendingFilter; +use std::sync::Arc; +use time::Date; +use tokio::sync::RwLock; + +pub(crate) struct TicketDoubleSpendingFilter { + built_on: Date, + params_id: i64, + + today_filter: DoubleSpendingFilter, + global_filter: DoubleSpendingFilter, +} + +impl TicketDoubleSpendingFilter { + pub(crate) fn new( + built_on: Date, + params_id: i64, + global_filter: DoubleSpendingFilter, + today_filter: DoubleSpendingFilter, + ) -> TicketDoubleSpendingFilter { + TicketDoubleSpendingFilter { + built_on, + params_id, + today_filter, + global_filter, + } + } + + pub(crate) fn built_on(&self) -> Date { + self.built_on + } + + pub(crate) fn params(&self) -> BloomfilterParameters { + self.today_filter.params() + } + + pub(crate) fn params_id(&self) -> i64 { + self.params_id + } + + pub(crate) fn check(&self, sn: &Vec) -> bool { + self.global_filter.check(sn) + } + + /// Returns boolean to indicate if the entry was already present + pub(crate) fn insert_both(&mut self, sn: &Vec) -> bool { + self.today_filter.set(sn); + self.insert_global_only(sn) + } + + /// Returns boolean to indicate if the entry was already present + pub(crate) fn insert_global_only(&mut self, sn: &Vec) -> bool { + let existed = self.global_filter.check(sn); + self.global_filter.set(sn); + existed + } + + pub(crate) fn export_today_bitmap(&self) -> Vec { + self.today_filter.dump_bitmap() + } + + pub(crate) fn export_global_bitmap(&self) -> Vec { + self.global_filter.dump_bitmap() + } + + pub(crate) fn advance_day(&mut self, date: Date, new_global: DoubleSpendingFilter) { + self.built_on = date; + self.global_filter = new_global; + self.today_filter.reset(); + } +} + +pub(crate) struct LocalEcashState { + pub(crate) ecash_keypair: KeyPair, + pub(crate) identity_keypair: identity::KeyPair, + + pub(crate) partial_coin_index_signatures: CachedImmutableEpochItem, + pub(crate) partial_expiration_date_signatures: + CachedImmutableItems, + pub(crate) double_spending_filter: Arc>, +} + +impl LocalEcashState { + pub(crate) fn new( + ecash_keypair: KeyPair, + identity_keypair: identity::KeyPair, + double_spending_filter: TicketDoubleSpendingFilter, + ) -> Self { + LocalEcashState { + ecash_keypair, + identity_keypair, + partial_coin_index_signatures: Default::default(), + partial_expiration_date_signatures: Default::default(), + double_spending_filter: Arc::new(RwLock::new(double_spending_filter)), + } + } +} diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs new file mode 100644 index 0000000000..b790e9a84c --- /dev/null +++ b/nym-api/src/ecash/state/mod.rs @@ -0,0 +1,863 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::client::Client as LocalClient; +use crate::ecash::comm::APICommunicationChannel; +use crate::ecash::deposit::validate_deposit; +use crate::ecash::error::{EcashError, RedemptionError, Result}; +use crate::ecash::helpers::{IssuedCoinIndicesSignatures, IssuedExpirationDateSignatures}; +use crate::ecash::keys::KeyPair; +use crate::ecash::state::auxiliary::AuxiliaryEcashState; +use crate::ecash::state::global::GlobalEcachState; +use crate::ecash::state::helpers::{ + ensure_sane_expiration_date, prepare_partial_bloomfilter_builder, query_all_threshold_apis, + try_rebuild_bloomfilter, +}; +use crate::ecash::state::local::LocalEcashState; +use crate::ecash::storage::models::{SerialNumberWrapper, TicketProvider}; +use crate::ecash::storage::EcashStorageExt; +use crate::support::storage::NymApiStorage; +use cosmwasm_std::{from_binary, CosmosMsg, WasmMsg}; +use cw3::Status; +use nym_api_requests::ecash::helpers::issued_credential_plaintext; +use nym_api_requests::ecash::models::BatchRedeemTicketsBody; +use nym_api_requests::ecash::BlindSignRequestBody; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::scheme::coin_indices_signatures::{ + aggregate_annotated_indices_signatures, sign_coin_indices, CoinIndexSignatureShare, +}; +use nym_compact_ecash::scheme::expiration_date_signatures::{ + aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare, +}; +use nym_compact_ecash::{ + constants, scheme::expiration_date_signatures::sign_expiration_date, BlindedSignature, + SecretKeyAuth, VerificationKeyAuth, +}; +use nym_config::defaults::BloomfilterParameters; +use nym_credentials::ecash::utils::EcashTime; +use nym_credentials::{aggregate_verification_keys, CredentialSpendingData}; +use nym_crypto::asymmetric::identity; +use nym_ecash_contract_common::deposit::{Deposit, DepositId}; +use nym_ecash_contract_common::msg::ExecuteMsg; +use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; +use nym_ecash_double_spending::DoubleSpendingFilter; +use nym_ecash_time::cred_exp_date; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::EcashApiClient; +use time::ext::NumericalDuration; +use time::{Date, OffsetDateTime}; +use tokio::sync::RwLockReadGuard; +use tokio::time::Instant; + +pub(crate) mod auxiliary; +pub(crate) mod bloom; +pub(crate) mod global; +mod helpers; +pub(crate) mod local; + +pub struct EcashState { + // state global to the system, like aggregated keys, addresses, etc. + pub(crate) global: GlobalEcachState, + + // state local to the api instance, like partial signatures, keys, etc. + pub(crate) local: LocalEcashState, + + // auxiliary data used for resolving requests like clients, storage, etc. + pub(crate) aux: AuxiliaryEcashState, +} + +impl EcashState { + pub(crate) async fn new( + contract_address: AccountId, + client: C, + identity_keypair: identity::KeyPair, + key_pair: KeyPair, + comm_channel: D, + storage: NymApiStorage, + ) -> Result + where + C: LocalClient + Send + Sync + 'static, + D: APICommunicationChannel + Send + Sync + 'static, + { + let double_spending_filter = try_rebuild_bloomfilter(&storage).await?; + + Ok(Self { + global: GlobalEcachState::new(contract_address), + local: LocalEcashState::new(key_pair, identity_keypair, double_spending_filter), + aux: AuxiliaryEcashState::new(client, comm_channel, storage), + }) + } + + pub(crate) async fn ecash_signing_key(&self) -> Result> { + self.local.ecash_keypair.signing_key().await + } + + #[allow(dead_code)] + pub(crate) async fn current_master_verification_key( + &self, + ) -> Result> { + self.master_verification_key(None).await + } + + pub(crate) async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.aux.current_epoch().await?, + }; + + self.global + .master_verification_key + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(stored) = self + .aux + .storage + .get_master_verification_key(epoch_id) + .await? + { + return Ok(stored); + } + + // 2. perform actual aggregation + let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?; + let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?; + + if all_apis.len() < threshold as usize { + return Err(EcashError::InsufficientNumberOfShares { + threshold, + shares: all_apis.len(), + }); + } + + let master_key = aggregate_verification_keys(&all_apis)?; + + // 3. save the key in the storage for when we reboot + self.aux + .storage + .insert_master_verification_key(epoch_id, &master_key) + .await?; + + Ok(master_key) + }) + .await + } + + pub(crate) async fn master_coin_index_signatures( + &self, + epoch_id: Option, + ) -> Result> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.aux.current_epoch().await?, + }; + + self.global + .coin_index_signatures + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(master_sigs) = self + .aux + .storage + .get_master_coin_index_signatures(epoch_id) + .await? + { + return Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures: master_sigs, + }); + } + + log::info!( + "attempting to establish master coin index signatures for epoch {epoch_id}..." + ); + + // 2. go around APIs and attempt to aggregate the data + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?; + let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?; + + // let mut shares = Mutex::new(Vec::with_capacity(all_apis.len())); + let cosmos_address = self.aux.client.address().await; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + // check if we're attempting to query ourselves, in that case just get local signature + // rather than making the http query + let partial = if api.cosmos_address == cosmos_address { + self.partial_coin_index_signatures(Some(epoch_id)) + .await? + .signatures + .clone() + } else { + api.api_client + .partial_coin_indices_signatures(Some(epoch_id)) + .await? + .signatures + }; + Ok(CoinIndexSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis, threshold, get_partial_signatures).await?; + + let aggregated = aggregate_annotated_indices_signatures( + nym_credentials_interface::ecash_parameters(), + &master_vk, + &shares, + )?; + + // 3. save the signatures in the storage for when we reboot + self.aux + .storage + .insert_master_coin_index_signatures(epoch_id, &aggregated) + .await?; + + Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures: aggregated, + }) + }) + .await + } + + pub(crate) async fn partial_coin_index_signatures( + &self, + epoch_id: Option, + ) -> Result> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.aux.current_epoch().await?, + }; + + self.local + .partial_coin_index_signatures + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(partial_sigs) = self + .aux + .storage + .get_partial_coin_index_signatures(epoch_id) + .await? + { + return Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures: partial_sigs, + }); + } + + + // 2. perform actual issuance + let signing_keys = self.local.ecash_keypair.keys().await?; + if signing_keys.issued_for_epoch != epoch_id { + // TODO: this should get handled at some point, + // because if it was a past epoch we **do** have those keys. + // they're just archived + + log::error!("received partial coin index signature request for an invalid epoch ({epoch_id}). our key was derived for epoch {}", signing_keys.issued_for_epoch); + return Err(EcashError::InvalidSigningKeyEpoch { + requested: epoch_id, + available: signing_keys.issued_for_epoch, + }) + } + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let signatures = sign_coin_indices( + nym_compact_ecash::ecash_parameters(), + &master_vk, + signing_keys.keys.secret_key(), + )?; + + // 3. save the signatures in the storage for when we reboot + self.aux.storage.insert_partial_coin_index_signatures(epoch_id, &signatures).await?; + + Ok(IssuedCoinIndicesSignatures { + epoch_id, + signatures, + }) + }) + .await + } + + pub(crate) async fn master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result> { + self.global + .expiration_date_signatures + .get_or_init(expiration_date, || async { + // 1. sanity check to see if the expiration_date is not nonsense + ensure_sane_expiration_date(expiration_date)?; + + // 2. check the storage + if let Some(master_sigs) = self + .aux + .storage + .get_master_expiration_date_signatures(expiration_date) + .await? + { + return Ok(master_sigs); + } + + // 3. go around APIs and attempt to aggregate the data + let epoch_id = self.aux.comm_channel.current_epoch().await?; + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?; + let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?; + + let cosmos_address = self.aux.client.address().await; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + // check if we're attempting to query ourselves, in that case just get local signature + // rather than making the http query + let partial = if api.cosmos_address == cosmos_address { + self.partial_expiration_date_signatures(expiration_date) + .await? + .signatures + .clone() + } else { + api.api_client + .partial_expiration_date_signatures(Some(expiration_date)) + .await? + .signatures + }; + Ok(ExpirationDateSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis, threshold, get_partial_signatures).await?; + + let aggregated = aggregate_annotated_expiration_signatures( + &master_vk, + expiration_date.ecash_unix_timestamp(), + &shares, + )?; + + let issued = IssuedExpirationDateSignatures { + epoch_id, + signatures: aggregated, + }; + + // 4. save the signatures in the storage for when we reboot + self.aux + .storage + .insert_master_expiration_date_signatures(expiration_date, &issued) + .await?; + + Ok(issued) + }) + .await + } + + pub(crate) async fn partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result> { + self.local + .partial_expiration_date_signatures + .get_or_init(expiration_date, || async { + // 1. sanity check to see if the expiration_date is not nonsense + ensure_sane_expiration_date(expiration_date)?; + + // 2. check the storage + if let Some(partial_sigs) = self + .aux + .storage + .get_partial_expiration_date_signatures(expiration_date) + .await? + { + return Ok(partial_sigs); + } + + // 3. perform actual issuance + let signing_keys = self.local.ecash_keypair.keys().await?; + + let signatures = sign_expiration_date( + signing_keys.keys.secret_key(), + expiration_date.ecash_unix_timestamp(), + )?; + + let issued = IssuedExpirationDateSignatures { + epoch_id: signing_keys.issued_for_epoch, + signatures, + }; + + // 4. save the signatures in the storage for when we reboot + self.aux + .storage + .insert_partial_expiration_date_signatures(expiration_date, &issued) + .await?; + + Ok(issued) + }) + .await + } + + pub(crate) async fn ensure_dkg_not_in_progress(&self) -> Result<()> { + if self.aux.comm_channel.dkg_in_progress().await? { + return Err(EcashError::DkgInProgress); + } + Ok(()) + } + + /// Check if this nym-api has already issued a credential for the provided deposit id. + /// If so, return it. + pub async fn already_issued(&self, deposit_id: DepositId) -> Result> { + self.aux + .storage + .get_issued_bandwidth_credential_by_deposit_id(deposit_id) + .await? + .map(|cred| cred.try_into()) + .transpose() + } + + pub async fn get_deposit(&self, deposit_id: DepositId) -> Result { + self.aux + .client + .get_deposit(deposit_id) + .await? + .deposit + .ok_or(EcashError::NonExistentDeposit { deposit_id }) + } + + pub async fn validate_request( + &self, + request: &BlindSignRequestBody, + deposit: Deposit, + ) -> Result<()> { + validate_deposit(request, deposit).await + } + + pub(crate) async fn validate_redemption_proposal( + &self, + request: &BatchRedeemTicketsBody, + ) -> std::result::Result<(), RedemptionError> { + let proposal_id = request.proposal_id; + + // retrieve the proposal itself + let mut proposal = self + .aux + .client + .get_proposal(proposal_id) + .await + .map_err(|_| RedemptionError::ProposalRetrievalFailure { proposal_id })?; + + if proposal.title != BATCH_REDEMPTION_PROPOSAL_TITLE { + return Err(RedemptionError::InvalidProposalTitle { + proposal_id, + received: proposal.title, + }); + } + + // make sure you can still vote on it + match proposal.status { + Status::Pending => return Err(RedemptionError::StillPending { proposal_id }), + Status::Open => {} + Status::Rejected => return Err(RedemptionError::AlreadyRejected { proposal_id }), + + // TODO: need to double check with the multisig whether it wouldn't always be thrown on threshold + // i.e. whether after the 2+/3 vote, the remaining 1-/3 would return this error + Status::Passed => return Err(RedemptionError::AlreadyPassed { proposal_id }), + Status::Executed => return Err(RedemptionError::AlreadyExecuted { proposal_id }), + } + + let encoded_digest = bs58::encode(&request.digest).into_string(); + + // check if the description matches the expected digest + if encoded_digest != proposal.description { + return Err(RedemptionError::InvalidProposalDescription { + proposal_id, + received: proposal.description, + expected: encoded_digest, + }); + } + + // check if it was actually created by the ecash contract + if proposal.proposer != self.global.contract_address.as_ref() { + return Err(RedemptionError::InvalidProposer { + proposal_id, + received: proposal.proposer.into_string(), + expected: self.global.contract_address.clone(), + }); + } + + // check if contains exactly the content we expect, + // i.e. single `RedeemTickets` message with no funds, etc. + if proposal.msgs.len() != 1 { + return Err(RedemptionError::TooManyMessages { proposal_id }); + } + + // SAFETY: we just checked we have exactly one message + #[allow(clippy::unwrap_used)] + let msg = proposal.msgs.pop().unwrap(); + let CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds, + }) = msg + else { + return Err(RedemptionError::InvalidMessage { proposal_id }); + }; + + if !funds.is_empty() { + return Err(RedemptionError::InvalidMessage { proposal_id }); + } + + if contract_addr != self.global.contract_address.as_ref() { + return Err(RedemptionError::InvalidContract { proposal_id }); + } + + let Ok(ExecuteMsg::RedeemTickets { n, gw }) = from_binary(&msg) else { + return Err(RedemptionError::InvalidMessage { proposal_id }); + }; + + if gw != request.gateway_cosmos_addr.as_ref() { + return Err(RedemptionError::InvalidRedemptionTarget { + proposal_id, + proposed: gw, + received: request.gateway_cosmos_addr.to_string(), + }); + } + + if n as usize != request.included_serial_numbers.len() { + return Err(RedemptionError::InvalidRedemptionTicketCount { + proposal_id, + proposed: n, + received: request.included_serial_numbers.len() as u16, + }); + } + + Ok(()) + } + + pub(crate) async fn accept_proposal(&self, proposal_id: u64) -> Result<()> { + //SW NOTE: What to do if this fails + if let Err(err) = self.aux.client.vote_proposal(proposal_id, true, None).await { + log::debug!("failed to vote on proposal {proposal_id}: {err}"); + } + + Ok(()) + } + + // pub(crate) async fn blacklist(&self, public_key: String) { + // let client = self.aux.client.clone(); + // tokio::spawn(async move { + // //SW TODO error handling with one log at the end + // let response = client.propose_for_blacklist(public_key.clone()).await?; + // let proposal_id = find_proposal_id(&response.logs)?; + // + // let proposal = client.get_proposal(proposal_id).await?; + // if proposal.status == Status::Open { + // if public_key != proposal.description { + // return Err(EcashError::IncorrectProposal { + // reason: String::from("incorrect publickey in description"), + // }); + // } + // let ret = client.vote_proposal(proposal_id, true, None).await; + // + // accepted_vote_err(ret)?; + // + // if let Ok(proposal) = client.get_proposal(proposal_id).await { + // if proposal.status == Status::Passed { + // client.execute_proposal(proposal_id).await? + // } + // } + // } + // Ok(()) + // }); + // } + + pub(crate) async fn sign_and_store_credential( + &self, + current_epoch: EpochId, + request_body: BlindSignRequestBody, + blinded_signature: &BlindedSignature, + ) -> Result { + let encoded_commitments = request_body.encode_commitments(); + + let plaintext = issued_credential_plaintext( + current_epoch as u32, + request_body.deposit_id, + blinded_signature, + &encoded_commitments, + request_body.expiration_date, + ); + + let signature = self.local.identity_keypair.private_key().sign(plaintext); + + // note: we have a UNIQUE constraint on the tx_hash column of the credential + // and so if the api is processing request for the same hash at the same time, + // only one of them will be successfully inserted to the database + let credential_id = self + .aux + .storage + .store_issued_credential( + current_epoch as u32, + request_body.deposit_id, + blinded_signature, + signature, + encoded_commitments, + request_body.expiration_date, + ) + .await?; + + Ok(credential_id) + } + + pub async fn store_issued_credential( + &self, + request_body: BlindSignRequestBody, + blinded_signature: &BlindedSignature, + ) -> Result<()> { + let current_epoch = self.aux.current_epoch().await?; + + // note: we have a UNIQUE constraint on the tx_hash column of the credential + // and so if the api is processing request for the same hash at the same time, + // only one of them will be successfully inserted to the database + let credential_id = self + .sign_and_store_credential(current_epoch, request_body, blinded_signature) + .await?; + self.aux + .storage + .update_epoch_credentials_entry(current_epoch, credential_id) + .await?; + debug!("the stored credential has id {credential_id}"); + + Ok(()) + } + + pub async fn store_verified_ticket( + &self, + ticket_data: &CredentialSpendingData, + gateway_addr: &AccountId, + ) -> Result<()> { + self.aux + .storage + .store_verified_ticket(ticket_data, gateway_addr) + .await + .map_err(Into::into) + } + + pub async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result> { + self.aux + .storage + .get_ticket_provider(gateway_address) + .await + .map_err(Into::into) + } + + pub async fn get_redeemable_tickets( + &self, + provider_info: TicketProvider, + ) -> Result> { + let since = provider_info + .last_batch_verification + .unwrap_or(OffsetDateTime::UNIX_EPOCH); + + self.aux + .storage + .get_verified_tickets_since(provider_info.id, since) + .await + .map_err(Into::into) + } + + pub async fn get_ticket_data_by_serial_number( + &self, + serial_number: &[u8], + ) -> Result> { + self.aux + .storage + .get_credential_data(serial_number) + .await + .map_err(Into::into) + } + + pub async fn check_bloomfilter(&self, serial_number: &Vec) -> bool { + self.local + .double_spending_filter + .read() + .await + .check(serial_number) + } + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + params: BloomfilterParameters, + sn: &Vec, + ) -> Result<(), EcashError> { + let mut filter = match self + .aux + .storage + .try_load_partial_bloomfilter_bitmap(date, params_id) + .await? + { + Some(bitmap) => DoubleSpendingFilter::from_bytes(params, &bitmap), + None => { + warn!("no existing partial bloomfilter for {date}"); + DoubleSpendingFilter::new_empty(params) + } + }; + filter.set(sn); + let updated_bitmap = filter.dump_bitmap(); + self.aux + .storage + .update_archived_partial_bloomfilter(date, &updated_bitmap) + .await?; + + Ok(()) + } + + /// Attempt to insert the provided serial number into the bloomfilter. + /// Furthermore, attempt to rotate the filter if we have advanced into a next day. + pub async fn update_bloomfilter( + &self, + serial_number: &Vec, + spending_date: Date, + today: Date, + ) -> Result { + let mut guard = self.local.double_spending_filter.write().await; + + let filter_date = guard.built_on(); + let yesterday = today.previous_day().unwrap(); + + let params_id = guard.params_id(); + let params = guard.params(); + + // if the filter is up-to-date, we just insert the entry and call it a day + if filter_date == today { + if spending_date == today { + return Ok(guard.insert_both(serial_number)); + } + // sanity check because this should NEVER happen, + // but when it inevitably does, we don't want to crash + if spending_date != yesterday { + log::error!("attempted to insert a ticket with spending date of {spending_date} while it's {today} today!!"); + } + + // this shouldn't be happening too often, so it's fine to interact with the storage + warn!("updating archived partial bloomfilter for {spending_date}. those logs have to be closely controlled to make sure they're not too frequent"); + self.update_archived_partial_bloomfilter( + spending_date, + params_id, + params, + serial_number, + ) + .await?; + + return Ok(guard.insert_global_only(serial_number)); + } + + log::info!("we need to advance our bloomfilter"); + let previous_bitmap = guard.export_today_bitmap(); + + // archive the BF for today's date + self.aux + .storage + .insert_partial_bloomfilter(filter_date, params_id, &previous_bitmap) + .await?; + + let new_global_filter = if filter_date == yesterday { + // normal case when we update filter daily + let two_days_ago = yesterday.previous_day().unwrap(); + let mut filter_builder = prepare_partial_bloomfilter_builder( + &self.aux.storage, + params, + params_id, + two_days_ago, + constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 2, + ) + .await?; + // add the bitmap from 'old today', i.e. yesterday + // (we have it on hand so no point in retrieving it from storage) + filter_builder.add_bytes(&previous_bitmap); + filter_builder.build() + } else { + // initial deployment case when we don't even get tickets daily + prepare_partial_bloomfilter_builder( + &self.aux.storage, + params, + params_id, + yesterday, + constants::CRED_VALIDITY_PERIOD_DAYS as i64 - 1, + ) + .await? + .build() + }; + + guard.advance_day(today, new_global_filter); + + // drop guard so other tasks could read the filter already whilst we clean-up the storage + let res = if spending_date == today { + Ok(guard.insert_both(serial_number)) + } else { + Ok(guard.insert_global_only(serial_number)) + }; + drop(guard); + + let cutoff = cred_exp_date().ecash_date(); + + // sanity check: + assert_eq!( + cutoff, + today + (constants::CRED_VALIDITY_PERIOD_DAYS as i64).days() + ); + + // remove the data we no longer need to hold, i.e. partial bloomfilters beyond max credential validity + // and the ticket data for those + self.aux + .storage + .remove_old_partial_bloomfilters(cutoff) + .await?; + self.aux + .storage + .remove_expired_verified_tickets(cutoff) + .await?; + + res + } + + pub async fn export_bloomfilter(&self) -> Vec { + let export_start = Instant::now(); + let bytes = self + .local + .double_spending_filter + .read() + .await + .export_global_bitmap(); + + let taken = export_start.elapsed(); + match taken.as_millis() { + // at that point it should somehow be cached; especially **IF** we have more reads than writes + ms if ms > 100 => error!("exporting the bloomfilter took {ms}ms to complete"), + ms if ms > 50 => warn!("exporting the bloomfilter took {ms}ms to complete"), + ms if ms > 10 => info!("exporting the bloomfilter took {ms}ms to complete"), + ms if ms > 2 => debug!("exporting the bloomfilter took {ms}ms to complete"), + ms => trace!("exporting the bloomfilter took {ms}ms to complete"), + } + + bytes + } +} diff --git a/nym-api/src/ecash/storage/helpers.rs b/nym-api/src/ecash/storage/helpers.rs new file mode 100644 index 0000000000..c0be8a9183 --- /dev/null +++ b/nym-api/src/ecash/storage/helpers.rs @@ -0,0 +1,52 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::NymApiStorageError; +use bincode::Options; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +struct StorageBorrowedSerdeWrapper<'a, T>(&'a T); + +#[derive(Serialize, Deserialize)] +struct StorageSerdeWrapper(T); + +pub(crate) fn serialise_coin_index_signatures(sigs: &[AnnotatedCoinIndexSignature]) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_coin_index_signatures( + raw: &[u8], +) -> Result, NymApiStorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + NymApiStorageError::database_inconsistency("malformed stored coin index signatures") + })?; + Ok(de.0) +} + +pub(crate) fn serialise_expiration_date_signatures( + sigs: &[AnnotatedExpirationDateSignature], +) -> Vec { + storage_serialiser() + .serialize(&StorageBorrowedSerdeWrapper(&sigs)) + .unwrap() +} + +pub(crate) fn deserialise_expiration_date_signatures( + raw: &[u8], +) -> Result, NymApiStorageError> { + let de: StorageSerdeWrapper<_> = storage_serialiser().deserialize(raw).map_err(|_| { + NymApiStorageError::database_inconsistency("malformed expiration date signatures") + })?; + Ok(de.0) +} + +pub(crate) fn storage_serialiser() -> impl bincode::Options { + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/nym-api/src/ecash/storage/manager.rs b/nym-api/src/ecash/storage/manager.rs new file mode 100644 index 0000000000..4689ffa10a --- /dev/null +++ b/nym-api/src/ecash/storage/manager.rs @@ -0,0 +1,809 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::storage::models::{ + EpochCredentials, IssuedCredential, RawExpirationDateSignatures, SerialNumberWrapper, + StoredBloomfilterParams, TicketProvider, VerifiedTicket, +}; +use crate::support::storage::manager::StorageManager; +use nym_coconut_dkg_common::types::EpochId; +use nym_ecash_contract_common::deposit::DepositId; +use time::{Date, OffsetDateTime}; + +#[async_trait] +pub trait EcashStorageManagerExt { + /// Gets the information about all issued partial credentials in this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, sqlx::Error>; + + /// Creates new entry for EpochCredentials for this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error>; + + /// Update the EpochCredentials by incrementing the total number of issued credentials, + /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) + /// + /// # Arguments + /// * `epoch_id`: Id of the (coconut) epoch in question. + /// * `credential_id`: (database) Id of the coconut credential that triggered the update. + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), sqlx::Error>; + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `credential_id`: (database) id of the issued credential + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, sqlx::Error>; + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `deposit_id`: id the deposit used in the issued bandwidth credential + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, sqlx::Error>; + + /// Store the provided issued credential information and return its (database) id. + async fn store_issued_credential( + &self, + epoch_id: u32, + deposit_id: DepositId, + bs58_partial_credential: String, + bs58_signature: String, + joined_private_commitments: String, + expiration_date: Date, + ) -> Result; + + /// Attempts to retrieve issued credentials from the data store using provided ids. + /// + /// # Arguments + /// + /// * `credential_ids`: (database) ids of the issued credentials + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, sqlx::Error>; + + /// Attempts to retrieve issued credentials from the data store using pagination specification. + /// + /// # Arguments + /// + /// * `start_after`: the value preceding the first retrieved result + /// * `limit`: the maximum number of entries to retrieve + async fn get_issued_credentials_paged( + &self, + start_after: i64, + limit: u32, + ) -> Result, sqlx::Error>; + + async fn insert_ticket_provider(&self, gateway_address: &str) -> Result; + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, sqlx::Error>; + + async fn insert_verified_ticket( + &self, + provider_id: i64, + spending_date: Date, + verified_at: OffsetDateTime, + ticket_data: Vec, + serial_number: Vec, + ) -> Result<(), sqlx::Error>; + + async fn get_ticket(&self, serial_number: &[u8]) + -> Result, sqlx::Error>; + + async fn get_provider_ticket_serial_numbers( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, sqlx::Error>; + + async fn get_spent_tickets_on( + &self, + date: Date, + ) -> Result, sqlx::Error>; + + async fn get_master_verification_key( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error>; + async fn insert_master_verification_key( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error>; + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_master_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error>; + async fn insert_master_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error>; + async fn insert_partial_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error>; + async fn insert_master_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn insert_double_spending_filter_params( + &self, + num_hashes: u32, + bitmap_size: u32, + sip0_key0: &[u8], + sip0_key1: &[u8], + sip1_key0: &[u8], + sip1_key1: &[u8], + ) -> Result; + + async fn get_latest_double_spending_filter_params( + &self, + ) -> Result, sqlx::Error>; + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, sqlx::Error>; + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), sqlx::Error>; + + async fn remove_old_partial_bloomfilters(&self, cutoff: Date) -> Result<(), sqlx::Error>; + + async fn remove_expired_verified_tickets(&self, cutoff: Date) -> Result<(), sqlx::Error>; +} + +#[async_trait] +impl EcashStorageManagerExt for StorageManager { + /// Gets the information about all issued partial credentials in this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + sqlx::query_as!( + EpochCredentials, + r#" + SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" + FROM epoch_credentials + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Creates new entry for EpochCredentials for this (coconut) epoch. + /// + /// # Arguments + /// + /// * `epoch_id`: Id of the (coconut) epoch in question. + async fn create_epoch_credentials_entry(&self, epoch_id: EpochId) -> Result<(), sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + sqlx::query!( + r#" + INSERT INTO epoch_credentials + (epoch_id, start_id, total_issued) + VALUES (?, ?, ?); + "#, + epoch_id_downcasted, + -1, + 0 + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + // the logic in this function can be summarised with: + // 1. get the current EpochCredentials for this epoch + // 2. if it exists -> increment `total_issued` + // 3. it has invalid (i.e. -1) `start_id` set it to the provided value + // 4. if it didn't exist, create new entry + /// Update the EpochCredentials by incrementing the total number of issued credentials, + /// and setting `start_id` if unset (i.e. this is the first credential issued this epoch) + /// + /// # Arguments + /// * `epoch_id`: Id of the (coconut) epoch in question. + /// * `credential_id`: (database) Id of the coconut credential that triggered the update. + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), sqlx::Error> { + // even if we were changing epochs every second, it's rather impossible to overflow here + // within any sane amount of time + assert!(epoch_id <= u32::MAX as u64); + let epoch_id_downcasted = epoch_id as u32; + + // make the atomic transaction in case other tasks are attempting to use the pool + let mut tx = self.connection_pool.begin().await?; + + if let Some(existing) = sqlx::query_as!( + EpochCredentials, + r#" + SELECT epoch_id as "epoch_id: u32", start_id, total_issued as "total_issued: u32" + FROM epoch_credentials + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .fetch_optional(&mut tx) + .await? + { + // the entry has existed before -> update it + if existing.total_issued == 0 { + // no credentials has been issued -> we have to set the `start_id` + sqlx::query!( + r#" + UPDATE epoch_credentials + SET total_issued = 1, start_id = ? + WHERE epoch_id = ? + "#, + credential_id, + epoch_id_downcasted + ) + .execute(&mut tx) + .await?; + } else { + // we have issued credentials in this epoch before -> just increment `total_issued` + sqlx::query!( + r#" + UPDATE epoch_credentials + SET total_issued = total_issued + 1 + WHERE epoch_id = ? + "#, + epoch_id_downcasted + ) + .execute(&mut tx) + .await?; + } + } else { + // the entry has never been created -> probably some race condition; create it instead + sqlx::query!( + r#" + INSERT INTO epoch_credentials + (epoch_id, start_id, total_issued) + VALUES (?, ?, ?); + "#, + epoch_id_downcasted, + credential_id, + 1 + ) + .execute(&mut tx) + .await?; + } + + // finally commit the transaction + tx.commit().await + } + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `credential_id`: (database) id of the issued credential + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedCredential, + r#" + SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: DepositId", bs58_partial_credential, bs58_signature,joined_private_commitments, expiration_date as "expiration_date: Date" + FROM issued_credential + WHERE id = ? + "#, + credential_id + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Attempts to retrieve an issued credential from the data store. + /// + /// # Arguments + /// + /// * `deposit_id`: id the deposit used in the issued bandwidth credential + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedCredential, + r#" + SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: u32", bs58_partial_credential, bs58_signature,joined_private_commitments, expiration_date as "expiration_date: Date" + FROM issued_credential + WHERE deposit_id = ? + "#, + deposit_id + ) + .fetch_optional(&self.connection_pool) + .await + } + + /// Store the provided issued credential information and return its (database) id. + async fn store_issued_credential( + &self, + epoch_id: u32, + deposit_id: DepositId, + bs58_partial_credential: String, + bs58_signature: String, + joined_private_commitments: String, + expiration_date: Date, + ) -> Result { + let row_id = sqlx::query!( + r#" + INSERT INTO issued_credential + (epoch_id, deposit_id, bs58_partial_credential, bs58_signature, joined_private_commitments, expiration_date) + VALUES + (?, ?, ?, ?, ?, ?) + "#, + epoch_id, deposit_id, bs58_partial_credential, bs58_signature, joined_private_commitments, expiration_date + ).execute(&self.connection_pool).await?.last_insert_rowid(); + + Ok(row_id) + } + + /// Attempts to retrieve issued credentials from the data store using provided ids. + /// + /// # Arguments + /// + /// * `credential_ids`: (database) ids of the issued credentials + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, sqlx::Error> { + // that sucks : ( + // https://stackoverflow.com/a/70032524 + let params = format!("?{}", ", ?".repeat(credential_ids.len() - 1)); + let query_str = format!("SELECT * FROM issued_credential WHERE id IN ( {params} )"); + let mut query = sqlx::query_as(&query_str); + for id in credential_ids { + query = query.bind(id) + } + + query.fetch_all(&self.connection_pool).await + } + + /// Attempts to retrieve issued credentials from the data store using pagination specification. + /// + /// # Arguments + /// + /// * `start_after`: the value preceding the first retrieved result + /// * `limit`: the maximum number of entries to retrieve + async fn get_issued_credentials_paged( + &self, + start_after: i64, + limit: u32, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + IssuedCredential, + r#" + SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: u32", bs58_partial_credential, bs58_signature,joined_private_commitments, expiration_date as "expiration_date: Date" + FROM issued_credential + WHERE id > ? + ORDER BY id + LIMIT ? + "#, + start_after, + limit + ) + .fetch_all(&self.connection_pool) + .await + } + + async fn insert_ticket_provider(&self, gateway_address: &str) -> Result { + let id = sqlx::query!( + "INSERT INTO ticket_providers(gateway_address) VALUES (?)", + gateway_address + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(id) + } + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM ticket_providers WHERE gateway_address = ?") + .bind(gateway_address) + .fetch_optional(&self.connection_pool) + .await + } + async fn insert_verified_ticket( + &self, + provider_id: i64, + spending_date: Date, + verified_at: OffsetDateTime, + ticket_data: Vec, + serial_number: Vec, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO verified_tickets(ticket_data, serial_number, spending_date, verified_at, gateway_id) + VALUES (?, ?, ?, ?, ?) + "#, + ticket_data, + serial_number, + spending_date, + verified_at, + provider_id + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + async fn get_ticket( + &self, + serial_number: &[u8], + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM verified_tickets WHERE serial_number = ?") + .bind(serial_number) + .fetch_optional(&self.connection_pool) + .await + } + + async fn get_provider_ticket_serial_numbers( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + SerialNumberWrapper, + r#" + SELECT serial_number + FROM verified_tickets + WHERE gateway_id = ? + AND verified_at > ? + ORDER BY verified_at ASC + LIMIT 65535 + "#, + provider_id, + since + ) + .fetch_all(&self.connection_pool) + .await + } + + async fn get_spent_tickets_on( + &self, + date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + SerialNumberWrapper, + r#" + SELECT serial_number + FROM verified_tickets + WHERE spending_date = ? + "#, + date + ) + .fetch_all(&self.connection_pool) + .await + } + + async fn get_master_verification_key( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_key FROM master_verification_key WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_key)) + } + + async fn insert_master_verification_key( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO master_verification_key(epoch_id, serialised_key) VALUES (?, ?)", + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_signatures FROM partial_coin_index_signatures WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_signatures)) + } + + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO partial_coin_index_signatures(epoch_id, serialised_signatures) VALUES (?, ?)", + epoch_id, + data + ).execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_master_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT serialised_signatures FROM global_coin_index_signatures WHERE epoch_id = ?", + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.serialised_signatures)) + } + + async fn insert_master_coin_index_signatures( + &self, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO global_coin_index_signatures(epoch_id, serialised_signatures) VALUES (?, ?)", + epoch_id, + data + ).execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawExpirationDateSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures + FROM partial_expiration_date_signatures + WHERE expiration_date = ? + "#, + expiration_date + ) + .fetch_optional(&self.connection_pool) + .await + } + + async fn insert_partial_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO partial_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)", + expiration_date, + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawExpirationDateSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures + FROM global_expiration_date_signatures + WHERE expiration_date = ? + "#, + expiration_date + ) + .fetch_optional(&self.connection_pool) + .await + } + + async fn insert_master_expiration_date_signatures( + &self, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO global_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)", + expiration_date, + epoch_id, + data + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn insert_double_spending_filter_params( + &self, + num_hashes: u32, + bitmap_size: u32, + sip0_key0: &[u8], + sip0_key1: &[u8], + sip1_key0: &[u8], + sip1_key1: &[u8], + ) -> Result { + let row_id = sqlx::query!( + r#" + INSERT INTO bloomfilter_parameters(num_hashes, bitmap_size,sip0_key0, sip0_key1, sip1_key0, sip1_key1) + VALUES (?, ?, ?, ?, ?, ?) + "#, + num_hashes, + bitmap_size, + sip0_key0, + sip0_key1, + sip1_key0, + sip1_key1 + ).execute(&self.connection_pool).await?.last_insert_rowid(); + Ok(row_id) + } + + async fn get_latest_double_spending_filter_params( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM bloomfilter_parameters ORDER BY id DESC LIMIT 1") + .fetch_optional(&self.connection_pool) + .await + } + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE partial_bloomfilter SET bitmap = ? WHERE date = ?", + new_bitmap, + date + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, sqlx::Error> { + sqlx::query!( + "SELECT bitmap FROM partial_bloomfilter WHERE date = ? AND parameters = ?", + date, + params_id + ) + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.bitmap)) + } + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO partial_bloomfilter(date, parameters, bitmap) VALUES (?, ?, ?)", + date, + params_id, + bitmap + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn remove_old_partial_bloomfilters(&self, cutoff: Date) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM partial_bloomfilter WHERE date > ?", cutoff) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn remove_expired_verified_tickets(&self, cutoff: Date) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM verified_tickets WHERE spending_date > ?", + cutoff + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } +} diff --git a/nym-api/src/ecash/storage/mod.rs b/nym-api/src/ecash/storage/mod.rs new file mode 100644 index 0000000000..cc7621b1ae --- /dev/null +++ b/nym-api/src/ecash/storage/mod.rs @@ -0,0 +1,635 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::helpers::IssuedExpirationDateSignatures; +use crate::ecash::storage::helpers::{ + deserialise_coin_index_signatures, deserialise_expiration_date_signatures, + serialise_coin_index_signatures, serialise_expiration_date_signatures, +}; +use crate::ecash::storage::manager::EcashStorageManagerExt; +use crate::ecash::storage::models::{ + join_attributes, EpochCredentials, IssuedCredential, SerialNumberWrapper, TicketProvider, +}; +use crate::node_status_api::models::NymApiStorageError; +use crate::support::storage::NymApiStorage; +use nym_api_requests::ecash::models::Pagination; +use nym_coconut_dkg_common::types::EpochId; +use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; +use nym_compact_ecash::BlindedSignature; +use nym_compact_ecash::{Base58, VerificationKeyAuth}; +use nym_config::defaults::BloomfilterParameters; +use nym_credentials::CredentialSpendingData; +use nym_crypto::asymmetric::identity; +use nym_ecash_contract_common::deposit::DepositId; +use nym_validator_client::nyxd::AccountId; +use time::{Date, OffsetDateTime}; + +mod helpers; +pub(crate) mod manager; +pub(crate) mod models; + +const DEFAULT_CREDENTIALS_PAGE_LIMIT: u32 = 100; + +#[async_trait] +pub trait EcashStorageExt { + async fn get_double_spending_filter_params( + &self, + ) -> Result<(i64, BloomfilterParameters), NymApiStorageError>; + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), NymApiStorageError>; + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, NymApiStorageError>; + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), NymApiStorageError>; + + async fn remove_old_partial_bloomfilters(&self, cutoff: Date) + -> Result<(), NymApiStorageError>; + + async fn remove_expired_verified_tickets(&self, cutoff: Date) + -> Result<(), NymApiStorageError>; + + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError>; + + #[allow(dead_code)] + async fn create_epoch_credentials_entry( + &self, + epoch_id: EpochId, + ) -> Result<(), NymApiStorageError>; + + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), NymApiStorageError>; + + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, NymApiStorageError>; + + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, NymApiStorageError>; + + async fn store_issued_credential( + &self, + epoch_id: u32, + deposit_id: DepositId, + partial_credential: &BlindedSignature, + signature: identity::Signature, + private_commitments: Vec, + expiration_date: Date, + ) -> Result; + + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, NymApiStorageError>; + + async fn get_issued_credentials_paged( + &self, + pagination: Pagination, + ) -> Result, NymApiStorageError>; + // + // async fn insert_credential( + // &self, + // credential: &CredentialSpendingData, + // serial_number_bs58: String, + // gateway_addr: &AccountId, + // proposal_id: u64, + // ) -> Result<(), NymApiStorageError>; + // + async fn get_credential_data( + &self, + serial_number: &[u8], + ) -> Result, NymApiStorageError>; + + async fn store_verified_ticket( + &self, + ticket_data: &CredentialSpendingData, + gateway_addr: &AccountId, + ) -> Result<(), NymApiStorageError>; + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, NymApiStorageError>; + + async fn get_verified_tickets_since( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, NymApiStorageError>; + + async fn get_all_spent_tickets_on( + &self, + date: Date, + ) -> Result, NymApiStorageError>; + + async fn get_or_create_ticket_provider_with_id( + &self, + gateway_address: &str, + ) -> Result; + + async fn get_master_verification_key( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError>; + async fn insert_master_verification_key( + &self, + epoch_id: EpochId, + key: &VerificationKeyAuth, + ) -> Result<(), NymApiStorageError>; + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError>; + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError>; + + async fn get_master_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError>; + async fn insert_master_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError>; + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError>; + async fn insert_partial_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError>; + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError>; + async fn insert_master_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError>; +} + +#[async_trait] +impl EcashStorageExt for NymApiStorage { + async fn get_double_spending_filter_params( + &self, + ) -> Result<(i64, BloomfilterParameters), NymApiStorageError> { + match self + .manager + .get_latest_double_spending_filter_params() + .await? + { + Some(raw) => Ok((raw.id, (&raw).try_into()?)), + None => { + let default = BloomfilterParameters::default_ecash(); + info!("using default bloomfilter parameters: {default:?}"); + let id = self + .manager + .insert_double_spending_filter_params( + default.num_hashes, + default.bitmap_size as u32, + &default.sip_keys[0].0.to_be_bytes(), + &default.sip_keys[0].1.to_be_bytes(), + &default.sip_keys[1].0.to_be_bytes(), + &default.sip_keys[1].1.to_be_bytes(), + ) + .await?; + Ok((id, default)) + } + } + } + + async fn update_archived_partial_bloomfilter( + &self, + date: Date, + new_bitmap: &[u8], + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .update_archived_partial_bloomfilter(date, new_bitmap) + .await?) + } + + async fn try_load_partial_bloomfilter_bitmap( + &self, + date: Date, + params_id: i64, + ) -> Result>, NymApiStorageError> { + Ok(self + .manager + .try_load_partial_bloomfilter_bitmap(date, params_id) + .await?) + } + + async fn insert_partial_bloomfilter( + &self, + date: Date, + params_id: i64, + bitmap: &[u8], + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .insert_partial_bloomfilter(date, params_id, bitmap) + .await?) + } + + async fn remove_old_partial_bloomfilters( + &self, + cutoff: Date, + ) -> Result<(), NymApiStorageError> { + Ok(self.manager.remove_old_partial_bloomfilters(cutoff).await?) + } + + async fn remove_expired_verified_tickets( + &self, + cutoff: Date, + ) -> Result<(), NymApiStorageError> { + Ok(self.manager.remove_expired_verified_tickets(cutoff).await?) + } + + async fn get_epoch_credentials( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_epoch_credentials(epoch_id).await?) + } + + async fn create_epoch_credentials_entry( + &self, + epoch_id: EpochId, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .create_epoch_credentials_entry(epoch_id) + .await?) + } + + async fn update_epoch_credentials_entry( + &self, + epoch_id: EpochId, + credential_id: i64, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .update_epoch_credentials_entry(epoch_id, credential_id) + .await?) + } + + async fn get_issued_credential( + &self, + credential_id: i64, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_issued_credential(credential_id).await?) + } + + async fn get_issued_bandwidth_credential_by_deposit_id( + &self, + deposit_id: DepositId, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_issued_bandwidth_credential_by_deposit_id(deposit_id) + .await?) + } + + async fn store_issued_credential( + &self, + epoch_id: u32, + deposit_id: DepositId, + partial_credential: &BlindedSignature, + signature: identity::Signature, + private_commitments: Vec, + expiration_date: Date, + ) -> Result { + Ok(self + .manager + .store_issued_credential( + epoch_id, + deposit_id, + partial_credential.to_bs58(), + signature.to_base58_string(), + join_attributes(private_commitments), + expiration_date, + ) + .await?) + } + + async fn get_issued_credentials( + &self, + credential_ids: Vec, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_issued_credentials(credential_ids).await?) + } + + async fn get_issued_credentials_paged( + &self, + pagination: Pagination, + ) -> Result, NymApiStorageError> { + // rows start at 1 + let start_after = pagination.last_key.unwrap_or(0); + let limit = match pagination.limit { + Some(v) => { + if v == 0 || v > DEFAULT_CREDENTIALS_PAGE_LIMIT { + DEFAULT_CREDENTIALS_PAGE_LIMIT + } else { + v + } + } + None => DEFAULT_CREDENTIALS_PAGE_LIMIT, + }; + + Ok(self + .manager + .get_issued_credentials_paged(start_after, limit) + .await?) + } + + // async fn insert_credential( + // &self, + // credential: &CredentialSpendingData, + // serial_number_bs58: String, + // gateway_addr: &AccountId, + // proposal_id: u64, + // ) -> Result<(), NymApiStorageError> { + // self.manager + // .insert_credential( + // credential.to_bs58(), + // serial_number_bs58, + // gateway_addr.to_string(), + // proposal_id as i64, + // ) + // .await + // .map_err(|err| err.into()) + // } + + async fn get_credential_data( + &self, + serial_number: &[u8], + ) -> Result, NymApiStorageError> { + let ticket = self.manager.get_ticket(serial_number).await?; + ticket + .map(|ticket| { + CredentialSpendingData::try_from_bytes(&ticket.ticket_data).map_err(|_| { + NymApiStorageError::DatabaseInconsistency { + reason: "impossible to deserialize verified ticket".to_string(), + } + }) + }) + .transpose() + } + + async fn store_verified_ticket( + &self, + ticket_data: &CredentialSpendingData, + gateway_addr: &AccountId, + ) -> Result<(), NymApiStorageError> { + let provider_id = self + .get_or_create_ticket_provider_with_id(gateway_addr.as_ref()) + .await?; + + let now = OffsetDateTime::now_utc(); + + let ticket_bytes = ticket_data.to_bytes(); + let encoded_serial_number = ticket_data.encoded_serial_number(); + self.manager + .insert_verified_ticket( + provider_id, + ticket_data.spend_date, + now, + ticket_bytes, + encoded_serial_number, + ) + .await + .map_err(Into::into) + } + + async fn get_ticket_provider( + &self, + gateway_address: &str, + ) -> Result, NymApiStorageError> { + self.manager + .get_ticket_provider(gateway_address) + .await + .map_err(Into::into) + } + + async fn get_verified_tickets_since( + &self, + provider_id: i64, + since: OffsetDateTime, + ) -> Result, NymApiStorageError> { + self.manager + .get_provider_ticket_serial_numbers(provider_id, since) + .await + .map_err(Into::into) + } + + async fn get_all_spent_tickets_on( + &self, + date: Date, + ) -> Result, NymApiStorageError> { + self.manager + .get_spent_tickets_on(date) + .await + .map_err(Into::into) + } + + async fn get_or_create_ticket_provider_with_id( + &self, + gateway_address: &str, + ) -> Result { + if let Some(provider) = self.get_ticket_provider(gateway_address).await? { + Ok(provider.id) + } else { + Ok(self.manager.insert_ticket_provider(gateway_address).await?) + } + } + + async fn get_master_verification_key( + &self, + epoch_id: EpochId, + ) -> Result, NymApiStorageError> { + let Some(raw) = self + .manager + .get_master_verification_key(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + let master_vk = VerificationKeyAuth::from_bytes(&raw).map_err(|_| { + NymApiStorageError::database_inconsistency("malformed stored master verification key") + })?; + + Ok(Some(master_vk)) + } + + async fn insert_master_verification_key( + &self, + epoch_id: EpochId, + key: &VerificationKeyAuth, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .insert_master_verification_key(epoch_id as i64, &key.to_bytes()) + .await?) + } + + async fn get_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError> { + let Some(raw) = self + .manager + .get_partial_coin_index_signatures(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_coin_index_signatures(&raw)?)) + } + + async fn insert_partial_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_partial_coin_index_signatures( + epoch_id as i64, + &serialise_coin_index_signatures(sigs), + ) + .await?; + Ok(()) + } + + async fn get_master_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result>, NymApiStorageError> { + let Some(raw) = self + .manager + .get_master_coin_index_signatures(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + Ok(Some(deserialise_coin_index_signatures(&raw)?)) + } + + async fn insert_master_coin_index_signatures( + &self, + epoch_id: EpochId, + sigs: &[AnnotatedCoinIndexSignature], + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_master_coin_index_signatures( + epoch_id as i64, + &serialise_coin_index_signatures(sigs), + ) + .await?; + Ok(()) + } + + async fn get_partial_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError> { + let Some(raw) = self + .manager + .get_partial_expiration_date_signatures(expiration_date) + .await? + else { + return Ok(None); + }; + + let signatures = deserialise_expiration_date_signatures(&raw.serialised_signatures)?; + + Ok(Some(IssuedExpirationDateSignatures { + epoch_id: raw.epoch_id as u64, + signatures, + })) + } + + async fn insert_partial_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_partial_expiration_date_signatures( + sigs.epoch_id as i64, + expiration_date, + &serialise_expiration_date_signatures(&sigs.signatures), + ) + .await?; + Ok(()) + } + + async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, NymApiStorageError> { + let Some(raw) = self + .manager + .get_master_expiration_date_signatures(expiration_date) + .await? + else { + return Ok(None); + }; + + let signatures = deserialise_expiration_date_signatures(&raw.serialised_signatures)?; + + Ok(Some(IssuedExpirationDateSignatures { + epoch_id: raw.epoch_id as u64, + signatures, + })) + } + + async fn insert_master_expiration_date_signatures( + &self, + expiration_date: Date, + sigs: &IssuedExpirationDateSignatures, + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_master_expiration_date_signatures( + sigs.epoch_id as i64, + expiration_date, + &serialise_expiration_date_signatures(&sigs.signatures), + ) + .await?; + Ok(()) + } +} diff --git a/nym-api/src/ecash/storage/models.rs b/nym-api/src/ecash/storage/models.rs new file mode 100644 index 0000000000..c73d759b4e --- /dev/null +++ b/nym-api/src/ecash/storage/models.rs @@ -0,0 +1,198 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::error::EcashError; +use crate::node_status_api::models::NymApiStorageError; +use nym_api_requests::ecash::models::{ + EpochCredentialsResponse, IssuedCredential as ApiIssuedCredential, + IssuedCredentialBody as ApiIssuedCredentialInner, +}; +use nym_api_requests::ecash::BlindedSignatureResponse; +use nym_compact_ecash::{Base58, BlindedSignature}; +use nym_config::defaults::BloomfilterParameters; +use nym_ecash_contract_common::deposit::DepositId; +use sqlx::FromRow; +use std::fmt::Display; +use std::ops::Deref; +use time::{Date, OffsetDateTime}; + +pub struct EpochCredentials { + pub epoch_id: u32, + pub start_id: i64, + pub total_issued: u32, +} + +impl From for EpochCredentialsResponse { + fn from(value: EpochCredentials) -> Self { + let first_epoch_credential_id = if value.start_id == -1 { + None + } else { + Some(value.start_id) + }; + + EpochCredentialsResponse { + epoch_id: value.epoch_id as u64, + first_epoch_credential_id, + total_issued: value.total_issued, + } + } +} + +#[derive(FromRow)] +#[allow(unused)] +pub struct TicketProvider { + pub(crate) id: i64, + pub(crate) gateway_address: String, + pub(crate) last_batch_verification: Option, +} + +#[derive(FromRow)] +pub struct SerialNumberWrapper { + pub serial_number: Vec, +} + +impl Deref for SerialNumberWrapper { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.serial_number + } +} + +#[derive(FromRow)] +#[allow(unused)] +pub struct VerifiedTicket { + pub(crate) id: i64, + pub(crate) ticket_data: Vec, + pub(crate) serial_number: Vec, + pub(crate) spending_date: Date, + pub(crate) verified_at: OffsetDateTime, + pub(crate) gateway_id: i64, +} + +#[derive(FromRow)] +pub struct IssuedCredential { + pub id: i64, + pub epoch_id: u32, + pub deposit_id: DepositId, + + /// base58-encoded issued credential + pub bs58_partial_credential: String, + + /// base58-encoded signature on the issued credential (and the attributes) + pub bs58_signature: String, + + // i.e. "'attr1','attr2',..." + pub joined_private_commitments: String, + + pub expiration_date: Date, +} + +#[derive(FromRow)] +pub struct RawExpirationDateSignatures { + pub epoch_id: u32, + pub serialised_signatures: Vec, +} + +#[derive(FromRow)] +pub(crate) struct StoredBloomfilterParams { + pub(crate) id: i64, + pub(crate) num_hashes: u32, + pub(crate) bitmap_size: u32, + + pub(crate) sip0_key0: Vec, + pub(crate) sip0_key1: Vec, + + pub(crate) sip1_key0: Vec, + pub(crate) sip1_key1: Vec, +} + +impl<'a> TryFrom<&'a StoredBloomfilterParams> for BloomfilterParameters { + type Error = NymApiStorageError; + fn try_from(value: &'a StoredBloomfilterParams) -> Result { + let Ok(sip0_key0) = <[u8; 8]>::try_from(value.sip0_key0.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip0 key0", + )); + }; + let Ok(sip0_key1) = <[u8; 8]>::try_from(value.sip0_key1.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip0 key1", + )); + }; + let Ok(sip1_key0) = <[u8; 8]>::try_from(value.sip1_key0.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip1 key0", + )); + }; + let Ok(sip1_key1) = <[u8; 8]>::try_from(value.sip1_key1.as_ref()) else { + return Err(NymApiStorageError::database_inconsistency( + "malformed sip1 key1", + )); + }; + Ok(BloomfilterParameters { + num_hashes: value.num_hashes, + bitmap_size: value.bitmap_size as u64, + sip_keys: [ + (u64::from_be_bytes(sip0_key0), u64::from_be_bytes(sip0_key1)), + (u64::from_be_bytes(sip1_key0), u64::from_be_bytes(sip1_key1)), + ], + }) + } +} + +impl TryFrom for ApiIssuedCredentialInner { + type Error = EcashError; + + fn try_from(value: IssuedCredential) -> Result { + Ok(ApiIssuedCredentialInner { + credential: ApiIssuedCredential { + id: value.id, + epoch_id: value.epoch_id, + deposit_id: value.deposit_id, + blinded_partial_credential: BlindedSignature::try_from_bs58( + value.bs58_partial_credential, + )?, + bs58_encoded_private_attributes_commitments: split_attributes( + &value.joined_private_commitments, + ), + expiration_date: value.expiration_date, + }, + signature: value.bs58_signature.parse()?, + }) + } +} + +impl TryFrom for BlindedSignatureResponse { + type Error = EcashError; + + fn try_from(value: IssuedCredential) -> Result { + Ok(BlindedSignatureResponse { + blinded_signature: BlindedSignature::try_from_bs58(value.bs58_partial_credential)?, + }) + } +} + +impl TryFrom for BlindedSignature { + type Error = EcashError; + + fn try_from(value: IssuedCredential) -> Result { + Ok(BlindedSignature::try_from_bs58( + value.bs58_partial_credential, + )?) + } +} + +pub fn join_attributes(attrs: I) -> String +where + I: IntoIterator, + M: Display, +{ + // I could have used `attrs.into_iter().join(",")`, + // but my IDE didn't like it (compiler was fine) + itertools::Itertools::join(&mut attrs.into_iter(), ",") +} + +pub fn split_attributes(attrs: &str) -> Vec { + attrs.split(',').map(|s| s.to_string()).collect() +} diff --git a/nym-api/src/coconut/tests/fixtures.rs b/nym-api/src/ecash/tests/fixtures.rs similarity index 94% rename from nym-api/src/coconut/tests/fixtures.rs rename to nym-api/src/ecash/tests/fixtures.rs index 38e964f395..72e0a3ee92 100644 --- a/nym-api/src/coconut/tests/fixtures.rs +++ b/nym-api/src/ecash/tests/fixtures.rs @@ -1,17 +1,17 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg; -use crate::coconut::dkg::client::DkgClient; -use crate::coconut::dkg::controller::keys::persist_coconut_keypair; -use crate::coconut::dkg::controller::DkgController; -use crate::coconut::dkg::state::State; -use crate::coconut::keys::KeyPair; -use crate::coconut::tests::{DummyClient, SharedFakeChain}; +use crate::ecash::dkg; +use crate::ecash::dkg::client::DkgClient; +use crate::ecash::dkg::controller::keys::persist_coconut_keypair; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::dkg::state::State; +use crate::ecash::keys::KeyPair; +use crate::ecash::tests::{DummyClient, SharedFakeChain}; use cosmwasm_std::Addr; -use nym_coconut::VerificationKey; use nym_coconut_dkg_common::dealer::DealerRegistrationDetails; use nym_coconut_dkg_common::types::{DealerDetails, EpochId}; +use nym_compact_ecash::VerificationKeyAuth; use nym_crypto::asymmetric::identity; use nym_dkg::bte::keys::KeyPair as DkgKeyPair; use nym_dkg::{NodeIndex, Threshold}; @@ -170,7 +170,11 @@ impl TestingDkgControllerBuilder { state_guard.dkg_contract.epoch.epoch_id = epoch_id; } if let Some(threshold) = self.threshold { - state_guard.dkg_contract.threshold = Some(threshold) + let epoch_id = state_guard.dkg_contract.epoch.epoch_id; + state_guard + .dkg_contract + .threshold + .insert(epoch_id, threshold); } let epoch_id = state_guard.dkg_contract.epoch.epoch_id; @@ -266,7 +270,7 @@ impl TestingDkgController { Addr::unchecked(self.address().await.as_ref()) } - pub(crate) async fn unchecked_coconut_vk(&self) -> VerificationKey { + pub(crate) async fn unchecked_coconut_vk(&self) -> VerificationKeyAuth { self.state .unchecked_coconut_keypair() .await diff --git a/nym-api/src/coconut/tests/helpers.rs b/nym-api/src/ecash/tests/helpers.rs similarity index 96% rename from nym-api/src/coconut/tests/helpers.rs rename to nym-api/src/ecash/tests/helpers.rs index a92a448502..5cd410056f 100644 --- a/nym-api/src/coconut/tests/helpers.rs +++ b/nym-api/src/ecash/tests/helpers.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::tests::fixtures::{TestingDkgController, TestingDkgControllerBuilder}; -use crate::coconut::tests::SharedFakeChain; +use crate::ecash::tests::fixtures::{TestingDkgController, TestingDkgControllerBuilder}; +use crate::ecash::tests::SharedFakeChain; use nym_coconut_dkg_common::types::EpochState; use nym_dkg::bte::PublicKeyWithProof; @@ -80,7 +80,7 @@ pub(crate) async fn submit_public_keys(controllers: &mut [TestingDkgController], let mut guard = controllers[0].chain_state.lock().unwrap(); guard.dkg_contract.epoch.state = EpochState::DealingExchange { resharing }; - guard.dkg_contract.threshold = Some(threshold) + guard.dkg_contract.threshold.insert(epoch, threshold); } pub(crate) async fn exchange_dealings(controllers: &mut [TestingDkgController], resharing: bool) { diff --git a/nym-api/src/coconut/tests/issued_credentials.rs b/nym-api/src/ecash/tests/issued_credentials.rs similarity index 81% rename from nym-api/src/coconut/tests/issued_credentials.rs rename to nym-api/src/ecash/tests/issued_credentials.rs index fadfdad4e8..2dfbf1c473 100644 --- a/nym-api/src/coconut/tests/issued_credentials.rs +++ b/nym-api/src/ecash/tests/issued_credentials.rs @@ -1,21 +1,20 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::tests::{voucher_fixture, TestFixture}; -use cosmwasm_std::coin; -use nym_api_requests::coconut::models::{ +use crate::ecash::tests::{voucher_fixture, TestFixture}; +use nym_api_requests::ecash::models::{ EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, Pagination, }; -use nym_api_requests::coconut::CredentialsRequestBody; -use nym_validator_client::nym_api::routes::{API_VERSION, BANDWIDTH, COCONUT_ROUTES}; +use nym_api_requests::ecash::CredentialsRequestBody; +use nym_validator_client::nym_api::routes::{API_VERSION, ECASH_ROUTES}; use rocket::http::Status; use std::collections::BTreeMap; #[tokio::test] async fn epoch_credentials() { - let route_epoch1 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/1"); - let route_epoch2 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/2"); - let route_epoch42 = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/epoch-credentials/42"); + let route_epoch1 = format!("/{API_VERSION}/{ECASH_ROUTES}/epoch-credentials/1"); + let route_epoch2 = format!("/{API_VERSION}/{ECASH_ROUTES}/epoch-credentials/2"); + let route_epoch42 = format!("/{API_VERSION}/{ECASH_ROUTES}/epoch-credentials/42"); let test_fixture = TestFixture::new().await; @@ -94,27 +93,25 @@ async fn epoch_credentials() { #[tokio::test] async fn issued_credential() { fn route(id: i64) -> String { - format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credential/{id}") + format!("/{API_VERSION}/{ECASH_ROUTES}/issued-credential/{id}") } // let test_fixture = TestFixture::new() - let hash1 = "6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E".to_string(); - let hash2 = "9F4DF28B36189B4410BC23D97FD757FC74B919122E80534CC2CA6F3D646F6518".to_string(); + let deposit_id1 = 123; + let deposit_id2 = 321; - let voucher1 = voucher_fixture(coin(1234, "unym"), Some(hash1.clone())); - let voucher2 = voucher_fixture(coin(1234, "unym"), Some(hash2.clone())); + let voucher1 = voucher_fixture(Some(deposit_id1)); + let voucher2 = voucher_fixture(Some(deposit_id2)); let signing_data1 = voucher1.prepare_for_signing(); - let voucher_data1 = voucher1.get_variant_data().voucher_data().unwrap(); - let request1 = voucher_data1.create_blind_sign_request_body(&signing_data1); + let request1 = voucher1.create_blind_sign_request_body(&signing_data1); let signing_data2 = voucher2.prepare_for_signing(); - let voucher_data2 = voucher2.get_variant_data().voucher_data().unwrap(); - let request2 = voucher_data2.create_blind_sign_request_body(&signing_data2); + let request2 = voucher2.create_blind_sign_request_body(&signing_data2); let test_fixture = TestFixture::new().await; - test_fixture.add_deposit_tx(voucher_data1); - test_fixture.add_deposit_tx(voucher_data2); + test_fixture.add_deposit(&voucher1); + test_fixture.add_deposit(&voucher2); // random credential that was never issued let response = test_fixture.rocket.get(route(42)).dispatch().await; @@ -143,11 +140,12 @@ async fn issued_credential() { // TODO: currently we have no signature checks assert_eq!(1, issued1.credential.id); assert_eq!(1, issued1.credential.epoch_id); - assert_eq!(voucher_data1.tx_hash(), issued1.credential.tx_hash); + assert_eq!(voucher1.deposit_id(), issued1.credential.deposit_id); assert_eq!( cred1.to_bytes(), issued1.credential.blinded_partial_credential.to_bytes() ); + assert_eq!( request1.encode_commitments(), issued1 @@ -155,17 +153,18 @@ async fn issued_credential() { .bs58_encoded_private_attributes_commitments ); assert_eq!( - voucher1.get_plain_public_attributes(), - issued1.credential.public_attributes + voucher1.expiration_date(), + issued1.credential.expiration_date ); assert_eq!(2, issued2.credential.id); assert_eq!(3, issued2.credential.epoch_id); - assert_eq!(voucher_data2.tx_hash(), issued2.credential.tx_hash); + assert_eq!(voucher2.deposit_id(), issued2.credential.deposit_id); assert_eq!( cred2.to_bytes(), issued2.credential.blinded_partial_credential.to_bytes() ); + assert_eq!( request2.encode_commitments(), issued2 @@ -173,14 +172,14 @@ async fn issued_credential() { .bs58_encoded_private_attributes_commitments ); assert_eq!( - voucher2.get_plain_public_attributes(), - issued2.credential.public_attributes + voucher2.expiration_date(), + issued2.credential.expiration_date ); } #[tokio::test] async fn issued_credentials() { - let route = format!("/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credentials"); + let route = format!("/{API_VERSION}/{ECASH_ROUTES}/issued-credentials"); let test_fixture = TestFixture::new().await; diff --git a/nym-api/src/coconut/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs similarity index 59% rename from nym-api/src/coconut/tests/mod.rs rename to nym-api/src/ecash/tests/mod.rs index 43f3de399e..6184ac7899 100644 --- a/nym-api/src/coconut/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1,26 +1,21 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::{CoconutError, Result}; -use crate::coconut::keys::KeyPairWithEpoch; -use crate::coconut::state::State; -use crate::coconut::storage::CoconutStorageExt; +use crate::ecash; +use crate::ecash::error::{EcashError, Result}; +use crate::ecash::keys::KeyPairWithEpoch; +use crate::ecash::state::EcashState; +use crate::ecash::storage::EcashStorageExt; use crate::support::storage::NymApiStorage; use async_trait::async_trait; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{ - coin, from_binary, to_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, MessageInfo, WasmMsg, + from_binary, to_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, MessageInfo, WasmMsg, }; use cw3::{Proposal, ProposalResponse, Vote, VoteInfo, VoteResponse, Votes}; use cw4::{Cw4Contract, MemberResponse}; -use nym_api_requests::coconut::models::{IssuedCredentialBody, IssuedCredentialResponse}; -use nym_api_requests::coconut::{BlindSignRequestBody, BlindedSignatureResponse}; -use nym_coconut::{BlindedSignature, Parameters, VerificationKey}; -use nym_coconut_bandwidth_contract_common::events::{ - DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO, - DEPOSIT_VALUE, -}; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; +use nym_api_requests::ecash::models::{IssuedCredentialBody, IssuedCredentialResponse}; +use nym_api_requests::ecash::{BlindSignRequestBody, BlindedSignatureResponse}; use nym_coconut_dkg_common::dealer::{ DealerDetails, DealerDetailsResponse, DealerType, RegisteredDealerDetails, }; @@ -34,21 +29,19 @@ use nym_coconut_dkg_common::types::{ EpochId, EpochState, PartialContractDealingData, State as ContractState, }; use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; +use nym_compact_ecash::BlindedSignature; +use nym_compact_ecash::{ttp_keygen, VerificationKeyAuth}; use nym_contracts_common::IdentityKey; -use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuanceData; -use nym_credentials::coconut::bandwidth::CredentialType; -use nym_credentials::IssuanceBandwidthCredential; -use nym_crypto::asymmetric::{encryption, identity}; +use nym_credentials::IssuanceTicketBook; +use nym_crypto::asymmetric::identity; use nym_dkg::{NodeIndex, Threshold}; -use nym_validator_client::nym_api::routes::{ - API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_ROUTES, -}; +use nym_ecash_contract_common::blacklist::{BlacklistedAccountResponse, Blacklisting}; +use nym_ecash_contract_common::deposit::{Deposit, DepositId, DepositResponse}; +use nym_validator_client::nym_api::routes::{API_VERSION, ECASH_BLIND_SIGN, ECASH_ROUTES}; use nym_validator_client::nyxd::cosmwasm_client::logs::Log; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; -use nym_validator_client::nyxd::Coin; -use nym_validator_client::nyxd::{ - AccountId, Algorithm, Event, EventAttribute, ExecTxResult, Fee, Hash, TxResponse, -}; +use nym_validator_client::nyxd::{AccountId, ExecTxResult, Fee, Hash, TxResponse}; +use nym_validator_client::{EcashApiClient, NymApiClient}; use rand::rngs::OsRng; use rand::RngCore; use rocket::http::Status; @@ -59,6 +52,7 @@ use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use tempfile::{tempdir, TempDir}; +use tokio::sync::RwLock; pub(crate) mod fixtures; pub(crate) mod helpers; @@ -72,6 +66,9 @@ struct InternalCounters { node_index_counter: NodeIndex, tx_hash_counter: u64, proposal_id_counter: u64, + + #[allow(dead_code)] + deposit_id_counter: u32, } impl InternalCounters { @@ -92,6 +89,12 @@ impl InternalCounters { self.tx_hash_counter += 1; Hash::Sha256(sha2::Sha256::digest(&self.tx_hash_counter.to_be_bytes()).into()) } + + #[allow(dead_code)] + fn next_deposit_id(&mut self) -> DepositId { + self.deposit_id_counter += 1; + self.deposit_id_counter + } } #[derive(Debug)] @@ -151,7 +154,7 @@ pub(crate) struct FakeDkgContractState { pub(crate) epoch: Epoch, pub(crate) contract_state: ContractState, - pub(crate) threshold: Option, + pub(crate) threshold: HashMap, } impl FakeDkgContractState { @@ -168,9 +171,7 @@ impl FakeDkgContractState { // .collect() // } - fn reset_dkg_state(&mut self) { - self.threshold = None; - } + fn reset_dkg_state(&mut self) {} pub(crate) fn reset_epoch_in_reshare_mode(&mut self) { self.reset_dkg_state(); @@ -267,16 +268,17 @@ pub(crate) struct FakeMultisigContractState { } impl FakeMultisigContractState { + #[allow(dead_code)] pub(crate) fn reset_votes(&mut self) { self.votes = HashMap::new() } } #[derive(Debug)] -pub(crate) struct FakeBandwidthContractState { +pub(crate) struct FakeEcashContractState { pub(crate) address: Addr, - pub(crate) admin: Option, - pub(crate) spent_credentials: HashMap, + pub(crate) deposits: HashMap, + pub(crate) blacklist: HashMap, } #[derive(Debug, Clone, Default)] @@ -300,7 +302,7 @@ pub(crate) struct FakeChainState { pub(crate) dkg_contract: FakeDkgContractState, pub(crate) group_contract: FakeGroupContractState, pub(crate) multisig_contract: FakeMultisigContractState, - pub(crate) bandwidth_contract: FakeBandwidthContractState, + pub(crate) ecash_contract: FakeEcashContractState, } impl Default for FakeChainState { @@ -311,14 +313,9 @@ impl Default for FakeChainState { Addr::unchecked("n1pd7kfgvr5tpcv0xnlv46c4jsq9jg2r799xxrcwqdm4l2jhq2pjwqrmz5ju"); let dkg_contract = Addr::unchecked("n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a"); - let bandwidth_contract = + let ecash_contract = Addr::unchecked("n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l"); - let bandwidth_contract_admin = - "n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a" - .parse() - .unwrap(); - FakeChainState { _counters: Default::default(), @@ -339,7 +336,7 @@ impl Default for FakeChainState { }, dealings: HashMap::new(), verification_shares: HashMap::new(), - threshold: None, + threshold: HashMap::new(), }, group_contract: FakeGroupContractState { address: group_contract, @@ -350,10 +347,10 @@ impl Default for FakeChainState { proposals: Default::default(), votes: Default::default(), }, - bandwidth_contract: FakeBandwidthContractState { - address: bandwidth_contract, - admin: Some(bandwidth_contract_admin), - spent_credentials: Default::default(), + ecash_contract: FakeEcashContractState { + address: ecash_contract, + deposits: Default::default(), + blacklist: Default::default(), }, } } @@ -380,6 +377,7 @@ impl FakeChainState { self.group_contract.add_member(address, weight) } + #[allow(dead_code)] pub(crate) fn reset_votes(&mut self) { self.multisig_contract.reset_votes() } @@ -429,7 +427,7 @@ impl FakeChainState { if contract == &self.multisig_contract.address { unimplemented!("multisig contract exec") } - if contract == &self.bandwidth_contract.address { + if contract == &self.ecash_contract.address { unimplemented!("bandwidth contract exec") } if contract == self.dkg_contract.address.as_ref() { @@ -504,109 +502,6 @@ impl DummyClient { pub fn chain_state(&self) -> SharedFakeChain { self.state.clone() } - - // pub fn with_tx_db(mut self, tx_db: &Arc>>) -> Self { - // todo!() - // // self.tx_db = Arc::clone(tx_db); - // // self - // } - // - // pub fn with_proposal_db( - // mut self, - // proposal_db: &Arc>>, - // ) -> Self { - // todo!() - // // self.proposal_db = Arc::clone(proposal_db); - // // self - // } - // - // pub fn with_spent_credential_db( - // mut self, - // spent_credential_db: &Arc>>, - // ) -> Self { - // todo!() - // // self.spent_credential_db = Arc::clone(spent_credential_db); - // // self - // } - // - // pub fn _with_epoch(mut self, epoch: &Arc>) -> Self { - // todo!() - // // self.epoch = Arc::clone(epoch); - // // self - // } - // - // pub fn with_dealer_details( - // mut self, - // dealer_details: &Arc>>, - // ) -> Self { - // todo!() - // // self.dealer_details = Arc::clone(dealer_details); - // // self - // } - // - // pub fn with_threshold(mut self, threshold: &Arc>>) -> Self { - // todo!() - // // self.threshold = Arc::clone(threshold); - // // self - // } - // - // // it's a really bad practice, but I'm not going to be changing it now... - // #[allow(clippy::type_complexity)] - // pub fn with_dealings( - // mut self, - // dealings: &Arc>>>>, - // ) -> Self { - // todo!() - // // self.dealings = Arc::clone(dealings); - // // self - // } - // - // pub fn with_verification_share( - // mut self, - // verification_share: &Arc>>, - // ) -> Self { - // todo!() - // // self.verification_share = Arc::clone(verification_share); - // // self - // } - // - // pub fn _with_group_db( - // mut self, - // group_db: &Arc>>, - // ) -> Self { - // todo!() - // // self.group_db = Arc::clone(group_db); - // // self - // } - // - // pub fn with_initial_dealers_db( - // mut self, - // initial_dealers: &Arc>>, - // ) -> Self { - // todo!() - // // self.initial_dealers_db = Arc::clone(initial_dealers); - // // self - // } - - // async fn get_dealer_by_address(&self, address: &str) -> Option { - // let guard = self.state.lock().unwrap(); - // for dealer in guard.dkg_contract.dealers.values() { - // if dealer.address.as_str() == address { - // return Some(dealer.clone()); - // } - // } - // None - // } - // - // async fn get_past_dealer_by_address(&self, address: &str) -> Option { - // let guard = self.state.lock().unwrap(); - // for dealer in guard.dkg_contract.past_dealers.values() { - // if dealer.address.as_str() == address { - // return Some(dealer.clone()); - // } - // } - // None - // } } #[async_trait] @@ -619,19 +514,20 @@ impl super::client::Client for DummyClient { Ok(self.state.lock().unwrap().dkg_contract.address.clone()) } - async fn bandwidth_contract_admin(&self) -> Result> { - Ok(self.state.lock().unwrap().bandwidth_contract.admin.clone()) - } - - async fn get_tx(&self, tx_hash: Hash) -> Result { - Ok(self + async fn get_deposit(&self, deposit_id: DepositId) -> Result { + let deposit = self .state .lock() .unwrap() - .txs - .get(&tx_hash) - .cloned() - .unwrap()) + .ecash_contract + .deposits + .get(&deposit_id) + .cloned(); + + Ok(DepositResponse { + id: deposit_id, + deposit, + }) } async fn get_proposal(&self, proposal_id: u64) -> Result { @@ -641,7 +537,7 @@ impl super::client::Client for DummyClient { .proposals .get(&proposal_id) .cloned() - .ok_or(CoconutError::IncorrectProposal { + .ok_or(EcashError::IncorrectProposal { reason: String::from("proposal not found"), })?; @@ -678,20 +574,19 @@ impl super::client::Client for DummyClient { Ok(VoteResponse { vote }) } - async fn get_spent_credential( + async fn get_blacklisted_account( &self, - blinded_serial_number: String, - ) -> Result { - self.state - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .get(&blinded_serial_number) - .cloned() - .ok_or(CoconutError::InvalidCredentialStatus { - status: String::from("spent credential not found"), - }) + public_key: String, + ) -> Result { + Ok(BlacklistedAccountResponse::new( + self.state + .lock() + .unwrap() + .ecash_contract + .blacklist + .get(&public_key) + .cloned(), + )) } async fn contract_state(&self) -> Result { @@ -721,7 +616,20 @@ impl super::client::Client for DummyClient { } async fn get_current_epoch_threshold(&self) -> Result> { - Ok(self.state.lock().unwrap().dkg_contract.threshold) + let guard = self.state.lock().unwrap(); + let current_epoch = guard.dkg_contract.epoch.epoch_id; + Ok(guard.dkg_contract.threshold.get(¤t_epoch).cloned()) + } + + async fn get_epoch_threshold(&self, epoch_id: EpochId) -> Result> { + Ok(self + .state + .lock() + .unwrap() + .dkg_contract + .threshold + .get(&epoch_id) + .cloned()) } async fn get_self_registered_dealer_details(&self) -> Result { @@ -915,6 +823,15 @@ impl super::client::Client for DummyClient { } } + async fn get_registered_ecash_clients(&self, epoch_id: EpochId) -> Result> { + Ok(self + .get_verification_key_shares(epoch_id) + .await? + .into_iter() + .map(|s| s.try_into().unwrap()) + .collect()) + } + async fn vote_proposal( &self, proposal_id: u64, @@ -924,7 +841,7 @@ impl super::client::Client for DummyClient { let voter = self.validator_address.to_string(); let mut chain = self.state.lock().unwrap(); if !chain.multisig_contract.proposals.contains_key(&proposal_id) { - return Err(CoconutError::IncorrectProposal { + return Err(EcashError::IncorrectProposal { reason: String::from("proposal not found"), }); } @@ -970,7 +887,7 @@ impl super::client::Client for DummyClient { let multisig_address: AccountId = chain.multisig_contract.address.as_str().parse().unwrap(); let Some(proposal) = chain.multisig_contract.proposals.get_mut(&proposal_id) else { - return Err(CoconutError::ProposalIdError { + return Err(EcashError::ProposalIdError { reason: String::from("proposal id not found"), }); }; @@ -1034,8 +951,8 @@ impl super::client::Client for DummyClient { events: vec![cosmwasm_std::Event::new("wasm") .add_attribute(NODE_INDEX, assigned_index.to_string())], }], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) @@ -1068,8 +985,8 @@ impl super::client::Client for DummyClient { Ok(ExecuteResult { logs: vec![], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) @@ -1103,8 +1020,8 @@ impl super::client::Client for DummyClient { Ok(ExecuteResult { logs: vec![], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) @@ -1121,7 +1038,7 @@ impl super::client::Client for DummyClient { let epoch_id = chain.dkg_contract.epoch.epoch_id; let Some(dealer_details) = chain.dkg_contract.get_dealer_details(&address, epoch_id) else { // Just throw some error, not really the correct one - return Err(CoconutError::DepositEncrKeyNotFound); + return Err(EcashError::DepositInfoNotFound); }; let dkg_contract = chain.dkg_contract.address.clone(); @@ -1180,28 +1097,43 @@ impl super::client::Client for DummyClient { events: vec![cosmwasm_std::Event::new("wasm") .add_attribute(DKG_PROPOSAL_ID, proposal_id.to_string())], }], + msg_responses: Default::default(), events: Default::default(), - data: Default::default(), transaction_hash, gas_info: Default::default(), }) } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct DummyCommunicationChannel { current_epoch: Arc, - aggregated_verification_key: VerificationKey, + ecash_clients: Arc>>>, } impl DummyCommunicationChannel { - pub fn new(aggregated_verification_key: VerificationKey) -> Self { + pub fn new(ecash_clients: Vec) -> Self { + let epoch_id = 1; + let mut ecash_clients_map = HashMap::new(); + ecash_clients_map.insert(epoch_id, ecash_clients); DummyCommunicationChannel { - current_epoch: Arc::new(AtomicU64::new(1)), - aggregated_verification_key, + current_epoch: Arc::new(AtomicU64::new(epoch_id)), + ecash_clients: Arc::new(RwLock::new(ecash_clients_map)), } } + pub fn new_single_dummy(aggregated_verification_key: VerificationKeyAuth) -> Self { + let client = EcashApiClient { + api_client: NymApiClient::new("http://localhost:1234".parse().unwrap()), + verification_key: aggregated_verification_key, + node_id: 1, + cosmos_address: "n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l" + .parse() + .unwrap(), + }; + Self::new(vec![client]) + } + pub fn with_epoch(mut self, current_epoch: Arc) -> Self { self.current_epoch = current_epoch; self @@ -1214,11 +1146,37 @@ impl super::comm::APICommunicationChannel for DummyCommunicationChannel { Ok(self.current_epoch.load(Ordering::Relaxed)) } - async fn aggregated_verification_key(&self, _epoch_id: EpochId) -> Result { - Ok(self.aggregated_verification_key.clone()) + async fn dkg_in_progress(&self) -> Result { + // deal with this later lol + Ok(false) + } + + async fn ecash_clients(&self, epoch_id: EpochId) -> Result> { + Ok(self + .ecash_clients + .read() + .await + .get(&epoch_id) + .cloned() + .unwrap_or_default()) + } + + async fn ecash_threshold(&self, _epoch_id: EpochId) -> Result { + todo!() } } +#[allow(dead_code)] +pub fn deposit_fixture() -> Deposit { + let mut rng = OsRng; + let identity_keypair = identity::KeyPair::new(&mut rng); + + Deposit { + bs58_encoded_ed25519_pubkey: identity_keypair.public_key().to_base58_string(), + } +} + +#[allow(dead_code)] pub fn tx_entry_fixture(hash: Hash) -> TxResponse { TxResponse { hash, @@ -1239,53 +1197,6 @@ pub fn tx_entry_fixture(hash: Hash) -> TxResponse { } } -pub fn deposit_tx_fixture(voucher_data: &BandwidthVoucherIssuanceData) -> TxResponse { - TxResponse { - hash: voucher_data.tx_hash(), - height: Default::default(), - index: 0, - tx_result: ExecTxResult { - code: Default::default(), - data: Default::default(), - log: "".to_string(), - info: "".to_string(), - gas_wanted: 0, - gas_used: 0, - events: vec![Event { - kind: format!("wasm-{}", DEPOSITED_FUNDS_EVENT_TYPE), - attributes: vec![ - EventAttribute { - key: DEPOSIT_VALUE.to_string(), - value: voucher_data.value_plain(), - index: false, - }, - EventAttribute { - key: DEPOSIT_INFO.to_string(), - value: CredentialType::Voucher.to_string(), - index: false, - }, - EventAttribute { - key: DEPOSIT_IDENTITY_KEY.to_string(), - value: voucher_data.identity_key().public_key().to_base58_string(), - index: false, - }, - EventAttribute { - key: DEPOSIT_ENCRYPTION_KEY.parse().unwrap(), - value: voucher_data - .encryption_key() - .public_key() - .to_base58_string(), - index: false, - }, - ], - }], - codespace: "".to_string(), - }, - tx: vec![], - proof: None, - } -} - pub fn blinded_signature_fixture() -> BlindedSignature { let gen1_bytes = [ 151u8, 241, 211, 167, 49, 151, 215, 148, 38, 149, 99, 140, 79, 169, 172, 15, 195, 104, 140, @@ -1302,25 +1213,17 @@ pub fn blinded_signature_fixture() -> BlindedSignature { BlindedSignature::from_bytes(&dummy_bytes).unwrap() } -pub fn voucher_fixture>( - amount: C, - tx_hash: Option, -) -> IssuanceBandwidthCredential { +pub fn voucher_fixture(deposit_id: Option) -> IssuanceTicketBook { let mut rng = OsRng; - let tx_hash = if let Some(provided) = &tx_hash { - provided.parse().unwrap() - } else { - Hash::from_str("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E").unwrap() - }; + let deposit_id = deposit_id.unwrap_or(69); let identity_keypair = identity::KeyPair::new(&mut rng); - let encryption_keypair = encryption::KeyPair::new(&mut rng); + let id_priv = identity::PrivateKey::from_bytes(&identity_keypair.private_key().to_bytes()).unwrap(); - let enc_priv = - encryption::PrivateKey::from_bytes(&encryption_keypair.private_key().to_bytes()).unwrap(); - - IssuanceBandwidthCredential::new_voucher(amount.into(), tx_hash, id_priv, enc_priv) + let identifier = [44u8; 32]; + // (voucher, request) + IssuanceTicketBook::new(deposit_id, identifier, id_priv) } fn dummy_signature() -> identity::Signature { @@ -1340,13 +1243,12 @@ struct TestFixture { impl TestFixture { async fn new() -> Self { - let mut rng = crate::coconut::tests::fixtures::test_rng([69u8; 32]); - let params = Parameters::new(4).unwrap(); - let coconut_keypair = nym_coconut::ttp_keygen(¶ms, 1, 1).unwrap().remove(0); + let mut rng = crate::ecash::tests::fixtures::test_rng([69u8; 32]); + let coconut_keypair = ttp_keygen(1, 1).unwrap().remove(0); let identity = identity::KeyPair::new(&mut rng); let epoch = Arc::new(AtomicU64::new(1)); let comm_channel = - DummyCommunicationChannel::new(coconut_keypair.verification_key().clone()) + DummyCommunicationChannel::new_single_dummy(coconut_keypair.verification_key().clone()) .with_epoch(epoch.clone()); // TODO: it's AWFUL to test with actual storage, we should somehow abstract it away @@ -1355,7 +1257,7 @@ impl TestFixture { .await .unwrap(); - let staged_key_pair = crate::coconut::KeyPair::new(); + let staged_key_pair = crate::ecash::keys::KeyPair::new(); staged_key_pair .set(KeyPairWithEpoch { keys: coconut_keypair, @@ -1370,14 +1272,33 @@ impl TestFixture { chain_state.clone(), ); - let rocket = rocket::build().attach(crate::coconut::stage( - nyxd_client, - TEST_COIN_DENOM.to_string(), - identity, - staged_key_pair, - comm_channel, - storage.clone(), - )); + let ecash_contract = chain_state + .lock() + .unwrap() + .ecash_contract + .address + .clone() + .as_str() + .parse() + .unwrap(); + + let rocket = rocket::build() + .manage( + EcashState::new( + ecash_contract, + nyxd_client, + identity, + staged_key_pair, + comm_channel, + storage.clone(), + ) + .await + .unwrap(), + ) + .mount( + "/v1/ecash", + ecash::routes_open_api(&Default::default(), true).0, + ); TestFixture { rocket: Client::tracked(rocket) @@ -1394,39 +1315,43 @@ impl TestFixture { self.epoch.store(epoch, Ordering::Relaxed) } + #[allow(dead_code)] fn add_tx(&self, hash: Hash, tx: TxResponse) { self.chain_state.lock().unwrap().txs.insert(hash, tx); } - fn add_deposit_tx(&self, voucher: &BandwidthVoucherIssuanceData) { - let mut guard = self.chain_state.lock().unwrap(); - let fixture = deposit_tx_fixture(voucher); - - guard.txs.insert(voucher.tx_hash(), fixture); + fn add_deposit(&self, voucher_data: &IssuanceTicketBook) { + let mut chain = self.chain_state.lock().unwrap(); + let deposit = Deposit { + bs58_encoded_ed25519_pubkey: voucher_data + .identity_key() + .public_key() + .to_base58_string(), + }; + let existing = chain + .ecash_contract + .deposits + .insert(voucher_data.deposit_id(), deposit); + assert!(existing.is_none()); } async fn issue_dummy_credential(&self) { let mut rng = OsRng; - let mut tx_hash = [0u8; 32]; - rng.fill_bytes(&mut tx_hash); - let tx_hash = Hash::from_bytes(Algorithm::Sha256, &tx_hash).unwrap(); + let deposit_id = rng.next_u32(); - let voucher = voucher_fixture(coin(1234, "unym"), Some(tx_hash.to_string())); + let voucher = voucher_fixture(Some(deposit_id)); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let req = voucher_data.create_blind_sign_request_body(&signing_data); + let req = voucher.create_blind_sign_request_body(&signing_data); - self.add_deposit_tx(voucher_data); + self.add_deposit(&voucher); self.issue_credential(req).await; } async fn issue_credential(&self, req: BlindSignRequestBody) -> BlindedSignatureResponse { let response = self .rocket - .post(format!( - "/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/{COCONUT_BLIND_SIGN}", - )) + .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) .json(&req) .dispatch() .await; @@ -1439,7 +1364,7 @@ impl TestFixture { let response = self .rocket .get(format!( - "/{API_VERSION}/{COCONUT_ROUTES}/{BANDWIDTH}/issued-credential/{id}" + "/{API_VERSION}/{ECASH_ROUTES}/issued-credential/{id}" )) .dispatch() .await; @@ -1459,41 +1384,38 @@ impl TestFixture { #[cfg(test)] mod credential_tests { use super::*; - use crate::coconut::tests::helpers::init_chain; - use nym_api_requests::coconut::{VerifyCredentialBody, VerifyCredentialResponse}; - use nym_coconut::{blind_sign, hash_to_scalar, ttp_keygen}; - use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredential; - use nym_credentials::coconut::bandwidth::bandwidth_credential_params; - use nym_validator_client::nym_api::routes::COCONUT_VERIFY_BANDWIDTH_CREDENTIAL; + use nym_compact_ecash::ttp_keygen; #[tokio::test] async fn already_issued() { - let voucher = voucher_fixture(coin(1234, TEST_COIN_DENOM), None); + let voucher = voucher_fixture(None); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let request_body = voucher_data.create_blind_sign_request_body(&signing_data); + let request_body = voucher.create_blind_sign_request_body(&signing_data); - let tx_hash = request_body.tx_hash; - let tx_entry = tx_entry_fixture(tx_hash); + let deposit_id = request_body.deposit_id; let test_fixture = TestFixture::new().await; - test_fixture.add_tx(tx_hash, tx_entry); + test_fixture.add_deposit(&voucher); let sig = blinded_signature_fixture(); let commitments = request_body.encode_commitments(); - let public = request_body.public_attributes_plain.clone(); + let expiration_date = request_body.expiration_date; test_fixture .storage - .store_issued_credential(42, tx_hash, &sig, dummy_signature(), commitments, public) + .store_issued_credential( + 42, + deposit_id, + &sig, + dummy_signature(), + commitments, + expiration_date, + ) .await .unwrap(); let response = test_fixture .rocket - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_BLIND_SIGN - )) + .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) .json(&request_body) .dispatch() .await; @@ -1524,15 +1446,15 @@ mod credential_tests { AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), Default::default(), ); - let params = Parameters::new(4).unwrap(); - let key_pair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0); + let key_pair = ttp_keygen(1, 1).unwrap().remove(0); let tmp_dir = tempdir().unwrap(); let storage = NymApiStorage::init(tmp_dir.path().join("storage.db")) .await .unwrap(); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); - let staged_key_pair = crate::coconut::KeyPair::new(); + let comm_channel = + DummyCommunicationChannel::new_single_dummy(key_pair.verification_key().clone()); + let staged_key_pair = crate::ecash::keys::KeyPair::new(); staged_key_pair .set(KeyPairWithEpoch { keys: key_pair, @@ -1541,43 +1463,44 @@ mod credential_tests { .await; staged_key_pair.validate(); - let state = State::new( + let state = EcashState::new( + "n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l" + .parse() + .unwrap(), nyxd_client, - TEST_COIN_DENOM.to_string(), identity, staged_key_pair, comm_channel, storage.clone(), - ); + ) + .await + .unwrap(); - let tx_hash = "6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E" - .parse() - .unwrap(); - assert!(state.already_issued(tx_hash).await.unwrap().is_none()); + let deposit_id = 42; + assert!(state.already_issued(deposit_id).await.unwrap().is_none()); - let voucher = voucher_fixture(coin(1234, TEST_COIN_DENOM), None); + let voucher = voucher_fixture(None); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let request_body = voucher_data.create_blind_sign_request_body(&signing_data); + let request_body = voucher.create_blind_sign_request_body(&signing_data); let commitments = request_body.encode_commitments(); - let public = request_body.public_attributes_plain.clone(); + let expiration_date = request_body.expiration_date; let sig = blinded_signature_fixture(); storage .store_issued_credential( 42, - tx_hash, + deposit_id, &sig, dummy_signature(), commitments.clone(), - public.clone(), + expiration_date, ) .await .unwrap(); assert_eq!( state - .already_issued(tx_hash) + .already_issued(deposit_id) .await .unwrap() .unwrap() @@ -1599,28 +1522,26 @@ mod credential_tests { let storage_err = storage .store_issued_credential( 42, - tx_hash, + deposit_id, &blinded_signature, dummy_signature(), commitments.clone(), - public.clone(), + expiration_date, ) .await; assert!(storage_err.is_err()); - // And use a new hash to store a new signature - let tx_hash = "97D64C38D6601B1F0FD3A82E20D252685CB7A210AFB0261018590659AB82B0BF" - .parse() - .unwrap(); + // And use a new deposit to store a new signature + let deposit_id = 69; storage .store_issued_credential( 42, - tx_hash, + deposit_id, &blinded_signature, dummy_signature(), commitments.clone(), - public.clone(), + expiration_date, ) .await .unwrap(); @@ -1628,7 +1549,7 @@ mod credential_tests { // Check that the same value for tx_hash is returned assert_eq!( state - .already_issued(tx_hash) + .already_issued(deposit_id) .await .unwrap() .unwrap() @@ -1639,76 +1560,38 @@ mod credential_tests { #[tokio::test] async fn blind_sign_correct() { - let tx_hash = - Hash::from_str("7C41AF8266D91DE55E1C8F4712E6A952A165ED3D8C27C7B00428CBD0DE00A52B") - .unwrap(); + let deposit_id = 42; - let params = Parameters::new(4).unwrap(); let mut rng = OsRng; - let nym_api_identity = identity::KeyPair::new(&mut rng); - let identity_keypair = identity::KeyPair::new(&mut rng); - let encryption_keypair = encryption::KeyPair::new(&mut rng); - let voucher = IssuanceBandwidthCredential::new_voucher( - coin(1234, "unym"), - tx_hash, + let identifier = [42u8; 32]; + let voucher = IssuanceTicketBook::new( + deposit_id, + identifier, identity::PrivateKey::from_base58_string( identity_keypair.private_key().to_base58_string(), ) .unwrap(), - encryption::PrivateKey::from_bytes(&encryption_keypair.private_key().to_bytes()) - .unwrap(), ); - let key_pair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0); - let tmp_dir = tempdir().unwrap(); - let storage = NymApiStorage::init(tmp_dir.path().join("storage.db")) - .await - .unwrap(); + let deposit = Deposit { + bs58_encoded_ed25519_pubkey: voucher.identity_key().public_key().to_base58_string(), + }; - let chain = init_chain(); - - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let tx_entry = deposit_tx_fixture(voucher_data); - - chain.lock().unwrap().txs.insert(tx_hash, tx_entry.clone()); - - let nyxd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - chain.clone(), - ); - - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); - let staged_key_pair = crate::coconut::KeyPair::new(); - staged_key_pair - .set(KeyPairWithEpoch { - keys: key_pair, - issued_for_epoch: 1, - }) - .await; - staged_key_pair.validate(); - - let rocket = rocket::build().attach(crate::coconut::stage( - nyxd_client, - TEST_COIN_DENOM.to_string(), - nym_api_identity, - staged_key_pair, - comm_channel, - storage.clone(), - )); - let client = Client::tracked(rocket) - .await - .expect("valid rocket instance"); + let test = TestFixture::new().await; + test.chain_state + .lock() + .unwrap() + .ecash_contract + .deposits + .insert(voucher.deposit_id(), deposit); let signing_data = voucher.prepare_for_signing(); - let voucher_data = voucher.get_variant_data().voucher_data().unwrap(); - let request_body = voucher_data.create_blind_sign_request_body(&signing_data); + let request_body = voucher.create_blind_sign_request_body(&signing_data); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_BLIND_SIGN - )) + let response = test + .rocket + .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}")) .json(&request_body) .dispatch() .await; @@ -1722,410 +1605,16 @@ mod credential_tests { assert!(blinded_signature_response.is_ok()); } - #[tokio::test] - async fn verification_of_bandwidth_credential() { - // Setup variables - let chain = init_chain(); - let validator_address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); - chain - .lock() - .unwrap() - .add_member(validator_address.as_ref(), 100); + #[test] + fn blind_sign_request_body_serde() { + let deposit_id = 123; + let issuance = voucher_fixture(Some(deposit_id)); + let signing_data = issuance.prepare_for_signing(); + let request = issuance.create_blind_sign_request_body(&signing_data); - let nyxd_client = DummyClient::new(validator_address.clone(), chain.clone()); - let db_dir = tempdir().unwrap(); + let json_bytes = serde_json::to_vec(&request).unwrap(); + let recovered: BlindSignRequestBody = serde_json::from_slice(&json_bytes).unwrap(); - // generate all the credential requests - let params = bandwidth_credential_params(); - let key_pair = nym_coconut::keygen(params); - let epoch = 1; - - let voucher_amount = coin(1234, "unym"); - let issuance = voucher_fixture(coin(1234, "unym"), None); - let sig_req = issuance.prepare_for_signing(); - let pub_attrs_hashed = sig_req - .public_attributes_plain - .iter() - .map(hash_to_scalar) - .collect::>(); - let pub_attrs = pub_attrs_hashed.iter().collect::>(); - let blind_sig = blind_sign( - params, - key_pair.secret_key(), - &sig_req.blind_sign_request, - &pub_attrs, - ) - .unwrap(); - let sig = blind_sig.unblind( - key_pair.verification_key(), - &sig_req.pedersen_commitments_openings, - ); - - let issued = issuance.into_issued_credential(sig, epoch); - let spending = issued - .prepare_for_spending(key_pair.verification_key()) - .unwrap(); - - let storage1 = NymApiStorage::init(db_dir.path().join("storage.db")) - .await - .unwrap(); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key().clone()); - let staged_key_pair = crate::coconut::KeyPair::new(); - staged_key_pair - .set(KeyPairWithEpoch { - keys: key_pair, - issued_for_epoch: epoch, - }) - .await; - staged_key_pair.validate(); - let mut rng = OsRng; - let identity = identity::KeyPair::new(&mut rng); - - let rocket = rocket::build().attach(crate::coconut::stage( - nyxd_client.clone(), - TEST_COIN_DENOM.to_string(), - identity, - staged_key_pair, - comm_channel.clone(), - storage1.clone(), - )); - - let client = Client::tracked(rocket) - .await - .expect("valid rocket instance"); - - let proposal_id = 42; - // The address is not used, so we can use a duplicate - let gateway_cosmos_addr = validator_address.clone(); - let req = - VerifyCredentialBody::new(spending.clone(), proposal_id, gateway_cosmos_addr.clone()); - - // Test endpoint with not proposal for the proposal id - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::IncorrectProposal { - reason: "proposal not found".to_string() - } - .to_string() - ); - - let mut proposal = Proposal { - title: String::new(), - description: String::from("25mnnoCcUfeizfC85avvroFg2prpEZBgJbJM2SLtkgyyUkoAU3cqJiqWmg8cMHEPjfFf5sQF92SMAM2vbEoLZvUjenvXhadTLdA4TqMYArJpihyqirW2AhGoNehtcdcK5gnH"), - msgs: vec![], - status: cw3::Status::Open, - expires: cw_utils::Expiration::Never {}, - threshold: cw_utils::Threshold::AbsolutePercentage { - percentage: Decimal::from_ratio(2u32, 3u32), - }, - total_weight: chain.lock().unwrap().total_group_weight(), - votes: Votes::yes(0), - proposer: Addr::unchecked("proposer"), - deposit: None, - start_height: 0 - }; - - // Test the endpoint with a different blinded serial number in the description - - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::IncorrectProposal { - reason: "incorrect blinded serial number in description".to_string() - } - .to_string() - ); - - // Test the endpoint with no msg in the proposal action - proposal.description = spending - .verify_credential_request - .blinded_serial_number_bs58(); - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::IncorrectProposal { - reason: "action is not to release funds".to_string() - } - .to_string() - ); - - // Test the endpoint without any credential recorded in the Coconut Bandwidth Contract - let funds = voucher_amount.clone(); - let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { - funds: funds.clone(), - }; - let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::new(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }); - proposal.msgs = vec![cosmos_msg]; - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::InvalidCredentialStatus { - status: "spent credential not found".to_string() - } - .to_string() - ); - - chain - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .insert( - spending - .verify_credential_request - .blinded_serial_number_bs58(), - SpendCredentialResponse::new(None), - ); - - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::InvalidCredentialStatus { - status: "Inexistent".to_string() - } - .to_string() - ); - - // Test the endpoint with a credential that doesn't verify correctly - let mut spent_credential = SpendCredential::new( - funds.clone(), - spending - .verify_credential_request - .blinded_serial_number_bs58(), - Addr::unchecked("unimportant"), - ); - chain - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .insert( - spending - .verify_credential_request - .blinded_serial_number_bs58(), - SpendCredentialResponse::new(Some(spent_credential.clone())), - ); - - // TODO: somehow restore that test - // let bad_credential = Credential::new( - // 4, - // theta.clone(), - // voucher_value, - // String::from("bad voucher info"), - // 0, - // ); - // let bad_req = VerifyCredentialBody::new( - // bad_credential, - // epoch_id, - // proposal_id, - // gateway_cosmos_addr.clone(), - // ); - // let response = client - // .post(format!( - // "/{}/{}/{}/{}", - // API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - // )) - // .json(&bad_req) - // .dispatch() - // .await; - // assert_eq!(response.status(), Status::Ok); - // let verify_credential_response = serde_json::from_str::( - // &response.into_string().await.unwrap(), - // ) - // .unwrap(); - // assert!(!verify_credential_response.verification_result); - // assert_eq!( - // cw3::Status::Rejected, - // chain - // .lock() - // .unwrap() - // .multisig_contract - // .proposals - // .get(&proposal_id) - // .unwrap() - // .status - // ); - - // Test the endpoint with a proposal that has a different value for the funds to be released - // then what's in the credential - let funds = Coin::new(voucher_amount.amount.u128() + 10, TEST_COIN_DENOM); - let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { - funds: funds.clone().into(), - }; - let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::new(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }); - proposal.msgs = vec![cosmos_msg]; - chain.lock().unwrap().reset_votes(); - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - let verify_credential_response = serde_json::from_str::( - &response.into_string().await.unwrap(), - ) - .unwrap(); - assert!(!verify_credential_response.verification_result); - assert_eq!( - cw3::Status::Rejected, - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .get(&proposal_id) - .unwrap() - .status - ); - - // Test the endpoint with every dependency met - let funds = voucher_amount; - let msg = nym_coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { - funds: funds.clone(), - }; - let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::new(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }); - proposal.msgs = vec![cosmos_msg]; - chain.lock().unwrap().reset_votes(); - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .insert(proposal_id, proposal.clone()); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - let verify_credential_response = serde_json::from_str::( - &response.into_string().await.unwrap(), - ) - .unwrap(); - assert!(verify_credential_response.verification_result); - assert_eq!( - cw3::Status::Passed, - chain - .lock() - .unwrap() - .multisig_contract - .proposals - .get(&proposal_id) - .unwrap() - .status - ); - - // Test the endpoint with the credential marked as Spent in the Coconut Bandwidth Contract - spent_credential.mark_as_spent(); - chain - .lock() - .unwrap() - .bandwidth_contract - .spent_credentials - .insert( - spending - .verify_credential_request - .blinded_serial_number_bs58(), - SpendCredentialResponse::new(Some(spent_credential)), - ); - let response = client - .post(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL - )) - .json(&req) - .dispatch() - .await; - assert_eq!(response.status(), Status::BadRequest); - assert_eq!( - response.into_string().await.unwrap(), - CoconutError::InvalidCredentialStatus { - status: "Spent".to_string() - } - .to_string() - ); + assert_eq!(recovered, request) } } diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 5731c1ade3..0bebd493ed 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -4,8 +4,8 @@ #[macro_use] extern crate rocket; -use crate::coconut::dkg::controller::keys::{ - can_validate_coconut_keys, load_bte_keypair, load_coconut_keypair_if_exists, +use crate::ecash::dkg::controller::keys::{ + can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, }; use crate::epoch_operations::RewardedSetUpdater; use crate::network::models::NetworkDetails; @@ -19,7 +19,7 @@ use crate::support::storage::NymApiStorage; use ::nym_config::defaults::setup_env; use circulating_supply_api::cache::CirculatingSupplyCache; use clap::Parser; -use coconut::dkg::controller::DkgController; +use ecash::dkg::controller::DkgController; use node_status_api::NodeStatusCache; use nym_bin_common::logging::setup_logging; use nym_config::defaults::NymNetworkDetails; @@ -30,7 +30,7 @@ use rand::rngs::OsRng; use support::{http, nyxd}; mod circulating_supply_api; -mod coconut; +mod ecash; mod epoch_operations; pub(crate) mod network; mod network_monitor; @@ -70,10 +70,10 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result let nym_network_details = NymNetworkDetails::new_from_env(); let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); - let coconut_keypair_wrapper = coconut::keys::KeyPair::new(); + let coconut_keypair_wrapper = ecash::keys::KeyPair::new(); // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen - if let Some(loaded_keys) = load_coconut_keypair_if_exists(&config.coconut_signer)? { + if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.coconut_signer)? { let issued_for = loaded_keys.issued_for_epoch; coconut_keypair_wrapper.set(loaded_keys).await; diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 7d13d81522..d0c172d7a3 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -418,3 +418,11 @@ pub enum NymApiStorageError { #[error("failed to perform startup SQL migration - {0}")] StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), } + +impl NymApiStorageError { + pub fn database_inconsistency>(reason: S) -> NymApiStorageError { + NymApiStorageError::DatabaseInconsistency { + reason: reason.into(), + } + } +} diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index c1ea22e825..d200a3445d 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -52,18 +52,18 @@ impl NymContractCacheRefresher { let mixnet = query_guard!(client_guard, mixnet_contract_address()); let vesting = query_guard!(client_guard, vesting_contract_address()); - let coconut_bandwidth = query_guard!(client_guard, coconut_bandwidth_contract_address()); let coconut_dkg = query_guard!(client_guard, dkg_contract_address()); let group = query_guard!(client_guard, group_contract_address()); let multisig = query_guard!(client_guard, multisig_contract_address()); + let ecash = query_guard!(client_guard, ecash_contract_address()); for (address, name) in [ (mixnet, "nym-mixnet-contract"), (vesting, "nym-vesting-contract"), - (coconut_bandwidth, "nym-coconut-bandwidth-contract"), (coconut_dkg, "nym-coconut-dkg-contract"), (group, "nym-cw4-group-contract"), (multisig, "nym-cw3-multisig-contract"), + (ecash, "nym-ecash-contract"), ] { let (cw2, build_info) = if let Some(address) = address { let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await); diff --git a/nym-api/src/status/mod.rs b/nym-api/src/status/mod.rs index 1a88a9ab33..053cb8ba88 100644 --- a/nym-api/src/status/mod.rs +++ b/nym-api/src/status/mod.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut; +use crate::ecash; use nym_bin_common::bin_info; use nym_bin_common::build_information::BinaryBuildInformation; use okapi::openapi3::OpenApi; @@ -26,7 +26,7 @@ pub(crate) struct SignerState { pub announce_address: String, - pub(crate) coconut_keypair: coconut::keys::KeyPair, + pub(crate) coconut_keypair: ecash::keys::KeyPair, } impl ApiStatusState { diff --git a/nym-api/src/status/routes.rs b/nym-api/src/status/routes.rs index 614785eeba..560ac765d9 100644 --- a/nym-api/src/status/routes.rs +++ b/nym-api/src/status/routes.rs @@ -5,7 +5,7 @@ use crate::node_status_api::models::ErrorResponse; use crate::status::ApiStatusState; use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; use nym_bin_common::build_information::BinaryBuildInformationOwned; -use nym_coconut::Base58; +use nym_compact_ecash::Base58; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; diff --git a/nym-api/src/support/config/helpers.rs b/nym-api/src/support/config/helpers.rs index 9f965e7815..6521c8f7f4 100644 --- a/nym-api/src/support/config/helpers.rs +++ b/nym-api/src/support/config/helpers.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::dkg::controller::keys::init_bte_keypair; +use crate::ecash::dkg::controller::keys::init_bte_keypair; use crate::support::config; use crate::support::config::{ default_config_directory, default_data_directory, upgrade_helpers, Config, diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index 99cd925e80..a0696d4955 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::coconut::client::Client; -use crate::coconut::{self, comm::QueryCommunicationChannel}; +use crate::ecash::client::Client; +use crate::ecash::state::EcashState; +use crate::ecash::{self, comm::QueryCommunicationChannel}; use crate::network::models::NetworkDetails; use crate::network::network_routes; use crate::node_describe_cache::DescribedNodes; @@ -16,7 +17,7 @@ use crate::support::caching::cache::SharedCache; use crate::support::config::Config; use crate::support::{nyxd, storage}; use crate::{circulating_supply_api, nym_contract_cache, nym_nodes::nym_node_routes}; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use nym_crypto::asymmetric::identity; use nym_validator_client::nyxd::Coin; use rocket::http::Method; @@ -33,7 +34,7 @@ pub(crate) async fn setup_rocket( network_details: NetworkDetails, nyxd_client: nyxd::Client, identity_keypair: identity::KeyPair, - coconut_keypair: coconut::keys::KeyPair, + coconut_keypair: ecash::keys::KeyPair, ) -> anyhow::Result> { let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); let mut rocket = rocket::build(); @@ -50,11 +51,11 @@ pub(crate) async fn setup_rocket( "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), "/network" => network_routes(&openapi_settings), "/api-status" => api_status_routes(&openapi_settings), + "/ecash" => ecash::routes_open_api(&openapi_settings, config.coconut_signer.enabled), "" => nym_node_routes(&openapi_settings), // => when we move those routes, we'll need to add a redirection for backwards compatibility "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings) - } let rocket = rocket @@ -83,9 +84,9 @@ pub(crate) async fn setup_rocket( let rocket = if config.coconut_signer.enabled { // make sure we have some tokens to cover multisig fees let balance = nyxd_client.balance(&mix_denom).await?; - if balance.amount < coconut::MINIMUM_BALANCE { + if balance.amount < ecash::MINIMUM_BALANCE { let address = nyxd_client.address().await; - let min = Coin::new(coconut::MINIMUM_BALANCE, mix_denom); + let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") } @@ -103,15 +104,24 @@ pub(crate) async fn setup_rocket( coconut_keypair: coconut_keypair.clone(), }); + let ecash_contract = nyxd_client + .get_ecash_contract_address() + .await + .context("e-cash contract address is required to setup the zk-nym signer")?; + let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); - rocket.attach(coconut::stage( + + let ecash_state = EcashState::new( + ecash_contract, nyxd_client.clone(), - mix_denom, identity_keypair, coconut_keypair, comm_channel, storage.clone().unwrap(), - )) + ) + .await?; + + rocket.manage(ecash_state) } else { rocket }; diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 833d891473..69f9a49360 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -1,14 +1,13 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::coconut::error::CoconutError; +use crate::ecash::error::EcashError; use crate::epoch_operations::MixnodeWithPerformance; use crate::support::config::Config; use anyhow::Result; use async_trait::async_trait; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; -use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use nym_coconut_dkg_common::dealer::RegisteredDealerDetails; use nym_coconut_dkg_common::dealing::{ DealerDealingsStatusResponse, DealingChunkInfo, DealingMetadata, DealingStatusResponse, @@ -22,6 +21,9 @@ use nym_coconut_dkg_common::{ verification_key::{ContractVKShare, VerificationKeyShare}, }; use nym_config::defaults::{ChainDetails, NymNetworkDetails}; +use nym_dkg::Threshold; +use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; +use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::reward_params::RewardingParams; @@ -29,14 +31,14 @@ use nym_mixnet_contract_common::{ CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, LayerAssignment, MixId, RewardedSetNodeStatus, }; +use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{ contract_traits::{ - CoconutBandwidthQueryClient, DkgQueryClient, DkgSigningClient, GroupQueryClient, - MixnetQueryClient, MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, - NymContractsProvider, PagedMixnetQueryClient, PagedMultisigQueryClient, - PagedVestingQueryClient, + DkgQueryClient, DkgSigningClient, EcashQueryClient, GroupQueryClient, MixnetQueryClient, + MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, NymContractsProvider, + PagedMixnetQueryClient, PagedMultisigQueryClient, PagedVestingQueryClient, }, cosmwasm_client::types::ExecuteResult, CosmWasmClient, Fee, @@ -45,7 +47,9 @@ use nym_validator_client::nyxd::{ hash::{Hash, SHA256_HASH_SIZE}, AccountId, Coin, TendermintTime, }; -use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; +use nym_validator_client::{ + nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient, QueryHttpRpcNyxdClient, +}; use nym_vesting_contract_common::AccountVestingCoins; use serde::Deserialize; use std::sync::Arc; @@ -153,6 +157,15 @@ impl Client { nyxd_query!(self, current_chain_details().clone()) } + pub(crate) async fn get_ecash_contract_address(&self) -> Result { + nyxd_query!( + self, + ecash_contract_address() + .cloned() + .ok_or_else(|| NyxdError::unavailable_contract_address("ecash contract").into()) + ) + } + pub(crate) async fn get_rewarding_validator_address(&self) -> Result { let cosmwasm_addr = nyxd_query!( self, @@ -338,12 +351,12 @@ impl Client { } #[async_trait] -impl crate::coconut::client::Client for Client { +impl crate::ecash::client::Client for Client { async fn address(&self) -> AccountId { self.client_address().await } - async fn dkg_contract_address(&self) -> Result { + async fn dkg_contract_address(&self) -> Result { nyxd_query!( self, dkg_contract_address() @@ -352,37 +365,21 @@ impl crate::coconut::client::Client for Client { ) } - async fn bandwidth_contract_admin(&self) -> crate::coconut::error::Result> { - let guard = self.inner.read().await; - - let bandwidth_contract = query_guard!( - guard, - coconut_bandwidth_contract_address() - .ok_or(CoconutError::MissingBandwidthContractAddress) - )?; - - let contract = query_guard!(guard, get_contract(bandwidth_contract)).await?; - - Ok(contract.contract_info.admin) - } - - async fn get_tx(&self, tx_hash: Hash) -> crate::coconut::error::Result { - nyxd_query!(self, get_tx(tx_hash).await).map_err(|source| { - CoconutError::TxRetrievalFailure { - tx_hash: tx_hash.to_string(), - source, - } - }) + async fn get_deposit( + &self, + deposit_id: DepositId, + ) -> crate::ecash::error::Result { + Ok(nyxd_query!(self, get_deposit(deposit_id).await?)) } async fn get_proposal( &self, proposal_id: u64, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!(self, query_proposal(proposal_id).await?)) } - async fn list_proposals(&self) -> crate::coconut::error::Result> { + async fn list_proposals(&self) -> crate::ecash::error::Result> { Ok(nyxd_query!(self, get_all_proposals().await?)) } @@ -390,41 +387,58 @@ impl crate::coconut::client::Client for Client { &self, proposal_id: u64, voter: String, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!(self, query_vote(proposal_id, voter).await?)) } - async fn get_spent_credential( + // async fn propose_for_blacklist( + // &self, + // public_key: String, + // ) -> crate::ecash::error::Result { + // Ok(nyxd_signing!( + // self, + // propose_for_blacklist(public_key, None).await? + // )) + // } + + async fn get_blacklisted_account( &self, - blinded_serial_number: String, - ) -> crate::coconut::error::Result { + public_key: String, + ) -> crate::ecash::error::Result { Ok(nyxd_query!( self, - get_spent_credential(blinded_serial_number).await? + get_blacklisted_account(public_key).await? )) } - async fn contract_state(&self) -> crate::coconut::error::Result { + async fn contract_state(&self) -> crate::ecash::error::Result { Ok(nyxd_query!(self, get_state().await?)) } - async fn get_current_epoch(&self) -> crate::coconut::error::Result { + async fn get_current_epoch(&self) -> crate::ecash::error::Result { Ok(nyxd_query!(self, get_current_epoch().await?)) } - async fn group_member(&self, addr: String) -> crate::coconut::error::Result { + async fn group_member(&self, addr: String) -> crate::ecash::error::Result { Ok(nyxd_query!(self, member(addr, None).await?)) } async fn get_current_epoch_threshold( &self, - ) -> crate::coconut::error::Result> { + ) -> crate::ecash::error::Result> { Ok(nyxd_query!(self, get_current_epoch_threshold().await?)) } + async fn get_epoch_threshold( + &self, + epoch_id: EpochId, + ) -> crate::ecash::error::Result> { + Ok(nyxd_query!(self, get_epoch_threshold(epoch_id).await?)) + } + async fn get_self_registered_dealer_details( &self, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { let self_address = &self.address().await; Ok(nyxd_query!(self, get_dealer_details(self_address).await?)) } @@ -433,7 +447,7 @@ impl crate::coconut::client::Client for Client { &self, epoch_id: EpochId, dealer: String, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { let dealer = dealer .as_str() .parse() @@ -448,7 +462,7 @@ impl crate::coconut::client::Client for Client { &self, epoch_id: EpochId, dealer: String, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!( self, get_dealer_dealings_status(epoch_id, dealer).await? @@ -460,14 +474,14 @@ impl crate::coconut::client::Client for Client { epoch_id: EpochId, dealer: String, dealing_index: DealingIndex, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_query!( self, get_dealing_status(epoch_id, dealer, dealing_index).await? )) } - async fn get_current_dealers(&self) -> crate::coconut::error::Result> { + async fn get_current_dealers(&self) -> crate::ecash::error::Result> { Ok(nyxd_query!(self, get_all_current_dealers().await?)) } @@ -476,7 +490,7 @@ impl crate::coconut::client::Client for Client { epoch_id: EpochId, dealer: String, dealing_index: DealingIndex, - ) -> crate::coconut::error::Result> { + ) -> crate::ecash::error::Result> { Ok(nyxd_query!( self, get_dealings_metadata(epoch_id, dealer, dealing_index) @@ -491,7 +505,7 @@ impl crate::coconut::client::Client for Client { dealer: &str, dealing_index: DealingIndex, chunk_index: ChunkIndex, - ) -> crate::coconut::error::Result> { + ) -> crate::ecash::error::Result> { Ok(nyxd_query!( self, get_dealing_chunk(epoch_id, dealer.to_string(), dealing_index, chunk_index) @@ -504,40 +518,52 @@ impl crate::coconut::client::Client for Client { &self, epoch_id: EpochId, dealer: String, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(nyxd_query!(self, get_vk_share(epoch_id, dealer).await?).share) } async fn get_verification_key_shares( &self, epoch_id: EpochId, - ) -> Result, CoconutError> { + ) -> Result, EcashError> { Ok(nyxd_query!( self, get_all_verification_key_shares(epoch_id).await? )) } + async fn get_registered_ecash_clients( + &self, + epoch_id: EpochId, + ) -> Result, EcashError> { + Ok(self + .get_verification_key_shares(epoch_id) + .await? + .into_iter() + .map(TryInto::try_into) + .collect::, EcashApiError>>()?) + } + async fn vote_proposal( &self, proposal_id: u64, vote_yes: bool, fee: Option, - ) -> Result<(), CoconutError> { + ) -> Result<(), EcashError> { nyxd_signing!(self, vote_proposal(proposal_id, vote_yes, fee).await?); Ok(()) } - async fn execute_proposal(&self, proposal_id: u64) -> crate::coconut::error::Result<()> { + async fn execute_proposal(&self, proposal_id: u64) -> crate::ecash::error::Result<()> { nyxd_signing!(self, execute_proposal(proposal_id, None).await?); Ok(()) } - async fn can_advance_epoch_state(&self) -> crate::coconut::error::Result { + async fn can_advance_epoch_state(&self) -> crate::ecash::error::Result { Ok(nyxd_query!(self, can_advance_state().await?.can_advance())) } - async fn advance_epoch_state(&self) -> crate::coconut::error::Result<()> { + async fn advance_epoch_state(&self) -> crate::ecash::error::Result<()> { nyxd_signing!(self, advance_dkg_epoch_state(None).await?); Ok(()) } @@ -548,7 +574,7 @@ impl crate::coconut::client::Client for Client { identity_key: IdentityKey, announce_address: String, resharing: bool, - ) -> Result { + ) -> Result { Ok(nyxd_signing!( self, register_dealer(bte_key, identity_key, announce_address, resharing, None).await? @@ -560,7 +586,7 @@ impl crate::coconut::client::Client for Client { dealing_index: DealingIndex, chunks: Vec, resharing: bool, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_signing!( self, submit_dealing_metadata(dealing_index, chunks, resharing, None).await? @@ -570,7 +596,7 @@ impl crate::coconut::client::Client for Client { async fn submit_dealing_chunk( &self, chunk: PartialContractDealing, - ) -> Result { + ) -> Result { Ok(nyxd_signing!( self, submit_dealing_chunk(chunk, None).await? @@ -581,7 +607,7 @@ impl crate::coconut::client::Client for Client { &self, share: VerificationKeyShare, resharing: bool, - ) -> crate::coconut::error::Result { + ) -> crate::ecash::error::Result { Ok(nyxd_signing!( self, submit_verification_key_share(share, resharing, None).await? diff --git a/nym-node/src/cli/commands/migrate.rs b/nym-node/src/cli/commands/migrate.rs index f44bdc399b..bf9e88d370 100644 --- a/nym-node/src/cli/commands/migrate.rs +++ b/nym-node/src/cli/commands/migrate.rs @@ -17,6 +17,7 @@ use nym_gateway::GatewayError; use nym_mixnode::MixnodeError; use nym_network_requester::{CustomGatewayDetails, GatewayDetails}; use nym_node::config; +use nym_node::config::entry_gateway::ZkNymTicketHandlerDebug; use nym_node::config::mixnode::DEFAULT_VERLOC_PORT; use nym_node::config::Config; use nym_node::config::{default_config_filepath, ConfigBuilder, NodeMode}; @@ -411,6 +412,16 @@ async fn migrate_gateway(mut args: Args) -> Result<(), NymNodeError> { announce_wss_port: cfg.gateway.clients_wss_port, debug: config::entry_gateway::Debug { message_retrieval_limit: cfg.debug.message_retrieval_limit, + zk_nym_tickets: ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: + cfg.debug.zk_nym_tickets.revocation_bandwidth_penalty, + pending_poller: cfg.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: cfg.debug.zk_nym_tickets.minimum_api_quorum, + minimum_redemption_tickets: + cfg.debug.zk_nym_tickets.minimum_redemption_tickets, + maximum_time_between_redemption: + cfg.debug.zk_nym_tickets.maximum_time_between_redemption, + }, }, }, )) diff --git a/nym-node/src/cli/commands/sign.rs b/nym-node/src/cli/commands/sign.rs index 67c44977cf..d22b3c9e28 100644 --- a/nym-node/src/cli/commands/sign.rs +++ b/nym-node/src/cli/commands/sign.rs @@ -69,8 +69,6 @@ fn print_signed_contract_msg( } pub async fn execute(args: Args) -> Result<(), NymNodeError> { - println!("args: {args:?}"); - let config = try_load_current_config(args.config.config_path()).await?; let identity_keypair = load_ed25519_identity_keypair(config.storage_paths.keys.ed25519_identity_storage_paths())?; diff --git a/nym-node/src/config/entry_gateway.rs b/nym-node/src/config/entry_gateway.rs index d4ef188da9..dd3670f42f 100644 --- a/nym-node/src/config/entry_gateway.rs +++ b/nym-node/src/config/entry_gateway.rs @@ -12,6 +12,7 @@ use nym_gateway::node::LocalAuthenticatorOpts; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::path::Path; +use std::time::Duration; use super::helpers::{base_client_config, EphemeralConfig}; use super::LocalWireguardOpts; @@ -48,9 +49,12 @@ pub struct EntryGatewayConfig { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] +#[serde(default)] pub struct Debug { /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. pub message_retrieval_limit: i64, + + pub zk_nym_tickets: ZkNymTicketHandlerDebug, } impl Debug { @@ -61,6 +65,52 @@ impl Default for Debug { fn default() -> Self { Debug { message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct ZkNymTicketHandlerDebug { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + #[serde(with = "humantime_serde")] + pub pending_poller: Duration, + + pub minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than 30 days. + #[serde(with = "humantime_serde")] + pub maximum_time_between_redemption: Duration, +} + +impl ZkNymTicketHandlerDebug { + pub const DEFAULT_REVOCATION_BANDWIDTH_PENALTY: f32 = 10.0; + pub const DEFAULT_PENDING_POLLER: Duration = Duration::from_secs(300); + pub const DEFAULT_MINIMUM_API_QUORUM: f32 = 0.8; + pub const DEFAULT_MINIMUM_REDEMPTION_TICKETS: usize = 100; + pub const DEFAULT_MAXIMUM_TIME_BETWEEN_REDEMPTION: Duration = Duration::from_secs(86400 * 25); +} + +impl Default for ZkNymTicketHandlerDebug { + fn default() -> Self { + ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: Self::DEFAULT_REVOCATION_BANDWIDTH_PENALTY, + pending_poller: Self::DEFAULT_PENDING_POLLER, + minimum_api_quorum: Self::DEFAULT_MINIMUM_API_QUORUM, + minimum_redemption_tickets: Self::DEFAULT_MINIMUM_REDEMPTION_TICKETS, + maximum_time_between_redemption: Self::DEFAULT_MAXIMUM_TIME_BETWEEN_REDEMPTION, } } } diff --git a/nym-outfox/tests/unittests.rs b/nym-outfox/tests/unittests.rs index 8c258c4b7f..08ffdd8a43 100644 --- a/nym-outfox/tests/unittests.rs +++ b/nym-outfox/tests/unittests.rs @@ -57,15 +57,21 @@ mod tests { ) .unwrap(); - assert!(new_buffer[mix_params.payload_range()] != buffer[mix_params.payload_range()]); - assert!(new_buffer[mix_params.routing_data_range()] != routing[..]); + assert_ne!( + new_buffer[mix_params.payload_range()], + buffer[mix_params.payload_range()] + ); + assert_ne!(new_buffer[mix_params.routing_data_range()], routing[..]); let _ = mix_params .decode_mix_layer(&mut new_buffer[..], &mix_secret) .unwrap(); - assert!(new_buffer[mix_params.payload_range()] == buffer[mix_params.payload_range()]); - assert!(new_buffer[mix_params.routing_data_range()] == routing[..]); + assert_eq!( + new_buffer[mix_params.payload_range()], + buffer[mix_params.payload_range()] + ); + assert_eq!(new_buffer[mix_params.routing_data_range()], routing[..]); } #[test] @@ -75,14 +81,14 @@ mod tests { let mut message_clone = message.clone(); lion_transform(&mut message_clone[..], &key, [1, 2, 3]).unwrap(); - assert!(message_clone[..] != message[..]); + assert_ne!(message_clone[..], message[..]); let mut message_clone_2 = message.clone(); lion_transform_encrypt(&mut message_clone_2, &key).unwrap(); assert_eq!(message_clone_2, message_clone); lion_transform(&mut message_clone[..], &key[..], [3, 2, 1]).unwrap(); - assert!(message_clone[..] == message[..]); + assert_eq!(message_clone[..], message[..]); } #[test] diff --git a/nym-validator-rewarder/Cargo.toml b/nym-validator-rewarder/Cargo.toml index 545d62b194..b8198fb7df 100644 --- a/nym-validator-rewarder/Cargo.toml +++ b/nym-validator-rewarder/Cargo.toml @@ -33,7 +33,8 @@ humantime-serde.workspace = true # internal nym-bin-common = { path = "../common/bin-common", features = ["output_format", "basic_tracing"] } nym-config = { path = "../common/config" } -nym-coconut = { path = "../common/nymcoconut" } +nym-ecash-time = { path = "../common/ecash-time" } +nym-compact-ecash = { path = "../common/nym_offline_compact_ecash" } nym-crypto = { path = "../common/crypto", features = ["asymmetric"] } nym-credentials = { path = "../common/credentials" } nym-network-defaults = { path = "../common/network-defaults" } diff --git a/nym-validator-rewarder/migrations/03_use_deposit_id.sql b/nym-validator-rewarder/migrations/03_use_deposit_id.sql new file mode 100644 index 0000000000..9c6aef5da3 --- /dev/null +++ b/nym-validator-rewarder/migrations/03_use_deposit_id.sql @@ -0,0 +1,28 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +DROP TABLE validated_deposit; +CREATE TABLE validated_deposit +( + operator_identity_bs58 TEXT NOT NULL, + credential_id INTEGER NOT NULL, + deposit_id INTEGER NOT NULL, + + signed_plaintext BLOB NOT NULL, + signature_bs58 TEXT NOT NULL +); + +-- evidence of attempting to re-use the same deposit id for multiple credentials +DROP TABLE double_signing_evidence; +CREATE TABLE double_signing_evidence +( + operator_identity_bs58 TEXT NOT NULL, + credential_id INTEGER NOT NULL, + original_credential_id INTEGER NOT NULL, + deposit_id INTEGER NOT NULL, + + signed_plaintext BLOB NOT NULL, + signature_bs58 TEXT NOT NULL +); \ No newline at end of file diff --git a/nym-validator-rewarder/src/error.rs b/nym-validator-rewarder/src/error.rs index cc7a756363..25807722d9 100644 --- a/nym-validator-rewarder/src/error.rs +++ b/nym-validator-rewarder/src/error.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::RewardingRatios; -use nym_coconut::CoconutError; +use nym_compact_ecash::error::CompactEcashError; use nym_crypto::asymmetric::ed25519; use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::tx::ErrorReport; -use nym_validator_client::nyxd::{AccountId, Coin, Hash}; +use nym_validator_client::nyxd::{AccountId, Coin}; use std::io; use std::path::PathBuf; use thiserror::Error; @@ -119,14 +119,14 @@ pub enum NymRewarderError { MalformedCredentialCommitment { raw: String, #[source] - source: CoconutError, + source: CompactEcashError, }, #[error("the partial verification key for runner {runner} is malformed: {source}")] MalformedPartialVerificationKey { runner: String, #[source] - source: CoconutError, + source: CompactEcashError, }, #[error("the signature on issued credential with id {credential_id} is invalid")] @@ -135,29 +135,26 @@ pub enum NymRewarderError { #[error("could not verify the blinded credential")] BlindVerificationFailure, - #[error("the same deposit transaction ({tx_hash}) has been used for multiple issued credentials! {first} and {other}")] - DuplicateDepositHash { - tx_hash: Hash, + #[error("the same deposit ({deposit_id}) has been used for multiple issued credentials! {first} and {other}")] + DuplicateDepositId { + deposit_id: u32, first: i64, other: i64, }, - #[error("could not find the deposit value in the event of transaction {tx_hash}")] - DepositValueNotFound { tx_hash: Hash }, + #[error("could not find the deposit details for deposit id {deposit_id}")] + DepositNotFound { deposit_id: u32 }, - #[error("could not find the deposit info in the event of transaction {tx_hash}")] - DepositInfoNotFound { tx_hash: Hash }, - - #[error("the provided deposit value of transaction {tx_hash} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] + #[error("the provided deposit value of deposit {deposit_id} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] InconsistentDepositValue { - tx_hash: Hash, + deposit_id: u32, request: Option, on_chain: String, }, - #[error("the provided deposit info of transaction {tx_hash} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] + #[error("the provided deposit info of deposit {deposit_id} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] InconsistentDepositInfo { - tx_hash: Hash, + deposit_id: u32, request: Option, on_chain: String, }, diff --git a/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs b/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs index 4df0395ec4..d497bd818a 100644 --- a/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs +++ b/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs @@ -11,11 +11,11 @@ use crate::rewarder::nyxd_client::NyxdClient; use crate::rewarder::storage::RewarderStorage; use bip39::rand::prelude::SliceRandom; use bip39::rand::thread_rng; -use nym_coconut::{ - hash_to_scalar, verify_partial_blind_signature, Base58, G1Projective, VerificationKey, -}; use nym_coconut_dkg_common::types::EpochId; -use nym_credentials::coconut::bandwidth::bandwidth_credential_params; +use nym_compact_ecash::scheme::expiration_date_signatures::date_scalar; +use nym_compact_ecash::scheme::withdrawal::verify_partial_blind_signature; +use nym_compact_ecash::{Base58, G1Projective, VerificationKeyAuth}; +use nym_credentials::ecash::utils::EcashTime; use nym_crypto::asymmetric::ed25519; use nym_task::TaskClient; use nym_validator_client::nym_api::{IssuedCredential, IssuedCredentialBody, NymApiClientExt}; @@ -69,10 +69,10 @@ impl CredentialIssuanceMonitor { credential_info: &IssuedCredentialBody, ) -> Result { let credential_id = credential_info.credential.id; - let deposit_tx = credential_info.credential.tx_hash; + let deposit_id = credential_info.credential.deposit_id; let prior_id = self .storage - .get_deposit_credential_id(issuer_identity.to_string(), deposit_tx.to_string()) + .get_deposit_credential_id(issuer_identity.to_string(), deposit_id) .await?; match prior_id { @@ -82,7 +82,7 @@ impl CredentialIssuanceMonitor { debug!("we have already verified this credential before"); Ok(true) } else { - error!("double signing detected!! used deposit {deposit_tx} for credentials {prior} and {credential_id}!!"); + error!("double signing detected!! used deposit {deposit_id} for credentials {prior} and {credential_id}!!"); self.storage .insert_double_signing_evidence( issuer_identity.to_string(), @@ -90,8 +90,8 @@ impl CredentialIssuanceMonitor { credential_info, ) .await?; - Err(NymRewarderError::DuplicateDepositHash { - tx_hash: deposit_tx, + Err(NymRewarderError::DuplicateDepositId { + deposit_id, first: prior, other: credential_id, }) @@ -105,48 +105,23 @@ impl CredentialIssuanceMonitor { issued_credential: &IssuedCredentialBody, ) -> Result<(), NymRewarderError> { // check if this deposit even exists - let deposit_tx = issued_credential.credential.tx_hash; + let deposit_id = issued_credential.credential.deposit_id; - let (deposit_value, deposit_info) = self - .nyxd_client - .get_deposit_transaction_attributes(deposit_tx) - .await?; + //not using value anymore, but it should still be there + let _ = self.nyxd_client.get_deposit_details(deposit_id).await?; trace!("deposit exists"); - // check if the deposit values match - let credential_value = issued_credential.credential.public_attributes.first(); - let credential_info = issued_credential.credential.public_attributes.get(1); - - if credential_value != Some(&deposit_value) { - return Err(NymRewarderError::InconsistentDepositValue { - tx_hash: deposit_tx, - request: credential_value.cloned(), - on_chain: deposit_value, - }); - } - trace!("credential values matches the deposit"); - - if credential_info != Some(&deposit_info) { - return Err(NymRewarderError::InconsistentDepositInfo { - tx_hash: deposit_tx, - request: credential_info.cloned(), - on_chain: deposit_info, - }); - } - trace!("credential info matches the deposit"); Ok(()) } fn verify_credential( &mut self, - vk: &VerificationKey, + vk: &VerificationKeyAuth, credential: &IssuedCredential, ) -> Result<(), NymRewarderError> { - let public_attributes = credential - .public_attributes - .iter() - .map(hash_to_scalar) - .collect::>(); + let public_attributes = [date_scalar( + credential.expiration_date.ecash_unix_timestamp(), + )]; #[allow(clippy::map_identity)] let attributes_refs = public_attributes.iter().collect::>(); @@ -168,7 +143,6 @@ impl CredentialIssuanceMonitor { // actually do verify the credential now if !verify_partial_blind_signature( - bandwidth_credential_params(), &public_attribute_commitments, &attributes_refs, &credential.blinded_partial_credential, @@ -181,7 +155,7 @@ impl CredentialIssuanceMonitor { Ok(()) } - #[instrument(skip_all, fields(credential_id = %issued_credential.credential.id, deposit = %issued_credential.credential.tx_hash))] + #[instrument(skip_all, fields(credential_id = %issued_credential.credential.id, deposit_id = %issued_credential.credential.deposit_id))] async fn validate_issued_credential( &mut self, issuer: &CredentialIssuer, diff --git a/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs b/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs index ce8b65b659..dd39df1cd5 100644 --- a/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs +++ b/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs @@ -6,7 +6,7 @@ use crate::rewarder::epoch::Epoch; use crate::rewarder::helpers::api_client; use crate::rewarder::nyxd_client::NyxdClient; use cosmwasm_std::{Addr, Decimal, Uint128}; -use nym_coconut::VerificationKey; +use nym_compact_ecash::VerificationKeyAuth; use nym_crypto::asymmetric::ed25519; use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nyxd::{AccountId, Coin}; @@ -341,7 +341,7 @@ pub struct CredentialIssuer { pub public_key: ed25519::PublicKey, pub operator_account: AccountId, pub api_runner: String, - pub verification_key: VerificationKey, + pub verification_key: VerificationKeyAuth, } // safety: we're converting between different wrappers for bech32 addresses diff --git a/nym-validator-rewarder/src/rewarder/nyxd_client.rs b/nym-validator-rewarder/src/rewarder/nyxd_client.rs index 540245b5bc..a75aaa0929 100644 --- a/nym-validator-rewarder/src/rewarder/nyxd_client.rs +++ b/nym-validator-rewarder/src/rewarder/nyxd_client.rs @@ -1,18 +1,17 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::config::Config; use crate::error::NymRewarderError; use crate::rewarder::credential_issuance::types::{addr_to_account_id, CredentialIssuer}; -use nym_coconut::{Base58, VerificationKey}; -use nym_coconut_bandwidth_contract_common::events::{ - COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO, DEPOSIT_VALUE, -}; use nym_coconut_dkg_common::types::Epoch; +use nym_compact_ecash::{Base58, VerificationKeyAuth}; use nym_crypto::asymmetric::ed25519; use nym_network_defaults::NymNetworkDetails; -use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; -use nym_validator_client::nyxd::helpers::find_tx_attribute; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::{Deposit, DepositId}; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, EcashQueryClient, PagedDkgQueryClient, +}; use nym_validator_client::nyxd::module_traits::staking::{ QueryHistoricalInfoResponse, QueryValidatorsResponse, }; @@ -110,7 +109,7 @@ impl NyxdClient { public_key: ed25519::PublicKey::from_base58_string(&info.ed25519_identity)?, operator_account: addr_to_account_id(share.owner), api_runner: share.announce_address, - verification_key: VerificationKey::try_from_bs58(share.share).map_err( + verification_key: VerificationKeyAuth::try_from_bs58(share.share).map_err( |source| NymRewarderError::MalformedPartialVerificationKey { runner: info.address.to_string(), source, @@ -123,22 +122,12 @@ impl NyxdClient { Ok(issuers) } - pub(crate) async fn get_deposit_transaction_attributes( + pub(crate) async fn get_deposit_details( &self, - tx_hash: Hash, - ) -> Result<(String, String), NymRewarderError> { - let tx = self.inner.read().await.get_tx(tx_hash).await?; - - // todo: we need to make it more concrete that the first attribute is the deposit value - // and the second one is the deposit info - let deposit_value = - find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_VALUE) - .ok_or(NymRewarderError::DepositValueNotFound { tx_hash })?; - - let deposit_info = - find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO) - .ok_or(NymRewarderError::DepositInfoNotFound { tx_hash })?; - - Ok((deposit_value, deposit_info)) + deposit_id: DepositId, + ) -> Result { + let res = self.inner.read().await.get_deposit(deposit_id).await?; + res.deposit + .ok_or(NymRewarderError::DepositNotFound { deposit_id }) } } diff --git a/nym-validator-rewarder/src/rewarder/storage/manager.rs b/nym-validator-rewarder/src/rewarder/storage/manager.rs index 453668c3ef..3efa3d9308 100644 --- a/nym-validator-rewarder/src/rewarder/storage/manager.rs +++ b/nym-validator-rewarder/src/rewarder/storage/manager.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::rewarder::epoch::Epoch; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; #[derive(Clone)] pub(crate) struct StorageManager { @@ -186,7 +187,7 @@ impl StorageManager { &self, operator_identity_bs58: String, credential_id: i64, - deposit_tx: String, + deposit_id: DepositId, signed_plaintext: Vec, signature_bs58: String, ) -> Result<(), sqlx::Error> { @@ -195,14 +196,14 @@ impl StorageManager { INSERT INTO validated_deposit ( operator_identity_bs58, credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) VALUES (?, ?, ?, ?, ?) "#, operator_identity_bs58, credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) @@ -214,16 +215,16 @@ impl StorageManager { pub(crate) async fn get_deposit_credential_id( &self, operator_identity_bs58: String, - deposit_tx: String, + deposit_id: DepositId, ) -> Result, sqlx::Error> { Ok(sqlx::query!( r#" SELECT credential_id FROM validated_deposit - WHERE operator_identity_bs58 = ? AND deposit_tx = ? + WHERE operator_identity_bs58 = ? AND deposit_id = ? "#, operator_identity_bs58, - deposit_tx + deposit_id ) .fetch_optional(&self.connection_pool) .await? @@ -235,7 +236,7 @@ impl StorageManager { operator_identity_bs58: String, credential_id: i64, original_credential_id: i64, - deposit_tx: String, + deposit_id: DepositId, signed_plaintext: Vec, signature_bs58: String, ) -> Result<(), sqlx::Error> { @@ -245,7 +246,7 @@ impl StorageManager { operator_identity_bs58, credential_id, original_credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) VALUES (?, ?, ?, ?, ?, ?) @@ -253,7 +254,7 @@ impl StorageManager { operator_identity_bs58, credential_id, original_credential_id, - deposit_tx, + deposit_id, signed_plaintext, signature_bs58 ) diff --git a/nym-validator-rewarder/src/rewarder/storage/mod.rs b/nym-validator-rewarder/src/rewarder/storage/mod.rs index c0463700d0..20969b7c93 100644 --- a/nym-validator-rewarder/src/rewarder/storage/mod.rs +++ b/nym-validator-rewarder/src/rewarder/storage/mod.rs @@ -7,6 +7,7 @@ use crate::rewarder::epoch::Epoch; use crate::rewarder::storage::manager::StorageManager; use crate::rewarder::{EpochRewards, RewardingResult}; use nym_validator_client::nym_api::IssuedCredentialBody; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; use nym_validator_client::nyxd::Coin; use sqlx::ConnectOptions; use std::fmt::Debug; @@ -83,11 +84,11 @@ impl RewarderStorage { pub(crate) async fn get_deposit_credential_id( &self, operator_identity_bs58: String, - deposit_tx: String, + deposit_id: DepositId, ) -> Result, NymRewarderError> { Ok(self .manager - .get_deposit_credential_id(operator_identity_bs58, deposit_tx) + .get_deposit_credential_id(operator_identity_bs58, deposit_id) .await?) } @@ -100,7 +101,7 @@ impl RewarderStorage { .insert_validated_deposit( operator_identity_bs58, credential_info.credential.id, - credential_info.credential.tx_hash.to_string(), + credential_info.credential.deposit_id, credential_info.credential.signable_plaintext(), credential_info.signature.to_base58_string(), ) @@ -119,7 +120,7 @@ impl RewarderStorage { operator_identity_bs58, credential_info.credential.id, original_credential_id, - credential_info.credential.tx_hash.to_string(), + credential_info.credential.deposit_id, credential_info.credential.signable_plaintext(), credential_info.signature.to_base58_string(), ) diff --git a/nym-wallet/nym-wallet-types/src/network/qa.rs b/nym-wallet/nym-wallet-types/src/network/qa.rs index 46f33218e2..4fc4908196 100644 --- a/nym-wallet/nym-wallet-types/src/network/qa.rs +++ b/nym-wallet/nym-wallet-types/src/network/qa.rs @@ -48,9 +48,7 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), - coconut_bandwidth_contract_address: parse_optional_str( - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ), + ecash_contract_address: parse_optional_str(COCONUT_BANDWIDTH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index 41a21bdad4..d423854a02 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -48,9 +48,7 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), - coconut_bandwidth_contract_address: parse_optional_str( - COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - ), + ecash_contract_address: parse_optional_str(COCONUT_BANDWIDTH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 75a4046264..cbe431829b 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -50,7 +50,7 @@ base64 = "0.13" zeroize = { version = "1.5", features = ["zeroize_derive", "serde"] } cosmwasm-std = "1.3.0" -cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" } +cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev = "4b1332e6d8258ac845cef71589c8d362a669675a" } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index b657db8570..09dac6dc5e 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -29,6 +29,7 @@ bytecodec = { workspace = true } httpcodec = { workspace = true } bytes = { workspace = true } http = { workspace = true } +zeroize = { workspace = true } futures = { workspace = true } log = { workspace = true } diff --git a/sdk/rust/nym-sdk/examples/bandwidth.rs b/sdk/rust/nym-sdk/examples/bandwidth.rs index 96a064cf1d..36858ae654 100644 --- a/sdk/rust/nym-sdk/examples/bandwidth.rs +++ b/sdk/rust/nym-sdk/examples/bandwidth.rs @@ -18,10 +18,10 @@ async fn main() -> anyhow::Result<()> { .enable_credentials_mode() .build()?; - let bandwidth_client = mixnet_client.create_bandwidth_client(mnemonic)?; + let bandwidth_client = mixnet_client.create_bandwidth_client(mnemonic).await?; - // Get a bandwidth credential worth 1000000 unym for the mixnet_client - bandwidth_client.acquire(1000000).await?; + // Get a bandwidth credential for the mixnet_client + bandwidth_client.acquire().await?; // Connect using paid bandwidth credential let mut client = mixnet_client.connect_to_mixnet().await?; diff --git a/sdk/rust/nym-sdk/examples/simple.rs b/sdk/rust/nym-sdk/examples/simple.rs index 72b6568b6d..00ad24dc9a 100644 --- a/sdk/rust/nym-sdk/examples/simple.rs +++ b/sdk/rust/nym-sdk/examples/simple.rs @@ -6,6 +6,7 @@ async fn main() { nym_bin_common::logging::setup_logging(); // Passing no config makes the client fire up an ephemeral session and figure shit out on its own + // let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); // Be able to get our client address diff --git a/sdk/rust/nym-sdk/src/bandwidth.rs b/sdk/rust/nym-sdk/src/bandwidth.rs index b1548dcb96..de4272ef6b 100644 --- a/sdk/rust/nym-sdk/src/bandwidth.rs +++ b/sdk/rust/nym-sdk/src/bandwidth.rs @@ -15,10 +15,10 @@ //! .build() //! .unwrap(); //! -//! let bandwidth_client = mixnet_client.create_bandwidth_client(String::from("my super secret mnemonic")).unwrap(); +//! let bandwidth_client = mixnet_client.create_bandwidth_client(String::from("my super secret mnemonic")).await.unwrap(); //! -//! // Get a bandwidth credential worth 1000000 unym for the mixnet_client -//! bandwidth_client.acquire(1000000).await.unwrap(); +//! // Get a bandwidth credential for the mixnet_client +//! bandwidth_client.acquire().await.unwrap(); //! //! // Connect using paid bandwidth credential //! let mut client = mixnet_client.connect_to_mixnet().await.unwrap(); @@ -41,4 +41,4 @@ mod client; -pub use client::{BandwidthAcquireClient, VoucherBlob}; +pub use client::BandwidthAcquireClient; diff --git a/sdk/rust/nym-sdk/src/bandwidth/client.rs b/sdk/rust/nym-sdk/src/bandwidth/client.rs index 5051e5a1d4..162866452a 100644 --- a/sdk/rust/nym-sdk/src/bandwidth/client.rs +++ b/sdk/rust/nym-sdk/src/bandwidth/client.rs @@ -1,18 +1,12 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::{Error, Result}; -use nym_bandwidth_controller::acquire::state::State; +use crate::error::Result; use nym_credential_storage::storage::Storage; -use nym_credentials::coconut::bandwidth::IssuanceBandwidthCredential; +use nym_credential_utils::utils::issue_credential; use nym_network_defaults::NymNetworkDetails; -use nym_validator_client::nyxd::Coin; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; - -/// The serialized version of the yet untransformed bandwidth voucher. It can be used to complete -/// the acquirement process of a bandwidth credential. -/// Its serialized nature makes it easy to store and load it to e.g. disk. -pub type VoucherBlob = Vec; +use zeroize::Zeroizing; /// Represents a client that can be used to acquire bandwidth. You typically create one when you /// want to connect to the mixnet using paid coconut bandwidth credentials. @@ -20,9 +14,9 @@ pub type VoucherBlob = Vec; /// [`crate::mixnet::DisconnectedMixnetClient::create_bandwidth_client`] on the associated mixnet /// client. pub struct BandwidthAcquireClient<'a, St: Storage> { - network_details: NymNetworkDetails, client: DirectSigningHttpRpcNyxdClient, storage: &'a St, + client_id: Zeroizing, } impl<'a, St> BandwidthAcquireClient<'a, St> @@ -34,6 +28,7 @@ where network_details: NymNetworkDetails, mnemonic: String, storage: &'a St, + client_id: String, ) -> Result { let nyxd_url = network_details.endpoints[0].nyxd_url.as_str(); let config = nyxd::Config::try_from_nym_network_details(&network_details)?; @@ -44,40 +39,14 @@ where mnemonic.parse()?, )?; Ok(Self { - network_details, client, storage, + client_id: client_id.into(), }) } - /// Buy a credential worth amount utokens. If [`Error::UnconvertedDeposit`] is returned, it - /// means the tokens have been deposited, but the proper bandwidth credential hasn't yet been - /// created. A [`VoucherBlob`] is returned that can be used for a later recovery of the - /// associated bandwidth credential, using [`Self::recover`]. - pub async fn acquire(&self, amount: u128) -> Result<()> { - let amount = Coin::new(amount, &self.network_details.chain_details.mix_denom.base); - let state = nym_bandwidth_controller::acquire::deposit(&self.client, amount).await?; - nym_bandwidth_controller::acquire::get_bandwidth_voucher(&state, &self.client, self.storage) - .await - .map_err(|reason| Error::UnconvertedDeposit { - reason, - voucher_blob: state.voucher.to_recovery_bytes(), - }) - } - - /// In case of an error in the mid of the acquire process, this function should be used for - /// later retries to recover the bandwidth credential, either immediately or after some time. - pub async fn recover(&self, voucher_blob: &VoucherBlob) -> Result<()> { - let voucher = IssuanceBandwidthCredential::try_from_recovered_bytes(voucher_blob) - .map_err(|_| Error::InvalidVoucherBlob)?; - let state = State::new(voucher); - nym_bandwidth_controller::acquire::get_bandwidth_voucher( - &state, - &self.client, - self.storage, - ) - .await?; - + pub async fn acquire(&self) -> Result<()> { + issue_credential(&self.client, self.storage, self.client_id.as_bytes()).await?; Ok(()) } } diff --git a/sdk/rust/nym-sdk/src/error.rs b/sdk/rust/nym-sdk/src/error.rs index e47b8b784f..7727583529 100644 --- a/sdk/rust/nym-sdk/src/error.rs +++ b/sdk/rust/nym-sdk/src/error.rs @@ -55,15 +55,6 @@ pub enum Error { #[error("socks5 channel could not be started")] Socks5NotStarted, - #[error( - "deposited funds were not converted to a deposit - {reason}; the voucher blob can be used for \ - later retry" - )] - UnconvertedDeposit { - reason: nym_bandwidth_controller::error::BandwidthControllerError, - voucher_blob: crate::bandwidth::VoucherBlob, - }, - #[error("bandwidth controller error: {0}")] BandwidthControllerError(#[from] nym_bandwidth_controller::error::BandwidthControllerError), @@ -88,6 +79,12 @@ pub enum Error { source: Box, }, + #[error(transparent)] + CredentialIssuanceError { + #[from] + source: nym_credential_utils::Error, + }, + #[error("loaded shared gateway key without providing information about what gateway it corresponds to")] GatewayWithUnknownEndpoint, diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index 04c03659c0..6ecf8b2cc3 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -65,7 +65,7 @@ pub use nym_client_core::{ }; pub use nym_credential_storage::{ ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage, - models::StoredIssuedCredential, storage::Storage as CredentialStorage, + models::StoredIssuedTicketbook, storage::Storage as CredentialStorage, }; pub use nym_network_defaults::NymNetworkDetails; pub use nym_socks5_client_core::config::Socks5; diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 52bfa59883..c399538759 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -551,17 +551,29 @@ where /// Creates an associated [`BandwidthAcquireClient`] that can be used to acquire bandwidth /// credentials for this client to consume. - pub fn create_bandwidth_client( + pub async fn create_bandwidth_client( &self, mnemonic: String, ) -> Result> { if !self.config.enabled_credentials_mode { return Err(Error::DisabledCredentialsMode); } + let client_id = self + .storage + .key_store() + .load_keys() + .await + .map_err(|e| Error::KeyStorageError { + source: Box::new(e), + })? + .identity_keypair() + .private_key() + .to_base58_string(); BandwidthAcquireClient::new( self.config.network_details.clone(), mnemonic, self.storage.credential_store(), + client_id, ) } diff --git a/service-providers/ip-packet-router/src/cli/mod.rs b/service-providers/ip-packet-router/src/cli/mod.rs index 3de91de6be..d269d5045d 100644 --- a/service-providers/ip-packet-router/src/cli/mod.rs +++ b/service-providers/ip-packet-router/src/cli/mod.rs @@ -15,6 +15,7 @@ mod import_credential; mod init; mod list_gateways; mod run; +mod show_ticketbooks; mod sign; mod switch_gateway; @@ -76,6 +77,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Sign to prove ownership of this network requester Sign(sign::Sign), @@ -132,6 +136,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), IpPacketRouterError> { Commands::ListGateways(args) => list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::Sign(m) => sign::execute(&m).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), diff --git a/service-providers/ip-packet-router/src/cli/show_ticketbooks.rs b/service-providers/ip-packet-router/src/cli/show_ticketbooks.rs new file mode 100644 index 0000000000..20c58a412c --- /dev/null +++ b/service-providers/ip-packet-router/src/cli/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliIpPacketRouterClient; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; +use nym_ip_packet_router::error::IpPacketRouterError; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), IpPacketRouterError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/service-providers/network-requester/src/cli/mod.rs b/service-providers/network-requester/src/cli/mod.rs index d15d0e1621..11ef00cfd8 100644 --- a/service-providers/network-requester/src/cli/mod.rs +++ b/service-providers/network-requester/src/cli/mod.rs @@ -22,6 +22,7 @@ mod import_credential; mod init; mod list_gateways; mod run; +mod show_ticketbooks; mod sign; mod switch_gateway; @@ -86,6 +87,9 @@ pub(crate) enum Commands { /// Change the currently active gateway. Note that you must have already registered with the new gateway! SwitchGateway(switch_gateway::Args), + /// Display information associated with the imported ticketbooks, + ShowTicketbooks(show_ticketbooks::Args), + /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -152,6 +156,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), NetworkRequesterError> { Commands::ListGateways(args) => list_gateways::execute(args).await?, Commands::AddGateway(args) => add_gateway::execute(args).await?, Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, + Commands::ShowTicketbooks(args) => show_ticketbooks::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), diff --git a/service-providers/network-requester/src/cli/show_ticketbooks.rs b/service-providers/network-requester/src/cli/show_ticketbooks.rs new file mode 100644 index 0000000000..b0e7f2febd --- /dev/null +++ b/service-providers/network-requester/src/cli/show_ticketbooks.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliNetworkRequesterClient; +use crate::error::NetworkRequesterError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_show_ticketbooks::{ + show_ticketbooks, CommonShowTicketbooksArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonShowTicketbooksArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonShowTicketbooksArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkRequesterError> { + let output = args.output; + let res = show_ticketbooks::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/tools/internal/testnet-manager/Cargo.toml b/tools/internal/testnet-manager/Cargo.toml new file mode 100644 index 0000000000..d8c52671df --- /dev/null +++ b/tools/internal/testnet-manager/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "testnet-manager" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +bip39.workspace = true +bs58.workspace = true +console = "0.15.8" +cw-utils.workspace = true +clap = { workspace = true, features = ["cargo", "derive"] } +indicatif = "0.17.8" +rand.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] } +tempfile = { workspace = true } +thiserror.workspace = true +time = { workspace = true, features = ["parsing", "formatting"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "process"] } +toml = "0.8.14" +tracing.workspace = true +url.workspace = true +zeroize = { workspace = true, features = ["zeroize_derive"] } + + +nym-bin-common = { path = "../../../common/bin-common", features = ["output_format", "basic_tracing"] } +nym-crypto = { path = "../../../common/crypto", features = ["asymmetric", "rand", "serde"] } +nym-config = { path = "../../../common/config" } +nym-validator-client = { path = "../../../common/client-libs/validator-client" } +nym-compact-ecash = { path = "../../../common/nym_offline_compact_ecash" } +dkg-bypass-contract = { path = "dkg-bypass-contract" } + +# contracts: +nym-mixnet-contract-common = { path = "../../../common/cosmwasm-smart-contracts/mixnet-contract" } +nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common" } +nym-vesting-contract-common = { path = "../../../common/cosmwasm-smart-contracts/vesting-contract" } +nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/group-contract" } +nym-ecash-contract-common = { path = "../../../common/cosmwasm-smart-contracts/ecash-contract" } +nym-coconut-dkg-common = { path = "../../../common/cosmwasm-smart-contracts/coconut-dkg" } +nym-multisig-contract-common = { path = "../../../common/cosmwasm-smart-contracts/multisig-contract" } +nym-pemstore = { path = "../../../common/pemstore" } + + +[build-dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } diff --git a/tools/internal/testnet-manager/Makefile b/tools/internal/testnet-manager/Makefile new file mode 100644 index 0000000000..74399c646a --- /dev/null +++ b/tools/internal/testnet-manager/Makefile @@ -0,0 +1,2 @@ +build-bypass-contract: + $(MAKE) -C dkg-bypass-contract build \ No newline at end of file diff --git a/tools/internal/testnet-manager/build.rs b/tools/internal/testnet-manager/build.rs new file mode 100644 index 0000000000..cdd97f9505 --- /dev/null +++ b/tools/internal/testnet-manager/build.rs @@ -0,0 +1,25 @@ +use sqlx::{Connection, SqliteConnection}; +use std::env; + +#[tokio::main] +async fn main() { + let out_dir = env::var("OUT_DIR").unwrap(); + let database_path = format!("{}/nym-api-example.sqlite", out_dir); + + let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + .await + .expect("Failed to create SQLx database connection"); + + sqlx::migrate!("./migrations") + .run(&mut conn) + .await + .expect("Failed to perform SQLx migrations"); + + #[cfg(target_family = "unix")] + println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); + + #[cfg(target_family = "windows")] + // for some strange reason we need to add a leading `/` to the windows path even though it's + // not a valid windows path... but hey, it works... + println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); +} diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml b/tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml new file mode 100644 index 0000000000..fdd8eac22b --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "dkg-bypass-contract" +version = "0.1.0" +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-storage = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } + +nym-coconut-dkg-common = { path = "../../../../common/cosmwasm-smart-contracts/coconut-dkg" } +nym-contracts-common = { path = "../../../../common/cosmwasm-smart-contracts/contracts-common" } \ No newline at end of file diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/Makefile b/tools/internal/testnet-manager/dkg-bypass-contract/Makefile new file mode 100644 index 0000000000..2aa57e5ef9 --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/Makefile @@ -0,0 +1,5 @@ +all: build + +build: + RUSTFLAGS='-C link-arg=-s' cargo build --release --lib --target wasm32-unknown-unknown + wasm-opt --signext-lowering -O ../../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm -o ../../../../target/wasm32-unknown-unknown/release/dkg_bypass_contract.wasm \ No newline at end of file diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs b/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs new file mode 100644 index 0000000000..7a454c60bb --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs @@ -0,0 +1,137 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::msg::MigrateMsg; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + entry_point, Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdError, + StdResult, Storage, +}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; +use nym_coconut_dkg_common::dealer::DealerRegistrationDetails; +use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState, NodeIndex}; +use nym_coconut_dkg_common::verification_key::ContractVKShare; + +pub(crate) type Dealer<'a> = &'a Addr; + +pub(crate) const CURRENT_EPOCH: Item<'_, Epoch> = Item::new("current_epoch"); + +pub const THRESHOLD: Item = Item::new("threshold"); + +pub const EPOCH_THRESHOLDS: Map = Map::new("epoch_thresholds"); + +pub(crate) const NODE_INDEX_COUNTER: Item = Item::new("node_index_counter"); + +// use the same storage types as the actual DKG contract +pub(crate) const DEALERS_INDICES: Map = Map::new("dealer_index"); + +pub(crate) const EPOCH_DEALERS_MAP: Map<(EpochId, Dealer), DealerRegistrationDetails> = + Map::new("epoch_dealers"); + +type VKShareKey<'a> = (&'a Addr, EpochId); + +pub(crate) struct VkShareIndex<'a> { + pub(crate) epoch_id: MultiIndex<'a, EpochId, ContractVKShare, VKShareKey<'a>>, +} + +impl<'a> IndexList for VkShareIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.epoch_id]; + Box::new(v.into_iter()) + } +} + +pub(crate) fn vk_shares<'a>() -> IndexedMap<'a, VKShareKey<'a>, ContractVKShare, VkShareIndex<'a>> { + let indexes = VkShareIndex { + epoch_id: MultiIndex::new(|_pk, d| d.epoch_id, "vksp", "vkse"), + }; + IndexedMap::new("vksp", indexes) +} + +pub(crate) fn next_node_index(store: &mut dyn Storage) -> StdResult { + // make sure we don't start from 0, otherwise all the crypto breaks (kinda) + let id: NodeIndex = NODE_INDEX_COUNTER.may_load(store)?.unwrap_or_default() + 1; + NODE_INDEX_COUNTER.save(store, &id)?; + Ok(id) +} + +#[cw_serde] +pub enum EmptyMessage {} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + _: DepsMut<'_>, + _: Env, + _: MessageInfo, + _: EmptyMessage, +) -> Result { + Ok(Response::new()) +} + +/// Handle an incoming message +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + _: DepsMut<'_>, + _: Env, + _: MessageInfo, + _: EmptyMessage, +) -> Result { + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_: Deps<'_>, _: Env, _: EmptyMessage) -> Result { + Ok(Default::default()) +} + +// LIMITATION: we're not storing dealings themselves +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result { + // on migration immediately attempt to rewrite the storage + let threshold = (2 * msg.dealers.len() as u64 + 3 - 1) / 3; + let epoch = CURRENT_EPOCH.load(deps.storage)?; + assert_eq!(0, epoch.epoch_id); + + // set epoch data + THRESHOLD.save(deps.storage, &threshold)?; + EPOCH_THRESHOLDS.save(deps.storage, 0, &threshold)?; + let duration = epoch + .time_configuration + .state_duration(EpochState::InProgress); + + CURRENT_EPOCH.save( + deps.storage, + &Epoch { + state: EpochState::InProgress, + epoch_id: 0, + state_progress: Default::default(), + time_configuration: epoch.time_configuration, + deadline: duration.map(|d| env.block.time.plus_seconds(d)), + }, + )?; + + // set dealer data + for dealer in msg.dealers { + let node_index = next_node_index(deps.storage)?; + DEALERS_INDICES.save(deps.storage, &dealer.owner, &node_index)?; + + let registration_details = DealerRegistrationDetails { + bte_public_key_with_proof: "fakekey".to_string(), + ed25519_identity: dealer.ed25519_identity, + announce_address: dealer.announce.clone(), + }; + let vk_share = ContractVKShare { + share: dealer.vk, + announce_address: dealer.announce, + node_index, + owner: dealer.owner.clone(), + epoch_id: 0, + verified: true, + }; + + EPOCH_DEALERS_MAP.save(deps.storage, (0, &dealer.owner), ®istration_details)?; + vk_shares().save(deps.storage, (&dealer.owner, 0), &vk_share)?; + } + + Ok(Response::new()) +} diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/src/lib.rs b/tools/internal/testnet-manager/dkg-bypass-contract/src/lib.rs new file mode 100644 index 0000000000..69475aa119 --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/src/lib.rs @@ -0,0 +1,4 @@ +pub mod contract; +pub mod msg; + +pub use msg::MigrateMsg; diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/src/msg.rs b/tools/internal/testnet-manager/dkg-bypass-contract/src/msg.rs new file mode 100644 index 0000000000..fbdc165d69 --- /dev/null +++ b/tools/internal/testnet-manager/dkg-bypass-contract/src/msg.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; +use nym_coconut_dkg_common::verification_key::VerificationKeyShare; + +#[cw_serde] +pub struct FakeDealerData { + pub vk: VerificationKeyShare, + pub ed25519_identity: String, + pub announce: String, + pub owner: Addr, +} + +#[cw_serde] +pub struct MigrateMsg { + pub dealers: Vec, +} diff --git a/tools/internal/testnet-manager/migrations/01_initial_tables.sql b/tools/internal/testnet-manager/migrations/01_initial_tables.sql new file mode 100644 index 0000000000..6b621e2c99 --- /dev/null +++ b/tools/internal/testnet-manager/migrations/01_initial_tables.sql @@ -0,0 +1,40 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE metadata ( + id INTEGER PRIMARY KEY CHECK (id = 0), + latest_network_id INTEGER REFERENCES network(id), + + master_mnemonic TEXT NOT NULL, + rpc_endpoint TEXT NOT NULL +); + +CREATE TABLE network ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + mixnet_contract_id INTEGER NOT NULL REFERENCES contract(id), + vesting_contract_id INTEGER NOT NULL REFERENCES contract(id), + ecash_contract_id INTEGER NOT NULL REFERENCES contract(id), + cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract(id), + cw4_group_contract_id INTEGER NOT NULL REFERENCES contract(id), + dkg_contract_id INTEGER NOT NULL REFERENCES contract(id), + + rewarder_address TEXT NOT NULL REFERENCES account(address), + ecash_holding_account_address TEXT NOT NULL REFERENCES account(address) +); + +CREATE TABLE contract ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + address TEXT NOT NULL, + admin_address TEXT NOT NULL REFERENCES account(address) +); + +CREATE TABLE account ( + address TEXT NOT NULL UNIQUE, + mnemonic TEXT NOT NULL +); \ No newline at end of file diff --git a/tools/internal/testnet-manager/src/cli/build_info.rs b/tools/internal/testnet-manager/src/cli/build_info.rs new file mode 100644 index 0000000000..ed3d7ecadb --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/build_info.rs @@ -0,0 +1,17 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use nym_bin_common::bin_info_owned; +use nym_bin_common::output_format::OutputFormat; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) fn execute(args: Args) -> Result<(), NetworkManagerError> { + println!("{}", args.output.format(&bin_info_owned!())); + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/bypass_dkg.rs b/tools/internal/testnet-manager/src/cli/bypass_dkg.rs new file mode 100644 index 0000000000..a938e73d47 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/bypass_dkg.rs @@ -0,0 +1,50 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use crate::helpers::default_storage_dir; +use std::path::PathBuf; +use url::Url; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + #[clap(long)] + signer_data_output_directory: Option, + + #[clap(long)] + network_name: Option, + + /// The URLs of that the DKG parties would have put in the contract + #[clap(long, value_delimiter = ',')] + api_endpoints: Vec, + + /// Path to the contract built from the `dkg-bypass-contract` directory + #[clap(long)] + bypass_dkg_contract: PathBuf, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + let network = manager.load_existing_network(args.network_name).await?; + + let signer_data_output_directory = if let Some(explicit) = args.signer_data_output_directory { + explicit + } else { + default_storage_dir().join(&network.name) + }; + + manager + .attempt_bypass_dkg( + args.api_endpoints, + &network, + args.bypass_dkg_contract, + signer_data_output_directory, + ) + .await?; + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/initialise_new_network.rs b/tools/internal/testnet-manager/src/cli/initialise_new_network.rs new file mode 100644 index 0000000000..db9e4fd4ec --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/initialise_new_network.rs @@ -0,0 +1,48 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use std::time::Duration; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path containing .wasm files of all contracts + #[clap(long)] + built_contracts: PathBuf, + + #[clap(long)] + network_name: Option, + + /// Specifies custom duration of mixnet epochs + #[clap(long)] + custom_epoch_duration_secs: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let network = args + .common + .network_manager() + .await? + .initialise_new_network( + args.built_contracts, + args.network_name, + args.custom_epoch_duration_secs.map(Duration::from_secs), + ) + .await?; + + println!( + "add the following to your .env file: \n{}", + network.unchecked_to_env_file_section() + ); + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs b/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs new file mode 100644 index 0000000000..47e59a8c11 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs @@ -0,0 +1,76 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use crate::helpers::default_storage_dir; +use crate::manager::network::LoadedNetwork; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use std::time::Duration; +use url::Url; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path containing .wasm files of all contracts + #[clap(long)] + built_contracts: PathBuf, + + #[clap(long)] + network_name: Option, + + #[clap(long)] + signer_data_output_directory: Option, + + /// The URLs of that the DKG parties would have put in the contract + #[clap(long, value_delimiter = ',')] + api_endpoints: Vec, + + /// Path to the contract built from the `dkg-bypass-contract` directory + #[clap(long)] + bypass_dkg_contract: PathBuf, + + /// Specifies custom duration of mixnet epochs + #[clap(long)] + custom_epoch_duration_secs: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + + let network: LoadedNetwork = manager + .initialise_new_network( + args.built_contracts, + args.network_name, + args.custom_epoch_duration_secs.map(Duration::from_secs), + ) + .await? + .into(); + + let signer_data_output_directory = if let Some(explicit) = args.signer_data_output_directory { + explicit + } else { + default_storage_dir().join(&network.name) + }; + + let env = network.to_env_file_section(); + + manager + .attempt_bypass_dkg( + args.api_endpoints, + &network, + args.bypass_dkg_contract, + signer_data_output_directory, + ) + .await?; + + println!("add the following to your .env file: \n{env}",); + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/load_network_details.rs b/tools/internal/testnet-manager/src/cli/load_network_details.rs new file mode 100644 index 0000000000..32aa708e2d --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/load_network_details.rs @@ -0,0 +1,36 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::default_db_file; +use crate::manager::NetworkManager; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(long)] + network_name: Option, + + #[clap(long)] + storage_path: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let storage = args.storage_path.unwrap_or_else(default_db_file); + + let network = NetworkManager::new(storage, None, None) + .await? + .load_existing_network(args.network_name) + .await?; + + println!( + "add the following to your .env file: \n{}", + network.to_env_file_section() + ); + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/local_client.rs b/tools/internal/testnet-manager/src/cli/local_client.rs new file mode 100644 index 0000000000..f1ddc2fdc1 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/local_client.rs @@ -0,0 +1,38 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path to the `nym-client` binary + #[clap(long)] + nym_client_bin: PathBuf, + + #[clap(long)] + network_name: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + let network = manager.load_existing_network(args.network_name).await?; + + let run_cmd = manager + .init_local_nym_client(args.nym_client_bin, &network) + .await?; + + if !args.output.is_text() { + args.output.to_stderr(&run_cmd) + } + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs b/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs new file mode 100644 index 0000000000..f0f9a4475a --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs @@ -0,0 +1,80 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use crate::manager::network::LoadedNetwork; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; +use std::time::Duration; +use tempfile::tempdir; +use url::Url; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path to the `nym-api` binary + #[clap(long)] + nym_api_bin: PathBuf, + + /// Path containing .wasm files of all contracts + #[clap(long)] + built_contracts: PathBuf, + + #[clap(long)] + number_of_apis: usize, + + #[clap(long)] + network_name: Option, + + /// Path to the contract built from the `dkg-bypass-contract` directory + #[clap(long)] + bypass_dkg_contract: PathBuf, + + /// Specifies custom duration of mixnet epochs + #[clap(long)] + custom_epoch_duration_secs: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let endpoints = (0..args.number_of_apis) + .map(|i| format!("http://127.0.0.1:{}", 10000 + i).parse().unwrap()) + .collect::>(); + + let manager = args.common.network_manager().await?; + + let network: LoadedNetwork = manager + .initialise_new_network( + args.built_contracts, + args.network_name, + args.custom_epoch_duration_secs.map(Duration::from_secs), + ) + .await? + .into(); + + let temp_output = tempdir()?; + + let signer_details = manager + .attempt_bypass_dkg( + endpoints, + &network, + args.bypass_dkg_contract, + temp_output.path(), + ) + .await?; + + let run_cmds = manager + .setup_local_apis(args.nym_api_bin, &network, signer_details) + .await?; + + if !args.output.is_text() { + args.output.to_stderr(&run_cmds) + } + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/local_nodes.rs b/tools/internal/testnet-manager/src/cli/local_nodes.rs new file mode 100644 index 0000000000..54d26fd6fb --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/local_nodes.rs @@ -0,0 +1,38 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CommonArgs; +use crate::error::NetworkManagerError; +use nym_bin_common::output_format::OutputFormat; +use std::path::PathBuf; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(flatten)] + common: CommonArgs, + + /// Path to the `nym-node` binary + #[clap(long)] + nym_node_bin: PathBuf, + + #[clap(long)] + network_name: Option, + + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { + let manager = args.common.network_manager().await?; + let network = manager.load_existing_network(args.network_name).await?; + + let run_cmds = manager + .init_local_nym_nodes(args.nym_node_bin, &network) + .await?; + + if !args.output.is_text() { + args.output.to_stderr(&run_cmds) + } + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/cli/migrate.rs b/tools/internal/testnet-manager/src/cli/migrate.rs new file mode 100644 index 0000000000..13075aca1a --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/migrate.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use clap::Parser; +use nym_validator_client::nyxd::cosmwasm_client::types::{ContractCodeId, EmptyMsg}; + +// nyxd-style command so, for example `migrate ecash 123 '{}'` +#[derive(Debug, Parser)] +pub(crate) struct Args { + pub contract_name: String, + + pub code_id: ContractCodeId, + + pub message: serde_json::Value, +} + +pub(crate) fn execute(args: Args) -> Result<(), NetworkManagerError> { + todo!() +} diff --git a/tools/internal/testnet-manager/src/cli/mod.rs b/tools/internal/testnet-manager/src/cli/mod.rs new file mode 100644 index 0000000000..6fceb5b2b6 --- /dev/null +++ b/tools/internal/testnet-manager/src/cli/mod.rs @@ -0,0 +1,108 @@ +use std::path::PathBuf; +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::error::NetworkManagerError; +use crate::helpers::default_db_file; +use crate::manager::NetworkManager; +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use std::sync::OnceLock; +use url::Url; + +mod build_info; +mod bypass_dkg; +mod initialise_new_network; +mod initialise_post_dkg_network; +mod load_network_details; +mod local_client; +mod local_ecash_apis; +mod local_nodes; +// mod migrate; + +#[derive(clap::Args, Debug)] +pub(crate) struct CommonArgs { + #[clap(long)] + master_mnemonic: Option, + + #[clap(long)] + rpc_endpoint: Option, + + #[clap(long)] + storage_path: Option, +} + +impl CommonArgs { + pub(crate) async fn network_manager(self) -> Result { + let storage = self.storage_path.unwrap_or_else(default_db_file); + NetworkManager::new(storage, self.master_mnemonic, self.rpc_endpoint).await + } +} + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + #[clap(subcommand)] + command: Commands, +} + +impl Cli { + pub(crate) async fn execute(self) -> Result<(), NetworkManagerError> { + match self.command { + Commands::BuildInfo(args) => build_info::execute(args), + Commands::InitialiseNewNetwork(args) => initialise_new_network::execute(args).await, + Commands::LoadNetworkDetails(args) => load_network_details::execute(args).await, + Commands::BypassDkg(args) => bypass_dkg::execute(args).await, + Commands::InitialisePostDkgNetwork(args) => { + initialise_post_dkg_network::execute(args).await + } + Commands::CreateLocalEcashApis(args) => local_ecash_apis::execute(args).await, + Commands::BondLocalMixnet(args) => local_nodes::execute(args).await, + Commands::CreateLocalClient(args) => local_client::execute(args).await, + } + } +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Commands { + /// Show build information of this binary + BuildInfo(build_info::Args), + + /// Initialise new testnet network + InitialiseNewNetwork(initialise_new_network::Args), + + /// Attempt to load testnet network details + LoadNetworkDetails(load_network_details::Args), + + /// Attempt to bypass the DKG by ovewriting the contract state with pre-generated keys + BypassDkg(bypass_dkg::Args), + + /// Initialise new network and bypass the DKG. + /// Equivalent of running `initialise-new-network` and `bypass-dkg` separately. + InitialisePostDkgNetwork(initialise_post_dkg_network::Args), + + /// Attempt to create brand new network, in post DKG-state, using locally running nym-apis + CreateLocalEcashApis(local_ecash_apis::Args), + + /// Attempt to bond minimal local mixnet (3 mixnodes + 1 gateways) and output the run commands + BondLocalMixnet(local_nodes::Args), + + /// Initialise a locally run nym-client, adjust its config and output the run command + CreateLocalClient(local_client::Args), +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::CommandFactory; + + #[test] + fn verify_cli() { + Cli::command().debug_assert(); + } +} diff --git a/tools/internal/testnet-manager/src/error.rs b/tools/internal/testnet-manager/src/error.rs new file mode 100644 index 0000000000..d85d8c7dcb --- /dev/null +++ b/tools/internal/testnet-manager/src/error.rs @@ -0,0 +1,106 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use nym_compact_ecash::CompactEcashError; +use nym_validator_client::nyxd::error::NyxdError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NetworkManagerError { + #[error("io error: {0}")] + IoError(#[from] std::io::Error), + + #[error("failed to parse mnemonic: {0}")] + Bip39Error(#[from] bip39::Error), + + #[error("failed to parse the url: {0}")] + MalformedUrl(#[from] url::ParseError), + + #[error("one of the account addresses was malformed - the developer was too lazy to propagate the actual error message with the address")] + MalformedAccountAddress, + + #[error(transparent)] + Nyxd(#[from] NyxdError), + + #[error("you need to set the master mnemonic on initial run")] + MnemonicNotSet, + + #[error("you need to set the rpc endpoint on initial run")] + RpcEndpointNotSet, + + #[error("experienced internal database error: {0}")] + InternalDatabaseError(#[from] sqlx::Error), + + #[error("failed to perform startup SQL migration - {0}")] + StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), + + #[error("could not find .wasm file for {name} contract under the provided directory")] + ContractWasmNotFound { name: String }, + + #[error("could not find code_id for {name} contract")] + ContractNotUploaded { name: String }, + + #[error("could not find contract admin for {name} contract")] + ContractAdminNotSet { name: String }, + + #[error("could not find address for {name} contract")] + ContractNotInitialised { name: String }, + + #[error("could not find build information for {name} contract")] + ContractNotQueried { name: String }, + + #[error("contract {name} has been build before build information got standarised. this is not supported")] + MissingBuildInfo { name: String }, + + #[error("there aren't any initialised networks in the storage")] + NoNetworksInitialised, + + #[error("you must specify at least a single api endpoint for the DKG")] + NoApiEndpoints, + + #[error("the DKG process has already been started on the target network")] + DkgAlreadyStarted, + + #[error("the target network is already in non-zero DKG epoch")] + NonZeroEpoch, + + #[error("the target already has registered cw4 members")] + ExistingCW4Members, + + #[error("failed to compute ecash keys: {source}")] + EcashCryptoFailure { + #[from] + source: CompactEcashError, + }, + + #[error("the provided contract path does not point to a valid .wasm file")] + MalformedDkgBypassContractPath, + + #[error("nym api initialisation returned non-zero return code")] + NymApiExecutionFailure, + + #[error("nym node initialisation returned non-zero return code")] + NymNodeExecutionFailure, + + #[error("nym client initialisation returned non-zero return code")] + NymClientExecutionFailure, + + #[error("failed to deserialise nym-api config: {0}")] + TomlDeserialisationFailure(#[from] toml::de::Error), + + #[error("failed to deserialise nym-node output: {0}")] + JsonDeserialisationFailure(#[from] serde_json::Error), + + #[error( + "the corresponding env file hasn't been generated. you need to setup local apis first." + )] + EnvFileNotGenerated, + + #[error("the default, pre-generated, .env file does not have the nym-api endpoint set!")] + NymApiEndpointMissing, + + #[error("timed out while waiting for some gateway to appear in the directory (you don't need to run it)")] + ApiGatewayWaitTimeout, + + #[error("timed out while waiting for the gateway to start receiving traffic (you need to actually run it!)")] + GatewayWaitTimeout, +} diff --git a/tools/internal/testnet-manager/src/helpers.rs b/tools/internal/testnet-manager/src/helpers.rs new file mode 100644 index 0000000000..0c38c8238d --- /dev/null +++ b/tools/internal/testnet-manager/src/helpers.rs @@ -0,0 +1,153 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use indicatif::{HumanDuration, ProgressBar}; +use nym_config::{must_get_home, NYM_DIR}; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::fmt::{Display, Formatter}; +use std::future::Future; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; +use tokio::pin; +use tokio::time::interval; + +// struct Ctx<'a, T> { +// progress: ProgressTracker, +// network: LoadedNetwork<'a>, +// inner: T, +// } + +pub(crate) trait ProgressCtx { + fn progress_tracker(&self) -> &ProgressTracker; + + fn println>(&self, msg: I) { + self.progress_tracker().println(msg) + } + + fn set_pb_prefix(&self, prefix: impl Into>) { + self.progress_tracker().set_pb_prefix(prefix) + } + + fn set_pb_message(&self, msg: impl Into>) { + self.progress_tracker().set_pb_message(msg) + } + + async fn async_with_progress(&self, fut: F) -> T + where + F: Future, + { + async_with_progress(fut, &self.progress_tracker().progress_bar).await + } +} + +// pub(crate) trait NetworkCtx { +// fn loaded_network(&self) -> &LoadedNetwork; +// } + +#[derive(Serialize, Deserialize)] +pub struct RunCommands(pub(crate) Vec); + +impl Display for RunCommands { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for cmd in &self.0 { + writeln!(f, "{cmd}")? + } + Ok(()) + } +} + +pub(crate) struct ProgressTracker { + start: Instant, + pub(crate) progress_bar: ProgressBar, +} + +impl ProgressTracker { + pub(crate) fn new>(msg: I) -> Self { + let progress_bar = ProgressBar::new_spinner(); + progress_bar.println(msg); + + ProgressTracker { + start: Instant::now(), + progress_bar, + } + } + + pub(crate) fn println>(&self, msg: I) { + self.progress_bar.println(msg) + } + + pub(crate) fn set_pb_prefix(&self, prefix: impl Into>) { + self.progress_bar.set_prefix(prefix) + } + + pub(crate) fn set_pb_message(&self, msg: impl Into>) { + self.progress_bar.set_message(msg) + } + + pub(crate) fn output_run_commands(&self, cmds: &RunCommands) { + self.println("🏇 run the binaries with the following commands:"); + for cmd in &cmds.0 { + self.println(cmd) + } + } +} + +impl Default for ProgressTracker { + fn default() -> Self { + ProgressTracker { + start: Instant::now(), + progress_bar: ProgressBar::new_spinner(), + } + } +} + +impl Drop for ProgressTracker { + fn drop(&mut self) { + self.progress_bar.println(format!( + "✨ Done in {}", + HumanDuration(self.start.elapsed()) + )); + self.progress_bar.finish_and_clear(); + } +} + +pub(crate) fn default_storage_dir() -> PathBuf { + must_get_home().join(NYM_DIR).join("testnet-manager") +} + +pub(crate) fn default_db_file() -> PathBuf { + default_storage_dir().join("network-data.sqlite") +} + +pub(crate) async fn async_with_progress(fut: F, pb: &ProgressBar) -> T +where + F: Future, +{ + pb.tick(); + pin!(fut); + let mut update_interval = interval(Duration::from_millis(50)); + + loop { + tokio::select! { + _ = update_interval.tick() => { + pb.tick() + } + res = &mut fut => { + return res + } + } + } +} + +pub(crate) fn wasm_code>(path: P) -> Result, NetworkManagerError> { + let path = path.as_ref(); + assert!(path.exists()); + let mut file = std::fs::File::open(path)?; + let mut data = Vec::new(); + + file.read_to_end(&mut data)?; + Ok(data) +} diff --git a/tools/internal/testnet-manager/src/main.rs b/tools/internal/testnet-manager/src/main.rs new file mode 100644 index 0000000000..c92be01324 --- /dev/null +++ b/tools/internal/testnet-manager/src/main.rs @@ -0,0 +1,26 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::Cli; +use clap::Parser; +use nym_bin_common::logging::setup_tracing_logger; + +pub(crate) mod cli; +pub(crate) mod error; +mod helpers; +mod manager; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // std::env::set_var( + // "RUST_LOG", + // "trace,handlebars=warn,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn,tokio_util=warn,tokio_tungstenite=warn,tokio-util=warn", + // ); + + let cli = Cli::parse(); + setup_tracing_logger(); + + cli.execute().await?; + + Ok(()) +} diff --git a/tools/internal/testnet-manager/src/manager/contract.rs b/tools/internal/testnet-manager/src/manager/contract.rs new file mode 100644 index 0000000000..8295550dfc --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/contract.rs @@ -0,0 +1,283 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::error::NetworkManagerError; +use nym_mixnet_contract_common::ContractBuildInformation; +use nym_validator_client::nyxd::cosmwasm_client::types::{ + ContractCodeId, InstantiateResult, MigrateResult, UploadResult, +}; +use nym_validator_client::nyxd::{AccountId, Hash}; +use nym_validator_client::DirectSecp256k1HdWallet; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct LoadedNymContracts { + pub(crate) mixnet: LoadedContract, + pub(crate) vesting: LoadedContract, + pub(crate) ecash: LoadedContract, + pub(crate) cw3_multisig: LoadedContract, + pub(crate) cw4_group: LoadedContract, + pub(crate) dkg: LoadedContract, +} + +impl From for LoadedNymContracts { + fn from(value: NymContracts) -> Self { + LoadedNymContracts { + mixnet: value.mixnet.into(), + vesting: value.vesting.into(), + ecash: value.ecash.into(), + cw3_multisig: value.cw3_multisig.into(), + cw4_group: value.cw4_group.into(), + dkg: value.dkg.into(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct NymContracts { + pub(crate) mixnet: Contract, + pub(crate) vesting: Contract, + pub(crate) ecash: Contract, + pub(crate) cw3_multisig: Contract, + pub(crate) cw4_group: Contract, + pub(crate) dkg: Contract, +} + +impl NymContracts { + pub(crate) fn fake_iter(&self) -> Vec<&Contract> { + vec![ + &self.mixnet, + &self.vesting, + &self.ecash, + &self.cw3_multisig, + &self.cw4_group, + &self.dkg, + ] + } + + pub(crate) fn fake_iter_mut(&mut self) -> Vec<&mut Contract> { + vec![ + &mut self.mixnet, + &mut self.vesting, + &mut self.ecash, + &mut self.cw3_multisig, + &mut self.cw4_group, + &mut self.dkg, + ] + } + + pub(crate) fn count(&self) -> usize { + 6 + } + + pub(crate) fn discover_paths>( + &mut self, + base_path: P, + ) -> Result<(), NetworkManagerError> { + // just look in the base path, don't traverse + for entry_res in base_path.as_ref().read_dir()? { + let entry = entry_res?; + let Ok(name) = entry.file_name().into_string() else { + continue; + }; + + if name.ends_with(".wasm") { + if name.contains("mixnet") { + self.mixnet.wasm_path = Some(entry.path()) + } + if name.contains("vesting") { + self.vesting.wasm_path = Some(entry.path()) + } + if name.contains("ecash") { + self.ecash.wasm_path = Some(entry.path()) + } + if name.contains("cw4") { + self.cw4_group.wasm_path = Some(entry.path()) + } + if name.contains("cw3") { + self.cw3_multisig.wasm_path = Some(entry.path()) + } + if name.contains("dkg") { + self.dkg.wasm_path = Some(entry.path()) + } + } + } + + if let Some(no_path) = self.fake_iter().iter().find(|c| c.wasm_path.is_none()) { + return Err(NetworkManagerError::ContractWasmNotFound { + name: no_path.name.clone(), + }); + } + + Ok(()) + } +} + +impl Default for NymContracts { + fn default() -> Self { + NymContracts { + mixnet: Contract::new("mixnet"), + vesting: Contract::new("vesting"), + ecash: Contract::new("ecash"), + cw4_group: Contract::new("cw4_group"), + cw3_multisig: Contract::new("cw3_multisig"), + dkg: Contract::new("dkg"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Account { + pub(crate) address: AccountId, + pub(crate) mnemonic: bip39::Mnemonic, +} + +impl Account { + pub(crate) fn new() -> Account { + let mnemonic = bip39::Mnemonic::generate(24).unwrap(); + // sure, we're using hardcoded prefix, but realistically this will never change + let wallet = DirectSecp256k1HdWallet::from_mnemonic("n", mnemonic.clone()); + let acc = wallet.try_derive_accounts().unwrap().pop().unwrap(); + Account { + address: acc.address, + mnemonic, + } + } + + pub(crate) fn address(&self) -> AccountId { + self.address.clone() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct MinimalUploadInfo { + pub transaction_hash: Hash, + pub code_id: ContractCodeId, +} + +impl From for MinimalUploadInfo { + fn from(value: UploadResult) -> Self { + MinimalUploadInfo { + transaction_hash: value.transaction_hash, + code_id: value.code_id, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct MinimalInitInfo { + pub transaction_hash: Hash, + pub contract_address: AccountId, +} + +impl From for MinimalInitInfo { + fn from(value: InstantiateResult) -> Self { + MinimalInitInfo { + transaction_hash: value.transaction_hash, + contract_address: value.contract_address, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct MinimalMigrateInfo { + pub transaction_hash: Hash, +} + +impl From for MinimalMigrateInfo { + fn from(value: MigrateResult) -> Self { + MinimalMigrateInfo { + transaction_hash: value.transaction_hash, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct LoadedContract { + pub(crate) name: String, + pub(crate) address: AccountId, + pub(crate) admin_address: AccountId, + pub(crate) admin_mnemonic: bip39::Mnemonic, +} + +impl From for LoadedContract { + fn from(value: Contract) -> Self { + let admin = value.admin.expect("no admin set"); + LoadedContract { + name: value.name, + address: value.init_info.expect("uninitialised").contract_address, + admin_address: admin.address, + admin_mnemonic: admin.mnemonic, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Contract { + pub(crate) name: String, + pub(crate) wasm_path: Option, + pub(crate) upload_info: Option, + pub(crate) admin: Option, + pub(crate) init_info: Option, + pub(crate) migrate_info: Option, + pub(crate) build_info: Option, +} + +impl Contract { + pub(crate) fn new>(name: S) -> Self { + Contract { + name: name.into(), + wasm_path: None, + upload_info: None, + admin: None, + init_info: None, + migrate_info: None, + build_info: None, + } + } + + pub(crate) fn wasm_path(&self) -> Result<&PathBuf, NetworkManagerError> { + self.wasm_path + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractWasmNotFound { + name: self.name.clone(), + }) + } + + pub(crate) fn upload_info(&self) -> Result<&MinimalUploadInfo, NetworkManagerError> { + self.upload_info + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractNotUploaded { + name: self.name.clone(), + }) + } + + pub(crate) fn admin(&self) -> Result<&Account, NetworkManagerError> { + self.admin + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractAdminNotSet { + name: self.name.clone(), + }) + } + + pub(crate) fn init_info(&self) -> Result<&MinimalInitInfo, NetworkManagerError> { + self.init_info + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractNotInitialised { + name: self.name.clone(), + }) + } + + #[allow(dead_code)] + pub(crate) fn build_info(&self) -> Result<&ContractBuildInformation, NetworkManagerError> { + self.build_info + .as_ref() + .ok_or_else(|| NetworkManagerError::ContractNotQueried { + name: self.name.clone(), + }) + } + + pub(crate) fn address(&self) -> Result<&AccountId, NetworkManagerError> { + self.init_info().map(|info| &info.contract_address) + } +} diff --git a/tools/internal/testnet-manager/src/manager/dkg_skip.rs b/tools/internal/testnet-manager/src/manager/dkg_skip.rs new file mode 100644 index 0000000000..4cacc8acd4 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/dkg_skip.rs @@ -0,0 +1,473 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker}; +use crate::manager::contract::Account; +use crate::manager::network::LoadedNetwork; +use crate::manager::NetworkManager; +use console::style; +use dkg_bypass_contract::msg::FakeDealerData; +use nym_compact_ecash::{ttp_keygen, Base58, KeyPairAuth}; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::Addr; +use nym_pemstore::traits::PemStorableKey; +use nym_pemstore::{store_key, store_keypair, KeyPairPath}; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, GroupSigningClient, PagedGroupQueryClient, +}; +use nym_validator_client::nyxd::cosmwasm::ContractCodeId; +use nym_validator_client::nyxd::cw4::Member; +use nym_validator_client::nyxd::{AccountId, CosmWasmClient}; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use rand::rngs::OsRng; +use std::fs; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use url::Url; +use zeroize::Zeroizing; + +pub(crate) struct EcashSigner { + pub(crate) ed25519_keypair: ed25519::KeyPair, + pub(crate) ecash_keypair: nym_compact_ecash::KeyPairAuth, + pub(crate) cosmos_account: Account, + pub(crate) endpoint: Url, +} + +#[derive(Default)] +pub(crate) struct EcashSignerPaths { + pub(crate) ecash_key: PathBuf, + pub(crate) ed25519_keys: KeyPairPath, + pub(crate) mnemonic_path: PathBuf, + pub(crate) endpoint_path: PathBuf, +} + +pub(crate) struct EcashSignerWithPaths { + pub(crate) data: EcashSigner, + pub(crate) paths: EcashSignerPaths, +} + +// perform the same serialisation as the nym-api keys +struct FakeDkgKey<'a> { + inner: &'a KeyPairAuth, +} + +impl<'a> FakeDkgKey<'a> { + fn new(inner: &'a KeyPairAuth) -> Self { + FakeDkgKey { inner } + } +} + +impl<'a> PemStorableKey for FakeDkgKey<'a> { + type Error = NetworkManagerError; + + fn pem_type() -> &'static str { + "ECASH KEY WITH EPOCH" + } + + fn to_bytes(&self) -> Vec { + // our fake key is ALWAYS issued for epoch 0 + let mut bytes = vec![0u8; 8]; + bytes.append(&mut self.inner.secret_key().to_bytes()); + bytes + } + + fn from_bytes(_: &[u8]) -> Result { + unimplemented!("this is not meant to be ever called") + } +} + +impl EcashSignerWithPaths { + pub(crate) fn api_port(&self) -> u16 { + self.data.endpoint.port().unwrap() + } +} + +struct DkgSkipCtx<'a> { + progress: ProgressTracker, + network: &'a LoadedNetwork, + dkg_admin: DirectSigningHttpRpcNyxdClient, + ecash_signers: Vec, +} + +impl<'a> ProgressCtx for DkgSkipCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> DkgSkipCtx<'a> { + fn dkg_contract(&self) -> &AccountId { + &self.network.contracts.dkg.address + } + + fn new(network: &'a LoadedNetwork) -> Result { + let progress = ProgressTracker::new(format!( + "\n🥷 attempting to skip DKG on network '{}'", + network.name + )); + + Ok(DkgSkipCtx { + progress, + dkg_admin: network.dkg_signing_client()?, + network, + ecash_signers: vec![], + }) + } + + fn group_signing_client(&self) -> Result { + self.network.cw4_group_signing_client() + } + + fn admin_signing_client( + &self, + mnemonic: bip39::Mnemonic, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.network.client_config()?, + self.network.rpc_endpoint.as_str(), + mnemonic, + )?) + } +} + +impl NetworkManager { + fn generate_ecash_signer_data( + &self, + ctx: &mut DkgSkipCtx, + api_endpoints: Vec, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📝 {}Generating ecash keys for all signers...", + style("[1/8]").bold().dim() + )); + + // generate required materials + let n = api_endpoints.len(); + let threshold = (2 * n + 3 - 1) / 3; + + let ecash_keys = ttp_keygen(threshold as u64, n as u64)?; + + let mut ecash_signers = Vec::new(); + let mut rng = OsRng; + for (endpoint, ecash_keypair) in api_endpoints.into_iter().zip(ecash_keys.into_iter()) { + let ed25519_keypair = ed25519::KeyPair::new(&mut rng); + let data = EcashSigner { + ed25519_keypair, + ecash_keypair, + cosmos_account: Account::new(), + endpoint, + }; + ctx.println(format!( + "\t{} will be managed by {}", + data.endpoint, data.cosmos_account.address + )); + let full = EcashSignerWithPaths { + data, + paths: EcashSignerPaths::default(), + }; + ecash_signers.push(full) + } + ctx.ecash_signers = ecash_signers; + + ctx.println("\t✅ generated ecash keys for all signers"); + Ok(()) + } + + async fn validate_existing_contracts<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + ) -> Result { + ctx.println(format!( + "🔬 {}Validating the current DKG and group contracts...", + style("[2/8]").bold().dim() + )); + + ctx.set_pb_prefix("[1/3]"); + ctx.set_pb_message("checking DKG epoch data..."); + let epoch_fut = ctx.dkg_admin.get_current_epoch(); + let dkg_epoch = ctx.async_with_progress(epoch_fut).await?; + if dkg_epoch.epoch_id != 0 { + return Err(NetworkManagerError::NonZeroEpoch); + } + + if !dkg_epoch.state.is_waiting_initialisation() { + return Err(NetworkManagerError::DkgAlreadyStarted); + } + + ctx.set_pb_prefix("[2/3]"); + ctx.set_pb_message("retrieving DKG contract code_id..."); + let code_fut = ctx + .dkg_admin + .get_contract_code_history(&ctx.network.contracts.dkg.address); + let code_history = ctx.async_with_progress(code_fut).await?; + + // SAFETY: + // if this is empty our abci query is invalid since we have just queried the contract so it must exist + let current_code = code_history.last().unwrap().code_id; + ctx.println("\tthe DKG contract is all good!"); + + ctx.set_pb_prefix("[3/3]"); + ctx.set_pb_message("checking cw4 group members data..."); + let members_fut = ctx.dkg_admin.get_all_members(); + let members = ctx.async_with_progress(members_fut).await?; + if !members.is_empty() { + return Err(NetworkManagerError::ExistingCW4Members); + } + + ctx.println("\tthe group contract is all good!"); + ctx.println("\t✅ the existing contracts are all good!"); + + Ok(current_code) + } + + async fn persist_dkg_keys<'a, P: AsRef>( + &self, + ctx: &mut DkgSkipCtx<'a>, + output_dir: P, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📦 {}Persisting the signer keys...", + style("[3/8]").bold().dim() + )); + + ctx.set_pb_message("storing the signer data on disk..."); + + let output_dir = output_dir.as_ref(); + let pb = &ctx.progress.progress_bar; + + for signer in &mut ctx.ecash_signers { + let address = &signer.data.cosmos_account.address; + let url = &signer.data.endpoint; + let signer_dir = output_dir.join(address.to_string()); + fs::create_dir_all(&signer_dir)?; + + let fake_ecash_key = FakeDkgKey::new(&signer.data.ecash_keypair); + + let ecash_path = signer_dir.join("ecash"); + + let ed25519_paths = KeyPairPath { + private_key_path: signer_dir.join("ed25519"), + public_key_path: signer_dir.join("ed25519.pub"), + }; + + let mnemonic_path = signer_dir.join("mnemonic"); + let endpoint_path = signer_dir.join("announce_address"); + + store_key(&fake_ecash_key, &ecash_path)?; + store_keypair(&signer.data.ed25519_keypair, &ed25519_paths)?; + + fs::write( + &mnemonic_path, + &Zeroizing::new(signer.data.cosmos_account.mnemonic.to_string()), + )?; + fs::write(&endpoint_path, url.as_str())?; + + signer.paths.ecash_key = ecash_path; + signer.paths.ed25519_keys = ed25519_paths; + signer.paths.mnemonic_path = mnemonic_path; + signer.paths.endpoint_path = endpoint_path; + + pb.println(format!( + "\tpersisted {address} (endpoint: {url}) data under {}", + signer_dir.display() + )); + } + + ctx.println("\t✅ persisted all the signer keys!"); + Ok(()) + } + + async fn upload_bypass_contract<'a, P: AsRef>( + &self, + ctx: &DkgSkipCtx<'a>, + dkg_bypass_contract: P, + ) -> Result { + ctx.println(format!( + "🚚 {}Uploading the bypass contract...", + style("[4/8]").bold().dim() + )); + + ctx.set_pb_message("uploading the bypass contract..."); + + let res = self + .upload_contract( + &ctx.dkg_admin, + &ctx.progress.progress_bar, + dkg_bypass_contract, + ) + .await?; + + ctx.println("\t✅ uploaded the bypass contract!"); + + Ok(res.code_id) + } + + async fn migrate_to_bypass_contract<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + code_id: ContractCodeId, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔀 {}Attempting to migrate into the bypass contract...", + style("[5/8]").bold().dim() + )); + + ctx.set_pb_message("migrating the DKG contract..."); + + let migrate_msg = dkg_bypass_contract::MigrateMsg { + dealers: ctx + .ecash_signers + .iter() + .map(|signer| FakeDealerData { + vk: signer.data.ecash_keypair.verification_key().to_bs58(), + ed25519_identity: signer.data.ed25519_keypair.public_key().to_base58_string(), + announce: signer.data.endpoint.to_string(), + owner: Addr::unchecked(signer.data.cosmos_account.address.as_ref()), + }) + .collect(), + }; + + let migrate_fut = ctx.dkg_admin.migrate( + ctx.dkg_contract(), + code_id, + &migrate_msg, + "migrating bypass DKG contract from testnet-manager", + None, + ); + ctx.async_with_progress(migrate_fut).await?; + + ctx.println("\t✅ migrated the DKG into the bypass contract!"); + + Ok(()) + } + + async fn restore_dkg_contract<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + code_id: ContractCodeId, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "↩️ {}Attempting to migrate back into the original DKG contract...", + style("[6/8]").bold().dim() + )); + + ctx.set_pb_message("migrating the DKG contract..."); + + let migrate_msg = nym_coconut_dkg_common::msg::MigrateMsg {}; + let migrate_fut = ctx.dkg_admin.migrate( + ctx.dkg_contract(), + code_id, + &migrate_msg, + "migrating initial DKG contract from testnet-manager", + None, + ); + ctx.async_with_progress(migrate_fut).await?; + + ctx.println("\t✅ restored the original DKG contract!"); + + Ok(()) + } + + async fn add_group_members<'a>(&self, ctx: &DkgSkipCtx<'a>) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "👪 {}Adding all the cw4 group members...", + style("[7/8]").bold().dim() + )); + + ctx.set_pb_message("⛽creating a new big cw4 family..."); + let admin = ctx.group_signing_client()?; + let new_members = ctx + .ecash_signers + .iter() + .map(|s| Member { + addr: s.data.cosmos_account.address.to_string(), + weight: 1, + }) + .collect(); + + let update_fut = admin.update_members(new_members, Vec::new(), None); + + ctx.async_with_progress(update_fut).await?; + ctx.println("\t✅ new cw4 group members got added"); + Ok(()) + } + + async fn transfer_signer_tokens<'a>( + &self, + ctx: &DkgSkipCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💸 {}Transferring tokens to the new signers...", + style("[8/8]").bold().dim() + )); + + let admin = ctx.admin_signing_client(self.admin.deref().clone())?; + + let mut receivers = Vec::new(); + for signer in &ctx.ecash_signers { + // send 101nym to the admin + receivers.push(( + signer.data.cosmos_account.address.clone(), + admin.mix_coins(101_000000), + )) + } + + ctx.set_pb_message("attempting to send signer tokens..."); + + let send_future = admin.send_multiple( + receivers, + "signers token transfer from testnet-manager", + None, + ); + let res = ctx.async_with_progress(send_future).await?; + + ctx.println(format!( + "\t✅ sent tokens in transaction: {} (height {})", + res.hash, res.height + )); + Ok(()) + } + + pub(crate) async fn attempt_bypass_dkg( + &self, + api_endpoints: Vec, + network: &LoadedNetwork, + dkg_bypass_contract: P1, + data_output_dir: P2, + ) -> Result, NetworkManagerError> + where + P1: AsRef, + P2: AsRef, + { + if api_endpoints.is_empty() { + return Err(NetworkManagerError::NoApiEndpoints); + } + + let dkg_bypass_contract = dkg_bypass_contract.as_ref(); + if !dkg_bypass_contract.is_file() { + return Err(NetworkManagerError::MalformedDkgBypassContractPath); + } + let Some(ext) = dkg_bypass_contract.extension() else { + return Err(NetworkManagerError::MalformedDkgBypassContractPath); + }; + if ext != "wasm" { + return Err(NetworkManagerError::MalformedDkgBypassContractPath); + } + + let mut ctx = DkgSkipCtx::new(network)?; + + self.generate_ecash_signer_data(&mut ctx, api_endpoints)?; + let current_code_id = self.validate_existing_contracts(&ctx).await?; + self.persist_dkg_keys(&mut ctx, data_output_dir).await?; + let new_code_id = self + .upload_bypass_contract(&ctx, dkg_bypass_contract) + .await?; + self.migrate_to_bypass_contract(&ctx, new_code_id).await?; + self.restore_dkg_contract(&ctx, current_code_id).await?; + self.add_group_members(&ctx).await?; + self.transfer_signer_tokens(&ctx).await?; + + Ok(ctx.ecash_signers) + } +} diff --git a/tools/internal/testnet-manager/src/manager/local_apis.rs b/tools/internal/testnet-manager/src/manager/local_apis.rs new file mode 100644 index 0000000000..89f6c94d25 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/local_apis.rs @@ -0,0 +1,224 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker, RunCommands}; +use crate::manager::dkg_skip::EcashSignerWithPaths; +use crate::manager::network::LoadedNetwork; +use crate::manager::NetworkManager; +use console::style; +use nym_config::{ + must_get_home, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_NYM_APIS_DIR, NYM_DIR, +}; +use std::fs; +use std::fs::File; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use tokio::process::Command; +use zeroize::Zeroizing; + +struct LocalApisCtx<'a> { + nym_api_binary: PathBuf, + progress: ProgressTracker, + network: &'a LoadedNetwork, + signers: Vec, +} + +impl<'a> ProgressCtx for LocalApisCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> LocalApisCtx<'a> { + fn signer_id(&self, signer: &EcashSignerWithPaths) -> String { + format!( + "{}-{}", + signer.data.cosmos_account.address, self.network.name + ) + } + + fn new( + nym_api_binary: PathBuf, + network: &'a LoadedNetwork, + signers: Vec, + ) -> Result { + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new local signing nym-APIs for network '{}' over {}", + network.name, network.rpc_endpoint + )); + + Ok(LocalApisCtx { + nym_api_binary, + network, + progress, + signers, + }) + } +} + +impl NetworkManager { + fn nym_api_config(&self, api_id: &str) -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join(DEFAULT_NYM_APIS_DIR) + .join(api_id) + .join(DEFAULT_CONFIG_DIR) + .join(DEFAULT_CONFIG_FILENAME) + } + + async fn initialise_api<'a>( + &self, + ctx: &LocalApisCtx<'a>, + info: &EcashSignerWithPaths, + ) -> Result<(), NetworkManagerError> { + let address = &info.data.cosmos_account.address; + + ctx.set_pb_message(format!("initialising api {address}...")); + + let id = ctx.signer_id(info); + + // setup the binary itself + let mut child = Command::new(&ctx.nym_api_binary) + .args([ + "init", + "--id", + &id, + "--nyxd-validator", + ctx.network.rpc_endpoint.as_ref(), + "--mnemonic", + &Zeroizing::new(info.data.cosmos_account.mnemonic.to_string()), + "--enable-zk-nym", + "--announce-address", + info.data.endpoint.as_ref(), + ]) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .stdout(Stdio::null()) + .kill_on_drop(true) + .spawn()?; + let child_fut = child.wait(); + let out = ctx.async_with_progress(child_fut).await?; + if !out.success() { + return Err(NetworkManagerError::NymApiExecutionFailure); + } + + // load the config (and do very nasty things to it) + let config_path = self.nym_api_config(&id); + let config_content = fs::read_to_string(config_path)?; + let parsed_config: toml::Table = toml::from_str(&config_content)?; + let storage_paths = &parsed_config["base"] + .as_table() + .expect("nym-api config serialisation has changed")["storage_paths"] + .as_table() + .expect("nym-api config serialisation has changed"); + + let priv_id = &storage_paths["private_identity_key_file"] + .as_str() + .expect("nym-api config serialisation has changed"); + let pub_id = &storage_paths["public_identity_key_file"] + .as_str() + .expect("nym-api config serialisation has changed"); + let ecash = &parsed_config["coconut_signer"] + .as_table() + .expect("nym-api config serialisation has changed")["storage_paths"] + .as_table() + .expect("nym-api config serialisation has changed")["coconut_key_path"] + .as_str() + .expect("nym-api config serialisation has changed"); + + // overwrite pre-generated files + fs::copy(&info.paths.ecash_key, ecash)?; + fs::copy(&info.paths.ed25519_keys.private_key_path, priv_id)?; + fs::copy(&info.paths.ed25519_keys.public_key_path, pub_id)?; + + ctx.println(format!("\t nym-API {address} got initialised")); + + Ok(()) + } + + async fn initialise_apis<'a>(&self, ctx: &LocalApisCtx<'a>) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔏 {}Initialising local nym-apis...", + style("[1/1]").bold().dim() + )); + + for signer in &ctx.signers { + self.initialise_api(ctx, signer).await? + } + + ctx.println("\t✅ all APIs got initialised!"); + Ok(()) + } + + fn prepare_api_run_commands>( + &self, + ctx: &LocalApisCtx, + env_file: P, + ) -> Result { + let bin_canon = fs::canonicalize(&ctx.nym_api_binary)?; + let env_canon = fs::canonicalize(env_file)?; + let bin_canon_display = bin_canon.display(); + let env_canon_display = env_canon.display(); + + let mut cmds = Vec::new(); + for signer in &ctx.signers { + let port = signer.api_port(); + let id = ctx.signer_id(signer); + + cmds.push(format!( + "ROCKET_PORT={port} {bin_canon_display} -c {env_canon_display} run --id {id}" + )); + } + Ok(RunCommands(cmds)) + } + + fn output_api_run_commands(&self, ctx: &LocalApisCtx, cmds: &RunCommands) { + ctx.progress.output_run_commands(cmds) + } + + fn prepare_env_file>( + &self, + ctx: &LocalApisCtx, + env_file: P, + ) -> Result<(), NetworkManagerError> { + let base_env = ctx.network.to_env_file_section(); + let updated_env = format!("{base_env}NYM_API={}", ctx.signers[0].data.endpoint); + + let env_file_path = env_file.as_ref(); + if let Some(parent) = env_file_path.parent() { + fs::create_dir_all(parent)?; + } + + let latest = self.default_latest_env_file_path(); + if fs::read_link(&latest).is_ok() { + fs::remove_file(&latest)?; + } + + let mut env_file = File::create(env_file_path)?; + env_file.write_all(updated_env.as_bytes())?; + + // make symlink for usability purposes + std::os::unix::fs::symlink(env_file_path, &latest)?; + + Ok(()) + } + + pub(crate) async fn setup_local_apis>( + &self, + nym_api_binary: P, + network: &LoadedNetwork, + signer_data: Vec, + ) -> Result { + let ctx = LocalApisCtx::new(nym_api_binary.as_ref().to_path_buf(), network, signer_data)?; + let env_file = ctx.network.default_env_file_path(); + + self.initialise_apis(&ctx).await?; + self.prepare_env_file(&ctx, &env_file)?; + let cmds = self.prepare_api_run_commands(&ctx, env_file)?; + self.output_api_run_commands(&ctx, &cmds); + + Ok(cmds) + } +} diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs new file mode 100644 index 0000000000..bd13c8a834 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -0,0 +1,265 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker}; +use crate::manager::network::LoadedNetwork; +use crate::manager::NetworkManager; +use console::style; +use nym_config::{must_get_home, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, NYM_DIR}; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::NymApiClient; +use rand::{thread_rng, RngCore}; +use std::fs; +use std::fs::OpenOptions; +use std::io::prelude::*; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::time::Duration; +use tokio::net::TcpStream; +use tokio::process::Command; +use tokio::time::sleep; +use url::Url; + +struct LocalClientCtx<'a> { + nym_client_binary: PathBuf, + client_id: String, + + progress: ProgressTracker, + network: &'a LoadedNetwork, +} + +impl<'a> ProgressCtx for LocalClientCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> LocalClientCtx<'a> { + fn new( + nym_client_binary: PathBuf, + network: &'a LoadedNetwork, + ) -> Result { + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new local nym-client for network '{}' over {}", + network.name, network.rpc_endpoint + )); + let mut rng = thread_rng(); + let client_id = format!("{}-client-{}", network.name, rng.next_u32()); + + Ok(LocalClientCtx { + nym_client_binary, + network, + progress, + client_id, + }) + } + + // hehe, that's disgusting, but it's not meant to be used by users + fn nym_api_url(&self) -> Result { + let env_file = fs::read_to_string(self.network.default_env_file_path())?; + for entry in env_file.lines() { + if let Some(raw_url) = entry.strip_prefix("NYM_API=") { + return Ok(raw_url.parse()?); + } + } + Err(NetworkManagerError::NymApiEndpointMissing) + } +} + +impl NetworkManager { + fn nym_client_config(&self, client_id: &str) -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join("clients") + .join(client_id) + .join(DEFAULT_CONFIG_DIR) + .join(DEFAULT_CONFIG_FILENAME) + } + + async fn wait_for_api_gateway<'a>( + &self, + ctx: &LocalClientCtx<'a>, + ) -> Result { + // create api client + // hehe, that's disgusting, but it's not meant to be used by users + let api_url = ctx.nym_api_url()?; + ctx.set_pb_message(format!( + "⌛waiting for any gateway to appear in the directory ({api_url})..." + )); + + let api_client = NymApiClient::new(api_url); + + let wait_fut = async { + let inner_fut = async { + loop { + let mut gateways = match api_client.nym_api.get_basic_gateways(None).await { + Ok(gateways) => gateways, + Err(err) => { + ctx.println(format!( + "❌ {} {err}", + style("[API QUERY FAILURE]: ").bold().dim() + )); + continue; + } + }; + + if let Some(node) = gateways.nodes.pop() { + return SocketAddr::new(node.ip_addresses[0], node.entry.unwrap().ws_port); + } + sleep(Duration::from_secs(10)).await; + } + }; + tokio::time::timeout(Duration::from_secs(240), inner_fut).await + }; + + match ctx.async_with_progress(wait_fut).await { + Ok(endpoint) => { + ctx.println(format!( + "\twe finally got a gateway in the directory! it's at: {endpoint}" + )); + Ok(endpoint) + } + Err(_) => Err(NetworkManagerError::ApiGatewayWaitTimeout), + } + } + + async fn wait_for_gateway_endpoint<'a>( + &self, + ctx: &LocalClientCtx<'a>, + gateway: SocketAddr, + ) -> Result<(), NetworkManagerError> { + ctx.set_pb_message(format!( + "⌛waiting for gateway at {gateway} to start receiving traffic..." + )); + + let wait_fut = async { + let inner_fut = async { + loop { + if TcpStream::connect(gateway).await.is_ok() { + break; + } + sleep(Duration::from_secs(10)).await; + } + }; + tokio::time::timeout(Duration::from_secs(240), inner_fut).await + }; + + if ctx.async_with_progress(wait_fut).await.is_err() { + return Err(NetworkManagerError::GatewayWaitTimeout); + } + + ctx.println(format!( + "\tthe gateway at {gateway} has finally come online" + )); + + Ok(()) + } + + async fn wait_for_gateway<'a>( + &self, + ctx: &LocalClientCtx<'a>, + ) -> Result<(), NetworkManagerError> { + let endpoint = self.wait_for_api_gateway(ctx).await?; + self.wait_for_gateway_endpoint(ctx, endpoint).await + } + + async fn prepare_nym_client<'a>( + &self, + ctx: &LocalClientCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔏 {}Initialising local nym-client...", + style("[1/1]").bold().dim() + )); + + let env = ctx.network.default_env_file_path(); + let id = &ctx.client_id; + + self.wait_for_gateway(ctx).await?; + + ctx.set_pb_message(format!("initialising client {id}...")); + ctx.println(format!("\tinitialising client {id}...")); + let mut child = Command::new(&ctx.nym_client_binary) + .args([ + "-c", + &env.display().to_string(), + "init", + "--id", + id, + "--enabled-credentials-mode", + "true", + ]) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .spawn()?; + + let child_fut = child.wait(); + let out = ctx.async_with_progress(child_fut).await?; + if !out.success() { + return Err(NetworkManagerError::NymClientExecutionFailure); + } + + ctx.println(format!("\tupdating client {id} config...")); + + let config_path = self.nym_client_config(id); + let mut config_file = OpenOptions::new().append(true).open(config_path)?; + + // make the client ignore the performance of the nodes since we're not running network monitor + writeln!( + config_file, + r#" + +[debug.topology] +minimum_mixnode_performance = 0 +minimum_gateway_performance = 0 +"# + )?; + + ctx.println(format!("\t✅client {id} is ready to use!")); + + Ok(()) + } + + fn prepare_client_run_command( + &self, + ctx: &LocalClientCtx, + ) -> Result { + let env_file = ctx.network.default_env_file_path(); + + let bin_canon = fs::canonicalize(&ctx.nym_client_binary)?; + let env_canon = fs::canonicalize(env_file)?; + let bin_canon_display = bin_canon.display(); + let env_canon_display = env_canon.display(); + + let id = &ctx.client_id; + + Ok(format!( + "{bin_canon_display} -c {env_canon_display} run --id {id}" + )) + } + + pub(crate) async fn init_local_nym_client>( + &self, + nym_client_binary: P, + network: &LoadedNetwork, + ) -> Result { + let ctx = LocalClientCtx::new(nym_client_binary.as_ref().to_path_buf(), network)?; + + let env_file = ctx.network.default_env_file_path(); + if !env_file.exists() { + return Err(NetworkManagerError::EnvFileNotGenerated); + } + + self.prepare_nym_client(&ctx).await?; + let cmd = self.prepare_client_run_command(&ctx)?; + + ctx.println("🏇 run the binary with the following commands:"); + ctx.println(&cmd); + + Ok(cmd) + } +} diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs new file mode 100644 index 0000000000..578d72b0fe --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -0,0 +1,546 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{ProgressCtx, ProgressTracker, RunCommands}; +use crate::manager::contract::Account; +use crate::manager::network::LoadedNetwork; +use crate::manager::NetworkManager; +use console::style; +use nym_contracts_common::signing::MessageSignature; +use nym_mixnet_contract_common::{ + construct_gateway_bonding_sign_payload, construct_mixnode_bonding_sign_payload, Addr, Gateway, + Layer, LayerAssignment, MixNode, MixNodeCostParams, Percent, +}; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; +use nym_validator_client::nyxd::CosmWasmCoin; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use tokio::process::Command; +use zeroize::Zeroizing; + +struct NymNode { + // host is always 127.0.0.1 + mix_port: u16, + verloc_port: u16, + http_port: u16, + clients_port: u16, + sphinx_key: String, + identity_key: String, + version: String, + + owner: Account, + bonding_signature: String, +} + +impl NymNode { + fn new_empty() -> NymNode { + NymNode { + mix_port: 0, + verloc_port: 0, + http_port: 0, + clients_port: 0, + sphinx_key: "".to_string(), + identity_key: "".to_string(), + version: "".to_string(), + owner: Account::new(), + bonding_signature: "".to_string(), + } + } + + fn pledge(&self) -> CosmWasmCoin { + CosmWasmCoin::new(100_000000, "unym") + } + + fn gateway(&self) -> Gateway { + Gateway { + host: "127.0.0.1".to_string(), + mix_port: self.mix_port, + clients_port: self.clients_port, + location: "foomp".to_string(), + sphinx_key: self.sphinx_key.clone(), + identity_key: self.identity_key.clone(), + version: self.version.clone(), + } + } + + fn mixnode(&self) -> MixNode { + MixNode { + host: "127.0.0.1".to_string(), + mix_port: self.mix_port, + verloc_port: self.verloc_port, + http_api_port: self.http_port, + sphinx_key: self.sphinx_key.clone(), + identity_key: self.identity_key.clone(), + version: self.version.clone(), + } + } + + fn cost_params(&self) -> MixNodeCostParams { + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: CosmWasmCoin::new(40_000000, "unym"), + } + } + + fn bonding_signature(&self) -> MessageSignature { + // this is a valid bs58 + self.bonding_signature.parse().unwrap() + } + + fn mixnode_bonding_payload(&self) -> String { + let payload = construct_mixnode_bonding_sign_payload( + 0, + Addr::unchecked(self.owner.address.to_string()), + None, + self.pledge(), + self.mixnode(), + self.cost_params(), + ); + payload.to_base58_string().unwrap() + } + + fn gateway_bonding_payload(&self) -> String { + let payload = construct_gateway_bonding_sign_payload( + 0, + Addr::unchecked(self.owner.address.to_string()), + None, + self.pledge(), + self.gateway(), + ); + payload.to_base58_string().unwrap() + } +} + +struct LocalNodesCtx<'a> { + nym_node_binary: PathBuf, + + progress: ProgressTracker, + network: &'a LoadedNetwork, + admin: DirectSigningHttpRpcNyxdClient, + + mix_nodes: Vec, + gateway: Option, +} + +impl<'a> ProgressCtx for LocalNodesCtx<'a> { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl<'a> LocalNodesCtx<'a> { + fn nym_node_id(&self, node: &NymNode) -> String { + format!("{}-{}", node.owner.address, self.network.name) + } + + fn new( + nym_node_binary: PathBuf, + network: &'a LoadedNetwork, + admin_mnemonic: bip39::Mnemonic, + ) -> Result { + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new local nym-nodes for network '{}' over {}", + network.name, network.rpc_endpoint + )); + + Ok(LocalNodesCtx { + nym_node_binary, + network, + admin: DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + network.client_config()?, + network.rpc_endpoint.as_str(), + admin_mnemonic, + )?, + mix_nodes: Vec::new(), + progress, + gateway: None, + }) + } + + fn signing_node_owner( + &self, + node: &NymNode, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.network.client_config()?, + self.network.rpc_endpoint.as_str(), + node.owner.mnemonic.clone(), + )?) + } + + fn signing_rewarder(&self) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.network.client_config()?, + self.network.rpc_endpoint.as_str(), + self.network + .auxiliary_addresses + .mixnet_rewarder + .mnemonic + .clone(), + )?) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "node_type")] +pub enum BondingInformationV1 { + Mixnode(MixnodeBondingInformation), + Gateway(GatewayBondingInformation), +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct MixnodeBondingInformation { + pub(crate) version: String, + pub(crate) host: String, + pub(crate) identity_key: String, + pub(crate) sphinx_key: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GatewayBondingInformation { + pub(crate) version: String, + pub(crate) host: String, + pub(crate) location: String, + pub(crate) identity_key: String, + pub(crate) sphinx_key: String, +} + +#[derive(Deserialize)] +struct ReducedSignatureOut { + encoded_signature: String, +} + +impl NetworkManager { + async fn initialise_nym_node<'a>( + &self, + ctx: &mut LocalNodesCtx<'a>, + offset: u16, + is_gateway: bool, + ) -> Result<(), NetworkManagerError> { + let mut node = NymNode::new_empty(); + let env = ctx.network.default_env_file_path(); + let id = ctx.nym_node_id(&node); + + let output_dir = tempfile::tempdir()?; + let output_file_path = output_dir.path().join("bonding_info.json"); + + ctx.set_pb_message(format!("initialising node {id}...")); + let mix_port = 5000 + offset; + let verloc_port = 6000 + offset; + let clients_port = 7000 + offset; + let http_port = 8000 + offset; + + node.mix_port = mix_port; + node.verloc_port = verloc_port; + node.clients_port = clients_port; + node.http_port = http_port; + + let mut cmd = Command::new(&ctx.nym_node_binary); + cmd.args([ + "-c", + &env.display().to_string(), + "run", + "--id", + &id, + "--init-only", + "--public-ips", + "127.0.0.1", + "--http-bind-address", + &format!("127.0.0.1:{http_port}"), + "--mixnet-bind-address", + &format!("127.0.0.1:{mix_port}"), + "--verloc-bind-address", + &format!("127.0.0.1:{verloc_port}"), + "--entry-bind-address", + &format!("127.0.0.1:{clients_port}"), + "--mnemonic", + &Zeroizing::new(node.owner.mnemonic.to_string()), + "--local", + "--output", + "json", + "--bonding-information-output", + &output_file_path.display().to_string(), + ]) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true); + + if is_gateway { + cmd.args(["--mode", "entry"]); + } + + let mut child = cmd.spawn()?; + let child_fut = child.wait(); + let out = ctx.async_with_progress(child_fut).await?; + if !out.success() { + return Err(NetworkManagerError::NymNodeExecutionFailure); + } + + let output_file = fs::File::open(&output_file_path)?; + let bonding_info: BondingInformationV1 = serde_json::from_reader(&output_file)?; + + match bonding_info { + BondingInformationV1::Mixnode(bonding_info) => { + node.identity_key = bonding_info.identity_key; + node.sphinx_key = bonding_info.sphinx_key; + node.version = bonding_info.version; + } + BondingInformationV1::Gateway(bonding_info) => { + node.identity_key = bonding_info.identity_key; + node.sphinx_key = bonding_info.sphinx_key; + node.version = bonding_info.version; + } + } + + ctx.set_pb_message(format!("generating bonding signature for node {id}...")); + + let msg = if is_gateway { + node.gateway_bonding_payload() + } else { + node.mixnode_bonding_payload() + }; + + let child = Command::new(&ctx.nym_node_binary) + .args([ + "--no-banner", + "sign", + "--id", + &id, + "--contract-msg", + &msg, + "--output", + "json", + ]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .output(); + let out = ctx.async_with_progress(child).await?; + if !out.status.success() { + return Err(NetworkManagerError::NymNodeExecutionFailure); + } + let signature: ReducedSignatureOut = serde_json::from_slice(&out.stdout)?; + node.bonding_signature = signature.encoded_signature; + + ctx.println(format!( + "\tinitialised node {} (gateway: {})", + node.identity_key, is_gateway + )); + + if is_gateway { + ctx.gateway = Some(node) + } else { + ctx.mix_nodes.push(node) + } + Ok(()) + } + + async fn initialise_nym_nodes<'a>( + &self, + ctx: &mut LocalNodesCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔏 {}Initialising local nym-nodes...", + style("[1/4]").bold().dim() + )); + + // 3 mixnodes, 1 gateway; maybe at some point make it configurable + for i in 0..4 { + let is_gateway = i == 0; + self.initialise_nym_node(ctx, i, is_gateway).await?; + } + + ctx.println("\t✅ all nym nodes got initialised!"); + + Ok(()) + } + + async fn transfer_bonding_tokens<'a>( + &self, + ctx: &LocalNodesCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💸 {}Transferring tokens to the bond owners...", + style("[2/4]").bold().dim() + )); + + let mut receivers = Vec::new(); + for node in ctx + .mix_nodes + .iter() + .chain(std::iter::once(ctx.gateway.as_ref().unwrap())) + { + // send 101nym to the owner + receivers.push((node.owner.address.clone(), ctx.admin.mix_coins(101_000000))) + } + + ctx.set_pb_message("attempting to send signer tokens..."); + + let send_future = ctx.admin.send_multiple( + receivers, + "bond owners token transfer from testnet-manager", + None, + ); + let res = ctx.async_with_progress(send_future).await?; + + ctx.println(format!( + "\t✅ sent tokens in transaction: {} (height {})", + res.hash, res.height + )); + Ok(()) + } + + async fn bond_node<'a>( + &self, + ctx: &LocalNodesCtx<'a>, + node: &NymNode, + is_gateway: bool, + ) -> Result<(), NetworkManagerError> { + let prefix = if is_gateway { "[gateway]" } else { "[mixnode]" }; + ctx.set_pb_prefix(prefix); + + let id = ctx.nym_node_id(node); + ctx.set_pb_message(format!("attempting to bond node {id}...")); + + let owner = ctx.signing_node_owner(node)?; + + let bonding_fut = if is_gateway { + owner.bond_gateway( + node.gateway(), + node.bonding_signature(), + node.pledge().into(), + None, + ) + } else { + owner.bond_mixnode( + node.mixnode(), + node.cost_params(), + node.bonding_signature(), + node.pledge().into(), + None, + ) + }; + let res = ctx.async_with_progress(bonding_fut).await?; + ctx.println(format!( + "\t{id} bonded in transaction: {}", + res.transaction_hash + )); + + Ok(()) + } + + async fn bond_nym_nodes<'a>(&self, ctx: &LocalNodesCtx<'a>) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "⛓️ {}Bonding the local nym-nodes...", + style("[3/4]").bold().dim() + )); + + self.bond_node(ctx, ctx.gateway.as_ref().unwrap(), true) + .await?; + for mix_node in &ctx.mix_nodes { + self.bond_node(ctx, mix_node, false).await?; + } + + ctx.println("\t✅ all nym nodes got bonded!"); + + Ok(()) + } + + async fn assign_to_active_set<'a>( + &self, + ctx: &LocalNodesCtx<'a>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🔌 {}Assigning mixnodes to the active set...", + style("[4/4]").bold().dim() + )); + + let rewarder = ctx.signing_rewarder()?; + + ctx.set_pb_message("starting epoch transition..."); + let fut = rewarder.begin_epoch_transition(None); + ctx.async_with_progress(fut).await?; + + ctx.set_pb_message("reconciling (no) epoch events..."); + let fut = rewarder.reconcile_epoch_events(None, None); + ctx.async_with_progress(fut).await?; + + ctx.set_pb_message("finally assigning the active set..."); + let fut = rewarder.get_rewarding_parameters(); + let rewarding_params = ctx.async_with_progress(fut).await?; + let active_set_size = rewarding_params.active_set_size; + + let layer_assignment = vec![ + LayerAssignment::new(1, Layer::One), + LayerAssignment::new(2, Layer::Two), + LayerAssignment::new(3, Layer::Three), + ]; + let fut = rewarder.advance_current_epoch(layer_assignment, active_set_size, None); + ctx.async_with_progress(fut).await?; + + Ok(()) + } + + fn prepare_nym_nodes_run_commands( + &self, + ctx: &LocalNodesCtx, + ) -> Result { + let env_file = ctx.network.default_env_file_path(); + + let bin_canon = fs::canonicalize(&ctx.nym_node_binary)?; + let env_canon = fs::canonicalize(env_file)?; + let bin_canon_display = bin_canon.display(); + let env_canon_display = env_canon.display(); + + let mut cmds = Vec::new(); + for node in ctx + .mix_nodes + .iter() + .chain(std::iter::once(ctx.gateway.as_ref().unwrap())) + { + let id = ctx.nym_node_id(node); + cmds.push(format!( + "{bin_canon_display} -c {env_canon_display} run --id {id} --local" + )); + } + + Ok(RunCommands(cmds)) + } + + fn output_nym_nodes_run_commands(&self, ctx: &LocalNodesCtx, cmds: &RunCommands) { + ctx.progress.output_run_commands(cmds) + } + + pub(crate) async fn init_local_nym_nodes>( + &self, + nym_node_binary: P, + network: &LoadedNetwork, + ) -> Result { + let mut ctx = LocalNodesCtx::new( + nym_node_binary.as_ref().to_path_buf(), + network, + self.admin.deref().clone(), + )?; + + let env_file = ctx.network.default_env_file_path(); + if !env_file.exists() { + return Err(NetworkManagerError::EnvFileNotGenerated); + } + + self.initialise_nym_nodes(&mut ctx).await?; + self.transfer_bonding_tokens(&ctx).await?; + self.bond_nym_nodes(&ctx).await?; + self.assign_to_active_set(&ctx).await?; + let cmds = self.prepare_nym_nodes_run_commands(&ctx)?; + self.output_nym_nodes_run_commands(&ctx, &cmds); + + Ok(cmds) + } +} diff --git a/tools/internal/testnet-manager/src/manager/mod.rs b/tools/internal/testnet-manager/src/manager/mod.rs new file mode 100644 index 0000000000..5502bc4fdd --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/mod.rs @@ -0,0 +1,127 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use crate::helpers::{async_with_progress, default_storage_dir, wasm_code}; +use crate::manager::network::LoadedNetwork; +use crate::manager::storage::NetworkManagerStorage; +use bip39::rand::prelude::SliceRandom; +use bip39::rand::thread_rng; +use indicatif::ProgressBar; +use nym_config::defaults::NymNetworkDetails; +use nym_validator_client::nyxd::cosmwasm_client::types::UploadResult; +use nym_validator_client::nyxd::Config; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; +use std::path::{Path, PathBuf}; +use url::Url; +use zeroize::Zeroizing; + +mod contract; +mod dkg_skip; +mod local_apis; +mod local_client; +mod local_nodes; +pub(crate) mod network; +mod network_init; +pub(crate) mod storage; + +pub(crate) struct NetworkManager { + admin: Zeroizing, + storage: NetworkManagerStorage, + rpc_endpoint: Url, +} + +impl NetworkManager { + pub(crate) async fn new>( + database_path: P, + mnemonic: Option, + rpc_endpoint: Option, + ) -> Result { + let storage = NetworkManagerStorage::init(database_path).await?; + + let (mnemonic, rpc_endpoint) = if !storage.metadata_set().await? { + let mnemonic = mnemonic.ok_or(NetworkManagerError::MnemonicNotSet)?; + let rpc_endpoint = rpc_endpoint.ok_or(NetworkManagerError::RpcEndpointNotSet)?; + + storage + .set_initial_metadata(&mnemonic, &rpc_endpoint) + .await?; + (mnemonic, rpc_endpoint) + } else { + let mnemonic = storage + .get_master_mnemonic() + .await? + .ok_or(NetworkManagerError::MnemonicNotSet)?; + + let rpc_endpoint = storage + .get_rpc_endpoint() + .await? + .ok_or(NetworkManagerError::RpcEndpointNotSet)?; + + (mnemonic, rpc_endpoint) + }; + + Ok(NetworkManager { + admin: Zeroizing::new(mnemonic), + storage, + rpc_endpoint, + }) + } + + pub fn default_latest_env_file_path(&self) -> PathBuf { + default_storage_dir().join("latest.env") + } + + #[allow(unused)] + pub(crate) fn query_client( + &self, + network: &LoadedNetwork, + ) -> Result { + let network_details = NymNetworkDetails::from(network); + let config = Config::try_from_nym_network_details(&network_details)?; + + Ok(QueryHttpRpcNyxdClient::connect( + config, + self.rpc_endpoint.as_str(), + )?) + } + + fn get_network_name(&self, user_provided: Option) -> String { + user_provided.unwrap_or_else(|| { + // a hack to get human-readable words without extra deps : ) + let mut rng = thread_rng(); + + let words = bip39::Language::English.word_list(); + let first = words.choose(&mut rng).unwrap(); + let second = words.choose(&mut rng).unwrap(); + format!("{first}-{second}") + }) + } + + async fn upload_contract>( + &self, + admin: &DirectSigningHttpRpcNyxdClient, + pb: &ProgressBar, + path: P, + ) -> Result { + let wasm = wasm_code(path)?; + let upload_future = admin.upload(wasm, "contract upload from testnet-manager", None); + + async_with_progress(upload_future, pb) + .await + .map_err(Into::into) + } + + pub(crate) async fn load_existing_network( + &self, + network_name: Option, + ) -> Result { + let network_name = if let Some(explicit) = network_name { + explicit + } else { + self.storage.get_latest_network_name().await? + }; + + self.storage.try_load_network(&network_name).await + } +} diff --git a/tools/internal/testnet-manager/src/manager/network.rs b/tools/internal/testnet-manager/src/manager/network.rs new file mode 100644 index 0000000000..aeda4a344d --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/network.rs @@ -0,0 +1,200 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use crate::helpers::default_storage_dir; +use crate::manager::contract::{Account, LoadedNymContracts, NymContracts}; +use nym_config::defaults::{NymNetworkDetails, ValidatorDetails}; +use nym_validator_client::nyxd::Config; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use time::OffsetDateTime; +use url::Url; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Network { + pub name: String, + + pub rpc_endpoint: Url, + + #[serde(with = "time::serde::rfc3339")] + pub created_at: OffsetDateTime, + + pub contracts: NymContracts, + + pub auxiliary_addresses: SpecialAddresses, +} + +impl Network { + pub fn unchecked_to_env_file_section(&self) -> String { + format!( + "CONFIGURED=true\n\ +\n\ +BECH32_PREFIX=n\n\ +MIX_DENOM=unym\n\ +MIX_DENOM_DISPLAY=nym\n\ +STAKE_DENOM=unyx\n\ +STAKE_DENOM_DISPLAY=nyx\n\ +DENOMS_EXPONENT=6\n\ +\n\ +REWARDING_VALIDATOR_ADDRESS={}\n\ +MIXNET_CONTRACT_ADDRESS={}\n\ +VESTING_CONTRACT_ADDRESS={}\n\ +ECASH_CONTRACT_ADDRESS={}\n\ +GROUP_CONTRACT_ADDRESS={}\n\ +MULTISIG_CONTRACT_ADDRESS={}\n\ +COCONUT_DKG_CONTRACT_ADDRESS={}\n\ +NYXD={}\n\ +", + self.auxiliary_addresses.mixnet_rewarder.address, + self.contracts.mixnet.address().unwrap(), + self.contracts.vesting.address().unwrap(), + self.contracts.ecash.address().unwrap(), + self.contracts.cw4_group.address().unwrap(), + self.contracts.cw3_multisig.address().unwrap(), + self.contracts.dkg.address().unwrap(), + self.rpc_endpoint, + ) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct LoadedNetwork { + pub(crate) name: String, + + pub(crate) rpc_endpoint: Url, + + #[serde(with = "time::serde::rfc3339")] + pub(crate) created_at: OffsetDateTime, + + pub(crate) contracts: LoadedNymContracts, + + pub(crate) auxiliary_addresses: SpecialAddresses, +} + +impl From for LoadedNetwork { + fn from(value: Network) -> Self { + LoadedNetwork { + name: value.name, + rpc_endpoint: value.rpc_endpoint, + created_at: value.created_at, + contracts: value.contracts.into(), + auxiliary_addresses: value.auxiliary_addresses, + } + } +} + +impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails { + fn from(value: &'a LoadedNetwork) -> Self { + let contracts = nym_config::defaults::NymContracts { + mixnet_contract_address: Some(value.contracts.mixnet.address.to_string()), + vesting_contract_address: Some(value.contracts.vesting.address.to_string()), + ecash_contract_address: Some(value.contracts.ecash.address.to_string()), + group_contract_address: Some(value.contracts.cw4_group.address.to_string()), + multisig_contract_address: Some(value.contracts.cw3_multisig.address.to_string()), + coconut_dkg_contract_address: Some(value.contracts.dkg.address.to_string()), + }; + // ASSUMPTION: same chain details like prefix, denoms, etc. as mainnet + let mainnet = NymNetworkDetails::new_mainnet(); + NymNetworkDetails { + chain_details: mainnet.chain_details, + network_name: "foomp".to_string(), + endpoints: vec![ValidatorDetails { + nyxd_url: value.rpc_endpoint.to_string(), + websocket_url: None, + api_url: None, + }], + contracts, + explorer_api: None, + } + } +} + +impl LoadedNetwork { + pub fn default_env_file_path(&self) -> PathBuf { + default_storage_dir() + .join(&self.name) + .join(format!("{}.env", &self.name)) + } + + #[allow(dead_code)] + pub fn query_client(&self) -> Result { + Ok(QueryHttpRpcNyxdClient::connect( + self.client_config()?, + self.rpc_endpoint.as_str(), + )?) + } + + pub fn dkg_signing_client( + &self, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.client_config()?, + self.rpc_endpoint.as_str(), + self.contracts.dkg.admin_mnemonic.clone(), + )?) + } + + pub fn client_config(&self) -> Result { + let network_details = NymNetworkDetails::from(self); + let config = Config::try_from_nym_network_details(&network_details)?; + Ok(config) + } + + pub fn cw4_group_signing_client( + &self, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.client_config()?, + self.rpc_endpoint.as_str(), + self.contracts.cw4_group.admin_mnemonic.clone(), + )?) + } + + pub fn to_env_file_section(&self) -> String { + format!( + "CONFIGURED=true\n\ +\n\ +BECH32_PREFIX=n\n\ +MIX_DENOM=unym\n\ +MIX_DENOM_DISPLAY=nym\n\ +STAKE_DENOM=unyx\n\ +STAKE_DENOM_DISPLAY=nyx\n\ +DENOMS_EXPONENT=6\n\ +\n\ +REWARDING_VALIDATOR_ADDRESS={}\n\ +MIXNET_CONTRACT_ADDRESS={}\n\ +VESTING_CONTRACT_ADDRESS={}\n\ +ECASH_CONTRACT_ADDRESS={}\n\ +GROUP_CONTRACT_ADDRESS={}\n\ +MULTISIG_CONTRACT_ADDRESS={}\n\ +COCONUT_DKG_CONTRACT_ADDRESS={}\n\ +NYXD={}\n\ +", + self.auxiliary_addresses.mixnet_rewarder.address, + self.contracts.mixnet.address, + self.contracts.vesting.address, + self.contracts.ecash.address, + self.contracts.cw4_group.address, + self.contracts.cw3_multisig.address, + self.contracts.dkg.address, + self.rpc_endpoint, + ) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SpecialAddresses { + pub ecash_holding_account: Account, + pub mixnet_rewarder: Account, +} + +impl Default for SpecialAddresses { + fn default() -> Self { + SpecialAddresses { + ecash_holding_account: Account::new(), + mixnet_rewarder: Account::new(), + } + } +} diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs new file mode 100644 index 0000000000..baeb614433 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -0,0 +1,701 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NetworkManagerError; +use crate::helpers::{async_with_progress, ProgressCtx, ProgressTracker}; +use crate::manager::contract::Account; +use crate::manager::network::Network; +use crate::manager::NetworkManager; +use console::style; +use cw_utils::Threshold; +use indicatif::HumanDuration; +use nym_coconut_dkg_common::types::TimeConfiguration; +use nym_config::defaults::NymNetworkDetails; +use nym_mixnet_contract_common::{Decimal, InitialRewardingParams, Percent}; +use nym_validator_client::nyxd::cosmwasm_client::types::InstantiateOptions; +use nym_validator_client::nyxd::Config; +use nym_validator_client::DirectSigningHttpRpcNyxdClient; +use std::ops::Deref; +use std::path::Path; +use std::time::Duration; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; +use url::Url; + +struct InitCtx { + progress: ProgressTracker, + network: Network, + admin: DirectSigningHttpRpcNyxdClient, +} + +impl InitCtx { + fn dummy_client_config() -> Result { + // ASSUMPTION: same chain details like prefix, denoms, etc. as mainnet + let mainnet = NymNetworkDetails::new_mainnet(); + let network_details = NymNetworkDetails { + chain_details: mainnet.chain_details, + network_name: "foomp".to_string(), // does this matter? + endpoints: vec![], + contracts: Default::default(), + explorer_api: None, + }; + Ok(Config::try_from_nym_network_details(&network_details)?) + } + + fn new( + network_name: String, + admin_mnemonic: bip39::Mnemonic, + rpc_endpoint: &Url, + ) -> Result { + let admin = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + Self::dummy_client_config()?, + rpc_endpoint.as_str(), + admin_mnemonic, + )?; + + let progress = ProgressTracker::new(format!( + "\n🚀 setting up new testnet '{network_name}' over {rpc_endpoint}", + )); + + Ok(InitCtx { + progress, + network: Network { + name: network_name, + rpc_endpoint: rpc_endpoint.clone(), + created_at: OffsetDateTime::now_utc(), + contracts: Default::default(), + auxiliary_addresses: Default::default(), + }, + admin, + }) + } + + fn mixnet_signing_client(&self) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + Self::dummy_client_config()?, + self.network.rpc_endpoint.as_str(), + self.network.contracts.mixnet.admin()?.mnemonic.clone(), + )?) + } + + fn multisig_signing_client( + &self, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + Self::dummy_client_config()?, + self.network.rpc_endpoint.as_str(), + self.network + .contracts + .cw3_multisig + .admin()? + .mnemonic + .clone(), + )?) + } +} + +impl ProgressCtx for InitCtx { + fn progress_tracker(&self) -> &ProgressTracker { + &self.progress + } +} + +impl NetworkManager { + fn mixnet_migrate_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_mixnet_contract_common::MigrateMsg { + vesting_contract_address: Some(ctx.network.contracts.vesting.address()?.to_string()), + }) + } + + fn multisig_migrate_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_multisig_contract_common::msg::MigrateMsg { + coconut_bandwidth_address: ctx.network.contracts.ecash.address()?.to_string(), + coconut_dkg_address: ctx.network.contracts.dkg.address()?.to_string(), + }) + } + + fn mixnet_init_message( + &self, + ctx: &InitCtx, + custom_epoch_duration: Option, + ) -> Result { + Ok(nym_mixnet_contract_common::InstantiateMsg { + rewarding_validator_address: ctx + .network + .auxiliary_addresses + .mixnet_rewarder + .address + .to_string(), + // PLACEHOLDER \/ + vesting_contract_address: ctx + .network + .auxiliary_addresses + .mixnet_rewarder + .address + .to_string(), + // PLACEHOLDER /\ + rewarding_denom: ctx.admin.mix_coin(0).denom, + epochs_in_interval: 720, + epoch_duration: custom_epoch_duration.unwrap_or(Duration::from_secs(60 * 60)), + initial_rewarding_params: InitialRewardingParams { + initial_reward_pool: Decimal::from_atomics(250_000_000_000_000u128, 0).unwrap(), + initial_staking_supply: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), + staking_supply_scale_factor: Percent::from_percentage_value(50).unwrap(), + sybil_resistance: Percent::from_percentage_value(30).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(2).unwrap(), + rewarded_set_size: 240, + active_set_size: 240, + }, + }) + } + + fn vesting_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_vesting_contract_common::InitMsg { + mixnet_contract_address: ctx.network.contracts.mixnet.address()?.to_string(), + mix_denom: ctx.admin.mix_coin(0).denom, + }) + } + + fn dkg_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_coconut_dkg_common::msg::InstantiateMsg { + group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), + multisig_addr: ctx.network.contracts.cw3_multisig.address()?.to_string(), + time_configuration: Some(TimeConfiguration { + public_key_submission_time_secs: 3600, + dealing_exchange_time_secs: 3600, + verification_key_submission_time_secs: 3600, + verification_key_validation_time_secs: 3600, + verification_key_finalization_time_secs: 3600, + in_progress_time_secs: 10000000000, + }), + mix_denom: ctx.admin.mix_coin(0).denom, + key_size: 5, + }) + } + + fn ecash_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_ecash_contract_common::msg::InstantiateMsg { + holding_account: ctx + .network + .auxiliary_addresses + .ecash_holding_account + .address + .to_string(), + multisig_addr: ctx.network.contracts.cw3_multisig.address()?.to_string(), + group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), + mix_denom: ctx.admin.mix_coin(0).denom, + }) + } + + fn cw3_multisig_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_multisig_contract_common::msg::InstantiateMsg { + group_addr: ctx.network.contracts.cw4_group.address()?.to_string(), + + // PLACEHOLDER \/ + coconut_bandwidth_contract_address: ctx + .network + .contracts + .cw4_group + .address()? + .to_string(), + coconut_dkg_contract_address: ctx.network.contracts.cw4_group.address()?.to_string(), + // PLACEHOLDER /\ + threshold: Threshold::AbsolutePercentage { + percentage: "0.67".parse().unwrap(), + }, + max_voting_period: cw_utils::Duration::Time(3600), + executor: None, + proposal_deposit: None, + }) + } + + fn cw4_group_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_group_contract_common::msg::InstantiateMsg { + admin: Some( + ctx.network + .contracts + .cw4_group + .admin()? + .address() + .to_string(), + ), + // TODO: prepopulate + members: vec![], + }) + } + + fn find_contracts>( + &self, + ctx: &mut InitCtx, + base_dir: P, + ) -> Result<(), NetworkManagerError> { + ctx.network.contracts.discover_paths(base_dir)?; + + ctx.println(format!( + "🔍 {}Locating .wasm files...", + style("[1/8]").bold().dim() + )); + ctx.println(format!( + "\tdiscovered mixnet contract at '{}'", + ctx.network.contracts.mixnet.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered vesting contract at '{}'", + ctx.network.contracts.vesting.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered ecash contract at '{}'", + ctx.network.contracts.ecash.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered cw4_group contract at '{}'", + ctx.network.contracts.cw4_group.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered cw3_multisig contract at '{}'", + ctx.network.contracts.cw3_multisig.wasm_path()?.display() + )); + ctx.println(format!( + "\tdiscovered dkg contract at '{}'", + ctx.network.contracts.dkg.wasm_path()?.display() + )); + + ctx.println("\t✅ found all the contracts!"); + + Ok(()) + } + + async fn upload_contracts(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🚚 {}Uploading contracts...", + style("[2/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + let pb = &ctx.progress.progress_bar; + + for (progress, contract) in ctx + .network + .contracts + .fake_iter_mut() + .into_iter() + .enumerate() + { + pb.set_prefix(format!("[{}/{total}]", progress + 1)); + let name = &contract.name; + pb.set_message(format!("uploading {name} contract...")); + let upload_res = self + .upload_contract( + &ctx.admin, + &ctx.progress.progress_bar, + &contract.wasm_path()?, + ) + .await?; + pb.println(format!( + "\t{name} contract uploaded with code: {}", + upload_res.code_id + )); + contract.upload_info = Some(upload_res.into()); + } + + ctx.println("\t✅ uploaded all the contracts!"); + + Ok(()) + } + + fn create_contract_admins_mnemonics( + &self, + ctx: &mut InitCtx, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📝 {}Generating admin mnemonics...", + style("[3/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + let pb = &ctx.progress.progress_bar; + for (progress, contract) in ctx + .network + .contracts + .fake_iter_mut() + .into_iter() + .enumerate() + { + pb.set_prefix(format!("[{}/{total}]", progress + 1)); + let name = &contract.name; + pb.set_message(format!("generating admin mnemonic for {name} contract...")); + let admin = Account::new(); + pb.println(format!( + "\t{} is going to be admin for the {name} contract", + admin.address + )); + contract.admin = Some(admin) + } + + ctx.println("\t✅ generated all admin mnemonics!"); + + Ok(()) + } + + async fn transfer_admin_tokens(&self, ctx: &InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💸 {}Transferring tokens to the admin accounts...", + style("[4/8]").bold().dim() + )); + + let mut receivers = Vec::new(); + for contract in ctx.network.contracts.fake_iter() { + // send 10nym to the admin + receivers.push((contract.admin()?.address(), ctx.admin.mix_coins(10_000000))) + } + + // also send them to the rewarder + receivers.push(( + ctx.network.auxiliary_addresses.mixnet_rewarder.address(), + ctx.admin.mix_coins(10_000000), + )); + + ctx.set_pb_message("attempting to send admin tokens..."); + + let send_future = + ctx.admin + .send_multiple(receivers, "admin token transfer from testnet-manager", None); + let res = ctx.async_with_progress(send_future).await?; + + ctx.println(format!( + "\t✅ sent tokens in transaction: {} (height {})", + res.hash, res.height + )); + + Ok(()) + } + + async fn instantiate_contracts( + &self, + ctx: &mut InitCtx, + custom_epoch_duration: Option, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "💽 {}Instantiating all the contracts...", + style("[5/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + + // mixnet + ctx.set_pb_prefix(format!("[1/{total}]")); + let name = &ctx.network.contracts.mixnet.name; + let code_id = ctx.network.contracts.mixnet.upload_info()?.code_id; + let admin = ctx.network.contracts.mixnet.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.mixnet_init_message(ctx, custom_epoch_duration)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.mixnet.init_info = Some(res.into()); + + // vesting + ctx.set_pb_prefix(format!("[2/{total}]")); + let name = &ctx.network.contracts.vesting.name; + let code_id = ctx.network.contracts.vesting.upload_info()?.code_id; + let admin = ctx.network.contracts.vesting.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.vesting_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.vesting.init_info = Some(res.into()); + + // group + ctx.set_pb_prefix(format!("[3/{total}]")); + let name = &ctx.network.contracts.cw4_group.name; + let code_id = ctx.network.contracts.cw4_group.upload_info()?.code_id; + let admin = ctx.network.contracts.cw4_group.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.cw4_group_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.cw4_group.init_info = Some(res.into()); + + // multisig + ctx.set_pb_prefix(format!("[4/{total}]")); + let name = &ctx.network.contracts.cw3_multisig.name; + let code_id = ctx.network.contracts.cw3_multisig.upload_info()?.code_id; + let admin = ctx.network.contracts.cw3_multisig.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.cw3_multisig_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.cw3_multisig.init_info = Some(res.into()); + + // dkg + ctx.set_pb_prefix(format!("[5/{total}]")); + let name = &ctx.network.contracts.dkg.name; + let code_id = ctx.network.contracts.dkg.upload_info()?.code_id; + let admin = ctx.network.contracts.dkg.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.dkg_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.dkg.init_info = Some(res.into()); + + // ecash + ctx.set_pb_prefix(format!("[6/{total}]")); + let name = &ctx.network.contracts.ecash.name; + let code_id = ctx.network.contracts.ecash.upload_info()?.code_id; + let admin = ctx.network.contracts.ecash.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.ecash_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.ecash.init_info = Some(res.into()); + + ctx.println("\t✅ instantiated all the contracts!"); + + Ok(()) + } + + async fn perform_final_migrations(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🧹 {}Performing final migrations and contract cleanup...", + style("[6/8]").bold().dim() + )); + + // migrate mixnet + ctx.set_pb_prefix("[1/2]"); + let name = &ctx.network.contracts.mixnet.name; + let code_id = ctx.network.contracts.mixnet.upload_info()?.code_id; + let address = ctx.network.contracts.mixnet.address()?; + ctx.set_pb_message(format!("attempting to migrate {name} contract...")); + let migrate_msg = self.mixnet_migrate_message(ctx)?; + let client = ctx.mixnet_signing_client()?; + let migrate_fut = client.migrate( + address, + code_id, + &migrate_msg, + "contract migration from testnet-manager", + None, + ); + let migrate_res = ctx.async_with_progress(migrate_fut).await?; + ctx.network.contracts.mixnet.migrate_info = Some(migrate_res.into()); + ctx.println(format!("\t{name} contract has been migrated")); + + // migrate multisig + ctx.set_pb_prefix("[2/2]"); + let name = &ctx.network.contracts.cw3_multisig.name; + let code_id = ctx.network.contracts.cw3_multisig.upload_info()?.code_id; + let address = ctx.network.contracts.cw3_multisig.address()?; + ctx.set_pb_message(format!("attempting to migrate {name} contract...")); + let migrate_msg = self.multisig_migrate_message(ctx)?; + let client = ctx.multisig_signing_client()?; + let migrate_fut = client.migrate( + address, + code_id, + &migrate_msg, + "contract migration from testnet-manager", + None, + ); + let migrate_res = ctx.async_with_progress(migrate_fut).await?; + ctx.network.contracts.cw3_multisig.migrate_info = Some(migrate_res.into()); + ctx.println(format!("\t{name} contract has been migrated")); + + ctx.println("\t✅ performed all the needed migrations!"); + + Ok(()) + } + + async fn get_build_info(&self, ctx: &mut InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🏗️ {}Obtaining contracts build information", + style("[7/8]").bold().dim() + )); + + let total = ctx.network.contracts.count() as u64; + + let pb = &ctx.progress.progress_bar; + for (progress, contract) in ctx + .network + .contracts + .fake_iter_mut() + .into_iter() + .enumerate() + { + pb.set_prefix(format!("[{}/{total}]", progress + 1)); + let name = &contract.name; + let address = contract.address()?; + pb.set_message(format!("querying {name} contract...")); + let build_info_fut = ctx.admin.try_get_contract_build_information(address); + let build_info = async_with_progress(build_info_fut, &ctx.progress.progress_bar) + .await + .ok_or_else(|| NetworkManagerError::MissingBuildInfo { + name: name.to_string(), + })?; + + let now = OffsetDateTime::now_utc(); + // SAFETY: all the information saved in our contracts should be well-formed + let commit_timestamp = OffsetDateTime::parse(&build_info.commit_timestamp, &Rfc3339) + .expect("malformed commit timestamp"); + + let age = now - commit_timestamp; + + pb.println(format!( + "\t{name} contract was built from branch: {} (sha: {}); age: {}", + build_info.commit_branch, + build_info.commit_sha, + HumanDuration(age.unsigned_abs()) + )); + + if age > time::Duration::days(30) { + pb.println(format!( + "\t\t️☠️️ {}", + style("this commit is ANCIENT - please double check if this is intended") + .bold() + .red() + )) + } else if age > time::Duration::days(7) { + pb.println(format!( + "\t\t️❗️ {}", + style("this commit is rather old - please double check if this is intended") + .bold() + .red() + )) + } else if age > time::Duration::days(1) { + pb.println(format!( + "\t\t️️⚠️ {}", + style("this commit seems outdated - please double check if this is intended") + .bold() + .yellow() + )) + } + + contract.build_info = Some(build_info); + } + + ctx.println("\t✅ updated all contract metadata!"); + + Ok(()) + } + + async fn persist_in_database(&self, ctx: &InitCtx) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "📦 {}Storing all the results in the database", + style("[8/8]").bold().dim() + )); + + ctx.set_pb_message("attempting to persist network data..."); + let save_future = self.storage.persist_network(&ctx.network); + ctx.async_with_progress(save_future).await?; + + ctx.println("\t✅ the network information got persisted in the database for future use"); + + Ok(()) + } + + pub(crate) async fn initialise_new_network>( + &self, + contracts: P, + network_name: Option, + custom_epoch_duration: Option, + ) -> Result { + let network_name = self.get_network_name(network_name); + let mut ctx = InitCtx::new(network_name, self.admin.deref().clone(), &self.rpc_endpoint)?; + + self.find_contracts(&mut ctx, contracts)?; + self.upload_contracts(&mut ctx).await?; + self.create_contract_admins_mnemonics(&mut ctx)?; + self.transfer_admin_tokens(&ctx).await?; + self.instantiate_contracts(&mut ctx, custom_epoch_duration) + .await?; + self.perform_final_migrations(&mut ctx).await?; + self.get_build_info(&mut ctx).await?; + self.persist_in_database(&ctx).await?; + + Ok(ctx.network.clone()) + } +} diff --git a/tools/internal/testnet-manager/src/manager/storage/manager.rs b/tools/internal/testnet-manager/src/manager/storage/manager.rs new file mode 100644 index 0000000000..7cde58aeed --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/storage/manager.rs @@ -0,0 +1,183 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::manager::storage::models::{RawAccount, RawContract, RawNetwork}; +use time::OffsetDateTime; + +#[derive(Clone)] +pub(crate) struct StorageManager { + pub(crate) connection_pool: sqlx::SqlitePool, +} + +// all SQL goes here +impl StorageManager { + pub(crate) async fn metadata_set(&self) -> Result { + Ok(sqlx::query("SELECT id FROM metadata") + .fetch_optional(&self.connection_pool) + .await? + .is_some()) + } + + pub(crate) async fn get_master_mnemonic(&self) -> Result, sqlx::Error> { + sqlx::query!("SELECT master_mnemonic FROM metadata") + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.master_mnemonic)) + } + + pub(crate) async fn get_rpc_endpoint(&self) -> Result, sqlx::Error> { + sqlx::query!("SELECT rpc_endpoint FROM metadata") + .fetch_optional(&self.connection_pool) + .await + .map(|maybe_record| maybe_record.map(|r| r.rpc_endpoint)) + } + + pub(crate) async fn get_latest_network_id(&self) -> Result, sqlx::Error> { + let maybe_record = sqlx::query!("SELECT latest_network_id FROM metadata") + .fetch_optional(&self.connection_pool) + .await?; + Ok(maybe_record.and_then(|r| r.latest_network_id)) + } + + pub(crate) async fn get_network_name(&self, network_id: i64) -> Result { + sqlx::query!("SELECT name FROM network WHERE id = ?", network_id) + .fetch_one(&self.connection_pool) + .await + .map(|record| record.name) + } + + pub(crate) async fn set_initial_metadata( + &self, + master_mnemonic: &str, + rpc_endpoint: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO metadata (id, master_mnemonic, rpc_endpoint) VALUES (0, ?, ?)", + master_mnemonic, + rpc_endpoint + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn save_latest_network_id( + &self, + latest_network_id: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE metadata SET latest_network_id = ?", + latest_network_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) async fn save_network( + &self, + name: &str, + created_at: OffsetDateTime, + mixnet_id: i64, + vesting_id: i64, + ecash_id: i64, + cw3_id: i64, + cw4_id: i64, + dkg_id: i64, + rewarder_address: &str, + ecash_holding_address: &str, + ) -> Result { + let network_id = sqlx::query!( + r#" + INSERT INTO network ( + name, + created_at, + mixnet_contract_id, + vesting_contract_id, + ecash_contract_id, + cw3_multisig_contract_id, + cw4_group_contract_id, + dkg_contract_id, + rewarder_address, + ecash_holding_account_address + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "#, + name, + created_at, + mixnet_id, + vesting_id, + ecash_id, + cw3_id, + cw4_id, + dkg_id, + rewarder_address, + ecash_holding_address, + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(network_id) + } + + pub(crate) async fn load_network(&self, name: &str) -> Result { + sqlx::query_as("SELECT * FROM network WHERE name = ?") + .bind(name) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_contract( + &self, + name: &str, + address: &str, + admin_address: &str, + ) -> Result { + let id = sqlx::query!( + "INSERT INTO contract (name, address, admin_address) VALUES (?, ?, ?)", + name, + address, + admin_address + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(id) + } + + pub(crate) async fn load_contract(&self, id: i64) -> Result { + sqlx::query_as( + r#" + SELECT t1.id, t1.name, t1.address, t1.admin_address, t2.mnemonic + FROM contract t1 + JOIN account t2 ON t1.admin_address = t2.address + WHERE t1.id = ?"#, + ) + .bind(id) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn save_account( + &self, + address: &str, + mnemonic: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO account (address, mnemonic) VALUES (?, ?)", + address, + mnemonic + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn load_account(&self, address: &str) -> Result { + sqlx::query_as("SELECT * FROM account WHERE address = ?") + .bind(address) + .fetch_one(&self.connection_pool) + .await + } +} diff --git a/tools/internal/testnet-manager/src/manager/storage/mod.rs b/tools/internal/testnet-manager/src/manager/storage/mod.rs new file mode 100644 index 0000000000..8a7960acd4 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/storage/mod.rs @@ -0,0 +1,243 @@ +use std::fs; +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only +use crate::error::NetworkManagerError; +use crate::manager::contract::{Account, Contract, LoadedNymContracts}; +use crate::manager::network::{LoadedNetwork, Network, SpecialAddresses}; +use crate::manager::storage::manager::StorageManager; +use sqlx::ConnectOptions; +use std::path::Path; +use tracing::{error, info}; +use url::Url; +use zeroize::Zeroizing; + +mod manager; +mod models; + +#[derive(Clone)] +pub(crate) struct NetworkManagerStorage { + manager: StorageManager, +} + +impl NetworkManagerStorage { + pub async fn init>(database_path: P) -> Result { + let database_path = database_path.as_ref(); + info!( + "attempting to initialise storage at {}", + database_path.display() + ); + + if let Some(parent) = database_path.parent() { + fs::create_dir_all(parent)?; + } + + // TODO: we can inject here more stuff based on our nym-api global config + // struct. Maybe different pool size or timeout intervals? + let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(true); + + // TODO: do we want auto_vacuum ? + + opts.disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(db) => db, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(err.into()); + } + }; + + if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + error!("Failed to initialize SQLx database: {err}"); + return Err(err.into()); + } + + info!("Database migration finished!"); + + let storage = NetworkManagerStorage { + manager: StorageManager { connection_pool }, + }; + + Ok(storage) + } + + pub(crate) async fn metadata_set(&self) -> Result { + Ok(self.manager.metadata_set().await?) + } + + pub(crate) async fn get_master_mnemonic( + &self, + ) -> Result, NetworkManagerError> { + Ok(self + .manager + .get_master_mnemonic() + .await? + .map(|m| m.parse()) + .transpose()?) + } + + pub(crate) async fn get_rpc_endpoint(&self) -> Result, NetworkManagerError> { + Ok(self + .manager + .get_rpc_endpoint() + .await? + .map(|m| m.parse()) + .transpose()?) + } + + pub(crate) async fn get_latest_network_name(&self) -> Result { + let Some(id) = self.manager.get_latest_network_id().await? else { + return Err(NetworkManagerError::NoNetworksInitialised); + }; + Ok(self.manager.get_network_name(id).await?) + } + + pub(crate) async fn set_initial_metadata( + &self, + master_mnemonic: &bip39::Mnemonic, + rpc_endpoint: &Url, + ) -> Result<(), NetworkManagerError> { + let master = Zeroizing::new(master_mnemonic.to_string()); + Ok(self + .manager + .set_initial_metadata(master.as_str(), rpc_endpoint.as_str()) + .await?) + } + + async fn persist_contract(&self, contract: &Contract) -> Result { + Ok(self + .manager + .save_contract( + &contract.name, + contract.init_info()?.contract_address.as_ref(), + contract.admin()?.address.as_ref(), + ) + .await?) + } + + async fn persist_account(&self, account: &Account) -> Result<(), NetworkManagerError> { + let as_str = Zeroizing::new(account.mnemonic.to_string()); + Ok(self + .manager + .save_account(account.address.as_ref(), as_str.as_str()) + .await?) + } + + pub(crate) async fn persist_network( + &self, + network: &Network, + ) -> Result<(), NetworkManagerError> { + self.persist_account(network.contracts.mixnet.admin()?) + .await?; + self.persist_account(network.contracts.vesting.admin()?) + .await?; + self.persist_account(network.contracts.ecash.admin()?) + .await?; + self.persist_account(network.contracts.cw3_multisig.admin()?) + .await?; + self.persist_account(network.contracts.cw4_group.admin()?) + .await?; + self.persist_account(network.contracts.dkg.admin()?).await?; + + self.persist_account(&network.auxiliary_addresses.mixnet_rewarder) + .await?; + self.persist_account(&network.auxiliary_addresses.ecash_holding_account) + .await?; + + let mixnet_id = self.persist_contract(&network.contracts.mixnet).await?; + let vesting_id = self.persist_contract(&network.contracts.vesting).await?; + let ecash_id = self.persist_contract(&network.contracts.ecash).await?; + let cw3_multisig_id = self + .persist_contract(&network.contracts.cw3_multisig) + .await?; + let cw4_group_id = self.persist_contract(&network.contracts.cw4_group).await?; + let dkg_id = self.persist_contract(&network.contracts.dkg).await?; + + let network_id = self + .manager + .save_network( + &network.name, + network.created_at, + mixnet_id, + vesting_id, + ecash_id, + cw3_multisig_id, + cw4_group_id, + dkg_id, + network.auxiliary_addresses.mixnet_rewarder.address.as_ref(), + network + .auxiliary_addresses + .ecash_holding_account + .address + .as_ref(), + ) + .await?; + + self.manager.save_latest_network_id(network_id).await?; + + Ok(()) + } + + pub(crate) async fn try_load_network( + &self, + name: &str, + ) -> Result { + let base_network = self.manager.load_network(name).await?; + let rpc_endpoint = self + .get_rpc_endpoint() + .await? + .ok_or_else(|| NetworkManagerError::RpcEndpointNotSet)?; + + Ok(LoadedNetwork { + name: base_network.name, + rpc_endpoint, + created_at: base_network.created_at, + contracts: LoadedNymContracts { + mixnet: self + .manager + .load_contract(base_network.mixnet_contract_id) + .await? + .try_into()?, + vesting: self + .manager + .load_contract(base_network.vesting_contract_id) + .await? + .try_into()?, + ecash: self + .manager + .load_contract(base_network.ecash_contract_id) + .await? + .try_into()?, + cw3_multisig: self + .manager + .load_contract(base_network.cw3_multisig_contract_id) + .await? + .try_into()?, + cw4_group: self + .manager + .load_contract(base_network.cw4_group_contract_id) + .await? + .try_into()?, + dkg: self + .manager + .load_contract(base_network.dkg_contract_id) + .await? + .try_into()?, + }, + auxiliary_addresses: SpecialAddresses { + ecash_holding_account: self + .manager + .load_account(&base_network.ecash_holding_account_address) + .await? + .try_into()?, + mixnet_rewarder: self + .manager + .load_account(&base_network.rewarder_address) + .await? + .try_into()?, + }, + }) + } +} diff --git a/tools/internal/testnet-manager/src/manager/storage/models.rs b/tools/internal/testnet-manager/src/manager/storage/models.rs new file mode 100644 index 0000000000..141b481272 --- /dev/null +++ b/tools/internal/testnet-manager/src/manager/storage/models.rs @@ -0,0 +1,77 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NetworkManagerError; +use crate::manager::contract::{Account, LoadedContract}; +use sqlx::FromRow; +use time::OffsetDateTime; + +#[derive(FromRow)] +pub(crate) struct RawAccount { + pub(crate) address: String, + pub(crate) mnemonic: String, +} + +impl TryFrom for Account { + type Error = NetworkManagerError; + + fn try_from(value: RawAccount) -> Result { + Ok(Account { + address: value + .address + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + mnemonic: value.mnemonic.parse()?, + }) + } +} + +#[derive(FromRow)] +pub(crate) struct RawContract { + #[allow(unused)] + pub(crate) id: i64, + pub(crate) name: String, + pub(crate) address: String, + pub(crate) admin_address: String, + pub(crate) mnemonic: String, +} + +impl TryFrom for LoadedContract { + type Error = NetworkManagerError; + + fn try_from(value: RawContract) -> Result { + Ok(LoadedContract { + name: value.name, + address: value + .address + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + admin_address: value + .admin_address + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + admin_mnemonic: value + .mnemonic + .parse() + .map_err(|_| NetworkManagerError::MalformedAccountAddress)?, + }) + } +} + +#[derive(FromRow)] +pub(crate) struct RawNetwork { + #[allow(unused)] + pub(crate) id: i64, + pub(crate) name: String, + pub(crate) created_at: OffsetDateTime, + + pub(crate) mixnet_contract_id: i64, + pub(crate) vesting_contract_id: i64, + pub(crate) ecash_contract_id: i64, + pub(crate) cw3_multisig_contract_id: i64, + pub(crate) cw4_group_contract_id: i64, + pub(crate) dkg_contract_id: i64, + + pub(crate) rewarder_address: String, + pub(crate) ecash_holding_account_address: String, +} diff --git a/tools/nym-cli/src/coconut/mod.rs b/tools/nym-cli/src/coconut/mod.rs index 2939175aed..6fda72e6a0 100644 --- a/tools/nym-cli/src/coconut/mod.rs +++ b/tools/nym-cli/src/coconut/mod.rs @@ -3,33 +3,26 @@ use nym_network_defaults::NymNetworkDetails; pub(crate) async fn execute( global_args: ClientArgs, - coconut: nym_cli_commands::coconut::Coconut, + coconut: nym_cli_commands::coconut::Ecash, network_details: &NymNetworkDetails, ) -> anyhow::Result<()> { match coconut.command { - nym_cli_commands::coconut::CoconutCommands::GenerateFreepass(args) => { - nym_cli_commands::coconut::generate_freepass::execute( + nym_cli_commands::coconut::EcashCommands::IssueTicketBook(args) => { + nym_cli_commands::coconut::issue_ticket_book::execute( args, create_signing_client(global_args, network_details)?, ) .await? } - nym_cli_commands::coconut::CoconutCommands::IssueCredentials(args) => { - nym_cli_commands::coconut::issue_credentials::execute( - args, - create_signing_client(global_args, network_details)?, - ) - .await? - } - nym_cli_commands::coconut::CoconutCommands::RecoverCredentials(args) => { - nym_cli_commands::coconut::recover_credentials::execute( + nym_cli_commands::coconut::EcashCommands::RecoverTicketBook(args) => { + nym_cli_commands::coconut::recover_ticket_book::execute( args, create_query_client(network_details)?, ) .await? } - nym_cli_commands::coconut::CoconutCommands::ImportCredential(args) => { - nym_cli_commands::coconut::import_credential::execute(args).await? + nym_cli_commands::coconut::EcashCommands::ImportTicketBook(args) => { + nym_cli_commands::coconut::import_ticket_book::execute(args).await? } } Ok(()) diff --git a/tools/nym-cli/src/main.rs b/tools/nym-cli/src/main.rs index 16b771d33e..12defe4817 100644 --- a/tools/nym-cli/src/main.rs +++ b/tools/nym-cli/src/main.rs @@ -62,8 +62,8 @@ pub(crate) enum Commands { Account(nym_cli_commands::validator::account::Account), /// Sign and verify messages Signature(nym_cli_commands::validator::signature::Signature), - /// Coconut related stuff - Coconut(nym_cli_commands::coconut::Coconut), + /// Ecash related stuff + Ecash(nym_cli_commands::coconut::Ecash), /// Query chain blocks Block(nym_cli_commands::validator::block::Block), /// Manage and execute WASM smart contracts @@ -104,7 +104,7 @@ async fn execute(cli: Cli) -> anyhow::Result<()> { Commands::Signature(signature) => { validator::signature::execute(signature, &network_details, mnemonic).await? } - Commands::Coconut(coconut) => coconut::execute(args, coconut, &network_details).await?, + Commands::Ecash(coconut) => coconut::execute(args, coconut, &network_details).await?, Commands::Block(block) => validator::block::execute(block, &network_details).await?, Commands::Cosmwasm(cosmwasm) => { validator::cosmwasm::execute(args, cosmwasm, &network_details).await? diff --git a/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs b/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs index aba0aaa9a5..5c576e5991 100644 --- a/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs +++ b/tools/nym-cli/src/validator/cosmwasm/generators/mod.rs @@ -10,9 +10,9 @@ pub(crate) async fn execute( args, ) => nym_cli_commands::validator::cosmwasm::generators::vesting::generate(args).await, - nym_cli_commands::validator::cosmwasm::generators::GenerateMessageCommands::CoconutBandwidth( + nym_cli_commands::validator::cosmwasm::generators::GenerateMessageCommands::EcashBandwidth( args, - ) => nym_cli_commands::validator::cosmwasm::generators::coconut_bandwidth::generate(args).await, + ) => nym_cli_commands::validator::cosmwasm::generators::ecash_bandwidth::generate(args).await, nym_cli_commands::validator::cosmwasm::generators::GenerateMessageCommands::CoconutDKG( args, diff --git a/wasm/zknym-lib/Cargo.toml b/wasm/zknym-lib/Cargo.toml index 6fdfb431bb..5ef55b0314 100644 --- a/wasm/zknym-lib/Cargo.toml +++ b/wasm/zknym-lib/Cargo.toml @@ -27,11 +27,12 @@ reqwest = { workspace = true } wasmtimer = { workspace = true } zeroize.workspace = true -rand = { version = "0.7.3", features = ["wasm-bindgen"] } +rand = { workspace = true } nym-bin-common = { path = "../../common/bin-common" } nym-coconut = { path = "../../common/nymcoconut", features = ["key-zeroize"] } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-credentials = { path = "../../common/credentials" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } nym-http-api-client = { path = "../../common/http-api-client" } diff --git a/wasm/zknym-lib/src/bandwidth_voucher.rs b/wasm/zknym-lib/src/bandwidth_voucher.rs index b047550c1e..d64fdd58f3 100644 --- a/wasm/zknym-lib/src/bandwidth_voucher.rs +++ b/wasm/zknym-lib/src/bandwidth_voucher.rs @@ -1,201 +1,201 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::ZkNymError; -use crate::generic_scheme::get_params; -use crate::types::{CredentialWrapper, ParametersWrapper, UnblindableShare}; -use crate::vpn_api_client::types::{ - AttributesResponse, BandwidthVoucherResponse, PartialVerificationKeysResponse, -}; -use nym_coconut::{ - Base58, BlindSignRequest, BlindedSignature, PrivateAttribute, PublicAttribute, Scalar, - SignatureShare, VerificationKey, -}; -use nym_credentials::coconut::bandwidth::issuance::Coin; -use nym_credentials::coconut::bandwidth::issued::BandwidthCredentialIssuedDataVariant; -use nym_credentials::coconut::bandwidth::voucher::BandwidthVoucherIssuedData; -use nym_credentials::IssuedBandwidthCredential; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use tsify::Tsify; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_utils::console_error; -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; - -// tiny 'hacks' to just allow passing responses from vpn-api queries -pub type NymIssuanceBandwidthVoucherOpts = AttributesResponse; -pub type VoucherShares = BandwidthVoucherResponse; -pub type VoucherIssuers = PartialVerificationKeysResponse; - -#[wasm_bindgen] -#[derive(Debug)] -#[allow(dead_code)] -pub struct NymIssuanceBandwidthVoucher { - serial_number: PrivateAttribute, - - binding_number: PrivateAttribute, - - credential_amount: Coin, - - prehashed_amount: PublicAttribute, - prehashed_type: PublicAttribute, - - blind_sign_request: BlindSignRequest, - pedersen_commitments_openings: Zeroizing>, -} - -#[wasm_bindgen] -impl NymIssuanceBandwidthVoucher { - #[wasm_bindgen(js_name = "prepareNew", constructor)] - pub fn prepare_new( - opts: NymIssuanceBandwidthVoucherOpts, - parameters: Option, - ) -> Result { - let deposit_amount: u128 = opts - .credential_amount_string - .parse() - .map_err(|_| ZkNymError::InvalidDepositAmount)?; - let credential_amount = Coin::new(deposit_amount, opts.credential_amount_denom); - - let params = get_params(¶meters); - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - - let prehashed_amount = Scalar::try_from_bs58(opts.bs58_prehashed_amount)?; - let prehashed_type = Scalar::try_from_bs58(opts.bs58_prehashed_type)?; - - let public_attributes = vec![&prehashed_amount, &prehashed_type]; - let private_attributes = vec![&serial_number, &binding_number]; - - let (pedersen_commitments_openings, blind_sign_request) = - nym_coconut::prepare_blind_sign(params, &private_attributes, &public_attributes)?; - - Ok(NymIssuanceBandwidthVoucher { - serial_number, - binding_number, - credential_amount, - prehashed_amount, - prehashed_type, - blind_sign_request, - pedersen_commitments_openings: Zeroizing::new(pedersen_commitments_openings), - }) - } - - #[wasm_bindgen(js_name = "getBlindSignRequest")] - pub fn get_blind_sign_request(&self) -> String { - self.blind_sign_request.to_bs58() - } - - #[wasm_bindgen(js_name = "unblindShare")] - pub fn unblind_share(&self, share: UnblindableShare) -> Result { - let blinded_sig = BlindedSignature::try_from_bs58(share.blinded_share_bs58)?; - let vk = VerificationKey::try_from_bs58(share.issuer_key_bs58)?; - - Ok(blinded_sig - .unblind(&vk, &self.pedersen_commitments_openings) - .into()) - } - - #[wasm_bindgen(js_name = "unblindShares")] - pub fn unblind_shares( - self, - shares: VoucherShares, - issuers: VoucherIssuers, - ) -> Result { - if shares.epoch_id != issuers.epoch_id { - console_error!( - "the provided shares and issuers are not from the same epoch! {} and {}", - shares.epoch_id, - issuers.epoch_id - ); - return Err(ZkNymError::InconsistentEpochId { - shares: shares.epoch_id, - issuers: issuers.epoch_id, - }); - } - - let mut decoded_keys = HashMap::new(); - for key in issuers.keys { - let vk = VerificationKey::try_from_bs58(key.bs58_encoded_key)?; - decoded_keys.insert(key.node_index, vk); - } - - let mut credential_shares = Vec::new(); - for share in shares.shares { - let blinded_sig = BlindedSignature::try_from_bs58(share.bs58_encoded_share)?; - let Some(vk) = decoded_keys.get(&share.node_index) else { - console_error!("received a share from issuer {} but did not receive a corresponding verification key!", share.node_index); - continue; - }; - let unblinded_sig = blinded_sig.unblind(vk, &self.pedersen_commitments_openings); - credential_shares.push(SignatureShare::new(unblinded_sig, share.node_index)); - } - - let signature = nym_coconut::aggregate_signature_shares(&credential_shares)?; - - let voucher_data = BandwidthCredentialIssuedDataVariant::Voucher( - BandwidthVoucherIssuedData::new(self.credential_amount), - ); - - Ok(NymIssuedBandwidthVoucher { - inner: IssuedBandwidthCredential::new( - self.serial_number, - self.binding_number, - signature, - voucher_data, - self.prehashed_type, - shares.epoch_id, - ), - }) - } -} - -#[wasm_bindgen] -pub struct NymIssuedBandwidthVoucher { - inner: IssuedBandwidthCredential, -} - -#[wasm_bindgen] -impl NymIssuedBandwidthVoucher { - #[wasm_bindgen(js_name = "ensureIsValid")] - pub fn ensure_is_valid( - &self, - master_vk: String, - parameters: Option, - ) -> bool { - let params = get_params(¶meters); - let Ok(master_vk) = VerificationKey::try_from_bs58(master_vk) else { - console_error!("malformed master verification key"); - return false; - }; - - let spending_req = match self.inner.prepare_for_spending(&master_vk) { - Ok(req) => req, - Err(err) => { - console_error!("failed to prepare spending request: {err}"); - return false; - } - }; - - spending_req.verify(params, &master_vk) - } - - pub fn serialise(self) -> SerialisedNymIssuedBandwidthVoucher { - SerialisedNymIssuedBandwidthVoucher { - serialisation_revision: - nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION, - bs58_encoded_data: bs58::encode(&self.inner.pack_v1()).into_string(), - } - } -} - -#[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct SerialisedNymIssuedBandwidthVoucher { - pub serialisation_revision: u8, - pub bs58_encoded_data: String, -} +// use crate::error::ZkNymError; +// use crate::generic_scheme::get_params; +// use crate::types::{CredentialWrapper, ParametersWrapper, UnblindableShare}; +// use crate::vpn_api_client::types::{ +// AttributesResponse, BandwidthVoucherResponse, PartialVerificationKeysResponse, +// }; +// use nym_coconut::{ +// Base58, BlindSignRequest, BlindedSignature, PrivateAttribute, PublicAttribute, Scalar, +// SignatureShare, VerificationKey, +// }; +// use nym_credentials::ecash::bandwidth::issuance::Coin; +// use nym_credentials::ecash::bandwidth::issued::BandwidthCredentialIssuedDataVariant; +// use nym_credentials::ecash::bandwidth::voucher::BandwidthVoucherIssuedData; +// use nym_credentials::IssuedBandwidthCredential; +// use serde::{Deserialize, Serialize}; +// use std::collections::HashMap; +// use tsify::Tsify; +// use wasm_bindgen::prelude::wasm_bindgen; +// use wasm_utils::console_error; +// use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; +// +// // tiny 'hacks' to just allow passing responses from vpn-api queries +// pub type NymIssuanceBandwidthVoucherOpts = AttributesResponse; +// pub type VoucherShares = BandwidthVoucherResponse; +// pub type VoucherIssuers = PartialVerificationKeysResponse; +// +// #[wasm_bindgen] +// #[derive(Debug)] +// #[allow(dead_code)] +// pub struct NymIssuanceBandwidthVoucher { +// serial_number: PrivateAttribute, +// +// binding_number: PrivateAttribute, +// +// credential_amount: Coin, +// +// prehashed_amount: PublicAttribute, +// prehashed_type: PublicAttribute, +// +// blind_sign_request: BlindSignRequest, +// pedersen_commitments_openings: Zeroizing>, +// } +// +// #[wasm_bindgen] +// impl NymIssuanceBandwidthVoucher { +// #[wasm_bindgen(js_name = "prepareNew", constructor)] +// pub fn prepare_new( +// opts: NymIssuanceBandwidthVoucherOpts, +// parameters: Option, +// ) -> Result { +// let deposit_amount: u128 = opts +// .credential_amount_string +// .parse() +// .map_err(|_| ZkNymError::InvalidDepositAmount)?; +// let credential_amount = Coin::new(deposit_amount, opts.credential_amount_denom); +// +// let params = get_params(¶meters); +// let serial_number = params.random_scalar(); +// let binding_number = params.random_scalar(); +// +// let prehashed_amount = Scalar::try_from_bs58(opts.bs58_prehashed_amount)?; +// let prehashed_type = Scalar::try_from_bs58(opts.bs58_prehashed_type)?; +// +// let public_attributes = vec![&prehashed_amount, &prehashed_type]; +// let private_attributes = vec![&serial_number, &binding_number]; +// +// let (pedersen_commitments_openings, blind_sign_request) = +// nym_coconut::prepare_blind_sign(params, &private_attributes, &public_attributes)?; +// +// Ok(NymIssuanceBandwidthVoucher { +// serial_number, +// binding_number, +// credential_amount, +// prehashed_amount, +// prehashed_type, +// blind_sign_request, +// pedersen_commitments_openings: Zeroizing::new(pedersen_commitments_openings), +// }) +// } +// +// #[wasm_bindgen(js_name = "getBlindSignRequest")] +// pub fn get_blind_sign_request(&self) -> String { +// self.blind_sign_request.to_bs58() +// } +// +// #[wasm_bindgen(js_name = "unblindShare")] +// pub fn unblind_share(&self, share: UnblindableShare) -> Result { +// let blinded_sig = BlindedSignature::try_from_bs58(share.blinded_share_bs58)?; +// let vk = VerificationKey::try_from_bs58(share.issuer_key_bs58)?; +// +// Ok(blinded_sig +// .unblind(&vk, &self.pedersen_commitments_openings) +// .into()) +// } +// +// #[wasm_bindgen(js_name = "unblindShares")] +// pub fn unblind_shares( +// self, +// shares: VoucherShares, +// issuers: VoucherIssuers, +// ) -> Result { +// if shares.epoch_id != issuers.epoch_id { +// console_error!( +// "the provided shares and issuers are not from the same epoch! {} and {}", +// shares.epoch_id, +// issuers.epoch_id +// ); +// return Err(ZkNymError::InconsistentEpochId { +// shares: shares.epoch_id, +// issuers: issuers.epoch_id, +// }); +// } +// +// let mut decoded_keys = HashMap::new(); +// for key in issuers.keys { +// let vk = VerificationKey::try_from_bs58(key.bs58_encoded_key)?; +// decoded_keys.insert(key.node_index, vk); +// } +// +// let mut credential_shares = Vec::new(); +// for share in shares.shares { +// let blinded_sig = BlindedSignature::try_from_bs58(share.bs58_encoded_share)?; +// let Some(vk) = decoded_keys.get(&share.node_index) else { +// console_error!("received a share from issuer {} but did not receive a corresponding verification key!", share.node_index); +// continue; +// }; +// let unblinded_sig = blinded_sig.unblind(vk, &self.pedersen_commitments_openings); +// credential_shares.push(SignatureShare::new(unblinded_sig, share.node_index)); +// } +// +// let signature = nym_coconut::aggregate_signature_shares(&credential_shares)?; +// +// let voucher_data = BandwidthCredentialIssuedDataVariant::Voucher( +// BandwidthVoucherIssuedData::new(self.credential_amount), +// ); +// +// Ok(NymIssuedBandwidthVoucher { +// inner: IssuedBandwidthCredential::new( +// self.serial_number, +// self.binding_number, +// signature, +// voucher_data, +// self.prehashed_type, +// shares.epoch_id, +// ), +// }) +// } +// } +// +// #[wasm_bindgen] +// pub struct NymIssuedBandwidthVoucher { +// inner: IssuedBandwidthCredential, +// } +// +// #[wasm_bindgen] +// impl NymIssuedBandwidthVoucher { +// #[wasm_bindgen(js_name = "ensureIsValid")] +// pub fn ensure_is_valid( +// &self, +// master_vk: String, +// parameters: Option, +// ) -> bool { +// let params = get_params(¶meters); +// let Ok(master_vk) = VerificationKey::try_from_bs58(master_vk) else { +// console_error!("malformed master verification key"); +// return false; +// }; +// +// let spending_req = match self.inner.prepare_for_spending(&master_vk) { +// Ok(req) => req, +// Err(err) => { +// console_error!("failed to prepare spending request: {err}"); +// return false; +// } +// }; +// +// spending_req.verify(params, &master_vk) +// } +// +// pub fn serialise(self) -> SerialisedNymIssuedBandwidthVoucher { +// SerialisedNymIssuedBandwidthVoucher { +// serialisation_revision: +// nym_credentials::coconut::bandwidth::issued::CURRENT_SERIALIZATION_REVISION, +// bs58_encoded_data: bs58::encode(&self.inner.pack_v1()).into_string(), +// } +// } +// } +// +// #[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +// #[tsify(into_wasm_abi, from_wasm_abi)] +// #[serde(rename_all = "camelCase")] +// pub struct SerialisedNymIssuedBandwidthVoucher { +// pub serialisation_revision: u8, +// pub bs58_encoded_data: String, +// } // #[cfg(test)] // mod tests { diff --git a/wasm/zknym-lib/src/error.rs b/wasm/zknym-lib/src/error.rs index b283908438..37c49aa672 100644 --- a/wasm/zknym-lib/src/error.rs +++ b/wasm/zknym-lib/src/error.rs @@ -7,12 +7,18 @@ use wasm_utils::wasm_error; #[derive(Debug, Error)] pub enum ZkNymError { - #[error("cryptographic failure: {source}")] + #[error("[coconut] cryptographic failure: {source}")] CoconutFailure { #[from] source: nym_coconut::CoconutError, }, + #[error("[ecash] cryptographic failure: {source}")] + EcashFailure { + #[from] + source: nym_compact_ecash::CompactEcashError, + }, + #[error("failed to contact the vpn api")] HttpClientFailure { #[from] diff --git a/wasm/zknym-lib/src/generic_scheme.rs b/wasm/zknym-lib/src/generic_scheme/coconut/mod.rs similarity index 88% rename from wasm/zknym-lib/src/generic_scheme.rs rename to wasm/zknym-lib/src/generic_scheme/coconut/mod.rs index 53f9c805f4..178f53106c 100644 --- a/wasm/zknym-lib/src/generic_scheme.rs +++ b/wasm/zknym-lib/src/generic_scheme/coconut/mod.rs @@ -1,24 +1,27 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::ZkNymError; -use crate::types::{ +use super::coconut::types::{ BlindSignRequestData, BlindSignRequestWrapper, BlindedCredentialWrapper, CredentialShareWrapper, CredentialWrapper, KeyPairWrapper, ParametersWrapper, ScalarsWrapper, VerificationKeyShareWrapper, VerificationKeyWrapper, VerifyCredentialRequestWrapper, }; -use crate::GLOBAL_PARAMS; +use crate::error::ZkNymError; +use crate::GLOBAL_COCONUT_PARAMS; use nym_coconut::{hash_to_scalar, Parameters, SignerIndex}; -use nym_credentials::IssuanceBandwidthCredential; use serde::{Deserialize, Serialize}; use std::sync::OnceLock; use tsify::Tsify; use wasm_bindgen::prelude::wasm_bindgen; +pub mod types; + // works under the assumption of having 4 attributes in the underlying credential(s) +const DEFAULT_ATTRIBUTES: u32 = 4; + pub fn default_bandwidth_credential_params() -> &'static Parameters { static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock = OnceLock::new(); - BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(IssuanceBandwidthCredential::default_parameters) + BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(|| Parameters::new(DEFAULT_ATTRIBUTES).unwrap()) } // attempt to extract appropriate system parameters in the following order: @@ -30,7 +33,7 @@ pub(crate) fn get_params(explicit_params: &Option) -> &Parame return explicit; } - if let Some(global) = GLOBAL_PARAMS.get() { + if let Some(global) = GLOBAL_COCONUT_PARAMS.get() { return global; } @@ -48,16 +51,14 @@ pub struct SetupOpts { pub set_global: Option, } -#[wasm_bindgen] +#[wasm_bindgen(js_name = "coconutSetup")] pub fn setup(opts: SetupOpts) -> Result { - let num_attributes = opts - .num_attributes - .unwrap_or(IssuanceBandwidthCredential::ENCODED_ATTRIBUTES); + let num_attributes = opts.num_attributes.unwrap_or(DEFAULT_ATTRIBUTES); let params = nym_coconut::setup(num_attributes)?; if let Some(true) = opts.set_global { - GLOBAL_PARAMS + GLOBAL_COCONUT_PARAMS .set(params.clone()) .map_err(|_| ZkNymError::GlobalParamsAlreadySet)?; } @@ -65,7 +66,7 @@ pub fn setup(opts: SetupOpts) -> Result { Ok(params.into()) } -#[wasm_bindgen] +#[wasm_bindgen(js_name = "coconutKeygen")] pub fn keygen(parameters: Option) -> KeyPairWrapper { let params = get_params(¶meters); nym_coconut::keygen(params).into() @@ -80,7 +81,7 @@ pub struct TtpKeygenOpts { pub authorities: u64, } -#[wasm_bindgen(js_name = "ttpKeygen")] +#[wasm_bindgen(js_name = "coconutTtpKeygen")] pub fn ttp_keygen( opts: TtpKeygenOpts, parameters: Option, @@ -91,7 +92,7 @@ pub fn ttp_keygen( Ok(keys.into_iter().map(Into::into).collect()) } -#[wasm_bindgen(js_name = "signSimple")] +#[wasm_bindgen(js_name = "coconutSignSimple")] pub fn sign_simple( attributes: Vec, keys: &KeyPairWrapper, @@ -110,7 +111,7 @@ pub fn sign_simple( .map_err(Into::into) } -#[wasm_bindgen(js_name = "prepareBlindSign")] +#[wasm_bindgen(js_name = "coconutPrepareBlindSign")] pub fn prepare_blind_sign( private_attributes: Vec, public_attributes: Vec, @@ -138,7 +139,7 @@ pub fn prepare_blind_sign( }) } -#[wasm_bindgen(js_name = "blindSign")] +#[wasm_bindgen(js_name = "coconutBlindSign")] pub fn blind_sign( keys: &KeyPairWrapper, blind_sign_request: &BlindSignRequestWrapper, @@ -163,7 +164,7 @@ pub fn blind_sign( .map_err(Into::into) } -#[wasm_bindgen(js_name = "unblindSignatureShare")] +#[wasm_bindgen(js_name = "coconutUnblindSignatureShare")] pub fn unblind_signature_share( blinded_signature: &BlindedCredentialWrapper, partial_verification_key: &VerificationKeyWrapper, @@ -176,7 +177,7 @@ pub fn unblind_signature_share( ) } -#[wasm_bindgen(js_name = "unblindAndVerifySignatureShare")] +#[wasm_bindgen(js_name = "coconutUnblindAndVerifySignatureShare")] pub fn unblind_and_verify_signature_share( blinded_signature: &BlindedCredentialWrapper, partial_verification_key: &VerificationKeyWrapper, @@ -195,7 +196,7 @@ pub fn unblind_and_verify_signature_share( ) } -#[wasm_bindgen(js_name = "aggregateSignatureShares")] +#[wasm_bindgen(js_name = "coconutAggregateSignatureShares")] pub fn aggregate_signature_shares( shares: Vec, ) -> Result { @@ -206,7 +207,7 @@ pub fn aggregate_signature_shares( .map_err(Into::into) } -#[wasm_bindgen(js_name = "aggregateSignatureSharesAndVerify")] +#[wasm_bindgen(js_name = "coconutAggregateSignatureSharesAndVerify")] pub fn aggregate_signature_shares_and_verify( verification_key: &VerificationKeyWrapper, parameters: Option, @@ -242,7 +243,7 @@ pub fn aggregate_signature_shares_and_verify( .map_err(Into::into) } -#[wasm_bindgen(js_name = "aggregateVerificationKeyShares")] +#[wasm_bindgen(js_name = "coconutAggregateVerificationKeyShares")] pub fn aggregate_verification_key_shares( shares: Vec, ) -> Result { @@ -253,7 +254,7 @@ pub fn aggregate_verification_key_shares( .map_err(Into::into) } -#[wasm_bindgen(js_name = "aggregateVerificationKeys")] +#[wasm_bindgen(js_name = "coconutAggregateVerificationKeys")] pub fn aggregate_verification_keys( keys: Vec, indices: Vec, @@ -265,7 +266,7 @@ pub fn aggregate_verification_keys( .map_err(Into::into) } -#[wasm_bindgen(js_name = "proveBandwidthCredential")] +#[wasm_bindgen(js_name = "coconutProveBandwidthCredential")] pub fn prove_bandwidth_credential( verification_key: &VerificationKeyWrapper, credential: &CredentialWrapper, @@ -286,7 +287,7 @@ pub fn prove_bandwidth_credential( .map_err(Into::into) } -#[wasm_bindgen(js_name = "verifyCredential")] +#[wasm_bindgen(js_name = "coconutVerifyCredential")] pub fn verify_credential( verification_key: &VerificationKeyWrapper, verification_request: &VerifyCredentialRequestWrapper, @@ -309,7 +310,7 @@ pub fn verify_credential( ) } -#[wasm_bindgen(js_name = "verifySimple")] +#[wasm_bindgen(js_name = "coconutVerifySimple")] pub fn verify_simple( verification_key: &VerificationKeyWrapper, attributes: Vec, @@ -327,7 +328,7 @@ pub fn verify_simple( nym_coconut::verify(params, verification_key, &attributes_ref, credential) } -#[wasm_bindgen(js_name = "simpleRandomiseCredential")] +#[wasm_bindgen(js_name = "coconutSimpleRandomiseCredential")] pub fn simple_randomise_credential( credential: &CredentialWrapper, parameters: Option, diff --git a/wasm/zknym-lib/src/generic_scheme/coconut/types.rs b/wasm/zknym-lib/src/generic_scheme/coconut/types.rs new file mode 100644 index 0000000000..8370fcc636 --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/coconut/types.rs @@ -0,0 +1,170 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::ZkNymError; +use crate::{data_pointer_clone, wasm_wrapper, wasm_wrapper_bs58}; +use nym_coconut::{ + hash_to_scalar, Base58, BlindSignRequest, BlindedSignature, KeyPair, Parameters, Scalar, + SecretKey, Signature, SignatureShare, SignerIndex, VerificationKey, VerificationKeyShare, + VerifyCredentialRequest, +}; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use std::str::FromStr; +use tsify::Tsify; +use wasm_bindgen::prelude::wasm_bindgen; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +wasm_wrapper!(Parameters, ParametersWrapper); +wasm_wrapper_bs58!(Signature, CredentialWrapper); +wasm_wrapper_bs58!(BlindedSignature, BlindedCredentialWrapper); +wasm_wrapper!(SignatureShare, CredentialShareWrapper); +wasm_wrapper_bs58!(Scalar, ScalarWrapper); + +wasm_wrapper!(KeyPair, KeyPairWrapper); +wasm_wrapper!(SecretKey, SecretKeyWrapper); +wasm_wrapper!(BlindSignRequest, BlindSignRequestWrapper); +wasm_wrapper_bs58!(VerificationKey, VerificationKeyWrapper); +wasm_wrapper_bs58!(VerifyCredentialRequest, VerifyCredentialRequestWrapper); +wasm_wrapper!(VerificationKeyShare, VerificationKeyShareWrapper); + +data_pointer_clone!(VerificationKeyShareWrapper); +data_pointer_clone!(CredentialShareWrapper); +data_pointer_clone!(BlindSignRequestWrapper); + +#[wasm_bindgen] +impl BlindedCredentialWrapper { + pub fn unblind( + &self, + partial_verification_key: &VerificationKeyWrapper, + pedersen_commitments_openings: &ScalarsWrapper, + ) -> CredentialWrapper { + self.inner + .unblind(partial_verification_key, pedersen_commitments_openings) + .into() + } + + #[wasm_bindgen(js_name = "unblindAndVerify")] + pub fn unblind_and_verify( + &self, + partial_verification_key: &VerificationKeyWrapper, + request: &BlindSignRequestData, + private_attributes: Vec, + public_attributes: Vec, + parameters: Option, + ) -> Result { + let params = super::get_params(¶meters); + + let public_attributes = public_attributes + .into_iter() + .map(hash_to_scalar) + .collect::>(); + let public_attributes_ref = public_attributes.iter().collect::>(); + + let private_attributes = private_attributes + .into_iter() + .map(hash_to_scalar) + .collect::>(); + let private_attributes_ref = private_attributes.iter().collect::>(); + + let unblinded_signature = self.inner.unblind_and_verify( + params, + partial_verification_key, + &private_attributes_ref, + &public_attributes_ref, + &request.blind_sign_request.get_commitment_hash(), + &request.pedersen_commitments_openings, + )?; + + Ok(unblinded_signature.into()) + } +} + +#[wasm_bindgen] +impl CredentialWrapper { + #[wasm_bindgen(js_name = "intoShare")] + pub fn into_share(self, index: SignerIndex) -> CredentialShareWrapper { + CredentialShareWrapper { + inner: SignatureShare::new(self.inner, index), + } + } +} + +#[wasm_bindgen] +impl KeyPairWrapper { + #[wasm_bindgen(js_name = "verificationKey")] + pub fn verification_key(&self) -> VerificationKeyWrapper { + self.inner.verification_key().clone().into() + } + + pub fn index(&self) -> Option { + self.inner.index + } + + #[wasm_bindgen(js_name = "verificationKeyShare")] + pub fn verification_key_share(&self) -> Option { + self.inner.to_verification_key_share().map(Into::into) + } +} + +#[wasm_bindgen] +pub struct BlindSignRequestData { + pub(crate) blind_sign_request: BlindSignRequest, + pub(crate) pedersen_commitments_openings: Vec, +} + +#[wasm_bindgen] +impl BlindSignRequestData { + #[wasm_bindgen(js_name = "blindSignRequest")] + pub fn blind_sign_request(&self) -> BlindSignRequestWrapper { + self.blind_sign_request.clone().into() + } + + #[wasm_bindgen(js_name = "pedersenCommitmentsOpenings")] + pub fn pedersen_commitments_openings(&self) -> ScalarsWrapper { + ScalarsWrapper(self.pedersen_commitments_openings.clone()) + } +} + +#[wasm_bindgen] +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct ScalarsWrapper(pub(crate) Vec); + +impl Deref for ScalarsWrapper { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive( + Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop, +)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct KeypairWrapper { + pub private_key: String, + pub public_key: String, +} + +#[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct UnblindableShare { + pub issuer_index: u64, + pub issuer_key_bs58: String, + pub blinded_share_bs58: String, +} + +#[wasm_bindgen] +impl UnblindableShare { + #[wasm_bindgen(constructor)] + pub fn new(issuer_index: u64, issuer_key_bs58: String, blinded_share_bs58: String) -> Self { + UnblindableShare { + issuer_index, + issuer_key_bs58, + blinded_share_bs58, + } + } +} diff --git a/wasm/zknym-lib/src/generic_scheme/ecash/mod.rs b/wasm/zknym-lib/src/generic_scheme/ecash/mod.rs new file mode 100644 index 0000000000..c2ddfbb025 --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/ecash/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod types; diff --git a/wasm/zknym-lib/src/generic_scheme/ecash/types.rs b/wasm/zknym-lib/src/generic_scheme/ecash/types.rs new file mode 100644 index 0000000000..cbb465df6c --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/ecash/types.rs @@ -0,0 +1,8 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::wasm_wrapper; +use nym_compact_ecash::GroupParameters; +use wasm_bindgen::prelude::wasm_bindgen; + +wasm_wrapper!(GroupParameters, GroupParametersWrapper); diff --git a/wasm/zknym-lib/src/generic_scheme/mod.rs b/wasm/zknym-lib/src/generic_scheme/mod.rs new file mode 100644 index 0000000000..ad786c7841 --- /dev/null +++ b/wasm/zknym-lib/src/generic_scheme/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod coconut; +pub mod ecash; diff --git a/wasm/zknym-lib/src/helpers.rs b/wasm/zknym-lib/src/helpers.rs new file mode 100644 index 0000000000..a99ec7753b --- /dev/null +++ b/wasm/zknym-lib/src/helpers.rs @@ -0,0 +1,80 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[macro_export] +macro_rules! wasm_wrapper { + ($base:ident, $wrapper:ident) => { + #[wasm_bindgen] + pub struct $wrapper { + pub(crate) inner: $base, + } + + impl std::ops::Deref for $wrapper { + type Target = $base; + + fn deref(&self) -> &Self::Target { + &self.inner + } + } + + impl From<$base> for $wrapper { + fn from(inner: $base) -> Self { + $wrapper { inner } + } + } + + impl From<$wrapper> for $base { + fn from(value: $wrapper) -> Self { + value.inner + } + } + }; +} + +#[macro_export] +macro_rules! data_pointer_clone { + ($wrapper:ident) => { + #[wasm_bindgen] + impl $wrapper { + #[wasm_bindgen(js_name = "cloneDataPointer")] + pub fn clone_data_pointer(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } + } + }; +} + +#[macro_export] +macro_rules! wasm_wrapper_bs58 { + ($base:ident, $wrapper:ident) => { + wasm_wrapper!($base, $wrapper); + + impl std::fmt::Display for $wrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.inner.to_bs58().fmt(f) + } + } + + impl FromStr for $wrapper { + type Err = ZkNymError; + + fn from_str(s: &str) -> Result { + Ok($base::try_from_bs58(s)?.into()) + } + } + + #[wasm_bindgen] + impl $wrapper { + pub fn stringify(&self) -> String { + self.to_string() + } + + #[wasm_bindgen(js_name = "fromString")] + pub fn from_string(raw: String) -> Result<$wrapper, ZkNymError> { + raw.parse() + } + } + }; +} diff --git a/wasm/zknym-lib/src/lib.rs b/wasm/zknym-lib/src/lib.rs index d94f90385e..540a386ee8 100644 --- a/wasm/zknym-lib/src/lib.rs +++ b/wasm/zknym-lib/src/lib.rs @@ -1,6 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#![allow(unknown_lints)] +// clippy::empty_docs is not on stable as of 1.77 + // due to the code generated by Tsify #![allow(clippy::empty_docs)] @@ -11,13 +14,15 @@ use wasm_bindgen::prelude::*; pub mod bandwidth_voucher; pub mod error; pub mod generic_scheme; +pub(crate) mod helpers; +pub mod ticketbook; pub mod types; // keep in internal to the crate since I'm not sure how temporary this thing is going to be // I mostly got it, so I could test the whole thing end to end pub(crate) mod vpn_api_client; -pub(crate) static GLOBAL_PARAMS: OnceLock = OnceLock::new(); +pub(crate) static GLOBAL_COCONUT_PARAMS: OnceLock = OnceLock::new(); #[wasm_bindgen(start)] // #[cfg(target_arch = "wasm32")] diff --git a/wasm/zknym-lib/src/ticketbook.rs b/wasm/zknym-lib/src/ticketbook.rs new file mode 100644 index 0000000000..257ac1a0e7 --- /dev/null +++ b/wasm/zknym-lib/src/ticketbook.rs @@ -0,0 +1,14 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use tsify::Tsify; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct SerialisedNymTicketBook { + pub serialisation_revision: u8, + pub bs58_encoded_data: String, +} diff --git a/wasm/zknym-lib/src/types.rs b/wasm/zknym-lib/src/types.rs index 552f44381e..755fb6cc8b 100644 --- a/wasm/zknym-lib/src/types.rs +++ b/wasm/zknym-lib/src/types.rs @@ -1,245 +1,2 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - -use crate::error::ZkNymError; -use crate::generic_scheme::get_params; -use nym_coconut::{ - hash_to_scalar, Base58, BlindSignRequest, BlindedSignature, KeyPair, Parameters, Scalar, - SecretKey, Signature, SignatureShare, SignerIndex, VerificationKey, VerificationKeyShare, - VerifyCredentialRequest, -}; -use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use std::str::FromStr; -use tsify::Tsify; -use wasm_bindgen::prelude::wasm_bindgen; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -macro_rules! wasm_wrapper { - ($base:ident, $wrapper:ident) => { - #[wasm_bindgen] - pub struct $wrapper { - pub(crate) inner: $base, - } - - impl Deref for $wrapper { - type Target = $base; - - fn deref(&self) -> &Self::Target { - &self.inner - } - } - - impl From<$base> for $wrapper { - fn from(inner: $base) -> Self { - $wrapper { inner } - } - } - - impl From<$wrapper> for $base { - fn from(value: $wrapper) -> Self { - value.inner - } - } - }; -} - -macro_rules! data_pointer_clone { - ($wrapper:ident) => { - #[wasm_bindgen] - impl $wrapper { - #[wasm_bindgen(js_name = "cloneDataPointer")] - pub fn clone_data_pointer(&self) -> Self { - Self { - inner: self.inner.clone(), - } - } - } - }; -} - -macro_rules! wasm_wrapper_bs58 { - ($base:ident, $wrapper:ident) => { - wasm_wrapper!($base, $wrapper); - - impl std::fmt::Display for $wrapper { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.inner.to_bs58().fmt(f) - } - } - - impl FromStr for $wrapper { - type Err = ZkNymError; - - fn from_str(s: &str) -> Result { - Ok($base::try_from_bs58(s)?.into()) - } - } - - #[wasm_bindgen] - impl $wrapper { - pub fn stringify(&self) -> String { - self.to_string() - } - - #[wasm_bindgen(js_name = "fromString")] - pub fn from_string(raw: String) -> Result<$wrapper, ZkNymError> { - raw.parse() - } - } - }; -} - -wasm_wrapper!(Parameters, ParametersWrapper); -wasm_wrapper_bs58!(Signature, CredentialWrapper); -wasm_wrapper_bs58!(BlindedSignature, BlindedCredentialWrapper); -wasm_wrapper!(SignatureShare, CredentialShareWrapper); -wasm_wrapper_bs58!(Scalar, ScalarWrapper); - -wasm_wrapper!(KeyPair, KeyPairWrapper); -wasm_wrapper!(SecretKey, SecretKeyWrapper); -wasm_wrapper!(BlindSignRequest, BlindSignRequestWrapper); -wasm_wrapper_bs58!(VerificationKey, VerificationKeyWrapper); -wasm_wrapper_bs58!(VerifyCredentialRequest, VerifyCredentialRequestWrapper); -wasm_wrapper!(VerificationKeyShare, VerificationKeyShareWrapper); - -data_pointer_clone!(VerificationKeyShareWrapper); -data_pointer_clone!(CredentialShareWrapper); -data_pointer_clone!(BlindSignRequestWrapper); - -#[wasm_bindgen] -impl BlindedCredentialWrapper { - pub fn unblind( - &self, - partial_verification_key: &VerificationKeyWrapper, - pedersen_commitments_openings: &ScalarsWrapper, - ) -> CredentialWrapper { - self.inner - .unblind(partial_verification_key, pedersen_commitments_openings) - .into() - } - - #[wasm_bindgen(js_name = "unblindAndVerify")] - pub fn unblind_and_verify( - &self, - partial_verification_key: &VerificationKeyWrapper, - request: &BlindSignRequestData, - private_attributes: Vec, - public_attributes: Vec, - parameters: Option, - ) -> Result { - let params = get_params(¶meters); - - let public_attributes = public_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let public_attributes_ref = public_attributes.iter().collect::>(); - - let private_attributes = private_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let private_attributes_ref = private_attributes.iter().collect::>(); - - let unblinded_signature = self.inner.unblind_and_verify( - params, - partial_verification_key, - &private_attributes_ref, - &public_attributes_ref, - &request.blind_sign_request.get_commitment_hash(), - &request.pedersen_commitments_openings, - )?; - - Ok(unblinded_signature.into()) - } -} - -#[wasm_bindgen] -impl CredentialWrapper { - #[wasm_bindgen(js_name = "intoShare")] - pub fn into_share(self, index: SignerIndex) -> CredentialShareWrapper { - CredentialShareWrapper { - inner: SignatureShare::new(self.inner, index), - } - } -} - -#[wasm_bindgen] -impl KeyPairWrapper { - #[wasm_bindgen(js_name = "verificationKey")] - pub fn verification_key(&self) -> VerificationKeyWrapper { - self.inner.verification_key().clone().into() - } - - pub fn index(&self) -> Option { - self.inner.index - } - - #[wasm_bindgen(js_name = "verificationKeyShare")] - pub fn verification_key_share(&self) -> Option { - self.inner.to_verification_key_share().map(Into::into) - } -} - -#[wasm_bindgen] -pub struct BlindSignRequestData { - pub(crate) blind_sign_request: BlindSignRequest, - pub(crate) pedersen_commitments_openings: Vec, -} - -#[wasm_bindgen] -impl BlindSignRequestData { - #[wasm_bindgen(js_name = "blindSignRequest")] - pub fn blind_sign_request(&self) -> BlindSignRequestWrapper { - self.blind_sign_request.clone().into() - } - - #[wasm_bindgen(js_name = "pedersenCommitmentsOpenings")] - pub fn pedersen_commitments_openings(&self) -> ScalarsWrapper { - ScalarsWrapper(self.pedersen_commitments_openings.clone()) - } -} - -#[wasm_bindgen] -#[derive(Zeroize, ZeroizeOnDrop)] -pub struct ScalarsWrapper(pub(crate) Vec); - -impl Deref for ScalarsWrapper { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive( - Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop, -)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct KeypairWrapper { - pub private_key: String, - pub public_key: String, -} - -#[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct UnblindableShare { - pub issuer_index: u64, - pub issuer_key_bs58: String, - pub blinded_share_bs58: String, -} - -#[wasm_bindgen] -impl UnblindableShare { - #[wasm_bindgen(constructor)] - pub fn new(issuer_index: u64, issuer_key_bs58: String, blinded_share_bs58: String) -> Self { - UnblindableShare { - issuer_index, - issuer_key_bs58, - blinded_share_bs58, - } - } -} diff --git a/wasm/zknym-lib/src/vpn_api_client/types.rs b/wasm/zknym-lib/src/vpn_api_client/types.rs index 691fb99cbf..cdb8d47534 100644 --- a/wasm/zknym-lib/src/vpn_api_client/types.rs +++ b/wasm/zknym-lib/src/vpn_api_client/types.rs @@ -83,13 +83,6 @@ pub struct AttributesResponse { pub bs58_prehashed_amount: String, } -#[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct FreepassCredentialResponse { - pub bs58_encoded_value: String, -} - #[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] #[tsify(into_wasm_abi, from_wasm_abi)] #[serde(rename_all = "camelCase")]