diff --git a/.changelog/unreleased/improvements/4646-register-fmd-keys.md b/.changelog/unreleased/improvements/4646-register-fmd-keys.md new file mode 100644 index 00000000000..ed55deec95a --- /dev/null +++ b/.changelog/unreleased/improvements/4646-register-fmd-keys.md @@ -0,0 +1,8 @@ +Partially closes [#4645](https://github.com/anoma/namada/issues/4645) + +This PR allows the namada client to perform two key management actions: +- add to a config file +- use the config file to register fmd keys with Kassandra services + +Furthermore, it adds hashes of FMD keys derived from viewing keys to the wallet file under the viewing key alias. This PR does not handle querying the services or updating the shielded context accordingly. +([\#4646](https://github.com/anoma/namada/pull/4646)) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 4539542bdb7..3e6acc97566 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -985,7 +985,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -1132,18 +1132,19 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.29" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" +checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.5.29" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" +checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" dependencies = [ "anstream", "anstyle", @@ -1170,6 +1171,18 @@ dependencies = [ "clap_complete", ] +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "clap_lex" version = "0.7.4" @@ -1188,6 +1201,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.12", +] + [[package]] name = "codepage" version = "0.1.2" @@ -1684,6 +1706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -1740,7 +1763,9 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", + "rand_core", "rustc_version", + "serde", "subtle", "zeroize", ] @@ -2138,7 +2163,7 @@ dependencies = [ "chrono", "rust_decimal", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "winnow 0.6.26", ] @@ -2307,7 +2332,7 @@ dependencies = [ "byteorder", "heapless 0.8.0", "num-traits", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2992,7 +3017,7 @@ dependencies = [ "rand_core", "serde", "serdect", - "thiserror 2.0.11", + "thiserror 2.0.12", "thiserror-nostd-notrait", "visibility", "zeroize", @@ -3476,6 +3501,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -4777,6 +4811,53 @@ dependencies = [ "signature", ] +[[package]] +name = "kassandra-client" +version = "0.0.11-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e59f26661380a4758cc7c7a0b8887218677ee76be33a279205fb9cee9a4a492" +dependencies = [ + "chacha20poly1305", + "clap", + "curve25519-dalek", + "hex", + "hkdf", + "kassandra-shared", + "polyfuzzy", + "rand_core", + "serde", + "serde_cbor", + "serde_json", + "sha2 0.10.8", + "thiserror 2.0.12", + "toml", + "tracing", + "tracing-log", + "tracing-subscriber", + "x25519-dalek", +] + +[[package]] +name = "kassandra-shared" +version = "0.0.3-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d0b36e1bea0a4147d47b33e8770253f6101d6efd3f0a89f522b999947ce834" +dependencies = [ + "borsh", + "chacha20poly1305", + "cobs 0.3.0", + "hex", + "once_cell", + "polyfuzzy", + "rand_core", + "serde", + "serde_cbor", + "sha2 0.10.8", + "tdx-quote", + "thiserror 2.0.12", + "x25519-dalek", +] + [[package]] name = "kdam" version = "0.6.2" @@ -5385,7 +5466,7 @@ dependencies = [ "nam-ledger-proto", "once_cell", "strum 0.26.3", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", "tracing-subscriber", @@ -5420,7 +5501,7 @@ dependencies = [ "displaydoc", "encdec", "num_enum", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5447,7 +5528,7 @@ dependencies = [ "pasta_curves", "rand_core", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] @@ -5575,7 +5656,7 @@ dependencies = [ "tendermint-config", "tendermint-rpc", "textwrap-macros", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "toml", "tracing", @@ -5614,7 +5695,7 @@ version = "0.149.1" dependencies = [ "namada_core", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5624,6 +5705,7 @@ dependencies = [ "arbitrary", "assert_matches", "bech32 0.11.0", + "bincode", "borsh", "chrono", "data-encoding", @@ -5647,6 +5729,7 @@ dependencies = [ "num-traits", "num256", "num_enum", + "polyfuzzy", "primitive-types 0.13.1", "proptest", "prost-types 0.13.5", @@ -5660,7 +5743,7 @@ dependencies = [ "smooth-operator", "tendermint 0.40.3", "tendermint-proto 0.40.3", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-keccak", "tokio", "toml", @@ -5721,7 +5804,7 @@ dependencies = [ "rand", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "toml", "tracing", ] @@ -5737,7 +5820,7 @@ dependencies = [ "namada_migrations", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -5793,7 +5876,7 @@ dependencies = [ "namada_migrations", "proptest", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5825,7 +5908,7 @@ dependencies = [ "serde", "serde_json", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -5870,7 +5953,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -5882,7 +5965,7 @@ dependencies = [ "kdam", "namada_core", "tendermint-rpc", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -5927,7 +6010,7 @@ dependencies = [ "namada_migrations", "proptest", "prost 0.13.5", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5962,6 +6045,7 @@ dependencies = [ "eyre", "futures", "itertools 0.14.0", + "kassandra-client", "lazy_static", "linkme", "masp_primitives", @@ -5995,7 +6079,7 @@ dependencies = [ "tar", "tempfile", "test-log", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-test", "toml", @@ -6018,7 +6102,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6051,7 +6135,7 @@ dependencies = [ "serde", "smooth-operator", "test-log", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "tracing-subscriber", "yansi 1.0.1", @@ -6087,6 +6171,7 @@ dependencies = [ "getrandom 0.3.1", "init-once", "itertools 0.14.0", + "kassandra-client", "lazy_static", "linkme", "masp_primitives", @@ -6131,7 +6216,7 @@ dependencies = [ "smooth-operator", "tempfile", "tendermint-rpc", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-bip39", "tokio", "toml", @@ -6150,6 +6235,7 @@ dependencies = [ "flume", "futures", "itertools 0.14.0", + "kassandra-client", "lazy_static", "linkme", "masp_primitives", @@ -6185,7 +6271,7 @@ dependencies = [ "tempfile", "tendermint-rpc", "test-log", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", "typed-builder", @@ -6218,7 +6304,7 @@ dependencies = [ "proptest", "smooth-operator", "test-log", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -6238,7 +6324,7 @@ dependencies = [ "regex", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -6364,7 +6450,7 @@ dependencies = [ "namada_vp", "namada_vp_env", "proptest", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -6398,7 +6484,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.8", - "thiserror 2.0.11", + "thiserror 2.0.12", "tonic-build", ] @@ -6456,7 +6542,7 @@ dependencies = [ "smooth-operator", "tempfile", "test-log", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "wasm-instrument", "wasmer", @@ -6500,7 +6586,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -6564,7 +6650,7 @@ dependencies = [ "serde", "slip10_ed25519", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-bip39", "toml", "zeroize", @@ -6772,9 +6858,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oorandom" @@ -6904,6 +6990,18 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.8", +] + [[package]] name = "pairing" version = "0.23.0" @@ -7104,7 +7202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.11", + "thiserror 2.0.12", "ucd-trie", ] @@ -7301,6 +7399,19 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyfuzzy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ad377e0383a3332e1deb427b97c8670a954efa00add87d2071daab421dae24" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "sha2 0.10.8", + "subtle", +] + [[package]] name = "portable-atomic" version = "1.10.0" @@ -7313,7 +7424,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" dependencies = [ - "cobs", + "cobs 0.2.3", "embedded-io 0.4.0", "embedded-io 0.6.1", "heapless 0.7.17", @@ -7388,6 +7499,15 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -7722,7 +7842,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -8396,9 +8516,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -8444,9 +8564,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -8466,9 +8586,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -8660,7 +8780,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", ] @@ -9044,6 +9164,17 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "tdx-quote" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecbbeffe2d73f07728bcbb581f8faa9098d813ba0fafbcab5e763a2fa4491e80" +dependencies = [ + "nom", + "p256", + "sha2 0.10.8", +] + [[package]] name = "tempfile" version = "3.16.0" @@ -9343,11 +9474,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -9363,9 +9494,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -11017,6 +11148,18 @@ dependencies = [ "tap", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + [[package]] name = "xattr" version = "1.4.0" diff --git a/Cargo.toml b/Cargo.toml index ddfb6bf3a3a..0678be66a56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,13 +117,14 @@ base58 = "0.2" base64 = "0.22" bech32 = "0.11" bimap = {version = "0.6", features = ["serde"]} +bincode = "1.3.3" bit-set = "0.8" bitflags = { version = "2.5", features = ["serde"] } blake2b-rs = "0.2" +borsh = {version = "1.2", features = ["unstable__schema", "derive"]} byte-unit = "5.1" byteorder = "1.4" bytes = "1.1" -borsh = {version = "1.2", features = ["unstable__schema", "derive"]} cargo_metadata = "0.19" chrono = {version = "0.4", default-features = false, features = ["clock", "std"]} circular-queue = "0.2" @@ -169,12 +170,12 @@ ibc-middleware-overflow-receive = { version = "0.5.0" } ibc-middleware-packet-forward = { version = "0.10.0", features = ["borsh"] } ibc-testkit = { version = "0.57.0", default-features = false } ics23 = "0.12" -usize-set = { version = "0.10", features = ["serialize-borsh", "serialize-serde"] } impl-num-traits = "0.2" indexmap = { package = "nam-indexmap", version = "2.7.1-nam.0", features = ["borsh-schema", "serde"] } init-once = "0.6" itertools = "0.14" jubjub = { package = "nam-jubjub", version = "0.10.1-nam.0" } +kassandra-client = "0.0.11-alpha" k256 = { version = "0.13", default-features = false, features = ["ecdsa", "pkcs8", "precomputed-tables", "serde", "std"]} kdam = "0.6" konst = { version = "0.3", default-features = false } @@ -204,6 +205,7 @@ owo-colors = "4.1" parity-wasm = { version = "0.45", features = ["sign_ext"] } paste = "1.0" patricia_tree = "0.8" +polyfuzzy = "0.5.0" pretty_assertions = "1.4" primitive-types = "0.13" proc-macro2 = "1.0" @@ -246,7 +248,7 @@ test-log = {version = "0.2", default-features = false, features = ["trace"]} textwrap-macros = "0.3" tiny-bip39 = {version = "2.0"} tiny-hderive = {package = "nam-tiny-hderive", version = "0.3.1-nam.0"} -tiny-keccak = { version = "2.0", features = ["keccak"] } +tiny-keccak = { version = "2.0", features = ["keccak", "k12"] } thiserror = "2.0" tokio = {version = "1.8", default-features = false} tokio-test = "0.4" @@ -261,6 +263,7 @@ tracing-log = "0.2" tracing-subscriber = {version = "0.3", default-features = false, features = ["env-filter", "fmt"]} typed-builder = "0.20" uint = "0.10" +usize-set = { version = "0.10", features = ["serialize-borsh", "serialize-serde"] } warp = "0.3" wasmparser = "0.121" wasm-instrument = {version = "0.4.0", features = ["sign_ext"]} diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index b918a1b34e3..50b3ce2af07 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -500,6 +500,7 @@ pub mod cmds { } #[derive(Clone, Debug)] + #[allow(clippy::large_enum_variant)] pub enum NamadaClientWithContext { // Ledger cmds TxCustom(TxCustom), @@ -560,6 +561,7 @@ pub mod cmds { QueryRewards(QueryRewards), QueryIbcRateLimit(QueryIbcRateLimit), ShieldedSync(ShieldedSync), + Fmd(FmdCommand), GenIbcShieldingTransfer(GenIbcShieldingTransfer), } @@ -1629,6 +1631,28 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct FmdCommand(pub args::FmdCommand); + + impl SubCmd for FmdCommand { + const CMD: &'static str = "fmd"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| FmdCommand(args::FmdCommand::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about(wrap!( + "Key management commands for fuzzy message detection and \ + interacting with Kassandra services." + )) + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct Bond(pub args::Bond); @@ -3401,7 +3425,9 @@ pub mod args { use data_encoding::HEXUPPER; use either::Either; - use namada_core::masp::{DiversifierIndex, MaspEpoch, PaymentAddress}; + use namada_core::masp::{ + DiversifierIndex, MaspEpoch, UnifiedPaymentAddress, + }; use namada_sdk::address::{Address, EstablishedAddress}; pub use namada_sdk::args::*; use namada_sdk::chain::{ChainId, ChainIdPrefix}; @@ -3442,6 +3468,7 @@ pub mod args { pub const ADDRESS: Arg = arg("address"); pub const ADDRESS_OPT: ArgOpt = arg_opt("address"); pub const ADD_PERSISTENT_PEERS: ArgFlag = flag("add-persistent-peers"); + pub const ADD_SERVICE: ArgOpt = arg_opt("add-service"); pub const ALIAS_OPT: ArgOpt = ALIAS.opt(); pub const ALIAS: Arg = arg("alias"); pub const ALIAS_FORCE: ArgFlag = flag("alias-force"); @@ -3505,6 +3532,8 @@ pub mod args { pub const DATA_PATH: Arg = arg("data-path"); pub const DATED_SPENDING_KEYS: ArgMulti = arg_multi("spending-keys"); + pub const DATED_VIEWING_KEY: Arg = + arg("viewing-key"); pub const DATED_VIEWING_KEYS: ArgMulti = arg_multi("viewing-keys"); pub const DB_KEY: Arg = arg("db-key"); @@ -3628,6 +3657,7 @@ pub mod args { pub const PAYMENT_ADDRESS_TARGET: Arg = arg("target"); pub const PAYMENT_ADDRESS_TARGET_OPT: ArgOpt = arg_opt("target-pa"); + pub const PAYMENT_ADDRESS_V0: ArgFlag = flag("v0"); pub const PORT_ID: ArgDefault = arg_default( "port-id", DefaultFn(|| PortId::from_str("transfer").unwrap()), @@ -3652,8 +3682,9 @@ pub mod args { pub const RAW_ADDRESS_ESTABLISHED: Arg = arg("address"); pub const RAW_ADDRESS_OPT: ArgOpt
= RAW_ADDRESS.opt(); pub const RAW_KEY_GEN: ArgFlag = flag("raw"); - pub const RAW_PAYMENT_ADDRESS: Arg = arg("payment-address"); - pub const RAW_PAYMENT_ADDRESS_OPT: ArgOpt = + pub const RAW_PAYMENT_ADDRESS: Arg = + arg("payment-address"); + pub const RAW_PAYMENT_ADDRESS_OPT: ArgOpt = RAW_PAYMENT_ADDRESS.opt(); pub const RAW_PUBLIC_KEY: Arg = arg("public-key"); pub const RAW_PUBLIC_KEY_OPT: ArgOpt = @@ -3664,6 +3695,7 @@ pub mod args { pub const RECEIVER: Arg = arg("receiver"); pub const REFUND_TARGET: ArgOpt = arg_opt("refund-target"); + pub const REGISTER_KEY: ArgFlag = flag("register"); pub const RELAYER: Arg
= arg("relayer"); pub const RETRIES: ArgOpt = arg_opt("retries"); pub const SCHEME: ArgDefault = @@ -7143,6 +7175,66 @@ pub mod args { } } + impl Args for FmdCommand { + fn parse(matches: &ArgMatches) -> Self { + let viewing_key = DATED_VIEWING_KEY.parse(matches); + if REGISTER_KEY.parse(matches) { + Self { + command: FmdCommandType::RegisterKey, + viewing_key, + service: None, + } + } else { + Self { + command: FmdCommandType::AddService, + viewing_key, + service: Some( + ADD_SERVICE.parse(matches).expect( + "Adding a Kassandra service requires a url", + ), + ), + } + } + } + + fn def(app: App) -> App { + app.arg( + DATED_VIEWING_KEY + .def() + .help(wrap!("The MASP viewing key being managed.")), + ) + .arg(REGISTER_KEY.def().help(wrap!( + "Register the viewing keys with Kassandra services using the \ + configuration file." + ))) + .arg(ADD_SERVICE.def().help(wrap!( + "Add a the url of a Kassandra service to the configuration \ + file." + ))) + .group( + ArgGroup::new("fmd_command_type") + .args([REGISTER_KEY.name, ADD_SERVICE.name]) + .required(true), + ) + } + } + + impl CliToSdk> for FmdCommand { + type Error = std::convert::Infallible; + + fn to_sdk( + self, + ctx: &mut Context, + ) -> Result, Self::Error> { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + Ok(FmdCommand { + command: self.command, + viewing_key: chain_ctx.get_cached(&self.viewing_key), + service: self.service, + }) + } + } + impl CliToSdk> for GenIbcShieldingTransfer { @@ -7976,6 +8068,7 @@ pub mod args { _ctx: &mut Context, ) -> Result { Ok(PayAddressGen { + v0: self.v0, alias: self.alias, alias_force: self.alias_force, viewing_key: self.viewing_key, @@ -7990,11 +8083,13 @@ pub mod args { let alias_force = ALIAS_FORCE.parse(matches); let diversifier_index = DIVERSIFIER_INDEX.parse(matches); let viewing_key = VIEWING_KEY_ALIAS.parse(matches); + let v0 = PAYMENT_ADDRESS_V0.parse(matches); Self { alias, alias_force, diversifier_index, viewing_key, + v0, } } @@ -8009,6 +8104,10 @@ pub mod args { "Set the viewing key's current diversifier index beforehand." ))) .arg(VIEWING_KEY.def().help(wrap!("The viewing key."))) + .arg(PAYMENT_ADDRESS_V0.def().help(wrap!( + "Force generating a v0 payment address. Not compatible with \ + FMD." + ))) } } diff --git a/crates/apps_lib/src/cli/client.rs b/crates/apps_lib/src/cli/client.rs index 0eb92b4766c..c0c321de9a0 100644 --- a/crates/apps_lib/src/cli/client.rs +++ b/crates/apps_lib/src/cli/client.rs @@ -1,8 +1,10 @@ use std::io::Read; use color_eyre::eyre::Result; +use namada_sdk::args::FmdCommandType; use namada_sdk::io::{Io, NamadaIo, display_line}; use namada_sdk::masp::ShieldedContext; +use namada_sdk::masp::fs::FsShieldedUtils; use namada_sdk::wallet::DatedViewingKey; use namada_sdk::{Namada, NamadaImpl}; @@ -386,6 +388,28 @@ impl CliApi { ) .await?; } + Sub::Fmd(FmdCommand(args)) => { + use namada_sdk::masp::fmd; + let args = args.to_sdk(&mut ctx)?; + let chain_ctx = ctx.take_chain_or_exit(); + match args.command { + FmdCommandType::RegisterKey => { + fmd::register_keys::( + args.viewing_key, + ) + .await; + } + FmdCommandType::AddService => { + fmd::add_service( + ShieldedContext::new(chain_ctx.shielded) + .into(), + args.viewing_key, + args.service.as_ref().unwrap(), + ) + .await + } + } + } Sub::GenIbcShieldingTransfer(GenIbcShieldingTransfer( args, )) => { diff --git a/crates/apps_lib/src/cli/context.rs b/crates/apps_lib/src/cli/context.rs index 7c4d570d5c7..0435b7241e0 100644 --- a/crates/apps_lib/src/cli/context.rs +++ b/crates/apps_lib/src/cli/context.rs @@ -12,8 +12,8 @@ use masp_primitives::zip32::{ ExtendedSpendingKey as MaspExtendedSpendingKey, }; use namada_core::masp::{ - BalanceOwner, ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, - TransferSource, TransferTarget, + BalanceOwner, ExtendedSpendingKey, ExtendedViewingKey, TransferSource, + TransferTarget, UnifiedPaymentAddress, }; use namada_sdk::address::{Address, InternalAddress}; use namada_sdk::chain::ChainId; @@ -60,7 +60,7 @@ pub type WalletDatedSpendingKey = FromContext; /// A raw payment address (bech32m encoding) or an alias of a payment address /// in the wallet -pub type WalletPaymentAddr = FromContext; +pub type WalletPaymentAddr = FromContext; /// A raw full viewing key (bech32m encoding) or an alias of a full viewing key /// in the wallet @@ -388,10 +388,11 @@ impl FromContext { } } - /// Converts this TransferTarget argument to a PaymentAddress. Call this - /// function only when certain that raw represents a PaymentAddress. - pub fn to_payment_address(&self) -> FromContext { - FromContext:: { + /// Converts this TransferTarget argument to a UnifiedPaymentAddress. Call + /// this function only when certain that raw represents a + /// UnifiedPaymentAddress. + pub fn to_payment_address(&self) -> FromContext { + FromContext:: { raw: self.raw.clone(), phantom: PhantomData, } @@ -690,7 +691,7 @@ impl ArgFromMutContext for DatedViewingKey { } } -impl ArgFromContext for PaymentAddress { +impl ArgFromContext for UnifiedPaymentAddress { fn arg_from_ctx( ctx: &ChainContext, raw: impl AsRef, @@ -743,7 +744,8 @@ impl ArgFromContext for TransferTarget { Address::arg_from_ctx(ctx, raw) .map(Self::Address) .or_else(|_| { - PaymentAddress::arg_from_ctx(ctx, raw).map(Self::PaymentAddress) + UnifiedPaymentAddress::arg_from_ctx(ctx, raw) + .map(Self::PaymentAddress) }) } } diff --git a/crates/apps_lib/src/cli/wallet.rs b/crates/apps_lib/src/cli/wallet.rs index 19f4261d3a4..3d3cda402b4 100644 --- a/crates/apps_lib/src/cli/wallet.rs +++ b/crates/apps_lib/src/cli/wallet.rs @@ -12,7 +12,7 @@ use ledger_transport_hid::hidapi::HidApi; use masp_primitives::zip32::ExtendedFullViewingKey; use namada_core::chain::BlockHeight; use namada_core::masp::{ - DiversifierIndex, ExtendedSpendingKey, MaspValue, PaymentAddress, + ExtendedSpendingKey, MaspValue, UnifiedPaymentAddress, }; use namada_sdk::address::{Address, DecodeError}; use namada_sdk::borsh::{BorshDeserialize, BorshSerializeExt}; @@ -403,6 +403,7 @@ fn payment_address_gen( alias_force, viewing_key: viewing_key_alias, diversifier_index, + v0, }: args::PayAddressGen, ) { let mut wallet = load_wallet(ctx); @@ -421,23 +422,21 @@ fn payment_address_gen( .copied() .unwrap_or_default() }); - let (diversifier_index, masp_payment_addr) = - ExtendedFullViewingKey::from(viewing_key) - .find_address(diversifier_index.into()) - .expect("exhausted payment addresses"); + let (diversifier_index, payment_addr) = if v0 { + UnifiedPaymentAddress::v0_from_zip32(viewing_key, diversifier_index) + } else { + UnifiedPaymentAddress::v1_from_zip32(viewing_key, diversifier_index) + }; let mut next_div_idx = diversifier_index; - next_div_idx - .increment() - .expect("exhausted payment addresses"); - let payment_addr = PaymentAddress::from(masp_payment_addr); + next_div_idx.increment(); let alias = wallet - .insert_payment_addr(alias, payment_addr, alias_force) + .insert_payment_addr(alias, payment_addr.clone(), alias_force) .unwrap_or_else(|| { edisplay_line!(io, "Payment address not added"); cli::safe_exit(1); }); wallet - .insert_diversifier_index(viewing_key_alias, next_div_idx.into()) + .insert_diversifier_index(viewing_key_alias, next_div_idx) .expect( "must be able to save next diversifier index under the alias of \ the viewing key", @@ -447,7 +446,7 @@ fn payment_address_gen( io, "Successfully generated payment address {} at index {} with alias {}", payment_addr, - DiversifierIndex::from(diversifier_index), + diversifier_index, alias, ); } @@ -1095,7 +1094,7 @@ fn payment_address_or_alias_find( ctx: Context, io: &impl Io, alias: Option, - payment_address: Option, + payment_address: Option, ) { let wallet = load_wallet(ctx); if payment_address.is_some() && alias.is_some() { diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index 6689cd51191..0b6ae83de22 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -1935,7 +1935,8 @@ pub async fn gen_ibc_shielding_transfer( ) -> Result<(), error::Error> { let output_folder = args.output_folder.clone(); - if let Some(masp_tx) = tx::gen_ibc_shielding_transfer(context, args).await? + if let Some((masp_tx, fmd_flags)) = + tx::gen_ibc_shielding_transfer(context, args).await? { let tx_id = masp_tx.txid().to_string(); let filename = format!("ibc_masp_tx_{}.memo", tx_id); @@ -1945,7 +1946,7 @@ pub async fn gen_ibc_shielding_transfer( }; let mut out = File::create(&output_path) .expect("Creating a new file for IBC MASP transaction failed."); - let bytes = convert_masp_tx_to_ibc_memo(&masp_tx); + let bytes = convert_masp_tx_to_ibc_memo(masp_tx, fmd_flags); out.write_all(bytes.as_bytes()) .expect("Writing IBC MASP transaction file failed."); println!( diff --git a/crates/benches/native_vps.rs b/crates/benches/native_vps.rs index 63ba6afebe2..e0b3a223a3e 100644 --- a/crates/benches/native_vps.rs +++ b/crates/benches/native_vps.rs @@ -572,7 +572,7 @@ fn setup_storage_for_masp_verification( let (shielded_ctx, shield_tx) = shielded_ctx.generate_masp_tx( amount, TransferSource::Address(defaults::albert_address()), - TransferTarget::PaymentAddress(albert_payment_addr), + TransferTarget::PaymentAddress(albert_payment_addr.clone()), ); shielded_ctx.shell.write().execute_tx(&shield_tx.to_ref()); diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9d3ce842959..5c9611d4730 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -16,13 +16,14 @@ rust-version.workspace = true [features] default = [] mainnet = [] -rand = ["dep:rand", "rand_core"] +rand = ["dep:rand", "dep:rand_core", "polyfuzzy/random-flag-ciphertexts"] +default-flag-ciphertext = ["rand"] ethers-derive = ["ethbridge-structs/ethers-derive"] # for tests and test utilities testing = ["rand", "proptest"] migrations = ["namada_migrations", "linkme"] benches = ["proptest"] -control_flow = ["lazy_static", "tokio", "wasmtimer"] +control_flow = ["dep:lazy_static", "tokio", "wasmtimer"] arbitrary = [ "dep:arbitrary", "chrono/arbitrary", @@ -39,6 +40,7 @@ namada_migrations = { workspace = true, optional = true } arbitrary = { workspace = true, optional = true } arse-merkle-tree.workspace = true bech32.workspace = true +bincode.workspace = true borsh.workspace = true chrono.workspace = true data-encoding.workspace = true @@ -60,6 +62,7 @@ num_enum.workspace = true num-integer.workspace = true num-rational.workspace = true num-traits.workspace = true +polyfuzzy = { workspace = true, features = ["serde"] } primitive-types.workspace = true proptest = { workspace = true, optional = true } prost-types.workspace = true diff --git a/crates/core/src/key/mod.rs b/crates/core/src/key/mod.rs index 330ff64ce7b..22f01be795d 100644 --- a/crates/core/src/key/mod.rs +++ b/crates/core/src/key/mod.rs @@ -364,6 +364,93 @@ impl From<&PK> for PublicKeyHash { } } +const FMD_KEY_HASH_LEN: usize = 32; +const FMD_HASH_HEX_LEN: usize = 64; + +/// Key hash derived of a json serialized FMD secret key +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Debug, + Clone, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, + BorshSchema, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Default, +)] +pub struct FmdKeyHash(pub(crate) [u8; FMD_KEY_HASH_LEN]); + +impl serde::Serialize for FmdKeyHash { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let encoded = self.to_string(); + serde::Serialize::serialize(&encoded, serializer) + } +} + +impl<'de> serde::Deserialize<'de> for FmdKeyHash { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let encoded: String = serde::Deserialize::deserialize(deserializer)?; + Self::from_str(&encoded).map_err(D::Error::custom) + } +} + +impl Display for FmdKeyHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", HEXUPPER.encode(&self.0)) + } +} + +impl FromStr for FmdKeyHash { + type Err = FmdhFromStringError; + + fn from_str(s: &str) -> Result { + if s.len() != FMD_HASH_HEX_LEN { + return Err(Self::Err::UnexpectedLen(s.len())); + } + let raw_bytes = HEXUPPER + .decode(s.as_bytes()) + .map_err(Self::Err::DecodeUpperHex)?; + let mut bytes: [u8; FMD_KEY_HASH_LEN] = Default::default(); + bytes.copy_from_slice(&raw_bytes); + Ok(Self(bytes)) + } +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum FmdhFromStringError { + #[error("Wrong FMD hash len. Expected {FMD_HASH_HEX_LEN}, got {0}")] + UnexpectedLen(usize), + #[error("Failed decoding upper hex with {0}")] + DecodeUpperHex(data_encoding::DecodeError), +} + +impl From<&polyfuzzy::FmdSecretKey> for FmdKeyHash { + fn from(sk: &polyfuzzy::FmdSecretKey) -> Self { + let mut hasher = sha2::Sha256::new(); + hasher.update(serde_json::to_string(sk).unwrap().as_bytes()); + Self(hasher.finalize().into()) + } +} + +impl From for FmdKeyHash { + fn from(sk: polyfuzzy::FmdSecretKey) -> Self { + Self::from(&sk) + } +} + /// Derive Tendermint raw hash from the public key pub trait PublicKeyTmRawHash { /// Derive Tendermint raw hash from the public key diff --git a/crates/core/src/masp.rs b/crates/core/src/masp.rs index fe8fecee579..cc193a82290 100644 --- a/crates/core/src/masp.rs +++ b/crates/core/src/masp.rs @@ -1,5 +1,7 @@ //! MASP types +mod fmd; + use std::collections::BTreeMap; use std::fmt::Display; use std::num::ParseIntError; @@ -20,13 +22,18 @@ use ripemd::Digest as RipemdDigest; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sha2::Sha256; +pub use self::fmd::{ + FlagCiphertext, GAMMA, PublicKey as FmdPublicKey, + PublicKeyBytes as FmdPublicKeyBytes, SecretKey as FmdSecretKey, +}; use crate::address::{Address, DecodeError, HASH_HEX_LEN, IBC, MASP}; use crate::borsh::BorshSerializeExt; use crate::chain::Epoch; +use crate::hash::Hash; use crate::impl_display_and_from_str_via_format; use crate::string_encoding::{ self, MASP_EXT_FULL_VIEWING_KEY_HRP, MASP_EXT_SPENDING_KEY_HRP, - MASP_PAYMENT_ADDRESS_HRP, + MASP_FMD_PAYMENT_ADDRESS_HRP, MASP_PAYMENT_ADDRESS_HRP, }; use crate::token::{Denomination, MaspDigitPos, NATIVE_MAX_DECIMAL_PLACES}; @@ -85,6 +92,44 @@ impl Display for MaspTxId { } } +/// Pointers to MASP data included in Namada transactions. +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Serialize, + Deserialize, + Clone, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Debug, + Eq, + PartialEq, + Copy, + Ord, + PartialOrd, + Hash, +)] +pub struct MaspTxData { + /// Id of the MASP transaction. + /// + /// This is used to look-up a MASP transaction section. + pub masp_tx_id: MaspTxId, + /// Section hash of the FMD flag ciphertext. + /// + /// This is used to look-up a transaction data section + /// containing an FMD flag ciphertext. + pub flag_ciphertext_sechash: Hash, +} + +impl Display for MaspTxData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("") + .field("masp_tx_id", &self.masp_tx_id) + .field("flag_ciphertext_sechash", &self.flag_ciphertext_sechash) + .finish() + } +} + /// Wrapper type around `Epoch` for type safe operations involving the masp /// epoch #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] @@ -300,10 +345,21 @@ pub type Precision = u128; // enough capacity to store the payment address const PAYMENT_ADDRESS_SIZE: usize = 43; +// enough capacity to store the payment address +const FMD_PAYMENT_ADDRESS_SIZE: usize = + PAYMENT_ADDRESS_SIZE + FmdPublicKeyBytes::LENGTH; + /// Wrapper for masp_primitive's DiversifierIndex #[derive(Clone, Debug, Copy, Eq, PartialEq, Default)] pub struct DiversifierIndex(masp_primitives::zip32::DiversifierIndex); +impl DiversifierIndex { + /// Return the next payment address index. + pub fn increment(&mut self) { + self.0.increment().expect("exhausted payment addresses"); + } +} + impl From for DiversifierIndex { fn from(idx: masp_primitives::zip32::DiversifierIndex) -> Self { Self(idx) @@ -481,6 +537,54 @@ impl string_encoding::Format for PaymentAddress { impl_display_and_from_str_via_format!(PaymentAddress); +impl string_encoding::Format for FmdPaymentAddress { + type EncodedBytes<'a> = Vec; + + const HRP: string_encoding::Hrp = + string_encoding::Hrp::parse_unchecked(MASP_FMD_PAYMENT_ADDRESS_HRP); + + fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(FMD_PAYMENT_ADDRESS_SIZE); + bytes.extend_from_slice(&self.payment_address.to_bytes()[..]); + bytes.extend_from_slice(&self.fmd_public_key[..]); + bytes + } + + fn decode_bytes( + bytes: &[u8], + ) -> Result { + if bytes.len() != FMD_PAYMENT_ADDRESS_SIZE { + return Err(DecodeError::InvalidInnerEncoding(format!( + "expected {FMD_PAYMENT_ADDRESS_SIZE} bytes for the fmd \ + payment address" + ))); + } + + let payment_address = + masp_primitives::sapling::PaymentAddress::from_bytes(&{ + let mut payment_addr = [0u8; PAYMENT_ADDRESS_SIZE]; + payment_addr.copy_from_slice(&bytes[0..PAYMENT_ADDRESS_SIZE]); + payment_addr + }) + .ok_or_else(|| { + DecodeError::InvalidInnerEncoding( + "invalid fmd payment address provided".to_string(), + ) + })?; + + let fmd_public_key = bytes[PAYMENT_ADDRESS_SIZE..] + .try_into() + .map_err(DecodeError::InvalidInnerEncoding)?; + + Ok(Self { + payment_address, + fmd_public_key, + }) + } +} + +impl_display_and_from_str_via_format!(FmdPaymentAddress); + impl From for masp_primitives::zip32::ExtendedFullViewingKey { @@ -553,6 +657,20 @@ impl PaymentAddress { // hex of the first 40 chars of the hash format!("{:.width$X}", hasher.finalize(), width = HASH_HEX_LEN) } + + /// Create a new [`FmdPaymentAddress`]. + pub fn with_fmd_key(self, sk: &FmdSecretKey) -> FmdPaymentAddress { + let Self(payment_address) = self; + + let fmd_public_key = sk + .randomized_public_key(&payment_address.diversifier().0) + .into_compressed_bytes(); + + FmdPaymentAddress { + payment_address, + fmd_public_key, + } + } } impl From for masp_primitives::sapling::PaymentAddress { @@ -591,6 +709,76 @@ impl<'de> serde::Deserialize<'de> for PaymentAddress { } } +/// [`PaymentAddress`] with FMD public key attached. +#[derive( + Clone, + Debug, + PartialOrd, + Ord, + Eq, + PartialEq, + Hash, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, +)] +pub struct FmdPaymentAddress { + /// Wrapped MASP payment address. + payment_address: masp_primitives::sapling::PaymentAddress, + /// FMD public key. + fmd_public_key: FmdPublicKeyBytes, +} + +impl FmdPaymentAddress { + /// Recover the [`PaymentAddress`] embedded within. + pub fn as_payment_address( + &self, + ) -> &masp_primitives::sapling::PaymentAddress { + &self.payment_address + } + + /// Recover the [`FmdPublicKey`] embedded within. + pub fn to_fmd_public_key(&self) -> Option { + FmdPublicKey::from_parts( + &self.payment_address.diversifier().0, + &self.fmd_public_key, + ) + } + + /// Hash this payment address + pub fn hash(&self) -> String { + let bytes = self.serialize_to_vec(); + let mut hasher = Sha256::new(); + hasher.update(bytes); + // hex of the first 40 chars of the hash + format!("{:.width$X}", hasher.finalize(), width = HASH_HEX_LEN) + } +} + +impl serde::Serialize for FmdPaymentAddress { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + let encoded = self.to_string(); + serde::Serialize::serialize(&encoded, serializer) + } +} + +impl<'de> serde::Deserialize<'de> for FmdPaymentAddress { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let encoded: String = serde::Deserialize::deserialize(deserializer)?; + Self::from_str(&encoded).map_err(D::Error::custom) + } +} + /// Wrapper for masp_primitive's ExtendedSpendingKey #[derive( Clone, @@ -805,6 +993,147 @@ pub fn addr_taddr(addr: Address) -> TransparentAddress { TAddrData::Addr(addr).taddress() } +/// Unifies FMD and non-FMD payment address types. +#[derive( + Debug, + Clone, + BorshDeserialize, + BorshSerialize, + BorshDeserializer, + Hash, + Eq, + PartialEq, + Ord, + PartialOrd, +)] +pub enum UnifiedPaymentAddress { + /// First payment address format introduced by Namada. + /// + /// Identical to a Zcash sapling payment address. + V0(PaymentAddress), + /// Payment address similar to [`Self::V0`], augmented with + /// [`FmdPublicKeyBytes`]. + V1(FmdPaymentAddress), +} + +impl UnifiedPaymentAddress { + /// Generate a [`FlagCiphertext`] from this payment address. + /// + /// Returns [`None`] if the FMD public key in the + /// [`UnifiedPaymentAddress::V1`] variant is invalid, and a random flag + /// ciphertext for the [`UnifiedPaymentAddress::V0`] variant. + #[cfg(feature = "rand")] + pub fn flag(&self, rng: &mut R) -> Option + where + R: rand_core::CryptoRng + rand_core::RngCore, + { + match self { + Self::V0(_) => Some(FlagCiphertext::random(rng)), + Self::V1(pa) => pa.to_fmd_public_key().map(|pk| pk.flag(rng)), + } + } + + /// Create a v0 payment address. + pub fn v0_from_zip32( + vk: ExtendedViewingKey, + div_index: DiversifierIndex, + ) -> (DiversifierIndex, Self) { + let (div_index, pa) = + masp_primitives::zip32::ExtendedFullViewingKey::from(vk) + .find_address(div_index.into()) + .expect("exhausted payment address diversifier indices"); + + (DiversifierIndex(div_index), Self::V0(PaymentAddress(pa))) + } + + /// Create a v1 payment address. + pub fn v1_from_zip32( + vk: ExtendedViewingKey, + div_index: DiversifierIndex, + ) -> (DiversifierIndex, Self) { + let fmd_sk: FmdSecretKey = vk.0.fvk.vk.ivk().into(); + + let (div_index, pa) = + masp_primitives::zip32::ExtendedFullViewingKey::from(vk) + .find_address(div_index.into()) + .expect("exhausted payment address diversifier indices"); + + ( + DiversifierIndex(div_index), + Self::V1(PaymentAddress(pa).with_fmd_key(&fmd_sk)), + ) + } +} + +impl serde::Serialize for UnifiedPaymentAddress { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + serde::Serialize::serialize(&self.to_string(), serializer) + } +} + +impl<'de> serde::Deserialize<'de> for UnifiedPaymentAddress { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let encoded: String = serde::Deserialize::deserialize(deserializer)?; + Self::from_str(&encoded).map_err(D::Error::custom) + } +} + +impl From<&UnifiedPaymentAddress> for masp_primitives::sapling::PaymentAddress { + fn from(pa: &UnifiedPaymentAddress) -> Self { + match pa { + UnifiedPaymentAddress::V0(pa) => pa.0, + UnifiedPaymentAddress::V1(pa) => *pa.as_payment_address(), + } + } +} + +impl From for masp_primitives::sapling::PaymentAddress { + fn from(pa: UnifiedPaymentAddress) -> Self { + (&pa).into() + } +} + +impl From for UnifiedPaymentAddress { + fn from(pa: PaymentAddress) -> Self { + Self::V0(pa) + } +} + +impl From for UnifiedPaymentAddress { + fn from(pa: FmdPaymentAddress) -> Self { + Self::V1(pa) + } +} + +impl Display for UnifiedPaymentAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::V0(pa) => pa.fmt(f), + Self::V1(pa) => pa.fmt(f), + } + } +} + +impl FromStr for UnifiedPaymentAddress { + type Err = DecodeError; + + fn from_str(s: &str) -> Result { + PaymentAddress::from_str(s) + .map(Self::V0) + .or_else(|_err| FmdPaymentAddress::from_str(s).map(Self::V1)) + } +} + /// Represents a target for the funds of a transfer #[derive( Debug, @@ -820,7 +1149,7 @@ pub enum TransferTarget { /// A transfer going to a transparent address Address(Address), /// A transfer going to a shielded address - PaymentAddress(PaymentAddress), + PaymentAddress(UnifiedPaymentAddress), /// A transfer going to an IBC address Ibc(String), } @@ -840,9 +1169,9 @@ impl TransferTarget { } /// Get the contained PaymentAddress, if any - pub fn payment_address(&self) -> Option { + pub fn payment_address(&self) -> Option { match self { - Self::PaymentAddress(address) => Some(*address), + Self::PaymentAddress(address) => Some(address.clone()), _ => None, } } @@ -869,7 +1198,12 @@ impl Display for TransferTarget { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Address(x) => x.fmt(f), - Self::PaymentAddress(address) => address.fmt(f), + Self::PaymentAddress(UnifiedPaymentAddress::V0(address)) => { + address.fmt(f) + } + Self::PaymentAddress(UnifiedPaymentAddress::V1(address)) => { + address.fmt(f) + } Self::Ibc(x) => x.fmt(f), } } @@ -928,7 +1262,7 @@ impl Display for BalanceOwner { #[derive(Debug, Clone)] pub enum MaspValue { /// A MASP PaymentAddress - PaymentAddress(PaymentAddress), + PaymentAddress(UnifiedPaymentAddress), /// A MASP ExtendedSpendingKey ExtendedSpendingKey(ExtendedSpendingKey), /// A MASP FullViewingKey @@ -939,9 +1273,10 @@ impl FromStr for MaspValue { type Err = DecodeError; fn from_str(s: &str) -> Result { - // Try to decode this value first as a PaymentAddress, then as an - // ExtendedSpendingKey, then as FullViewingKey - PaymentAddress::from_str(s) + // Try to decode this value first as a UnifiedPaymentAddress, + // then as an ExtendedSpendingKey, then as a + // FullViewingKey + UnifiedPaymentAddress::from_str(s) .map(Self::PaymentAddress) .or_else(|_err| { ExtendedSpendingKey::from_str(s).map(Self::ExtendedSpendingKey) @@ -1081,7 +1416,7 @@ mod test { masp_primitives::zip32::ExtendedSpendingKey::master(&[0_u8]), ); let (_diversifier, pa) = sk.0.default_address(); - let pa = PaymentAddress::from(pa); + let pa = PaymentAddress::from(pa).into(); let target = TransferTarget::PaymentAddress(pa); assert_eq!(target.effective_address(), MASP); @@ -1100,7 +1435,7 @@ mod test { masp_primitives::zip32::ExtendedSpendingKey::master(&[0_u8]), ); let (_diversifier, pa) = sk.0.default_address(); - let pa = PaymentAddress::from(pa); + let pa = PaymentAddress::from(pa).into(); let target = TransferTarget::PaymentAddress(pa).address(); assert!(target.is_none()); @@ -1122,7 +1457,7 @@ mod test { masp_primitives::zip32::ExtendedSpendingKey::master(&[0_u8]), ); let (_diversifier, pa) = sk.0.default_address(); - let pa = PaymentAddress::from(pa); + let pa = PaymentAddress::from(pa).into(); let target = TransferTarget::PaymentAddress(pa).t_addr_data(); assert!(target.is_none()); @@ -1138,7 +1473,7 @@ mod test { masp_primitives::zip32::ExtendedSpendingKey::master(&[0_u8]), ); let (_diversifier, pa) = sk.0.default_address(); - let pa = PaymentAddress::from(pa); + let pa: UnifiedPaymentAddress = PaymentAddress::from(pa).into(); const IBC_ADDR: &str = "noble18st0wqx84av8y6xdlss9d6m2nepyqwj6nfxxuv"; @@ -1223,7 +1558,7 @@ mod test { masp_primitives::zip32::ExtendedSpendingKey::master(&[0_u8]), ); let (_diversifier, pa) = sk.0.default_address(); - let pa = PaymentAddress::from(pa); + let pa = PaymentAddress::from(pa).into(); let target = TransferTarget::PaymentAddress(pa); let serialized = target.serialize_to_vec(); diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs new file mode 100644 index 00000000000..0fc7c7a7a78 --- /dev/null +++ b/crates/core/src/masp/fmd.rs @@ -0,0 +1,519 @@ +//! Fuzzy message detection MASP primitives. + +use std::collections::BTreeMap; +use std::io; +use std::ops::Deref; + +use ::polyfuzzy::KeyExpansion; +use borsh::schema::Definition; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use masp_primitives::sapling::SaplingIvk; +use masp_primitives::zip32::{ExtendedKey, PseudoExtendedKey}; +#[cfg(feature = "rand")] +use rand_core::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; +use tiny_keccak::{Hasher, IntoXof, KangarooTwelve, Xof}; + +use crate::masp::ExtendedViewingKey; + +mod polyfuzzy { + pub(super) use ::polyfuzzy::FmdSecretKey; + #[cfg(feature = "rand")] + pub(super) use ::polyfuzzy::MultiFmdScheme; + pub(super) use ::polyfuzzy::fmd2_compact::*; +} + +mod parameters { + //! Fuzzy message detection parameters used by Namada. + + /// Gamma parameter. + /// + /// This parameter defines the minimum false positive rate, + /// which is given by `2^-GAMMA`. + pub const GAMMA: usize = 20; + + /// Threshold parameter. + /// + /// This parameter affects the length of payment addresses. + /// The raw data of payment addresses will contain `THRESHOLD + 1` + /// extra compressed curve points (32 bytes each), to allow + /// flagging note ownership to their respective owner. + pub const THRESHOLD: usize = 1; + + /// Number of bytes required to store a public key. + pub const PUBLIC_KEY_LEN: usize = (THRESHOLD + 1) * 32; + + /// Evaluate whether the given compressed bit ciphertext is valid. + #[allow(clippy::arithmetic_side_effects)] + pub const fn valid_compressed_bit_ciphertext(bits: &[u8]) -> bool { + // Number of bytes required to represent a polyfuzzy bit ciphertext + // with a `GAMMA` parameter. + const COMPRESSED_BIT_CIPHERTEXT_LEN: usize = + GAMMA / 8 + (GAMMA % 8 != 0) as usize; + + // Mask with the padding bits that should be set to 0 (or, + // in other words, unset) in the bit ciphertext produced + // by polyfuzzy. Since the library doesn't set any of the + // upper bits, if they have been set it means someone has + // tampered with the flag ciphertext. + const UNSET_BITS_MASK: u8 = 0xff << (GAMMA % 8); + + bits.len() == COMPRESSED_BIT_CIPHERTEXT_LEN + && bits[COMPRESSED_BIT_CIPHERTEXT_LEN - 1] & UNSET_BITS_MASK == 0 + } +} + +pub use parameters::GAMMA; + +/// FMD public key bytes. +#[derive( + Clone, + Debug, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + BorshSerialize, + BorshDeserialize, + BorshSchema, +)] +#[repr(transparent)] +pub struct PublicKeyBytes(Box<[u8; parameters::PUBLIC_KEY_LEN]>); + +impl PublicKeyBytes { + /// Length of the byte payload. + pub const LENGTH: usize = parameters::PUBLIC_KEY_LEN; +} + +impl TryFrom<&[u8]> for PublicKeyBytes { + type Error = String; + + fn try_from(slice: &[u8]) -> Result { + if slice.len() != parameters::PUBLIC_KEY_LEN { + return Err(format!( + "FMD public key length must be {}", + parameters::PUBLIC_KEY_LEN + )); + } + + let mut bytes = + PublicKeyBytes(Box::new([0u8; parameters::PUBLIC_KEY_LEN])); + bytes.0.copy_from_slice(slice); + + Ok(bytes) + } +} + +impl Deref for PublicKeyBytes { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &[u8] { + self.0.as_slice() + } +} + +/// FMD public key. +//#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(transparent)] +pub struct PublicKey { + inner: polyfuzzy::CompactPublicKey, +} + +impl PublicKey { + /// Serialize this public key into its compressed representation. + pub fn into_compressed_bytes(self) -> PublicKeyBytes { + let repr = self.inner.compress().to_coeff_repr(); + + PublicKeyBytes(repr.into_boxed_slice().try_into().unwrap_or_else( + |_| { + panic!( + "FMD public key length should have been {}", + parameters::PUBLIC_KEY_LEN + ) + }, + )) + } + + /// Deserialize a public key from the given compressed representation and + /// diversifier. + pub fn from_parts( + diversifier: &[u8], + compressed_bytes: &[u8], + ) -> Option { + if compressed_bytes.len() != parameters::PUBLIC_KEY_LEN { + return None; + } + + let compressed_pk = + polyfuzzy::CompressedCompactPublicKey::from_coeff_repr( + compressed_bytes, + )?; + + Some(PublicKey { + inner: compressed_pk.var_decompress(diversifier), + }) + } + + /// Generate a new [`FlagCiphertext`]. + /// + /// This notifies the owner of a given viewing + /// key that a note can be decrypted by it, with + /// occasional false positives returned for + /// non-decrypting viewing keys. + #[cfg(feature = "rand")] + pub fn flag(&self, rng: &mut R) -> FlagCiphertext + where + R: CryptoRng + RngCore, + { + use polyfuzzy::MultiFmdScheme as _; + + let mut scheme = polyfuzzy::MultiFmd2CompactScheme::new( + parameters::GAMMA, + parameters::THRESHOLD, + ); + + scheme.flag(&self.inner, rng).into() + } +} + +impl AsRef for PublicKey { + fn as_ref(&self) -> &polyfuzzy::CompactPublicKey { + &self.inner + } +} + +/// FMD secret key. +//#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(transparent)] +pub struct SecretKey { + inner: polyfuzzy::CompactSecretKey, +} + +impl SecretKey { + /// Hash personalization string used when deriving a [`SecretKey`] + /// from a [`SaplingIvk`]. + const KDF_PERSONALIZATION: &str = "Namada FMD Secret Key"; + + /// Return the default public key. + /// + /// Flag ciphertexts generated using the default + /// public key are guaranteed to be unique on + /// every call to [`PublicKey::flag`]. + pub fn default_public_key(&self) -> PublicKey { + PublicKey { + inner: self.inner.master_public_key(), + } + } + + /// Return a public key randomized by the given `diversifier`. + pub fn randomized_public_key(&self, diversifier: &[u8]) -> PublicKey { + PublicKey { + inner: self.inner.var_randomized_public_key(diversifier), + } + } + + /// Expand this compact key to a full FMD secret key. + pub fn fmd_secret_key(&self) -> polyfuzzy::FmdSecretKey { + let cpk_key = self.inner.master_public_key(); + let mut scheme = + polyfuzzy::MultiFmd2CompactScheme::new(parameters::GAMMA, 1); + scheme.expand_keypair(&self.inner, &cpk_key).0 + } +} + +impl AsRef for SecretKey { + fn as_ref(&self) -> &polyfuzzy::CompactSecretKey { + &self.inner + } +} + +impl From<&SaplingIvk> for SecretKey { + fn from(ivk: &SaplingIvk) -> Self { + let mut xof_stream = { + let mut hasher = KangarooTwelve::new(Self::KDF_PERSONALIZATION); + + // derive key material from input viewing key + hasher.update(&ivk.to_repr()); + + hasher.into_xof() + }; + + Self { + inner: polyfuzzy::CompactSecretKey::derive_from_xof_stream( + parameters::THRESHOLD, + |buf| { + xof_stream.squeeze(buf); + }, + ), + } + } +} + +impl From for SecretKey { + #[inline] + fn from(ivk: SaplingIvk) -> Self { + (&ivk).into() + } +} + +impl From<&PseudoExtendedKey> for SecretKey { + fn from(psk: &PseudoExtendedKey) -> Self { + psk.to_viewing_key().fvk.vk.ivk().into() + } +} + +impl From for SecretKey { + fn from(psk: PseudoExtendedKey) -> Self { + (&psk).into() + } +} + +impl From<&ExtendedViewingKey> for SecretKey { + fn from(evk: &ExtendedViewingKey) -> Self { + Self::from(evk.as_viewing_key().ivk()) + } +} + +/// FMD flag ciphertexts. +//#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[repr(transparent)] +pub struct FlagCiphertext { + inner: polyfuzzy::FlagCiphertexts, +} + +impl FlagCiphertext { + /// Check if the flag ciphertext is valid, according to Namada's consensus + /// rules. + #[inline] + pub fn is_valid(&self) -> bool { + parameters::valid_compressed_bit_ciphertext(self.inner.bits()) + } + + /// Generate a random [`FlagCiphertext`]. + #[cfg(feature = "rand")] + pub fn random(rng: &mut R) -> Self + where + R: CryptoRng + RngCore, + { + Self { + inner: polyfuzzy::FlagCiphertexts::random(rng, parameters::GAMMA), + } + } +} + +impl From for FlagCiphertext { + fn from(flag_ciphertext: polyfuzzy::FlagCiphertexts) -> Self { + Self { + inner: flag_ciphertext, + } + } +} + +impl From for polyfuzzy::FlagCiphertexts { + fn from(flag_ciphertext: FlagCiphertext) -> Self { + flag_ciphertext.inner + } +} + +impl AsRef for FlagCiphertext { + fn as_ref(&self) -> &polyfuzzy::FlagCiphertexts { + &self.inner + } +} + +#[cfg(feature = "default-flag-ciphertext")] +impl Default for FlagCiphertext { + #[inline] + fn default() -> Self { + Self::random(&mut rand_core::OsRng) + } +} + +mod borsh_impls { + use super::*; + + macro_rules! borsh_derive_from_bincode { + ($($type:ty),+) => { + $( + impl BorshSerialize for $type { + #[inline] + fn serialize( + &self, + writer: &mut W, + ) -> io::Result<()> { + bincode_as_borsh_serialize(writer, self) + } + } + + impl BorshDeserialize for $type { + #[inline] + fn deserialize_reader( + reader: &mut R, + ) -> io::Result { + bincode_as_borsh_deserialize(reader) + } + } + + impl BorshSchema for $type { + fn add_definitions_recursively( + definitions: &mut BTreeMap, + ) { + let def = { + >::add_definitions_recursively(definitions); + definitions.get(&>::declaration()).unwrap().clone() + }; + + definitions.insert(Self::declaration(), def); + } + + fn declaration() -> String { + std::any::type_name::().into() + } + } + )+ + }; + } + + fn bincode_as_borsh_serialize( + writer: &mut W, + data: &T, + ) -> io::Result<()> { + // NOTE: serialize the size. borsh will only see an + // opaque vector of bytes + let size: u32 = bincode::serialized_size(data) + .map_err(from_bincode_err)? + .try_into() + .map_err(io::Error::other)?; + writer.write_all(&size.to_le_bytes())?; + + bincode::serialize_into(writer, data).map_err(from_bincode_err) + } + + fn bincode_as_borsh_deserialize< + R: io::Read, + T: for<'de> Deserialize<'de>, + >( + reader: &mut R, + ) -> io::Result { + // NOTE: skip the length of the fake vector of bytes + reader.read_exact(&mut [0u8; 4])?; + + bincode::deserialize_from(reader).map_err(from_bincode_err) + } + + #[allow(clippy::boxed_local)] + fn from_bincode_err(err: bincode::Error) -> io::Error { + match *err { + bincode::ErrorKind::Io(err) => err, + other => io::Error::other(other), + } + } + + borsh_derive_from_bincode!(FlagCiphertext); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fmd_pubkey_roundtrip_repr() { + let diversifier = b"bing-bong"; + let sk = { + let ivk = SaplingIvk(Default::default()); + SecretKey::from(ivk) + }; + + let pk = sk.randomized_public_key(diversifier); + let repr = pk.clone().into_compressed_bytes(); + let pk2 = PublicKey::from_parts(diversifier, &repr).unwrap(); + + assert_eq!(pk, pk2); + } + + #[test] + #[cfg(feature = "default-flag-ciphertext")] + fn test_flag_ciphertext_borsh_roundtrip() { + use crate::borsh::BorshSerializeExt; + + // run this test a couple of times + for _ in 0..5 { + let random_flag_ciphertext = FlagCiphertext::default(); + + let serialized = random_flag_ciphertext.serialize_to_vec(); + let deserialized = + FlagCiphertext::try_from_slice(&serialized).unwrap(); + + assert_eq!(random_flag_ciphertext, deserialized); + } + } + + #[test] + #[cfg(feature = "default-flag-ciphertext")] + fn test_random_flag_ciphertext_is_valid() { + // run this test a couple of times + for _ in 0..5 { + let random_flag_ciphertext = FlagCiphertext::default(); + assert!(random_flag_ciphertext.is_valid()); + } + } + + #[test] + fn test_flag_ciphertext_bits_validation() { + let mut bits: Vec = { + let mut bits = [false; parameters::GAMMA]; + + // set some random bits + bits[0] = true; + bits[5] = true; + bits[10] = true; + bits[parameters::GAMMA - 1] = true; + bits[parameters::GAMMA - 2] = true; + bits[parameters::GAMMA - 3] = true; + + // compress the bits + bits.chunks(8) + .map(|bits| { + bits.iter().copied().enumerate().fold( + 0u8, + |accum_byte, (i, bit)| { + #[allow(clippy::cast_lossless)] + { + accum_byte ^ ((bit as u8) << i) + } + }, + ) + }) + .collect() + }; + + // check validation of a proper flag ciphertext + assert!(parameters::valid_compressed_bit_ciphertext(&bits)); + + let all_bits_unset = (0..8 - (parameters::GAMMA % 8)) + .map(|i| bits[bits.len() - 1] & (0b1000_0000_u8 >> i)) + .all(|bit| bit == 0); + + assert!(all_bits_unset, "Invalid bit ciphertext"); + + if parameters::GAMMA % 8 != 0 { + let n = bits.len(); + bits[n - 1] |= 0b1000_0000_u8; + + // check validation of a flag ciphertext with padding bits + // that has been tampered with + assert!(!parameters::valid_compressed_bit_ciphertext(&bits)); + + let some_bit_unset = (0..8 - (parameters::GAMMA % 8)) + .map(|i| bits[bits.len() - 1] & (0b1000_0000_u8 >> i)) + .any(|bit| bit != 0); + + assert!(some_bit_unset, "Valid bit ciphertext"); + } + } +} diff --git a/crates/core/src/string_encoding.rs b/crates/core/src/string_encoding.rs index 82fd5d7b0c2..fe9a3757583 100644 --- a/crates/core/src/string_encoding.rs +++ b/crates/core/src/string_encoding.rs @@ -27,6 +27,8 @@ pub const ADDRESS_HRP: &str = "tnam"; pub const MASP_EXT_FULL_VIEWING_KEY_HRP: &str = "zvknam"; /// MASP payment address human-readable part pub const MASP_PAYMENT_ADDRESS_HRP: &str = "znam"; +/// MASP payment address with FMD public key human-readable part +pub const MASP_FMD_PAYMENT_ADDRESS_HRP: &str = "zfnam"; /// MASP extended spending key human-readable part pub const MASP_EXT_SPENDING_KEY_HRP: &str = "zsknam"; /// `common::PublicKey` human-readable part diff --git a/crates/ibc/src/lib.rs b/crates/ibc/src/lib.rs index 4a98d76afc0..4aa52e5267c 100644 --- a/crates/ibc/src/lib.rs +++ b/crates/ibc/src/lib.rs @@ -92,7 +92,7 @@ use namada_core::ibc::core::channel::types::commitment::{ AcknowledgementCommitment, PacketCommitment, compute_packet_commitment, }; pub use namada_core::ibc::*; -use namada_core::masp::{TAddrData, addr_taddr, ibc_taddr}; +use namada_core::masp::{FlagCiphertext, TAddrData, addr_taddr, ibc_taddr}; use namada_core::masp_primitives::transaction::components::ValueSum; use namada_core::token::Amount; use namada_events::EmitEvents; @@ -237,18 +237,19 @@ impl namada_systems::ibc::Read for Store where S: StorageRead, { - fn try_extract_masp_tx_from_envelope( + fn try_extract_shielding_data_from_envelope( tx_data: &[u8], - ) -> StorageResult> { + ) -> StorageResult)>> { let msg = decode_message::(tx_data) .into_storage_result() .ok(); let tx = if let Some(IbcMessage::Envelope(ref envelope)) = msg { - Some(extract_masp_tx_from_envelope(envelope).ok_or_else(|| { - StorageError::new_const( - "Missing MASP transaction in IBC message", - ) - })?) + let shielding_data = extract_shielding_data_from_envelope(envelope) + .ok_or(StorageError::new_const( + "Missing MASP shielding data in IBC message", + ))?; + + Some((shielding_data.masp_tx, shielding_data.flag_ciphertexts)) } else { None }; @@ -578,7 +579,7 @@ pub struct InternalData { /// The transparent transfer that happens in parallel to IBC processes pub transparent: Option, /// The shielded transaction that happens in parallel to IBC processes - pub shielded: Option, + pub shielded: Option, /// IBC tokens that are credited/debited to internal accounts pub ibc_tokens: BTreeSet
, } @@ -760,13 +761,16 @@ where .map_err(Error::Storage)?; tokens.insert(token); } - (extract_masp_tx_from_packet(&msg.packet), tokens) + ( + extract_shielding_data_from_packet(&msg.packet), + tokens, + ) } #[cfg(is_apple_silicon)] MsgEnvelope::Packet(PacketMsg::Ack(msg)) => { // NOTE: This is unneeded but wasm compilation error // happened if deleted on macOS with Apple Silicon - let _ = extract_masp_tx_from_packet(&msg.packet); + let _ = extract_shielding_data_from_packet(&msg.packet); (None, BTreeSet::new()) } _ => (None, BTreeSet::new()), diff --git a/crates/ibc/src/msg.rs b/crates/ibc/src/msg.rs index d38466bb5e5..67c44c6d8d7 100644 --- a/crates/ibc/src/msg.rs +++ b/crates/ibc/src/msg.rs @@ -20,6 +20,7 @@ use ibc::core::host::types::identifiers::PortId; use ibc::primitives::proto::Protobuf; use masp_primitives::transaction::Transaction as MaspTransaction; use namada_core::borsh::BorshSerializeExt; +use namada_core::masp::FlagCiphertext; use namada_core::string_encoding::StringEncoded; use serde::{Deserialize, Serialize}; @@ -238,7 +239,12 @@ impl BorshSchema for MsgNftTransfer { /// Shielding data in IBC packet memo #[derive(Debug, Clone, BorshDeserialize, BorshSerialize)] -pub struct IbcShieldingData(pub MaspTransaction); +pub struct IbcShieldingData { + /// MASP transaction forwarded over IBC. + pub masp_tx: MaspTransaction, + /// Flag ciphertexts to signal the owner(s) of the new note(s). + pub flag_ciphertexts: Vec, +} impl From<&IbcShieldingData> for String { fn from(data: &IbcShieldingData) -> Self { @@ -273,9 +279,17 @@ impl FromStr for IbcShieldingData { pub fn extract_masp_tx_from_envelope( envelope: &MsgEnvelope, ) -> Option { + extract_shielding_data_from_envelope(envelope) + .map(|IbcShieldingData { masp_tx, .. }| masp_tx) +} + +/// Extract IBC shielding data from IBC envelope +pub fn extract_shielding_data_from_envelope( + envelope: &MsgEnvelope, +) -> Option { match envelope { MsgEnvelope::Packet(PacketMsg::Recv(msg)) => { - extract_masp_tx_from_packet(&msg.packet) + extract_shielding_data_from_packet(&msg.packet) } _ => None, } @@ -300,8 +314,17 @@ pub fn decode_ibc_shielding_data( /// Extract MASP transaction from IBC packet memo pub fn extract_masp_tx_from_packet(packet: &Packet) -> Option { + extract_shielding_data_from_packet(packet) + .map(|IbcShieldingData { masp_tx, .. }| masp_tx) +} + +/// Extract IBC shielding data from IBC packet memo +pub fn extract_shielding_data_from_packet( + packet: &Packet, +) -> Option { let memo = extract_memo_from_packet(packet, &packet.port_id_on_b)?; - decode_ibc_shielding_data(memo).map(|data| data.0) + + decode_ibc_shielding_data(memo) } fn extract_memo_from_packet( @@ -366,6 +389,13 @@ pub fn extract_traces_from_recv_msg( } /// Get IBC memo string from MASP transaction for receiving -pub fn convert_masp_tx_to_ibc_memo(transaction: &MaspTransaction) -> String { - IbcShieldingData(transaction.clone()).into() +pub fn convert_masp_tx_to_ibc_memo( + masp_tx: MaspTransaction, + flag_ciphertexts: Vec, +) -> String { + IbcShieldingData { + masp_tx, + flag_ciphertexts, + } + .into() } diff --git a/crates/light_sdk/src/transaction/transfer.rs b/crates/light_sdk/src/transaction/transfer.rs index 2e0e6833908..e3c51c6a08d 100644 --- a/crates/light_sdk/src/transaction/transfer.rs +++ b/crates/light_sdk/src/transaction/transfer.rs @@ -2,7 +2,7 @@ use namada_sdk::address::Address; use namada_sdk::hash::Hash; use namada_sdk::key::common; pub use namada_sdk::token::{ - DenominatedAmount, MaspTransaction, MaspTxId, Transfer, + DenominatedAmount, MaspTransaction, MaspTxData, MaspTxId, Transfer, }; use namada_sdk::tx::data::GasLimit; use namada_sdk::tx::{Authorization, TX_TRANSFER_WASM, Tx, TxError}; @@ -27,10 +27,14 @@ impl TransferBuilder { /// Build a shielded transfer transaction from the given parameters pub fn shielded( shielded_section_hash: MaspTxId, + flag_ciphertext_sechash: Hash, transaction: MaspTransaction, args: GlobalArgs, ) -> Self { - let data = Transfer::masp(shielded_section_hash); + let data = Transfer::masp(MaspTxData { + masp_tx_id: shielded_section_hash, + flag_ciphertext_sechash, + }); let mut tx = transaction::build_tx(args, data, TX_TRANSFER_WASM.to_string()); tx.add_masp_tx_section(transaction); diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index a69b883bcd2..99768bfc6a4 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -64,6 +64,7 @@ ethbridge-events.workspace = true eyre.workspace = true futures.workspace = true itertools.workspace = true +kassandra-client.workspace = true lazy_static = { workspace = true, optional = true } linkme = { workspace = true, optional = true } masp_primitives = { workspace = true, features = ["transparent-inputs"] } diff --git a/crates/node/src/bench_utils.rs b/crates/node/src/bench_utils.rs index 2e746b18459..6432d4ab6fd 100644 --- a/crates/node/src/bench_utils.rs +++ b/crates/node/src/bench_utils.rs @@ -88,13 +88,18 @@ use namada_sdk::queries::{ use namada_sdk::state::StorageRead; use namada_sdk::state::write_log::StorageModification; use namada_sdk::storage::{Key, KeySeg, TxIndex}; +use namada_sdk::tendermint::abci::Event as AbciEvent; use namada_sdk::time::DateTimeUtc; -use namada_sdk::token::{self, Amount, DenominatedAmount, Transfer}; +use namada_sdk::token::{ + self, Amount, DenominatedAmount, MaspTxData, Transfer, +}; use namada_sdk::tx::data::pos::Bond; use namada_sdk::tx::data::{ BatchedTxResult, Fee, TxResult, VpsResult, compute_inner_tx_hash, }; -use namada_sdk::tx::event::{Batch, MaspEvent, MaspTxRef, new_tx_event}; +use namada_sdk::tx::event::{ + Batch, FmdSectionRef, MaspEvent, MaspTxKind, MaspTxRef, new_tx_event, +}; use namada_sdk::tx::{ Authorization, BatchedTx, BatchedTxRef, Code, Data, IndexedTx, Section, Tx, }; @@ -113,8 +118,9 @@ pub use namada_sdk::tx::{ }; use namada_sdk::wallet::{DatedSpendingKey, Wallet}; use namada_sdk::{ - Namada, NamadaImpl, PaymentAddress, TransferSource, TransferTarget, - parameters, proof_of_stake, tendermint, + FlagCiphertext, Namada, NamadaImpl, PaymentAddress, TransferSource, + TransferTarget, UnifiedPaymentAddress, parameters, proof_of_stake, + tendermint, }; use namada_test_utils::tx_data::TxWriteData; use namada_vm::wasm::run; @@ -141,6 +147,8 @@ const SPECULATIVE_FILE_NAME: &str = "speculative_shielded.dat"; const SPECULATIVE_TMP_FILE_NAME: &str = "speculative_shielded.tmp"; const CACHE_FILE_NAME: &str = "shielded_sync.cache"; const CACHE_FILE_TMP_PREFIX: &str = "shielded_sync.cache.tmp"; +const FMD_CONF_FILE_NAME: &str = "fmd_config.toml"; +const FMD_CONF_FILE_TMP_PREFIX: &str = "fmd_config.toml.tmp"; /// For `tracing_subscriber`, which fails if called more than once in the same /// process @@ -882,6 +890,22 @@ impl ShieldedUtils for BenchShieldedUtils { let mut file = File::open(file_name)?; DispatcherCache::try_from_reader(&mut file) } + + async fn fmd_config_save( + &self, + config: &mut kassandra_client::config::Config, + ) -> std::io::Result<()> { + config.save(FMD_CONF_FILE_TMP_PREFIX)?; + std::fs::rename( + FMD_CONF_FILE_TMP_PREFIX, + self.context_dir.0.path().join(FMD_CONF_FILE_NAME), + ) + } + + async fn fmd_config_load() + -> std::io::Result { + kassandra_client::config::Config::load_or_new(FMD_CONF_FILE_NAME) + } } #[async_trait::async_trait(?Send)] @@ -1059,43 +1083,71 @@ impl Client for BenchShell { let tx_event: Event = new_tx_event(tx, height.value()) .with(Batch(&tx_result)) .into(); - // Expect a single masp tx in the batch - let masp_ref = tx.sections.iter().find_map(|section| { - if let Section::MaspTx(transaction) = section { - Some(MaspTxRef::MaspSection(transaction.txid().into())) - } else { - None - } - }); let first_inner_tx_hash = compute_inner_tx_hash( tx.wrapper_hash().as_ref(), Either::Right(tx.first_commitments().unwrap()), ); - let masp_event = masp_ref.map(|data| { - let masp_event: Event = MaspEvent { - tx_index: IndexedTx { - block_height: namada_sdk::chain::BlockHeight( - u64::from(height), - ), - block_index: TxIndex::must_from_usize(idx), - batch_index: Some(0), - }, - kind: namada_sdk::tx::event::MaspEventKind::Transfer, - data, - } - .with(TxHash(tx.header_hash())) - .with(InnerTxHash(first_inner_tx_hash)) - .into(); - masp_event - }); - - res.push(namada_sdk::tendermint::abci::Event::from(tx_event)); + let wrapper_hash = tx.wrapper_hash().unwrap_or_default(); + let indexed_tx = IndexedTx { + block_height: namada_sdk::chain::BlockHeight(u64::from( + height, + )), + block_index: TxIndex::must_from_usize(idx), + batch_index: Some(0), + }; - if let Some(event) = masp_event { - res.push(namada_sdk::tendermint::abci::Event::from(event)); + let masp_tx_event = + tx.sections.iter().find_map(|section| match section { + Section::MaspTx(transaction) => { + Some(AbciEvent::from(Event::from( + MaspEvent::ShieldedOutput { + tx_index: indexed_tx, + kind: MaspTxKind::Transfer, + data: MaspTxRef::MaspSection( + transaction.txid().into(), + ), + } + .with(TxHash(wrapper_hash)) + .with(InnerTxHash(first_inner_tx_hash)), + ))) + } + _ => None, + }); + let masp_fmd_event = + tx.sections.iter().find_map(|section| match section { + sec @ Section::ExtraData(extra_data) + if extra_data.id().is_some_and(|extra_data| { + >::try_from_slice( + extra_data, + ) + .is_ok() + }) => + { + Some(AbciEvent::from(Event::from( + MaspEvent::FlagCiphertexts { + tx_index: indexed_tx, + section: FmdSectionRef::FmdSection( + sec.get_hash(), + ), + } + .with(TxHash(wrapper_hash)) + .with(InnerTxHash(first_inner_tx_hash)), + ))) + } + _ => None, + }); + + res.push(AbciEvent::from(tx_event)); + + if let Some((masp_tx_event, masp_fmd_event)) = + masp_tx_event.zip(masp_fmd_event) + { + res.push(masp_tx_event); + res.push(masp_fmd_event); } } + Some(res) } else { None @@ -1186,7 +1238,9 @@ impl Default for BenchShieldedCtx { .wallet .insert_payment_addr( alias, - PaymentAddress::from(payment_addr), + UnifiedPaymentAddress::V0(PaymentAddress::from( + payment_addr, + )), true, ) .unwrap(); @@ -1253,7 +1307,7 @@ impl BenchShieldedCtx { token: address::testing::nam(), amount: denominated_amount, }; - let shielded = async_runtime + let (shielded, fmd_flags) = async_runtime .block_on(async { let expiration = Namada::tx_builder(&namada).expiration.to_datetime(); @@ -1275,25 +1329,32 @@ impl BenchShieldedCtx { masp_tx, metadata: _, epoch: _, - }| masp_tx, + fmd_flags, + }| (masp_tx, fmd_flags), ) .expect("MASP must have shielded part"); - let shielded_section_hash = shielded.txid().into(); + let fmd_section = + Section::ExtraData(Code::from_borsh_encoded(&fmd_flags)); + let shielded_data = MaspTxData { + masp_tx_id: shielded.txid().into(), + flag_ciphertext_sechash: fmd_section.get_hash(), + }; + let tx = if source.effective_address() == MASP && target.effective_address() == MASP { namada.client().read().generate_tx( TX_TRANSFER_WASM, - Transfer::masp(shielded_section_hash), + Transfer::masp(shielded_data), Some(shielded), - None, + Some(vec![fmd_section]), vec![&defaults::albert_keypair()], ) } else if target.effective_address() == MASP { namada.client().read().generate_tx( TX_TRANSFER_WASM, - Transfer::masp(shielded_section_hash) + Transfer::masp(shielded_data) .transfer( source.effective_address(), MASP, @@ -1302,13 +1363,13 @@ impl BenchShieldedCtx { ) .unwrap(), Some(shielded), - None, + Some(vec![fmd_section]), vec![&defaults::albert_keypair()], ) } else { namada.client().read().generate_tx( TX_TRANSFER_WASM, - Transfer::masp(shielded_section_hash) + Transfer::masp(shielded_data) .transfer( MASP, target.effective_address(), @@ -1317,7 +1378,7 @@ impl BenchShieldedCtx { ) .unwrap(), Some(shielded), - None, + Some(vec![fmd_section]), vec![&defaults::albert_keypair()], ) }; @@ -1382,6 +1443,7 @@ impl BenchShieldedCtx { let vectorized_transfer = Transfer::deserialize(&mut tx.tx.data(&tx.cmt).unwrap().as_slice()) .unwrap(); + let masp_tx_id = vectorized_transfer.masp_tx_id().unwrap(); let sources = vec![vectorized_transfer.sources.into_iter().next().unwrap()] .into_iter() @@ -1390,18 +1452,24 @@ impl BenchShieldedCtx { vec![vectorized_transfer.targets.into_iter().next().unwrap()] .into_iter() .collect(); + let masp_tx = tx.tx.get_masp_section(&masp_tx_id).unwrap().clone(); + let fmd_section = Section::ExtraData(Code::from_borsh_encoded( + &std::iter::repeat_with(FlagCiphertext::default) + .take( + masp_tx + .sapling_bundle() + .map_or(0, |x| x.shielded_outputs.len()), + ) + .collect::>(), + )); let transfer = Transfer { sources, targets, - shielded_section_hash: Some( - vectorized_transfer.shielded_section_hash.unwrap(), - ), + shielded_data: Some(MaspTxData { + masp_tx_id, + flag_ciphertext_sechash: fmd_section.get_hash(), + }), }; - let masp_tx = tx - .tx - .get_masp_section(&transfer.shielded_section_hash.unwrap()) - .unwrap() - .clone(); let msg = MsgTransfer:: { message: msg, transfer: Some(transfer), @@ -1412,6 +1480,7 @@ impl BenchShieldedCtx { .read() .generate_ibc_tx(TX_IBC_WASM, msg.serialize_to_vec()); ibc_tx.tx.add_masp_tx_section(masp_tx); + ibc_tx.tx.add_section(fmd_section); (ctx, ibc_tx) } diff --git a/crates/node/src/protocol.rs b/crates/node/src/protocol.rs index 63e0b748ea9..d53f7459f7f 100644 --- a/crates/node/src/protocol.rs +++ b/crates/node/src/protocol.rs @@ -30,7 +30,7 @@ use namada_sdk::tx::data::{ BatchedTxResult, TxResult, VpStatusFlags, VpsResult, WrapperTx, compute_inner_tx_hash, }; -use namada_sdk::tx::event::{MaspEvent, MaspEventKind, MaspTxRef}; +use namada_sdk::tx::event::{FmdSectionRef, MaspEvent, MaspTxKind, MaspTxRef}; use namada_sdk::tx::{BatchedTxRef, IndexedTx, Tx, TxCommitments}; use namada_sdk::validation::{ EthBridgeNutVp, EthBridgePoolVp, EthBridgeVp, GovernanceVp, IbcVp, MaspVp, @@ -395,38 +395,32 @@ where Ok(mut batched_tx_result) if batched_tx_result.is_accepted() => { // If the transaction was a masp one generate the // appropriate event - if let Some(masp_ref) = get_optional_masp_ref( + if let Some((masp_ref, fmd_ref)) = get_optional_masp_refs( state, cmt, Either::Right(&batched_tx_result), ) .map_err(|e| Box::new(DispatchError::from(e)))? { - let inner_tx_hash = - compute_inner_tx_hash(wrapper_hash, Either::Right(cmt)); - batched_tx_result.events.insert( - MaspEvent { - tx_index: IndexedTx { - block_height: height, - block_index: tx_index, - batch_index: tx - .header - .batch - .get_index_of(cmt) - .map(|idx| { - TxIndex::must_from_usize(idx).into() - }), - }, - kind: MaspEventKind::Transfer, - data: masp_ref, - } - .with(TxHashAttr( - // Zero hash if the wrapper is not provided - // (governance proposal) - wrapper_hash.cloned().unwrap_or_default(), - )) - .with(InnerTxHashAttr(inner_tx_hash)) - .into(), + insert_masp_events( + &mut batched_tx_result, + // Zero hash if the wrapper is not provided + // (governance proposal) + TxHashAttr(wrapper_hash.cloned().unwrap_or_default()), + InnerTxHashAttr(compute_inner_tx_hash( + wrapper_hash, + Either::Right(cmt), + )), + IndexedTx { + block_height: height, + block_index: tx_index, + batch_index: tx.header.batch.get_index_of(cmt).map( + |idx| TxIndex::must_from_usize(idx).into(), + ), + }, + MaspTxKind::Transfer, + masp_ref, + fmd_ref, ); } @@ -467,6 +461,7 @@ where pub struct MaspTxResult { tx_result: BatchedTxResult, masp_section_ref: MaspTxRef, + fmd_section_ref: FmdSectionRef, } /// Performs the required operation on a wrapper transaction: @@ -526,22 +521,21 @@ where let first_commitments = tx.first_commitments().unwrap(); let mut batch = TxResult::default(); // Generate Masp event if needed - masp_tx_result.tx_result.events.insert( - MaspEvent { - tx_index: IndexedTx { - block_height: height, - block_index: tx_index.to_owned(), - batch_index: Some(0), - }, - kind: MaspEventKind::FeePayment, - data: masp_tx_result.masp_section_ref, - } - .with(TxHashAttr(tx.header_hash())) - .with(InnerTxHashAttr(compute_inner_tx_hash( + insert_masp_events( + &mut masp_tx_result.tx_result, + TxHashAttr(tx.header_hash()), + InnerTxHashAttr(compute_inner_tx_hash( tx.wrapper_hash().as_ref(), Either::Right(first_commitments), - ))) - .into(), + )), + IndexedTx { + block_height: height, + block_index: tx_index.to_owned(), + batch_index: Some(0), + }, + MaspTxKind::FeePayment, + masp_tx_result.masp_section_ref, + masp_tx_result.fmd_section_ref, ); batch.insert_inner_tx_result( @@ -839,20 +833,22 @@ where // Ensure that the transaction is actually a masp one, otherwise // reject if is_masp_transfer && result.is_accepted() { - let masp_section_ref = get_optional_masp_ref( - *state, - first_tx.cmt, - Either::Left(true), - )? - .ok_or_else(|| { - Error::FeeError( - "Missing expected masp section reference" - .to_string(), - ) - })?; + let (masp_section_ref, fmd_section_ref) = + get_optional_masp_refs( + *state, + first_tx.cmt, + Either::Left(true), + )? + .ok_or_else(|| { + Error::FeeError( + "Missing expected masp section reference" + .to_string(), + ) + })?; MaspTxResult { tx_result: result, masp_section_ref, + fmd_section_ref, } } else { state.write_log_mut().drop_tx(); @@ -893,11 +889,11 @@ where // messing up with indexers/clients. Also a transaction can only be of one of // the two types, not both at the same time (the MASP VP accepts a single // Transaction) -fn get_optional_masp_ref>( +fn get_optional_masp_refs>( state: &S, cmt: &TxCommitments, is_masp_tx: Either, -) -> Result> { +) -> Result> { // Always check that the transaction was indeed a MASP one by looking at the // changed keys. A malicious tx could push a MASP Action without touching // any storage keys associated with the shielded pool @@ -912,14 +908,27 @@ fn get_optional_masp_ref>( let masp_ref = if action::is_ibc_shielding_transfer(state) .map_err(Error::StateError)? { - Some(MaspTxRef::IbcData(cmt.data_sechash().to_owned())) + let ibc_data = cmt.data_sechash().to_owned(); + + Some(( + MaspTxRef::IbcData(ibc_data), + FmdSectionRef::IbcData(ibc_data), + )) } else { let actions = state.read_actions().map_err(Error::StateError)?; - action::get_masp_section_ref(&actions) + + let masp_tx = action::get_masp_section_ref(&actions) + .map_err(|msg| { + Error::StateError(state::Error::new_alloc(msg.to_string())) + })? + .map(MaspTxRef::MaspSection); + let fmd_sechash = action::get_fmd_flag_ciphertexts_ref(&actions) .map_err(|msg| { Error::StateError(state::Error::new_alloc(msg.to_string())) })? - .map(MaspTxRef::MaspSection) + .map(FmdSectionRef::FmdSection); + + masp_tx.zip(fmd_sechash) }; Ok(masp_ref) @@ -1499,6 +1508,35 @@ fn merge_vp_results( )) } +/// Insert MASP event data into the provided [`BatchedTxResult`]. +fn insert_masp_events( + batched_tx_result: &mut BatchedTxResult, + wrapper_tx_hash: TxHashAttr, + inner_tx_hash: InnerTxHashAttr, + tx_index: IndexedTx, + masp_tx_kind: MaspTxKind, + masp_tx_ref: MaspTxRef, + fmd_ref: FmdSectionRef, +) { + batched_tx_result.events.extend([ + MaspEvent::ShieldedOutput { + tx_index, + kind: masp_tx_kind, + data: masp_tx_ref, + } + .with(TxHashAttr(wrapper_tx_hash.0)) + .with(InnerTxHashAttr(inner_tx_hash.0)) + .into(), + MaspEvent::FlagCiphertexts { + tx_index, + section: fmd_ref, + } + .with(TxHashAttr(wrapper_tx_hash.0)) + .with(InnerTxHashAttr(inner_tx_hash.0)) + .into(), + ]); +} + #[cfg(test)] mod tests { use eyre::Result; diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 65c39f6d64a..36cfe2d9e90 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -72,7 +72,7 @@ migrations = [ [dependencies] namada_account.workspace = true -namada_core.workspace = true +namada_core = { workspace = true, features = ["default-flag-ciphertext"] } namada_ethereum_bridge.workspace = true namada_events.workspace = true namada_gas.workspace = true @@ -110,6 +110,7 @@ futures.workspace = true init-once.workspace = true itertools.workspace = true jubjub = { workspace = true, optional = true } +kassandra-client.workspace = true lazy_static.workspace = true linkme = { workspace = true, optional = true } masp_primitives.workspace = true @@ -150,7 +151,7 @@ getrandom = { workspace = true, features = ["wasm_js"] } [dev-dependencies] namada_account = { path = "../account", features = ["testing"] } -namada_core = { path = "../core", features = ["rand", "testing"] } +namada_core = { path = "../core", features = ["default-flag-ciphertext", "rand", "testing"] } namada_ethereum_bridge = { path = "../ethereum_bridge", features = ["testing"] } namada_governance = { path = "../governance", features = ["testing"] } namada_ibc = { path = "../ibc", features = ["testing"] } diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index b9ba33fc322..c50af479807 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -15,7 +15,7 @@ use namada_core::dec::Dec; use namada_core::ethereum_events::EthAddress; use namada_core::keccak::KeccakHash; use namada_core::key::{SchemeType, common}; -use namada_core::masp::{DiversifierIndex, MaspEpoch, PaymentAddress}; +use namada_core::masp::{DiversifierIndex, MaspEpoch}; use namada_core::string_encoding::StringEncoded; use namada_core::time::DateTimeUtc; use namada_core::token::Amount; @@ -131,7 +131,7 @@ impl NamadaTypes for SdkTypes { type EthereumAddress = (); type Keypair = namada_core::key::common::SecretKey; type MaspIndexerAddress = String; - type PaymentAddress = namada_core::masp::PaymentAddress; + type PaymentAddress = namada_core::masp::UnifiedPaymentAddress; type PublicKey = namada_core::key::common::PublicKey; type SpendingKey = PseudoExtendedKey; type TendermintAddress = tendermint_rpc::Url; @@ -718,7 +718,7 @@ impl TxOsmosisSwap { ), }; - let shielding_tx = tx::gen_ibc_shielding_transfer( + let (shielding_tx, fmd_flags) = tx::gen_ibc_shielding_transfer( ctx, GenIbcShieldingTransfer { query: Query { @@ -752,7 +752,10 @@ impl TxOsmosisSwap { serde_json::to_value(&NamadaMemo { namada: NamadaMemoData::OsmosisSwap { shielding_data: StringEncoded::new( - IbcShieldingData(shielding_tx), + IbcShieldingData { + masp_tx: shielding_tx, + flag_ciphertexts: fmd_flags, + }, ), shielded_amount: amount_to_shield, overflow_receiver, @@ -2517,6 +2520,39 @@ pub struct ShieldedSync { pub retry_strategy: RetryStrategy, } +/// The type of FMD key management command +#[derive(Copy, Clone, Debug)] +pub enum FmdCommandType { + /// Register a key with a Kassandra services configured + RegisterKey, + /// Add a Kassandra service to the configuration file + AddService, +} + +impl FromStr for FmdCommandType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.trim().to_lowercase().as_str() { + "register" => Ok(Self::RegisterKey), + "add" => Ok(Self::AddService), + _ => Err("Could not parse {s} as a valid FMD command".to_string()), + } + } +} + +/// An FMD key management command for interacting with +/// Kassandra services +#[derive(Clone, Debug)] +pub struct FmdCommand { + /// The type of command + pub command: FmdCommandType, + /// The key that is being managed + pub viewing_key: C::DatedViewingKey, + /// A url for a Kassandra service + pub service: Option, +} + /// Query PoS commission rate #[derive(Clone, Debug)] pub struct QueryCommissionRate { @@ -2952,7 +2988,7 @@ pub struct KeyAddressFind { /// Public key hash to lookup keypair with pub public_key_hash: Option, /// Payment address to find - pub payment_address: Option, + pub payment_address: Option, /// Find keys only pub keys_only: bool, /// Find addresses only @@ -3017,6 +3053,12 @@ pub struct KeyAddressRemove { /// Generate payment address arguments #[derive(Clone, Debug)] pub struct PayAddressGen { + /// Force generating a v0 payment address. + /// + /// This does not include an FMD public key, therefore + /// should not be shared as a payment target if you + /// intend to use FMD to speed up shielded sync. + pub v0: bool, /// Payment address alias pub alias: String, /// Whether to force overwrite the alias diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 06c2dce010a..f63524f9d7c 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -52,8 +52,8 @@ use namada_core::ethereum_events::EthAddress; use namada_core::ibc::core::host::types::identifiers::{ChannelId, PortId}; use namada_core::key::*; pub use namada_core::masp::{ - ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, TransferSource, - TransferTarget, + ExtendedSpendingKey, ExtendedViewingKey, FlagCiphertext, FmdPaymentAddress, + PaymentAddress, TransferSource, TransferTarget, UnifiedPaymentAddress, }; pub use namada_core::{control_flow, task_env}; use namada_io::{Client, Io, NamadaIo}; @@ -189,7 +189,7 @@ pub trait Namada: NamadaIo { /// arguments fn new_shielding_transfer( &self, - target: PaymentAddress, + target: UnifiedPaymentAddress, data: Vec, ) -> args::TxShieldingTransfer { args::TxShieldingTransfer { @@ -1146,6 +1146,9 @@ pub mod testing { if let Some((shielded_transfer, asset_types, build_params)) = aux { let shielded_section_hash = tx.add_masp_tx_section(shielded_transfer.masp_tx).1; + tx.add_fmd_flag_ciphertexts( + &shielded_transfer.fmd_flags, + ); tx.add_masp_builder(MaspBuilder { asset_types: asset_types.into_keys().collect(), // Store how the Info objects map to Descriptors/Outputs diff --git a/crates/sdk/src/masp.rs b/crates/sdk/src/masp.rs index 53f173c2bf0..669ce5d9fa1 100644 --- a/crates/sdk/src/masp.rs +++ b/crates/sdk/src/masp.rs @@ -1,5 +1,6 @@ //! MASP verification wrappers. +pub mod fmd; mod utilities; use std::str::FromStr; @@ -19,7 +20,7 @@ use namada_ibc::{IbcMessage, decode_message, extract_masp_tx_from_envelope}; use namada_io::client::Client; use namada_token::masp::shielded_wallet::ShieldedQueries; pub use namada_token::masp::{utils, *}; -use namada_tx::event::{MaspEvent, MaspEventKind, MaspTxRef}; +use namada_tx::event::{MaspTxEvent, MaspTxKind, MaspTxRef}; use namada_tx::{IndexedTx, Tx}; pub use utilities::{IndexerMaspClient, LedgerMaspClient}; @@ -92,10 +93,10 @@ fn extract_masp_tx( } // Retrieves all the masp events at the specified height. -async fn get_indexed_masp_events_at_height( +async fn get_indexed_masp_txs_at_height( client: &C, height: BlockHeight, -) -> Result, Error> { +) -> Result, Error> { let maybe_masp_events: Result, Error> = client .block_results(height.0) .await @@ -111,9 +112,9 @@ async fn get_indexed_masp_events_at_height( }; let kind = if kind == namada_tx::event::masp_types::TRANSFER { - MaspEventKind::Transfer + MaspTxKind::Transfer } else if kind == namada_tx::event::masp_types::FEE_PAYMENT { - MaspEventKind::FeePayment + MaspTxKind::FeePayment } else { return Ok(None); }; @@ -125,7 +126,7 @@ async fn get_indexed_masp_events_at_height( let tx_index = IndexedTx::read_from_event_attributes(&event.attributes)?; - Ok(Some(MaspEvent { + Ok(Some(MaspTxEvent { tx_index, kind, data, diff --git a/crates/sdk/src/masp/fmd.rs b/crates/sdk/src/masp/fmd.rs new file mode 100644 index 00000000000..0b2ed046e4a --- /dev/null +++ b/crates/sdk/src/masp/fmd.rs @@ -0,0 +1,39 @@ +//! Commands for FMD key management. +use namada_core::key::FmdKeyHash; +use namada_wallet::DatedViewingKey; + +use crate::{ShieldedUtils, ShieldedWallet}; + +/// Add an FMD key derived from the viewing key +/// to the config file. This file will be used +/// for registering and querying data from Kassandra +/// services. +pub async fn add_service( + wallet: ShieldedWallet, + DatedViewingKey { key, .. }: DatedViewingKey, + url: &str, +) { + let uuid = kassandra_client::get_host_uuid(url); + let fmd_key = namada_core::masp::FmdSecretKey::from(&key).fmd_secret_key(); + let enc_key = kassandra_client::encryption_key(&fmd_key, &uuid); + let key_hash = FmdKeyHash::from(fmd_key).to_string(); + let mut config = U::fmd_config_load().await.unwrap(); + config.add_service(key_hash, url, enc_key); + wallet.utils.fmd_config_save(&mut config).await.unwrap(); +} + +/// Register FND keys with Kassandra services as specified in the +/// config file. +pub async fn register_keys( + DatedViewingKey { key, birthday }: DatedViewingKey, +) { + let config = U::fmd_config_load().await.unwrap(); + let fmd_key = namada_core::masp::FmdSecretKey::from(&key).fmd_secret_key(); + let key_hash = FmdKeyHash::from(&fmd_key).to_string(); + kassandra_client::register_fmd_key( + &config, + key_hash, + &fmd_key, + Some(birthday.0), + ); +} diff --git a/crates/sdk/src/masp/utilities.rs b/crates/sdk/src/masp/utilities.rs index 0a7c31052fd..fe1906f022e 100644 --- a/crates/sdk/src/masp/utilities.rs +++ b/crates/sdk/src/masp/utilities.rs @@ -18,12 +18,12 @@ use namada_token::masp::utils::{ IndexedNoteEntry, MaspClient, MaspClientCapabilities, MaspIndexedTx, MaspTxKind, }; -use namada_tx::event::MaspEvent; +use namada_tx::event::MaspTxEvent; use namada_tx::{IndexedTx, Tx}; use tokio::sync::Semaphore; use crate::error::{Error, QueryError}; -use crate::masp::{extract_masp_tx, get_indexed_masp_events_at_height}; +use crate::masp::{extract_masp_tx, get_indexed_masp_txs_at_height}; struct LedgerMaspClientInner { client: C, @@ -82,7 +82,7 @@ impl LedgerMaspClient { for height in from.0..=to.0 { let maybe_txs_results = async { - get_indexed_masp_events_at_height( + get_indexed_masp_txs_at_height( &self.inner.client, height.into(), ) @@ -109,7 +109,7 @@ impl LedgerMaspClient { // Cache the last tx seen to avoid multiple deserializations let mut last_tx: Option<(Tx, TxIndex)> = None; - for MaspEvent { + for MaspTxEvent { tx_index, kind, data, @@ -133,7 +133,7 @@ impl LedgerMaspClient { txs.push(( MaspIndexedTx { indexed_tx: tx_index, - kind: kind.into(), + kind, }, extracted_masp_tx, )); diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs index abca6c7d978..0513876f566 100644 --- a/crates/sdk/src/signing.rs +++ b/crates/sdk/src/signing.rs @@ -1426,12 +1426,9 @@ pub async fn to_ledger_vector( // To facilitate lookups of MASP AssetTypes let mut asset_types = HashMap::new(); - let builder = find_masp_builder( - tx, - transfer.shielded_section_hash, - &mut asset_types, - ) - .map_err(|_| Error::Other("Invalid Data".to_string()))?; + let builder = + find_masp_builder(tx, transfer.masp_tx_id(), &mut asset_types) + .map_err(|_| Error::Other("Invalid Data".to_string()))?; make_ledger_token_transfer_endpoints( tokens, &mut tv.output, @@ -1526,7 +1523,7 @@ pub async fn to_ledger_vector( let mut asset_types = HashMap::new(); let builder = find_masp_builder( tx, - transfer.shielded_section_hash, + transfer.masp_tx_id(), &mut asset_types, ) .map_err(|_| Error::Other("Invalid Data".to_string()))?; @@ -1695,7 +1692,7 @@ pub async fn to_ledger_vector( let mut asset_types = HashMap::new(); let builder = find_masp_builder( tx, - transfer.shielded_section_hash, + transfer.masp_tx_id(), &mut asset_types, ) .map_err(|_| Error::Other("Invalid Data".to_string()))?; @@ -2190,7 +2187,7 @@ mod test_signing { use namada_core::hash::Hash; use namada_core::ibc::PGFIbcTarget; use namada_core::ibc::core::host::types::identifiers::{ChannelId, PortId}; - use namada_core::masp::TxIdInner; + use namada_core::masp::{MaspTxData, TxIdInner}; use namada_core::token::{Denomination, MaspDigitPos}; use namada_governance::storage::proposal::PGFInternalTarget; use namada_io::client::EncodedResponseQuery; @@ -2616,7 +2613,7 @@ mod test_signing { }, DenominatedAmount::new(Amount::from_u64(2), 0.into()), )]), - shielded_section_hash: None, + shielded_data: None, }; let tokens = HashMap::from([ (Address::Internal(InternalAddress::Governance), "SuperMoney"), @@ -2768,15 +2765,19 @@ mod test_signing { fn test_find_masp_builder() { let mut tx = Tx::new(ChainId::default(), None); let mut asset_types = Default::default(); - let shielded_section_hash = MaspTxId::from(TxIdInner::from_bytes([ + let masp_tx_id = MaspTxId::from(TxIdInner::from_bytes([ 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ])); + let shielded_data = MaspTxData { + masp_tx_id, + flag_ciphertext_sechash: Hash::zero(), + }; // no masp builder present assert_eq!( find_masp_builder( &tx, - Some(shielded_section_hash), + Some(shielded_data.masp_tx_id), &mut asset_types ) .expect("Test failed"), @@ -2837,9 +2838,13 @@ mod test_signing { assert!(asset_types.is_empty()); // now we should find the builder - find_masp_builder(&tx, Some(shielded_section_hash), &mut asset_types) - .expect("Test failed") - .expect("Test failed"); + find_masp_builder( + &tx, + Some(shielded_data.masp_tx_id), + &mut asset_types, + ) + .expect("Test failed") + .expect("Test failed"); assert_eq!( asset_types .values() diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index dc79c58e6da..591ecd71618 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -41,7 +41,10 @@ use namada_core::ibc::core::client::types::Height as IbcHeight; use namada_core::ibc::core::host::types::identifiers::{ChannelId, PortId}; use namada_core::ibc::primitives::{IntoTimestamp, Timestamp as IbcTimestamp}; use namada_core::key::{self, *}; -use namada_core::masp::{AssetData, MaspEpoch, TransferSource, TransferTarget}; +use namada_core::masp::{ + AssetData, FlagCiphertext, MaspEpoch, MaspTxData, TransferSource, + TransferTarget, +}; use namada_core::storage; use namada_core::time::DateTimeUtc; use namada_events::extend::EventAttributeEntry; @@ -2819,14 +2822,25 @@ pub async fn build_ibc_transfer( .map(|(shielded_transfer, asset_types)| { let masp_tx_hash = tx.add_masp_tx_section(shielded_transfer.masp_tx.clone()).1; - transfer.shielded_section_hash = Some(masp_tx_hash); + + let (fmd_section, flag_ciphertext_sechash) = + create_fmd_section(shielded_transfer.fmd_flags); + tx.add_section(fmd_section); + + transfer.shielded_data = Some(MaspTxData { + masp_tx_id: masp_tx_hash, + flag_ciphertext_sechash, + }); + signing_data.shielded_hash = Some(masp_tx_hash); + tx.add_masp_builder(MaspBuilder { asset_types, metadata: shielded_transfer.metadata, builder: shielded_transfer.builder, target: masp_tx_hash, }); + Result::Ok(transfer) }) .transpose()?; @@ -3249,9 +3263,16 @@ pub async fn build_shielded_transfer( masp_tx, metadata, epoch: _, + fmd_flags, }, asset_types, ) = shielded_parts; + + // Create FMD flags section + let (fmd_section, flag_ciphertext_sechash) = + create_fmd_section(fmd_flags); + tx.add_section(fmd_section); + // Add a MASP Transaction section to the Tx and get the tx hash let section_hash = tx.add_masp_tx_section(masp_tx).1; @@ -3265,7 +3286,10 @@ pub async fn build_shielded_transfer( target: section_hash, }); - data.shielded_section_hash = Some(section_hash); + data.shielded_data = Some(MaspTxData { + masp_tx_id: section_hash, + flag_ciphertext_sechash, + }); signing_data.shielded_hash = Some(section_hash); tracing::debug!("Transfer data {data:?}"); Ok(()) @@ -3385,7 +3409,7 @@ pub async fn build_shielding_transfer( transfer_data.push(MaspTransferData { source: TransferSource::Address(source.to_owned()), - target: TransferTarget::PaymentAddress(args.target), + target: TransferTarget::PaymentAddress(args.target.clone()), token: token.to_owned(), amount: validated_amount, }); @@ -3419,12 +3443,18 @@ pub async fn build_shielding_transfer( masp_tx, metadata, epoch: _, + fmd_flags, }, asset_types, ) = shielded_parts; // Add a MASP Transaction section to the Tx and get the tx hash let shielded_section_hash = tx.add_masp_tx_section(masp_tx).1; + // Create FMD flags section + let (fmd_section, flag_ciphertext_sechash) = + create_fmd_section(fmd_flags); + tx.add_section(fmd_section); + tx.add_masp_builder(MaspBuilder { asset_types, // Store how the Info objects map to Descriptors/Outputs @@ -3435,7 +3465,10 @@ pub async fn build_shielding_transfer( target: shielded_section_hash, }); - data.shielded_section_hash = Some(shielded_section_hash); + data.shielded_data = Some(MaspTxData { + masp_tx_id: shielded_section_hash, + flag_ciphertext_sechash, + }); signing_data.shielded_hash = Some(shielded_section_hash); tracing::debug!("Transfer data {data:?}"); Ok(()) @@ -3542,12 +3575,18 @@ pub async fn build_unshielding_transfer( masp_tx, metadata, epoch: _, + fmd_flags, }, asset_types, ) = shielded_parts; // Add a MASP Transaction section to the Tx and get the tx hash let shielded_section_hash = tx.add_masp_tx_section(masp_tx).1; + // Create FMD flags section + let (fmd_section, flag_ciphertext_sechash) = + create_fmd_section(fmd_flags); + tx.add_section(fmd_section); + tx.add_masp_builder(MaspBuilder { asset_types, // Store how the Info objects map to Descriptors/Outputs @@ -3558,7 +3597,10 @@ pub async fn build_unshielding_transfer( target: shielded_section_hash, }); - data.shielded_section_hash = Some(shielded_section_hash); + data.shielded_data = Some(MaspTxData { + masp_tx_id: shielded_section_hash, + flag_ciphertext_sechash, + }); signing_data.shielded_hash = Some(shielded_section_hash); tracing::debug!("Transfer data {data:?}"); Ok(()) @@ -3939,7 +3981,7 @@ pub async fn build_custom( pub async fn gen_ibc_shielding_transfer( context: &N, args: args::GenIbcShieldingTransfer, -) -> Result> { +) -> Result)>> { let source = IBC; let token = match args.asset { @@ -4001,7 +4043,7 @@ pub async fn gen_ibc_shielding_transfer( .map_err(|err| TxSubmitError::MaspError(err.to_string()))? }; - Ok(shielded_transfer.map(|st| st.masp_tx)) + Ok(shielded_transfer.map(|st| (st.masp_tx, st.fmd_flags))) } pub(crate) async fn get_ibc_src_port_channel( @@ -4162,8 +4204,8 @@ async fn get_refund_target( match (source, refund_target) { (_, Some(TransferTarget::PaymentAddress(pa))) => { Err(Error::Other(format!( - "Supporting only a transparent address as a refund target: {}", - pa, + "Supporting only a transparent address as a refund target: \ + {pa}" ))) } ( @@ -4307,3 +4349,10 @@ fn proposal_to_vec(proposal: OnChainProposal) -> Result> { borsh::to_vec(&proposal.content) .map_err(|e| Error::from(EncodingError::Conversion(e.to_string()))) } + +fn create_fmd_section(fmd_flags: Vec) -> (Section, Hash) { + let fmd_section = Section::ExtraData(Code::from_borsh_encoded(&fmd_flags)); + let fmd_sechash = fmd_section.get_hash(); + + (fmd_section, fmd_sechash) +} diff --git a/crates/shielded_token/Cargo.toml b/crates/shielded_token/Cargo.toml index 5ac9a0dbc1d..d4eb6b147d7 100644 --- a/crates/shielded_token/Cargo.toml +++ b/crates/shielded_token/Cargo.toml @@ -58,6 +58,7 @@ eyre.workspace = true futures.workspace = true flume = { workspace = true, optional = true } itertools.workspace = true +kassandra-client.workspace = true lazy_static.workspace = true linkme = { workspace = true, optional = true } masp_primitives.workspace = true diff --git a/crates/shielded_token/src/lib.rs b/crates/shielded_token/src/lib.rs index cf9f837961e..45850916508 100644 --- a/crates/shielded_token/src/lib.rs +++ b/crates/shielded_token/src/lib.rs @@ -31,7 +31,9 @@ use std::str::FromStr; use namada_core::borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; pub use namada_core::dec::Dec; -pub use namada_core::masp::{MaspEpoch, MaspTransaction, MaspTxId, MaspValue}; +pub use namada_core::masp::{ + MaspEpoch, MaspTransaction, MaspTxData, MaspTxId, MaspValue, +}; pub use namada_state::{ ConversionLeaf, ConversionState, Error, Key, OptionExt, Result, ResultExt, StorageRead, StorageWrite, WithConversionState, diff --git a/crates/shielded_token/src/masp.rs b/crates/shielded_token/src/masp.rs index b20b0ec5352..9c3601f4821 100644 --- a/crates/shielded_token/src/masp.rs +++ b/crates/shielded_token/src/masp.rs @@ -74,6 +74,11 @@ pub struct ShieldedTransfer { pub metadata: SaplingMetadata, /// Epoch in which the transaction was created pub epoch: MaspEpoch, + /// Vector of FMD flag ciphertexts. + /// + /// There must be a flag ciphertext per shielded output + /// in the `builder`. + pub fmd_flags: Vec, } /// The data for a masp fee payment @@ -191,6 +196,9 @@ pub enum TransferErr { /// Insufficient funds error #[error("Insufficient funds: {0}")] InsufficientFunds(MaspDataLog), + /// Invalid FMD public key + #[error("FMD public key included in the payment address is not valid")] + InvalidFmdPublicKey, /// Generic error #[error("{0}")] General(String), @@ -253,6 +261,16 @@ pub trait ShieldedUtils: /// Load a cache of data as part of shielded sync if that /// process gets interrupted. async fn cache_load(&self) -> std::io::Result; + + /// Save the configuration file for fuzzy message detection + async fn fmd_config_save( + &self, + _config: &mut kassandra_client::config::Config, + ) -> std::io::Result<()>; + + /// Load the fuzzy messaged detection configuration file + async fn fmd_config_load() + -> std::io::Result; } /// Make a ViewingKey that can view notes encrypted by given ExtendedSpendingKey @@ -984,7 +1002,8 @@ pub mod fs { const SPECULATIVE_TMP_FILE_PREFIX: &str = "speculative_shielded.tmp"; const CACHE_FILE_NAME: &str = "shielded_sync.cache"; const CACHE_FILE_TMP_PREFIX: &str = "shielded_sync.cache.tmp"; - + const FMD_CONF_FILE_NAME: &str = "fmd_config.toml"; + const FMD_CONF_FILE_TMP_PREFIX: &str = "fmd_config.toml.tmp"; #[derive(Debug, BorshSerialize, BorshDeserialize, Clone)] /// An implementation of ShieldedUtils for standard filesystems pub struct FsShieldedUtils { @@ -1186,5 +1205,21 @@ pub mod fs { let mut file = File::open(file_name)?; DispatcherCache::try_from_reader(&mut file) } + + async fn fmd_config_save( + &self, + config: &mut kassandra_client::config::Config, + ) -> std::io::Result<()> { + config.save(FMD_CONF_FILE_TMP_PREFIX)?; + std::fs::rename( + FMD_CONF_FILE_TMP_PREFIX, + self.context_dir.join(FMD_CONF_FILE_NAME), + ) + } + + async fn fmd_config_load() + -> std::io::Result { + kassandra_client::config::Config::load_or_new(FMD_CONF_FILE_NAME) + } } } diff --git a/crates/shielded_token/src/masp/shielded_sync/utils.rs b/crates/shielded_token/src/masp/shielded_sync/utils.rs index f88e4c55546..b263bb8bab0 100644 --- a/crates/shielded_token/src/masp/shielded_sync/utils.rs +++ b/crates/shielded_token/src/masp/shielded_sync/utils.rs @@ -12,42 +12,9 @@ use namada_core::chain::BlockHeight; use namada_core::collections::HashMap; use namada_state::TxIndex; use namada_tx::IndexedTx; -use namada_tx::event::MaspEventKind; +pub use namada_tx::event::MaspTxKind; use serde::{Deserialize, Serialize}; -/// The type of a MASP transaction -#[derive( - Debug, - Default, - Clone, - Copy, - BorshSerialize, - BorshDeserialize, - PartialOrd, - PartialEq, - Eq, - Ord, - Serialize, - Deserialize, - Hash, -)] -pub enum MaspTxKind { - /// A MASP transaction used for fee payment - FeePayment, - /// A general MASP transfer - #[default] - Transfer, -} - -impl From for MaspTxKind { - fn from(value: MaspEventKind) -> Self { - match value { - MaspEventKind::FeePayment => Self::FeePayment, - MaspEventKind::Transfer => Self::Transfer, - } - } -} - /// An indexed masp tx carrying information on whether it was a fee paying tx or /// a normal transfer #[derive( diff --git a/crates/shielded_token/src/masp/shielded_wallet.rs b/crates/shielded_token/src/masp/shielded_wallet.rs index 4c7312697dd..987e34bb47c 100644 --- a/crates/shielded_token/src/masp/shielded_wallet.rs +++ b/crates/shielded_token/src/masp/shielded_wallet.rs @@ -31,7 +31,8 @@ use namada_core::chain::BlockHeight; use namada_core::collections::{HashMap, HashSet}; use namada_core::control_flow; use namada_core::masp::{ - AssetData, MaspEpoch, TransferSource, TransferTarget, encode_asset_type, + AssetData, FlagCiphertext, FmdSecretKey, MaspEpoch, TransferSource, + TransferTarget, encode_asset_type, }; use namada_core::task_env::TaskEnvironment; use namada_core::time::{DateTimeUtc, DurationSecs}; @@ -45,7 +46,7 @@ use namada_io::{ }; use namada_wallet::{DatedKeypair, DatedSpendingKey}; use rand::prelude::StdRng; -use rand_core::{OsRng, SeedableRng}; +use rand_core::{CryptoRng, OsRng, RngCore, SeedableRng}; use super::utils::MaspIndexedTx; use crate::masp::utils::MaspClient; @@ -1215,9 +1216,12 @@ pub trait ShieldedApi: return Ok(None); }; + let mut fmd_flags = vec![]; + for (MaspSourceTransferData { source, token }, amount) in &source_data { self.add_inputs( context, + &mut rng, &mut builder, source, token, @@ -1225,6 +1229,7 @@ pub trait ShieldedApi: epoch, &mut denoms, &mut notes_tracker, + &mut fmd_flags, ) .await?; } @@ -1240,6 +1245,7 @@ pub trait ShieldedApi: { self.add_outputs( context, + &mut rng, &mut builder, source, &target, @@ -1247,6 +1253,7 @@ pub trait ShieldedApi: amount, epoch, &mut denoms, + &mut fmd_flags, ) .await?; } @@ -1312,11 +1319,31 @@ pub trait ShieldedApi: ) .map_err(|error| TransferErr::Build { error })?; + // Add remaining flag ciphertexts + fmd_flags.extend({ + let num_shielded_outputs = masp_tx + .sapling_bundle() + .map_or(0, |x| x.shielded_outputs.len()); + + let num_fmd_flags = fmd_flags.len(); + + let dummy_flag_ciphertexts = + checked!(num_shielded_outputs - num_fmd_flags).expect( + "number of shielded outputs in the masp bundle should be \ + greater than or equal to the number of flag ciphertexts \ + generated so far", + ); + + std::iter::repeat_with(|| FlagCiphertext::random(&mut rng)) + .take(dummy_flag_ciphertexts) + }); + Ok(Some(ShieldedTransfer { builder: builder_clone, masp_tx, metadata, epoch, + fmd_flags, })) } @@ -1509,9 +1536,10 @@ pub trait ShieldedApi: /// must be the current epoch. #[allow(async_fn_in_trait)] #[allow(clippy::too_many_arguments)] - async fn add_inputs( + async fn add_inputs( &mut self, context: &impl NamadaIo, + rng: &mut R, builder: &mut Builder, source: &TransferSource, token: &Address, @@ -1519,6 +1547,7 @@ pub trait ShieldedApi: epoch: MaspEpoch, denoms: &mut HashMap, notes_tracker: &mut SpentNotesTracker, + fmd_flags: &mut Vec, ) -> Result, TransferErr> { // We want to fund our transaction solely from supplied spending key let spending_key = source.spending_key(); @@ -1605,6 +1634,12 @@ pub trait ShieldedApi: .map_err(|e| TransferErr::Build { error: builder::Error::SaplingBuild(e), })?; + + fmd_flags.push({ + let fmd_sk: FmdSecretKey = + sk.to_viewing_key().fvk.vk.ivk().into(); + fmd_sk.default_public_key().flag(rng) + }); } // Commit the notes found to our transaction @@ -1669,9 +1704,10 @@ pub trait ShieldedApi: /// Add the necessary transaction outputs to the builder #[allow(clippy::too_many_arguments)] #[allow(async_fn_in_trait)] - async fn add_outputs( + async fn add_outputs( &mut self, context: &impl NamadaIo, + rng: &mut R, builder: &mut Builder, source: Option, target: &TransferTarget, @@ -1679,6 +1715,7 @@ pub trait ShieldedApi: amount: Amount, epoch: MaspEpoch, denoms: &mut HashMap, + fmd_flags: &mut Vec, ) -> Result<(), TransferErr> { // Anotate the asset type in the value balance with its decoding in // order to facilitate cross-epoch computations @@ -1724,7 +1761,7 @@ pub trait ShieldedApi: }); // Make transaction output tied to the current token, // denomination, and epoch. - if let Some(pa) = payment_address { + if let Some(ref pa) = payment_address { // If there is a shielded output builder .add_sapling_output( @@ -1737,6 +1774,10 @@ pub trait ShieldedApi: .map_err(|e| TransferErr::Build { error: builder::Error::SaplingBuild(e), })?; + + fmd_flags.push( + pa.flag(rng).ok_or(TransferErr::InvalidFmdPublicKey)?, + ); } else if let Some(t_addr_data) = target.t_addr_data() { // If there is a transparent output builder diff --git a/crates/shielded_token/src/vp.rs b/crates/shielded_token/src/vp.rs index 1875937c5c6..a79602e3961 100644 --- a/crates/shielded_token/src/vp.rs +++ b/crates/shielded_token/src/vp.rs @@ -17,7 +17,9 @@ use namada_core::address::{self, Address}; use namada_core::arith::{CheckedAdd, CheckedSub, checked}; use namada_core::booleans::BoolResultUnitExt; use namada_core::collections::HashSet; -use namada_core::masp::{MaspEpoch, TAddrData, addr_taddr, encode_asset_type}; +use namada_core::masp::{ + FlagCiphertext, MaspEpoch, TAddrData, addr_taddr, encode_asset_type, +}; use namada_core::storage::Key; use namada_core::token; use namada_core::token::{Amount, MaspDigitPos}; @@ -435,12 +437,13 @@ where .data(batched_tx.cmt) .ok_or_err_msg("No transaction data")?; let actions = ctx.read_actions()?; - // Try to get the Transaction object from the tx first (IBC) and from - // the actions afterwards - let shielded_tx = if let Some(tx) = - Ibc::try_extract_masp_tx_from_envelope::(&tx_data)? + + // Try to get the Transaction object and FMD flag ciphertexts + // from the tx first (IBC) and from the actions afterwards + let (shielded_tx, fmd_flags) = if let Some(shielding_data) = + Ibc::try_extract_shielding_data_from_envelope::(&tx_data)? { - tx + shielding_data } else { let masp_section_ref = namada_tx::action::get_masp_section_ref(&actions) @@ -450,14 +453,33 @@ where "Missing MASP section reference in action", ) })?; + let flag_ciphertexts_ref = + namada_tx::action::get_fmd_flag_ciphertexts_ref(&actions) + .map_err(Error::new_const)? + .ok_or_else(|| { + Error::new_const( + "Missing FMD flag ciphertexts reference in action", + ) + })?; - batched_tx + let masp_tx = batched_tx .tx .get_masp_section(&masp_section_ref) .cloned() .ok_or_else(|| { Error::new_const("Missing MASP section in transaction") - })? + })?; + let fmd_flags = batched_tx + .tx + .get_fmd_flag_ciphertexts(&flag_ciphertexts_ref) + .map_err(Error::new)? + .ok_or_else(|| { + Error::new_const( + "Missing FMD flag ciphertexts in transaction", + ) + })?; + + (masp_tx, fmd_flags) }; if u64::from(ctx.get_block_height()?) @@ -468,6 +490,8 @@ where return Err(error); } + validate_flag_ciphertexts(&shielded_tx, fmd_flags)?; + // Check the validity of the keys and get the transfer data let changed_balances = Self::validate_state_and_get_transfer_data( ctx, @@ -949,6 +973,39 @@ fn verify_sapling_balancing_value( } } +/// Check if the flag ciphertexts included in the tx are valid. +fn validate_flag_ciphertexts( + masp_tx: &Transaction, + fmd_flags: Vec, +) -> Result<()> { + let shielded_outputs_len = masp_tx + .sapling_bundle() + .map_or(0, |bundle| bundle.shielded_outputs.len()); + + if shielded_outputs_len != fmd_flags.len() { + let error = Error::new(format!( + "The number of shielded outputs in the MASP tx ({}) does not \ + match the number of FMD flag ciphertexts ({})", + shielded_outputs_len, + fmd_flags.len() + )); + tracing::debug!("{error}"); + return Err(error); + } + + fmd_flags + .iter() + .all(FlagCiphertext::is_valid) + .ok_or_else(|| { + let error = Error::new_const( + "Not all FMD flag ciphertexts in the MASP tx were considered \ + valid, either because of invalid gamma or tampered bits", + ); + tracing::debug!("{error}"); + error + }) +} + #[cfg(test)] mod shielded_token_tests { use std::cell::RefCell; diff --git a/crates/systems/src/ibc.rs b/crates/systems/src/ibc.rs index b492513b9e4..0fb0e5d31fd 100644 --- a/crates/systems/src/ibc.rs +++ b/crates/systems/src/ibc.rs @@ -6,16 +6,21 @@ use masp_primitives::transaction::TransparentAddress; use masp_primitives::transaction::components::ValueSum; use namada_core::address::Address; use namada_core::borsh::BorshDeserialize; -use namada_core::masp::TAddrData; +use namada_core::masp::{FlagCiphertext, TAddrData}; use namada_core::{masp_primitives, storage, token}; pub use namada_storage::Result; /// Abstract IBC storage read interface pub trait Read { - /// Extract MASP transaction from IBC envelope - fn try_extract_masp_tx_from_envelope( + /// Extract shielding data from IBC envelope + fn try_extract_shielding_data_from_envelope( tx_data: &[u8], - ) -> Result>; + ) -> Result< + Option<( + masp_primitives::transaction::Transaction, + Vec, + )>, + >; /// Apply relevant IBC packets to the changed balances structure fn apply_ibc_packet( diff --git a/crates/tests/src/integration/masp.rs b/crates/tests/src/integration/masp.rs index a252a605402..e5f1b8b8cb3 100644 --- a/crates/tests/src/integration/masp.rs +++ b/crates/tests/src/integration/masp.rs @@ -748,7 +748,7 @@ fn values_spanning_multiple_masp_digits() -> Result<()> { "--node", RPC, "--gas-limit", - "65000", + "75000", ]), ) }); @@ -867,7 +867,7 @@ fn values_spanning_multiple_masp_digits() -> Result<()> { "--gas-spending-key", C_SPENDING_KEY, "--gas-limit", - "65000", + "75000", ]), ) }); diff --git a/crates/token/src/lib.rs b/crates/token/src/lib.rs index 361a1424ff6..70dc5b78a63 100644 --- a/crates/token/src/lib.rs +++ b/crates/token/src/lib.rs @@ -182,8 +182,8 @@ pub struct Transfer { pub sources: BTreeMap, /// Targets of this transfer pub targets: BTreeMap, - /// Hash of tx section that contains the MASP transaction - pub shielded_section_hash: Option, + /// Pointers to MASP data within a transfer tx + pub shielded_data: Option, } /// References to the transparent sections of a [`Transfer`]. @@ -197,13 +197,19 @@ pub struct TransparentTransfersRef<'a> { impl Transfer { /// Create a MASP transaction - pub fn masp(hash: MaspTxId) -> Self { + pub fn masp(shielded_data: MaspTxData) -> Self { Self { - shielded_section_hash: Some(hash), + shielded_data: Some(shielded_data), ..Self::default() } } + /// Return the (optional) MASP tx id associated with this [`Transfer`]. + pub fn masp_tx_id(&self) -> Option { + self.shielded_data + .map(|shielded_data| shielded_data.masp_tx_id) + } + /// Set the key to the given amount fn set( map: &mut BTreeMap, @@ -351,7 +357,9 @@ pub mod testing { use namada_core::address::testing::arb_non_internal_address; use namada_core::address::{Address, MASP}; use namada_core::collections::HashMap; - use namada_core::masp::{AssetData, TAddrData, encode_asset_type}; + use namada_core::masp::{ + AssetData, FlagCiphertext, MaspTxData, TAddrData, encode_asset_type, + }; pub use namada_core::token::*; use namada_shielded_token::masp::testing::{ MockTxProver, TestCsprng, arb_masp_epoch, arb_output_descriptions, @@ -522,12 +530,27 @@ pub mod testing { &mut rng, &mut rng_build_params, ).unwrap(); - transfer.shielded_section_hash = Some(masp_tx.txid().into()); + let fmd_flags = std::iter::repeat_with( + || FlagCiphertext::random(&mut rng) + ) + .take(builder.sapling_outputs().len()) + .collect(); + let fmd_sechash = { + let sec = namada_tx::Section::ExtraData( + namada_tx::Code::from_borsh_encoded(&fmd_flags), + ); + sec.get_hash() + }; + transfer.shielded_data = Some(MaspTxData { + masp_tx_id: masp_tx.txid().into(), + flag_ciphertext_sechash: fmd_sechash, + }); (transfer, ShieldedTransfer { builder: builder.map_builder(WalletMap), metadata, masp_tx, epoch, + fmd_flags, }, asset_types, rng_build_params.to_stored().unwrap()) } } diff --git a/crates/token/src/tx.rs b/crates/token/src/tx.rs index d60eeb1a247..74146dc072f 100644 --- a/crates/token/src/tx.rs +++ b/crates/token/src/tx.rs @@ -5,6 +5,7 @@ use std::collections::{BTreeMap, BTreeSet}; use namada_core::arith::CheckedSub; use namada_core::collections::HashSet; +use namada_core::hash::Hash; use namada_core::masp::encode_asset_type; use namada_core::masp_primitives::transaction::Transaction; use namada_core::token::MaspDigitPos; @@ -42,10 +43,11 @@ where }; // Apply the shielded transfer if there is a link to one - if let Some(masp_section_ref) = transfers.shielded_section_hash { + if let Some(shielded_data) = transfers.shielded_data { apply_shielded_transfer( env, - masp_section_ref, + shielded_data.masp_tx_id, + shielded_data.flag_ciphertext_sechash, debited_accounts, tokens, tx_data, @@ -157,6 +159,7 @@ where pub fn apply_shielded_transfer( env: &mut ENV, masp_section_ref: MaspTxId, + fmd_flags_section_ref: Hash, debited_accounts: HashSet
, tokens: HashSet
, tx_data: &BatchedTx, @@ -180,6 +183,9 @@ where env.push_action(Action::Masp(MaspAction::MaspSectionRef( masp_section_ref, )))?; + env.push_action(Action::Masp(MaspAction::FmdSectionRef( + fmd_flags_section_ref, + )))?; update_undated_balances(env, &shielded, tokens)?; // Extract the debited accounts for the masp part of the transfer and // push the relative actions diff --git a/crates/tx/src/action.rs b/crates/tx/src/action.rs index 4b477a2d2ce..c0f54a72a9d 100644 --- a/crates/tx/src/action.rs +++ b/crates/tx/src/action.rs @@ -10,6 +10,7 @@ use std::fmt; use namada_core::address::Address; use namada_core::borsh::{BorshDeserialize, BorshSerialize}; +use namada_core::hash::Hash; use namada_core::masp::MaspTxId; use namada_core::storage::KeySeg; use namada_core::{address, storage}; @@ -71,6 +72,11 @@ pub enum PgfAction { pub enum MaspAction { /// The hash of the masp [`crate::Section`] MaspSectionRef(MaspTxId), + /// The hash of the fmd [`crate::Section`] + /// + /// The data section encodes a vector of flag ciphertexts, + /// one per shielded output + FmdSectionRef(Hash), /// A required authorizer for the transaction MaspAuthorizer(Address), } @@ -148,6 +154,31 @@ pub fn get_masp_section_ref( } } +/// Helper function to get the optional fmd section reference from the +/// [`Actions`]. If more than one [`MaspAction`] is found we return an error +pub fn get_fmd_flag_ciphertexts_ref( + actions: &Actions, +) -> Result, &'static str> { + let flag_ciphertext_refs: Vec<_> = actions + .iter() + .filter_map(|action| { + if let Action::Masp(MaspAction::FmdSectionRef(fmd_section_ref)) = + action + { + Some(*fmd_section_ref) + } else { + None + } + }) + .collect(); + + if flag_ciphertext_refs.len() > 1 { + Err("The transaction pushed multiple FMD flag ciphertext sections") + } else { + Ok(flag_ciphertext_refs.first().cloned()) + } +} + /// Helper function to check if the action is IBC shielding transfer pub fn is_ibc_shielding_transfer( reader: &T, diff --git a/crates/tx/src/event.rs b/crates/tx/src/event.rs index 4240076b4d3..7f756321871 100644 --- a/crates/tx/src/event.rs +++ b/crates/tx/src/event.rs @@ -4,6 +4,7 @@ use std::fmt::Display; use std::str::FromStr; use namada_core::borsh::{BorshDeserialize, BorshSerialize}; +use namada_core::hash::Hash; use namada_core::ibc::IbcTxDataHash; use namada_core::masp::MaspTxId; use namada_events::extend::{ @@ -112,9 +113,13 @@ pub mod masp_types { /// General MASP transfer event pub const TRANSFER: EventType = namada_events::event_type!(MaspEvent, "transfer"); + + /// FMD flag ciphertexts event + pub const FLAG_CIPHERTEXTS: EventType = + namada_events::event_type!(MaspEvent, "flag"); } -/// MASP event kind +/// The type of a MASP transaction #[derive( Debug, Default, @@ -130,7 +135,7 @@ pub mod masp_types { Deserialize, Hash, )] -pub enum MaspEventKind { +pub enum MaspTxKind { /// A MASP transaction used for fee payment FeePayment, /// A general MASP transfer @@ -138,24 +143,43 @@ pub enum MaspEventKind { Transfer, } -impl From<&MaspEventKind> for EventType { - fn from(masp_event_kind: &MaspEventKind) -> Self { - match masp_event_kind { - MaspEventKind::FeePayment => masp_types::FEE_PAYMENT, - MaspEventKind::Transfer => masp_types::TRANSFER, +impl From for EventType { + fn from(kind: MaspTxKind) -> Self { + match kind { + MaspTxKind::FeePayment => masp_types::FEE_PAYMENT, + MaspTxKind::Transfer => masp_types::TRANSFER, } } } -impl From for EventType { - fn from(masp_event_kind: MaspEventKind) -> Self { - (&masp_event_kind).into() +/// Represents a reference to an FMD flag ciphertext. +/// +/// Store either in an IBC packet memo, or a Namada tx section. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum FmdSectionRef { + /// Reference to a flag ciphertext tx section. + FmdSection(Hash), + /// Reference to an IBC tx data section. + IbcData(IbcTxDataHash), +} + +impl Display for FmdSectionRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", serde_json::to_string(self).unwrap()) + } +} + +impl FromStr for FmdSectionRef { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) } } /// A type representing the possible reference to some MASP data, either a masp /// section or ibc tx data -#[derive(Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum MaspTxRef { /// Reference to a MASP section MaspSection(MaspTxId), @@ -177,30 +201,78 @@ impl FromStr for MaspTxRef { } } -/// A list of MASP tx references -#[derive(Default, Clone, Serialize, Deserialize)] -pub struct MaspTxRefs(pub Vec<(IndexedTx, MaspTxRef)>); +/// MASP transaction event +#[derive(Debug, Clone)] +pub enum MaspEvent { + /// Emit emitted upon generating a new shielded output + ShieldedOutput { + /// The indexed transaction that generated this event + tx_index: IndexedTx, + /// A flag signaling the type of the MASP transaction + kind: MaspTxKind, + /// The reference to the masp data + data: MaspTxRef, + }, + /// Emit emitted after flagging a new shielded output + /// + /// Generally follows the creation of [`Self::ShieldedOutput`] + FlagCiphertexts { + /// The indexed transaction that generated this event + tx_index: IndexedTx, + /// The tx section hash of the FMD flag ciphertext + section: FmdSectionRef, + }, +} /// MASP transaction event -pub struct MaspEvent { +#[derive(Debug, Clone)] +pub struct MaspTxEvent { /// The indexed transaction that generated this event pub tx_index: IndexedTx, - /// A flag signaling the type of the masp transaction - pub kind: MaspEventKind, + /// A flag signaling the type of the MASP transaction + pub kind: MaspTxKind, /// The reference to the masp data pub data: MaspTxRef, } +impl From for MaspEvent { + fn from( + MaspTxEvent { + tx_index, + kind, + data, + }: MaspTxEvent, + ) -> Self { + Self::ShieldedOutput { + tx_index, + kind, + data, + } + } +} + impl EventToEmit for MaspEvent { const DOMAIN: &'static str = "masp"; } impl From for Event { fn from(masp_event: MaspEvent) -> Self { - Self::new(masp_event.kind.into(), EventLevel::Tx) - .with(masp_event.data) - .with(masp_event.tx_index) - .into() + match masp_event { + MaspEvent::ShieldedOutput { + tx_index, + kind, + data, + } => Self::new(kind.into(), EventLevel::Tx) + .with(data) + .with(tx_index) + .into(), + MaspEvent::FlagCiphertexts { tx_index, section } => { + Self::new(masp_types::FLAG_CIPHERTEXTS, EventLevel::Tx) + .with(section) + .with(tx_index) + .into() + } + } } } @@ -225,3 +297,14 @@ impl EventAttributeEntry<'static> for IndexedTx { self } } + +impl EventAttributeEntry<'static> for FmdSectionRef { + type Value = Self; + type ValueOwned = Self; + + const KEY: &'static str = "ciphertext"; + + fn into_value(self) -> Self::Value { + self + } +} diff --git a/crates/tx/src/section.rs b/crates/tx/src/section.rs index d288de3e058..a6649b17fcd 100644 --- a/crates/tx/src/section.rs +++ b/crates/tx/src/section.rs @@ -275,6 +275,12 @@ impl Data { } } + /// Make a new data section with the given borsh encodable data + #[inline] + pub fn from_borsh_encoded(data: &T) -> Self { + Self::new(data.serialize_to_vec()) + } + /// Hash this data section pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { hasher.update(self.serialize_to_vec()); @@ -370,6 +376,21 @@ impl Code { } } + /// Return the code data, if it is present verbatim. + pub fn id(&self) -> Option<&[u8]> { + if let Commitment::Id(code) = &self.code { + Some(&code[..]) + } else { + None + } + } + + /// Make a new code section with the given borsh encodable data + #[inline] + pub fn from_borsh_encoded(data: &T) -> Self { + Self::new(data.serialize_to_vec(), None) + } + /// Make a new code section with the given hash pub fn from_hash( hash: namada_core::hash::Hash, diff --git a/crates/tx/src/types.rs b/crates/tx/src/types.rs index e2ab2aa2be7..1fff8c148ae 100644 --- a/crates/tx/src/types.rs +++ b/crates/tx/src/types.rs @@ -6,6 +6,7 @@ use std::io; use std::ops::{Bound, RangeBounds}; use std::str::FromStr; +use data_encoding::HEXUPPER; use masp_primitives::transaction::Transaction; use namada_account::AccountPublicKeysMap; use namada_core::address::Address; @@ -15,7 +16,7 @@ use namada_core::borsh::{ use namada_core::chain::{BlockHeight, ChainId}; use namada_core::collections::{HashMap, HashSet}; use namada_core::key::*; -use namada_core::masp::MaspTxId; +use namada_core::masp::{FlagCiphertext, MaspTxId}; use namada_core::storage::TxIndex; use namada_core::time::DateTimeUtc; use namada_macros::BorshDeserializer; @@ -46,6 +47,8 @@ pub enum DecodeError { InvalidTimestamp(prost_types::TimestampError), #[error("Couldn't serialize transaction from JSON at {0}")] InvalidJSONDeserialization(String), + #[error("Could not decode FMD flag ciphertexts from tx section {0}")] + InvalidFlagCiphertexts(String), } #[allow(missing_docs)] @@ -287,6 +290,48 @@ impl Tx { None } + /// Add an FMD flag ciphertext section to the transaction + pub fn add_fmd_flag_ciphertexts( + &mut self, + flag_ciphertexts: &[FlagCiphertext], + ) -> &mut Self { + self.add_section(Section::ExtraData(Code::from_borsh_encoded( + flag_ciphertexts, + ))); + self + } + + /// Get the FMD flag ciphertext with the given hash + pub fn get_fmd_flag_ciphertexts( + &self, + hash: &namada_core::hash::Hash, + ) -> Result>, DecodeError> { + let maybe_section = self.get_section(hash); + + let code = match maybe_section.as_ref().map(Cow::as_ref) { + Some(Section::ExtraData(code)) => code, + Some(_) => { + return Err(DecodeError::InvalidFlagCiphertexts( + HEXUPPER.encode(&hash.0), + )); + } + None => return Ok(None), + }; + + let Some(data) = code.id() else { + return Err(DecodeError::InvalidFlagCiphertexts( + HEXUPPER.encode(&hash.0), + )); + }; + + let decoded = + BorshDeserialize::try_from_slice(data).map_err(|_err| { + DecodeError::InvalidFlagCiphertexts(HEXUPPER.encode(&hash.0)) + })?; + + Ok(Some(decoded)) + } + /// Remove the transaction section with the given hash pub fn remove_masp_section(&mut self, hash: &MaspTxId) { self.sections.retain(|section| { diff --git a/crates/wallet/src/lib.rs b/crates/wallet/src/lib.rs index d10f1d76075..6e4106bed52 100644 --- a/crates/wallet/src/lib.rs +++ b/crates/wallet/src/lib.rs @@ -20,7 +20,8 @@ use namada_core::chain::BlockHeight; use namada_core::collections::{HashMap, HashSet}; use namada_core::key::*; use namada_core::masp::{ - DiversifierIndex, ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, + DiversifierIndex, ExtendedSpendingKey, ExtendedViewingKey, + UnifiedPaymentAddress, }; use namada_core::time::DateTimeUtc; use namada_ibc::trace::is_ibc_denom; @@ -442,6 +443,17 @@ impl Wallet { }) } + /// Find the hash of an FMD secret key from the alias of the viewing + /// key it was derived from. + pub fn find_fmd_key_hash( + &self, + alias: impl AsRef, + ) -> Result<&FmdKeyHash, FindKeyError> { + self.store.find_fmd_key_hash(alias.as_ref()).ok_or_else(|| { + FindKeyError::KeyNotFound(alias.as_ref().to_string()) + }) + } + /// Find the birthday of the given alias pub fn find_birthday( &self, @@ -463,14 +475,14 @@ impl Wallet { pub fn find_payment_addr( &self, alias: impl AsRef, - ) -> Option<&PaymentAddress> { + ) -> Option<&UnifiedPaymentAddress> { self.store.find_payment_addr(alias.as_ref()) } /// Find an alias by the payment address if it's in the wallet. pub fn find_alias_by_payment_addr( &self, - payment_address: &PaymentAddress, + payment_address: &UnifiedPaymentAddress, ) -> Option<&Alias> { self.store.find_alias_by_payment_addr(payment_address) } @@ -508,11 +520,11 @@ impl Wallet { } /// Get all known payment addresses by their alias - pub fn get_payment_addrs(&self) -> HashMap { + pub fn get_payment_addrs(&self) -> HashMap { self.store .get_payment_addrs() .iter() - .map(|(alias, value)| (alias.into(), *value)) + .map(|(alias, value)| (alias.into(), value.clone())) .collect() } @@ -1231,7 +1243,7 @@ impl Wallet { pub fn insert_payment_addr( &mut self, alias: String, - payment_addr: PaymentAddress, + payment_addr: UnifiedPaymentAddress, force_alias: bool, ) -> Option { self.store diff --git a/crates/wallet/src/store.rs b/crates/wallet/src/store.rs index a27bbe446cc..bcc1c04390f 100644 --- a/crates/wallet/src/store.rs +++ b/crates/wallet/src/store.rs @@ -15,6 +15,7 @@ use namada_core::collections::HashSet; use namada_core::key::*; use namada_core::masp::{ DiversifierIndex, ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, + UnifiedPaymentAddress, }; use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; @@ -69,7 +70,7 @@ pub struct Store { /// Known spending keys spend_keys: BTreeMap>, /// Payment address book - payment_addrs: BiBTreeMap, + payment_addrs: BiBTreeMap, /// Diverisifier index of the next payment address to be generated for a /// given key. diversifier_indices: BTreeMap, @@ -84,6 +85,9 @@ pub struct Store { /// Known mappings of public key hashes to their aliases in the `keys` /// field. Used for look-up by a public key. pkhs: BTreeMap, + /// Known mappings of FMD secret key hashes to their aliases in the + /// `viewing_keys` field. + fmdhs: BTreeMap, /// Special keys if the wallet belongs to a validator pub(crate) validator_data: Option, /// Namada address vp type @@ -151,6 +155,15 @@ impl Store { self.view_keys.get(&alias.into()) } + /// Find the hash of an FMD secret key from the alias of the viewing + /// key it was derived from. + pub fn find_fmd_key_hash( + &self, + alias: impl AsRef, + ) -> Option<&FmdKeyHash> { + self.fmdhs.get(&alias.into()) + } + /// Find the birthday of the given alias pub fn find_birthday( &self, @@ -171,14 +184,14 @@ impl Store { pub fn find_payment_addr( &self, alias: impl AsRef, - ) -> Option<&PaymentAddress> { + ) -> Option<&UnifiedPaymentAddress> { self.payment_addrs.get_by_left(&alias.into()) } /// Find an alias by the address if it's in the wallet. pub fn find_alias_by_payment_addr( &self, - payment_address: &PaymentAddress, + payment_address: &UnifiedPaymentAddress, ) -> Option<&Alias> { self.payment_addrs.get_by_right(payment_address) } @@ -282,7 +295,9 @@ impl Store { } /// Get all known payment addresses by their alias. - pub fn get_payment_addrs(&self) -> &BiBTreeMap { + pub fn get_payment_addrs( + &self, + ) -> &BiBTreeMap { &self.payment_addrs } @@ -445,6 +460,9 @@ impl Store { } /// Insert viewing keys similarly to how it's done for keypairs + /// + /// Hashes of the FMD secret keys associated with a viewing key will + /// also be added to a map associating it with the alias provided. pub fn insert_viewing_key( &mut self, alias: Alias, @@ -476,6 +494,9 @@ impl Store { } self.remove_alias(&alias); birthday.map(|x| self.birthdays.insert(alias.clone(), x)); + let fmd_key = + namada_core::masp::FmdSecretKey::from(&viewkey).fmd_secret_key(); + self.fmdhs.insert(alias.clone(), fmd_key.into()); self.view_keys.insert(alias.clone(), viewkey); path.map(|p| self.derivation_paths.insert(alias.clone(), p)); Some(alias) @@ -537,7 +558,7 @@ impl Store { pub fn insert_payment_addr( &mut self, alias: Alias, - payment_addr: PaymentAddress, + payment_addr: UnifiedPaymentAddress, force: bool, ) -> Option { // abort if the alias is reserved @@ -670,6 +691,7 @@ impl Store { derivation_paths, addresses, pkhs, + fmdhs, validator_data: _, address_vp_types, } = self; @@ -683,6 +705,7 @@ impl Store { derivation_paths.extend(store.derivation_paths); addresses.extend(store.addresses); pkhs.extend(store.pkhs); + fmdhs.extend(store.fmdhs); address_vp_types.extend(store.address_vp_types); } @@ -910,12 +933,26 @@ pub struct StoreV0 { impl From for Store { fn from(store: StoreV0) -> Self { let mut to = Store { - payment_addrs: store.payment_addrs, + payment_addrs: store + .payment_addrs + .into_iter() + .map(|(alias, pa)| (alias, UnifiedPaymentAddress::V0(pa))) + .collect(), secret_keys: store.secret_keys, public_keys: store.public_keys, derivation_paths: store.derivation_paths, addresses: store.addresses, pkhs: store.pkhs, + fmdhs: store + .view_keys + .iter() + .map(|(alias, vk)| { + let fmd_key = + namada_core::masp::FmdSecretKey::from(&vk.key) + .fmd_secret_key(); + (alias.clone(), fmd_key.into()) + }) + .collect(), validator_data: store.validator_data, address_vp_types: store.address_vp_types, ..Store::default() @@ -962,7 +999,7 @@ pub struct StoreV1 { /// Known spending keys spend_keys: BTreeMap>, /// Payment address book - payment_addrs: BiBTreeMap, + payment_addrs: BiBTreeMap, /// Cryptographic keypairs secret_keys: BTreeMap>, /// Known public keys diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 9d082bffa66..ba29952de8d 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -101,12 +101,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.95" @@ -486,6 +530,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bip0039" version = "0.12.0" @@ -890,21 +943,36 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.29" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" +checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.5.29" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" +checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.98", ] [[package]] @@ -925,6 +993,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.12", +] + [[package]] name = "coins-bip32" version = "0.8.7" @@ -977,6 +1054,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "concat-idents" version = "1.1.5" @@ -1272,6 +1355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -1290,6 +1374,34 @@ dependencies = [ "cipher", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rand_core", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1560,7 +1672,7 @@ dependencies = [ "chrono", "rust_decimal", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "winnow 0.6.26", ] @@ -2319,7 +2431,7 @@ dependencies = [ "rand_core", "serde", "serdect", - "thiserror 2.0.11", + "thiserror 2.0.12", "thiserror-nostd-notrait", "visibility", "zeroize", @@ -2615,6 +2727,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "hash32" version = "0.2.1" @@ -2689,6 +2807,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3840,6 +3967,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.11.0" @@ -3935,6 +4068,53 @@ dependencies = [ "signature", ] +[[package]] +name = "kassandra-client" +version = "0.0.11-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e59f26661380a4758cc7c7a0b8887218677ee76be33a279205fb9cee9a4a492" +dependencies = [ + "chacha20poly1305", + "clap", + "curve25519-dalek", + "hex", + "hkdf", + "kassandra-shared", + "polyfuzzy", + "rand_core", + "serde", + "serde_cbor", + "serde_json", + "sha2 0.10.8", + "thiserror 2.0.12", + "toml", + "tracing", + "tracing-log", + "tracing-subscriber", + "x25519-dalek", +] + +[[package]] +name = "kassandra-shared" +version = "0.0.3-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d0b36e1bea0a4147d47b33e8770253f6101d6efd3f0a89f522b999947ce834" +dependencies = [ + "borsh", + "chacha20poly1305", + "cobs 0.3.0", + "hex", + "once_cell", + "polyfuzzy", + "rand_core", + "serde", + "serde_cbor", + "sha2 0.10.8", + "tdx-quote", + "thiserror 2.0.12", + "x25519-dalek", +] + [[package]] name = "kdam" version = "0.6.2" @@ -4340,7 +4520,7 @@ dependencies = [ "pasta_curves", "rand_core", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] @@ -4401,7 +4581,7 @@ version = "0.149.1" dependencies = [ "namada_core", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4409,6 +4589,7 @@ name = "namada_core" version = "0.149.1" dependencies = [ "bech32 0.11.0", + "bincode", "borsh", "chrono", "data-encoding", @@ -4430,6 +4611,7 @@ dependencies = [ "num-traits", "num256", "num_enum", + "polyfuzzy", "primitive-types 0.13.1", "proptest", "prost-types", @@ -4443,7 +4625,7 @@ dependencies = [ "smooth-operator", "tendermint 0.40.3", "tendermint-proto 0.40.3", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-keccak", "tokio", "tracing", @@ -4478,7 +4660,7 @@ dependencies = [ "namada_vp_env", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4491,7 +4673,7 @@ dependencies = [ "namada_macros", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4504,7 +4686,7 @@ dependencies = [ "namada_events", "namada_macros", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4526,7 +4708,7 @@ dependencies = [ "serde", "serde_json", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4562,7 +4744,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4574,7 +4756,7 @@ dependencies = [ "kdam", "namada_core", "tendermint-rpc", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -4600,7 +4782,7 @@ dependencies = [ "namada_core", "namada_macros", "prost", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4614,7 +4796,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -4637,7 +4819,7 @@ dependencies = [ "proptest", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4669,6 +4851,7 @@ dependencies = [ "getrandom 0.3.1", "init-once", "itertools 0.14.0", + "kassandra-client", "lazy_static", "masp_primitives", "masp_proofs", @@ -4711,7 +4894,7 @@ dependencies = [ "smooth-operator", "tempfile", "tendermint-rpc", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-bip39", "tokio", "toml", @@ -4730,6 +4913,7 @@ dependencies = [ "flume", "futures", "itertools 0.14.0", + "kassandra-client", "lazy_static", "masp_primitives", "masp_proofs", @@ -4755,7 +4939,7 @@ dependencies = [ "sha2 0.10.8", "smooth-operator", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "typed-builder", "xorf", @@ -4780,7 +4964,7 @@ dependencies = [ "patricia_tree", "proptest", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4798,7 +4982,7 @@ dependencies = [ "regex", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4887,7 +5071,7 @@ dependencies = [ "namada_tx", "namada_tx_env", "namada_vp_env", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -4916,7 +5100,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.8", - "thiserror 2.0.11", + "thiserror 2.0.12", "tonic-build", ] @@ -4969,7 +5153,7 @@ dependencies = [ "rayon", "smooth-operator", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "wasm-instrument", "wasmer", @@ -5008,7 +5192,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -5069,7 +5253,7 @@ dependencies = [ "serde", "slip10_ed25519", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-bip39", "toml", "zeroize", @@ -5129,6 +5313,16 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -5239,9 +5433,18 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "2611b99ab098a31bdc8be48b4f1a285ca0ced28bd5b4f23e45efa8c63b09efa5" +dependencies = [ + "once_cell", +] [[package]] name = "opaque-debug" @@ -5337,12 +5540,30 @@ dependencies = [ "zeroize", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owo-colors" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.8", +] + [[package]] name = "pairing" version = "0.23.0" @@ -5661,6 +5882,19 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyfuzzy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ad377e0383a3332e1deb427b97c8670a954efa00add87d2071daab421dae24" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "sha2 0.10.8", + "subtle", +] + [[package]] name = "portable-atomic" version = "1.10.0" @@ -5673,7 +5907,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" dependencies = [ - "cobs", + "cobs 0.2.3", "embedded-io 0.4.0", "embedded-io 0.6.1", "heapless", @@ -5711,6 +5945,15 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -5816,7 +6059,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck", - "itertools 0.11.0", + "itertools 0.14.0", "log", "multimap", "once_cell", @@ -5836,7 +6079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.98", @@ -6572,9 +6815,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -6608,11 +6851,21 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -6632,9 +6885,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -6814,7 +7067,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", ] @@ -7153,6 +7406,17 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "tdx-quote" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecbbeffe2d73f07728bcbb581f8faa9098d813ba0fafbcab5e763a2fa4491e80" +dependencies = [ + "nom", + "p256", + "sha2 0.10.8", +] + [[package]] name = "tempfile" version = "3.16.0" @@ -7384,11 +7648,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -7404,9 +7668,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -7777,6 +8041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", ] [[package]] @@ -7789,6 +8054,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.19" @@ -7796,12 +8072,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", + "nu-ansi-term", "once_cell", "regex", "sharded-slab", + "smallvec", "thread_local", "tracing", "tracing-core", + "tracing-log", ] [[package]] @@ -8224,6 +8503,12 @@ 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 = "uuid" version = "0.8.2" @@ -8240,6 +8525,12 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -8995,6 +9286,18 @@ dependencies = [ "tap", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + [[package]] name = "xorf" version = "0.11.0" diff --git a/wasm/tx_ibc/src/lib.rs b/wasm/tx_ibc/src/lib.rs index 01ca4f9733c..3df5590f415 100644 --- a/wasm/tx_ibc/src/lib.rs +++ b/wasm/tx_ibc/src/lib.rs @@ -12,7 +12,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { .execute::(&data) .into_storage_result()?; - let (masp_section_ref, mut token_addrs) = + let (maybe_masp_refs, mut token_addrs) = if let Some(transfers) = data.transparent { let (_debited_accounts, tokens) = if let Some(transparent) = transfers.transparent_part() { @@ -22,18 +22,18 @@ fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { Default::default() }; - (transfers.shielded_section_hash, tokens) + (transfers.shielded_data, tokens) } else { (None, Default::default()) }; token_addrs.extend(data.ibc_tokens); - let shielded = if let Some(masp_section_ref) = masp_section_ref { + let maybe_masp_tx = if let Some(shielded) = maybe_masp_refs { Some( tx_data .tx - .get_masp_section(&masp_section_ref) + .get_masp_section(&shielded.masp_tx_id) .cloned() .ok_or_err_msg( "Unable to find required shielded section in tx data", @@ -44,20 +44,25 @@ fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { ) } else { data.shielded + .map(|ibc_shielding_data| ibc_shielding_data.masp_tx) }; - if let Some(shielded) = shielded { - token::utils::handle_masp_tx(ctx, &shielded) + + if let Some(masp_tx) = maybe_masp_tx { + token::utils::handle_masp_tx(ctx, &masp_tx) .wrap_err("Encountered error while handling MASP transaction")?; - update_masp_note_commitment_tree(&shielded) + update_masp_note_commitment_tree(&masp_tx) .wrap_err("Failed to update the MASP commitment tree")?; - if let Some(masp_section_ref) = masp_section_ref { + if let Some(masp_refs) = maybe_masp_refs { ctx.push_action(Action::Masp(MaspAction::MaspSectionRef( - masp_section_ref, + masp_refs.masp_tx_id, + )))?; + ctx.push_action(Action::Masp(MaspAction::FmdSectionRef( + masp_refs.flag_ciphertext_sechash, )))?; } else { ctx.push_action(Action::IbcShielding)?; } - token::update_undated_balances(ctx, &shielded, token_addrs)?; + token::update_undated_balances(ctx, &masp_tx, token_addrs)?; } Ok(()) diff --git a/wasm/vp_implicit/src/lib.rs b/wasm/vp_implicit/src/lib.rs index 8ad708c6de6..b9b1e9e8a86 100644 --- a/wasm/vp_implicit/src/lib.rs +++ b/wasm/vp_implicit/src/lib.rs @@ -113,7 +113,9 @@ fn validate_tx( cmt, &addr, )?, - Action::Masp(MaspAction::MaspSectionRef(_)) => (), + Action::Masp( + MaspAction::MaspSectionRef(_) | MaspAction::FmdSectionRef(_), + ) => (), Action::IbcShielding => (), } } diff --git a/wasm/vp_user/src/lib.rs b/wasm/vp_user/src/lib.rs index d53ea783044..6e71b7e65f9 100644 --- a/wasm/vp_user/src/lib.rs +++ b/wasm/vp_user/src/lib.rs @@ -112,7 +112,9 @@ fn validate_tx( cmt, &addr, )?, - Action::Masp(MaspAction::MaspSectionRef(_)) => (), + Action::Masp( + MaspAction::MaspSectionRef(_) | MaspAction::FmdSectionRef(_), + ) => (), Action::IbcShielding => (), } } diff --git a/wasm_for_tests/Cargo.lock b/wasm_for_tests/Cargo.lock index d03c51deaf6..15b523813cd 100644 --- a/wasm_for_tests/Cargo.lock +++ b/wasm_for_tests/Cargo.lock @@ -76,6 +76,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.95" @@ -297,6 +347,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bip0039" version = "0.12.0" @@ -564,6 +623,46 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clap" +version = "4.5.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "clru" version = "0.6.2" @@ -576,6 +675,21 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.12", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "const-crc32-nostd" version = "1.3.1" @@ -718,9 +832,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rand_core", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1103,6 +1246,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -1167,7 +1316,7 @@ dependencies = [ "rand_core", "serde", "serdect", - "thiserror 2.0.11", + "thiserror 2.0.12", "thiserror-nostd-notrait", "visibility", "zeroize", @@ -1327,6 +1476,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "hash32" version = "0.2.1" @@ -1380,6 +1535,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2124,6 +2288,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.13.0" @@ -2186,6 +2356,53 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "kassandra-client" +version = "0.0.11-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e59f26661380a4758cc7c7a0b8887218677ee76be33a279205fb9cee9a4a492" +dependencies = [ + "chacha20poly1305", + "clap", + "curve25519-dalek", + "hex", + "hkdf", + "kassandra-shared", + "polyfuzzy", + "rand_core", + "serde", + "serde_cbor", + "serde_json", + "sha2 0.10.8", + "thiserror 2.0.12", + "toml", + "tracing", + "tracing-log", + "tracing-subscriber", + "x25519-dalek", +] + +[[package]] +name = "kassandra-shared" +version = "0.0.3-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d0b36e1bea0a4147d47b33e8770253f6101d6efd3f0a89f522b999947ce834" +dependencies = [ + "borsh", + "chacha20poly1305", + "cobs 0.3.0", + "hex", + "once_cell", + "polyfuzzy", + "rand_core", + "serde", + "serde_cbor", + "sha2 0.10.8", + "tdx-quote", + "thiserror 2.0.12", + "x25519-dalek", +] + [[package]] name = "keccak" version = "0.1.5" @@ -2431,7 +2648,7 @@ dependencies = [ "pasta_curves", "rand_core", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] @@ -2478,7 +2695,7 @@ version = "0.149.1" dependencies = [ "namada_core", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2486,6 +2703,7 @@ name = "namada_core" version = "0.149.1" dependencies = [ "bech32", + "bincode", "borsh", "chrono", "data-encoding", @@ -2506,6 +2724,7 @@ dependencies = [ "num-traits", "num256", "num_enum", + "polyfuzzy", "primitive-types 0.13.1", "prost-types", "rayon", @@ -2516,7 +2735,7 @@ dependencies = [ "smooth-operator", "tendermint", "tendermint-proto", - "thiserror 2.0.11", + "thiserror 2.0.12", "tiny-keccak", "tracing", "uint 0.10.0", @@ -2533,7 +2752,7 @@ dependencies = [ "namada_macros", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2546,7 +2765,7 @@ dependencies = [ "namada_events", "namada_macros", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2567,7 +2786,7 @@ dependencies = [ "serde", "serde_json", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2601,7 +2820,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2627,7 +2846,7 @@ dependencies = [ "namada_core", "namada_macros", "prost", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2641,7 +2860,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2663,7 +2882,7 @@ dependencies = [ "once_cell", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2683,6 +2902,7 @@ dependencies = [ "eyre", "futures", "itertools 0.14.0", + "kassandra-client", "lazy_static", "masp_primitives", "masp_proofs", @@ -2704,7 +2924,7 @@ dependencies = [ "sha2 0.10.8", "smooth-operator", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "typed-builder", "xorf", @@ -2728,7 +2948,7 @@ dependencies = [ "namada_tx", "patricia_tree", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2746,7 +2966,7 @@ dependencies = [ "regex", "serde", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2799,7 +3019,7 @@ dependencies = [ "namada_tx", "namada_tx_env", "namada_vp_env", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2827,7 +3047,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.8", - "thiserror 2.0.11", + "thiserror 2.0.12", "tonic-build", ] @@ -2879,7 +3099,7 @@ dependencies = [ "namada_tx", "namada_vp_env", "smooth-operator", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", ] @@ -2941,6 +3161,16 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3032,9 +3262,18 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2611b99ab098a31bdc8be48b4f1a285ca0ced28bd5b4f23e45efa8c63b09efa5" +dependencies = [ + "once_cell", +] [[package]] name = "opaque-debug" @@ -3048,6 +3287,24 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.8", +] + [[package]] name = "pairing" version = "0.23.0" @@ -3177,13 +3434,26 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyfuzzy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ad377e0383a3332e1deb427b97c8670a954efa00add87d2071daab421dae24" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "sha2 0.10.8", + "subtle", +] + [[package]] name = "postcard" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" dependencies = [ - "cobs", + "cobs 0.2.3", "embedded-io 0.4.0", "embedded-io 0.6.1", "heapless", @@ -3215,6 +3485,15 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -3275,7 +3554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck", - "itertools 0.13.0", + "itertools 0.14.0", "log", "multimap", "once_cell", @@ -3295,7 +3574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.98", @@ -3653,9 +3932,9 @@ checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -3678,11 +3957,21 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -3702,9 +3991,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -3723,6 +4012,15 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serdect" version = "0.2.0" @@ -3767,6 +4065,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3798,6 +4105,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + [[package]] name = "smooth-operator" version = "0.7.2" @@ -3849,6 +4162,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.27.0" @@ -3933,6 +4252,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tdx-quote" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecbbeffe2d73f07728bcbb581f8faa9098d813ba0fafbcab5e763a2fa4491e80" +dependencies = [ + "nom", + "p256", + "sha2 0.10.8", +] + [[package]] name = "tempfile" version = "3.16.0" @@ -4020,11 +4350,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -4040,9 +4370,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -4069,6 +4399,16 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.37" @@ -4123,23 +4463,47 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + [[package]] name = "tonic-build" version = "0.12.3" @@ -4183,6 +4547,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -4425,12 +4815,24 @@ dependencies = [ "serde", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -4595,6 +4997,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" @@ -4745,9 +5169,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.2" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -4770,6 +5194,18 @@ dependencies = [ "tap", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + [[package]] name = "xorf" version = "0.11.0"