From a10bb64c7f53fea8d6b51e11d9f3ebff749302c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex?= Date: Thu, 2 Apr 2026 06:55:21 +0100 Subject: [PATCH] feat(ledger): Channel Withdraw (#2440) Co-authored-by: Youngjoon Lee <5462944+youngjoon-lee@users.noreply.github.com> --- Cargo.lock | 862 +++++++++--------- c-bindings/src/api/wallet.rs | 6 +- core/src/mantle/encoding.rs | 167 +++- core/src/mantle/gas.rs | 32 +- core/src/mantle/genesis_tx.rs | 16 +- core/src/mantle/mod.rs | 41 +- core/src/mantle/ops/channel/mod.rs | 12 + core/src/mantle/ops/channel/withdraw.rs | 9 + core/src/mantle/ops/internal.rs | 19 +- core/src/mantle/ops/mod.rs | 15 +- core/src/mantle/ops/opcode.rs | 1 + core/src/mantle/tx.rs | 404 +++++++- core/src/mantle/tx_builder.rs | 39 +- core/src/proofs/channel_withdraw_proof.rs | 122 +++ core/src/proofs/mod.rs | 1 + ledger/src/cryptarchia/mod.rs | 10 +- ledger/src/lib.rs | 309 ++++++- ledger/src/mantle/channel.rs | 162 +++- ledger/src/mantle/helpers.rs | 53 ++ ledger/src/mantle/mod.rs | 16 +- nodes/api-common/src/bodies/wallet.rs | 8 +- nodes/api-common/src/paths.rs | 1 + nodes/node/binary/src/api/backend.rs | 4 +- nodes/node/binary/src/api/handlers.rs | 28 +- services/api/Cargo.toml | 1 + .../api/src/http/consensus/cryptarchia.rs | 25 + services/api/src/http/consensus/mod.rs | 1 + services/api/src/http/mantle.rs | 34 +- services/chain/chain-leader/src/lib.rs | 8 +- services/chain/chain-leader/src/wallet.rs | 6 +- services/sdp/src/lib.rs | 68 +- services/wallet/src/api.rs | 100 +- services/wallet/src/lib.rs | 67 +- .../steps/manual_transactions/utils.rs | 5 +- .../src/workloads/inscription/workload.rs | 1 - .../src/workloads/transaction/workload.rs | 16 +- wallet/src/lib.rs | 124 ++- 37 files changed, 2160 insertions(+), 633 deletions(-) create mode 100644 core/src/mantle/ops/channel/withdraw.rs create mode 100644 core/src/proofs/channel_withdraw_proof.rs create mode 100644 ledger/src/mantle/helpers.rs diff --git a/Cargo.lock b/Cargo.lock index 08a378ad3..2a9a88dfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,9 +71,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -86,15 +86,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -121,9 +121,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "approx" @@ -287,7 +287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -313,7 +313,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -415,7 +415,7 @@ checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -486,7 +486,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -498,7 +498,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -638,7 +638,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -680,7 +680,7 @@ dependencies = [ "cfg-if", "event-listener 5.4.1", "futures-lite", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -691,7 +691,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -706,7 +706,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.3", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -757,7 +757,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -774,7 +774,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1014,9 +1014,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "shlex", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1092,9 +1092,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecount" @@ -1153,16 +1153,16 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.116", + "syn 2.0.117", "tempfile", "toml", ] [[package]] name = "cc" -version = "1.2.56" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "jobserver", @@ -1199,7 +1199,7 @@ checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1240,9 +1240,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -1322,9 +1322,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.59" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -1332,9 +1332,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.59" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -1345,21 +1345,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "color-eyre" @@ -1376,9 +1376,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -1397,13 +1397,12 @@ checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" [[package]] name = "console" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "once_cell", "unicode-width", "windows-sys 0.61.2", ] @@ -1444,14 +1443,14 @@ dependencies = [ "tonic 0.12.3", "tracing", "tracing-core", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] name = "const-hex" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" +checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" dependencies = [ "cfg-if", "cpufeatures", @@ -1651,12 +1650,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.5.1" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", - "nix 0.30.1", + "nix 0.31.2", "windows-sys 0.61.2", ] @@ -1700,7 +1699,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.116", + "syn 2.0.117", "synthez", ] @@ -1743,14 +1742,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -1758,27 +1757,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1804,7 +1802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1842,9 +1840,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -1880,7 +1878,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1902,7 +1900,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.116", + "syn 2.0.117", "unicode-xid", ] @@ -1963,9 +1961,9 @@ dependencies = [ [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.0", "block2", @@ -1981,7 +1979,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2006,7 +2004,7 @@ checksum = "9556bc800956545d6420a640173e5ba7dfa82f38d3ea5a167eb555bc69ac3323" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2075,7 +2073,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2118,7 +2116,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2138,7 +2136,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2158,7 +2156,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2401,7 +2399,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2494,20 +2492,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -2524,7 +2522,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.116", + "syn 2.0.117", "textwrap", "thiserror 2.0.18", "typed-builder", @@ -2886,9 +2884,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -2901,7 +2899,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -2972,7 +2969,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -2990,7 +2987,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -3118,19 +3115,19 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.10.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabb0019d51a643781ff15c9c8a3e5dedc365c47211270f4e8f82812fedd8f0a" +checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] name = "if-watch" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf9d64cfcf380606e64f9a0bcf493616b65331199f984151a6fa11a7b3cde38" +checksum = "71c02a5161c313f0cbdbadc511611893584a10a7b6153cb554bdf83ddce99ec2" dependencies = [ "async-io", "core-foundation 0.9.4", @@ -3139,13 +3136,13 @@ dependencies = [ "if-addrs", "ipnet", "log", - "netlink-packet-core", - "netlink-packet-route", + "netlink-packet-core 0.8.1", + "netlink-packet-route 0.28.0", "netlink-proto", "netlink-sys", "rtnetlink", "smol", - "system-configuration", + "system-configuration 0.7.0", "tokio", "windows", ] @@ -3271,36 +3268,37 @@ dependencies = [ [[package]] name = "inventory" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] [[package]] name = "ipconfig" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "socket2 0.5.10", + "socket2 0.6.3", "widestring", - "windows-sys 0.48.0", - "winreg", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -3352,9 +3350,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jf-crhf" @@ -3402,7 +3400,7 @@ checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3417,10 +3415,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -3622,9 +3622,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libloading" @@ -4005,7 +4005,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4089,16 +4089,15 @@ dependencies = [ "thiserror 2.0.18", "tracing", "yamux 0.12.1", - "yamux 0.13.8", + "yamux 0.13.10", ] [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ - "bitflags 2.11.0", "libc", ] @@ -4117,9 +4116,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" dependencies = [ "cc", "pkg-config", @@ -4152,9 +4151,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -4193,6 +4192,7 @@ dependencies = [ "logos-blockchain-chain-leader-service", "logos-blockchain-chain-service", "logos-blockchain-core", + "logos-blockchain-ledger", "logos-blockchain-network-service", "logos-blockchain-sdp-service", "logos-blockchain-storage-service", @@ -4658,7 +4658,7 @@ dependencies = [ "logos-blockchain-core", "logos-blockchain-demo-sequencer", "owo-colors", - "redb 3.1.0", + "redb 3.1.1", "reqwest", "serde", "serde_json", @@ -4690,7 +4690,7 @@ dependencies = [ "tokio-util", "tower-http 0.6.8", "tracing", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -4775,7 +4775,7 @@ version = "0.2.1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4863,7 +4863,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tracing", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", "zerocopy", ] @@ -4900,7 +4900,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", "utoipa", ] @@ -5132,7 +5132,7 @@ dependencies = [ "time", "tokio", "tracing", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -5175,7 +5175,7 @@ dependencies = [ "tracing-gelf", "tracing-loki", "tracing-opentelemetry", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", "url", ] @@ -5191,7 +5191,7 @@ dependencies = [ "time", "tracing", "tracing-appender", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -5207,7 +5207,7 @@ dependencies = [ "reqwest", "serde_json", "tokio", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -5400,7 +5400,7 @@ checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5495,9 +5495,9 @@ checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -5506,9 +5506,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.13" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -5644,11 +5644,11 @@ dependencies = [ "dlopen2", "ipnet", "libc", - "netlink-packet-core", - "netlink-packet-route", + "netlink-packet-core 0.7.0", + "netlink-packet-route 0.17.1", "netlink-sys", "once_cell", - "system-configuration", + "system-configuration 0.6.1", "windows-sys 0.52.0", ] @@ -5663,6 +5663,15 @@ dependencies = [ "netlink-packet-utils", ] +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + [[package]] name = "netlink-packet-route" version = "0.17.1" @@ -5673,10 +5682,22 @@ dependencies = [ "bitflags 1.3.2", "byteorder", "libc", - "netlink-packet-core", + "netlink-packet-core 0.7.0", "netlink-packet-utils", ] +[[package]] +name = "netlink-packet-route" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +dependencies = [ + "bitflags 2.11.0", + "libc", + "log", + "netlink-packet-core 0.8.1", +] + [[package]] name = "netlink-packet-utils" version = "0.5.2" @@ -5691,14 +5712,14 @@ dependencies = [ [[package]] name = "netlink-proto" -version = "0.11.5" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" dependencies = [ "bytes", "futures", "log", - "netlink-packet-core", + "netlink-packet-core 0.8.1", "netlink-sys", "thiserror 2.0.18", ] @@ -5771,6 +5792,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -5837,9 +5870,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-format" @@ -5882,9 +5915,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -5892,21 +5925,21 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -5937,9 +5970,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -5955,9 +5988,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags 2.11.0", "cfg-if", @@ -5976,7 +6009,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5987,9 +6020,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -6019,7 +6052,7 @@ dependencies = [ "opentelemetry", "tracing", "tracing-core", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -6037,9 +6070,9 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" +checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" dependencies = [ "http 1.4.0", "opentelemetry", @@ -6050,7 +6083,7 @@ dependencies = [ "reqwest", "thiserror 2.0.18", "tokio", - "tonic 0.14.4", + "tonic 0.14.5", ] [[package]] @@ -6062,7 +6095,7 @@ dependencies = [ "opentelemetry", "opentelemetry_sdk", "prost 0.14.3", - "tonic 0.14.4", + "tonic 0.14.5", "tonic-prost", ] @@ -6128,14 +6161,14 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "owo-colors" -version = "4.2.3" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "parking" @@ -6254,7 +6287,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6307,29 +6340,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -6339,9 +6372,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -6402,7 +6435,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -6477,7 +6510,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6491,9 +6524,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -6541,7 +6574,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6573,7 +6606,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6592,9 +6625,9 @@ dependencies = [ [[package]] name = "proptest" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bitflags 2.11.0", "num-traits", @@ -6614,7 +6647,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6647,7 +6680,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6660,7 +6693,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6780,7 +6813,7 @@ checksum = "a9a28b8493dd664c8b171dd944da82d933f7d456b829bfb236738e1fe06c5ba4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6795,9 +6828,9 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", - "socket2 0.6.2", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -6815,7 +6848,7 @@ dependencies = [ "lru-slab", "rand 0.9.2", "ring", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", "rustls-pki-types", "slab", @@ -6834,16 +6867,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -6854,6 +6887,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -6881,7 +6920,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -6998,9 +7037,9 @@ dependencies = [ [[package]] name = "redb" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae323eb086579a3769daa2c753bb96deb95993c534711e0dbe881b5192906a06" +checksum = "ef99362319c782aa4639ad3a306b64c3bb90e12874e99b8df124cb679d988611" dependencies = [ "libc", ] @@ -7042,7 +7081,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7076,9 +7115,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -7144,9 +7183,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" dependencies = [ "bytemuck", ] @@ -7206,19 +7245,19 @@ dependencies = [ [[package]] name = "rtnetlink" -version = "0.13.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0" +checksum = "4b960d5d873a75b5be9761b1e73b146f52dddcd27bac75263f40fba686d4d7b5" dependencies = [ "async-global-executor", - "futures", + "futures-channel", + "futures-util", "log", - "netlink-packet-core", - "netlink-packet-route", - "netlink-packet-utils", + "netlink-packet-core 0.8.1", + "netlink-packet-route 0.28.0", "netlink-proto", "netlink-sys", - "nix 0.26.4", + "nix 0.30.1", "thiserror 1.0.69", "tokio", ] @@ -7243,7 +7282,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.116", + "syn 2.0.117", "walkdir", ] @@ -7271,9 +7310,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -7322,22 +7361,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", @@ -7424,9 +7463,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -7451,7 +7490,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7479,9 +7518,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.0", "core-foundation 0.10.1", @@ -7492,9 +7531,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -7561,7 +7600,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7600,9 +7639,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -7621,9 +7660,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", @@ -7636,14 +7675,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7661,9 +7700,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0b343e184fc3b7bb44dff0705fffcf4b3756ba6aff420dddd8b24ca145e555" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" dependencies = [ "futures-executor", "futures-util", @@ -7676,13 +7715,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f50427f258fb77356e4cd4aa0e87e2bd2c66dbcee41dc405282cae2bfc26c83" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7745,9 +7784,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "siphasher" @@ -7785,7 +7824,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7839,12 +7878,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7936,7 +7975,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7947,9 +7986,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symbolic-common" -version = "12.17.2" +version = "12.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751a2823d606b5d0a7616499e4130a516ebd01a44f39811be2b9600936509c23" +checksum = "52ca086c1eb5c7ee74b151ba83c6487d5d33f8c08ad991b86f3f58f6629e68d5" dependencies = [ "debugid", "memmap2", @@ -7959,9 +7998,9 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "12.17.2" +version = "12.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b237cfbe320601dd24b4ac817a5b68bb28f5508e33f08d42be0682cadc8ac9" +checksum = "baa911a28a62823aaf2cc2e074212492a3ee69d0d926cc8f5b12b4a108ff5c0c" dependencies = [ "rustc-demangle", "symbolic-common", @@ -7980,9 +8019,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -8006,7 +8045,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8015,7 +8054,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d8a928f38f1bc873f28e0d2ba8298ad65374a6ac2241dabd297271531a736cd" dependencies = [ - "syn 2.0.116", + "syn 2.0.117", "synthez-codegen", "synthez-core", ] @@ -8026,7 +8065,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fb83b8df4238e11746984dfb3819b155cd270de0e25847f45abad56b3671047" dependencies = [ - "syn 2.0.116", + "syn 2.0.117", "synthez-core", ] @@ -8039,7 +8078,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8053,6 +8092,17 @@ dependencies = [ "system-configuration-sys", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + [[package]] name = "system-configuration-sys" version = "0.6.0" @@ -8071,14 +8121,14 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.25.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -8106,12 +8156,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ - "rustix 1.1.3", - "windows-sys 0.60.2", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -8121,7 +8171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d53ac171c92a39e4769491c4b4dde7022c60042254b5fc044ae409d34a24d4" dependencies = [ "test-log-macros", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -8132,7 +8182,7 @@ checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8302,7 +8352,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8313,7 +8363,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8393,9 +8443,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -8408,9 +8458,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -8418,7 +8468,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "tracing", "windows-sys 0.61.2", @@ -8426,13 +8476,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8501,10 +8551,10 @@ dependencies = [ "indexmap 2.13.0", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -8517,31 +8567,40 @@ dependencies = [ ] [[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da053d28fe57e2c9d21b48261e14e7b4c8b670b54d2c684847b91feaf4c7dac5" dependencies = [ "indexmap 2.13.0", - "toml_datetime", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.1", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9" dependencies = [ - "winnow", + "winnow 1.0.1", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tonic" @@ -8575,9 +8634,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f32a6f80051a4111560201420c7885d0082ba9efe2ab61875c587bb6b18b9a0" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ "async-trait", "base64 0.22.1", @@ -8601,13 +8660,13 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f86539c0089bfd09b1f8c0ab0239d80392af74c21bc9e0f15e1b4aca4c1647f" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" dependencies = [ "bytes", "prost 0.14.3", - "tonic 0.14.4", + "tonic 0.14.5", ] [[package]] @@ -8721,7 +8780,7 @@ dependencies = [ "crossbeam-channel", "thiserror 2.0.18", "time", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -8732,7 +8791,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8771,7 +8830,7 @@ dependencies = [ "tokio-util", "tracing-core", "tracing-futures", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", ] [[package]] @@ -8802,7 +8861,7 @@ dependencies = [ "tracing-core", "tracing-log", "tracing-serde", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", "url", ] @@ -8817,7 +8876,7 @@ dependencies = [ "smallvec", "tracing", "tracing-core", - "tracing-subscriber 0.3.22", + "tracing-subscriber 0.3.23", "web-time", ] @@ -8842,9 +8901,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -8897,7 +8956,7 @@ checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8950,9 +9009,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -9036,7 +9095,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -9065,11 +9124,11 @@ checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d" [[package]] name = "uuid" -version = "1.21.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -9152,9 +9211,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" dependencies = [ "cfg-if", "once_cell", @@ -9165,23 +9224,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9189,22 +9244,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" dependencies = [ "unicode-ident", ] @@ -9258,9 +9313,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" dependencies = [ "js-sys", "wasm-bindgen", @@ -9348,22 +9403,23 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.53.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-core 0.53.0", - "windows-targets 0.52.6", + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", ] [[package]] -name = "windows-core" -version = "0.53.0" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcc5b895a6377f1ab9fa55acedab1fd5ac0db66ad1e6c7f47e28a22e446a5dd" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-core", ] [[package]] @@ -9375,10 +9431,21 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link", - "windows-result 0.4.1", + "windows-result", "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -9387,7 +9454,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -9398,7 +9465,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -9408,12 +9475,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-result" -version = "0.1.2" +name = "windows-numerics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-targets 0.52.6", + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -9434,15 +9513,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -9479,21 +9549,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -9528,10 +9583,13 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "windows-threading" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] [[package]] name = "windows_aarch64_gnullvm" @@ -9545,12 +9603,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -9563,12 +9615,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -9593,12 +9639,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -9611,12 +9651,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -9629,12 +9663,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -9647,12 +9675,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -9667,21 +9689,17 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] -name = "winreg" -version = "0.50.0" +name = "winnow" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "memchr", ] [[package]] @@ -9720,7 +9738,7 @@ dependencies = [ "heck", "indexmap 2.13.0", "prettyplease", - "syn 2.0.116", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -9736,7 +9754,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -9845,9 +9863,9 @@ dependencies = [ [[package]] name = "yamux" -version = "0.13.8" +version = "0.13.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deab71f2e20691b4728b349c6cee8fc7223880fa67b6b4f92225ec32225447e5" +checksum = "1991f6690292030e31b0144d73f5e8368936c58e45e7068254f7138b23b00672" dependencies = [ "futures", "log", @@ -9887,28 +9905,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -9928,7 +9946,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -9949,7 +9967,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -9982,7 +10000,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] diff --git a/c-bindings/src/api/wallet.rs b/c-bindings/src/api/wallet.rs index 4e5ee7b09..8ae9bd8c8 100644 --- a/c-bindings/src/api/wallet.rs +++ b/c-bindings/src/api/wallet.rs @@ -244,13 +244,13 @@ pub unsafe extern "C" fn get_known_addresses( /// /// if (result.status == OperationStatus_Ok) { /// KnownAddresses addresses = result.value; -/// +/// /// // Use the addresses... /// for (size_t i = 0; i < addresses.len; i++) { /// uint8_t* address = addresses.addresses[i]; /// // Process the 32-byte address... /// } -/// +/// /// // Free the memory when done /// free_known_addresses(addresses); /// } @@ -306,7 +306,7 @@ pub(crate) fn get_balance_sync( .await; api.get_balance(Some(tip), wallet_address) .await - .map(|tip_response| tip_response.response) + .map(|tip_response| tip_response.response.map(|balance| balance.balance)) }) .map_err(|_| OperationStatus::DynError) } diff --git a/core/src/mantle/encoding.rs b/core/src/mantle/encoding.rs index 0132afb8d..1a74003f0 100644 --- a/core/src/mantle/encoding.rs +++ b/core/src/mantle/encoding.rs @@ -5,8 +5,9 @@ use nom::{ bytes::complete::take, combinator::{map, map_res}, error::{Error, ErrorKind}, - multi::count, - number::complete::{le_u32, le_u64, u8 as decode_u8}, + multi::{count, length_count}, + number::complete::{le_u16, le_u32, le_u64, u8 as decode_u8}, + sequence::pair, }; use crate::{ @@ -76,6 +77,7 @@ pub fn decode_op(input: &[u8]) -> IResult<&[u8], Op> { opcode::INSCRIBE => map(decode_channel_inscribe, Op::ChannelInscribe).parse(input), opcode::SET_CHANNEL_KEYS => map(decode_channel_set_keys, Op::ChannelSetKeys).parse(input), opcode::CHANNEL_DEPOSIT => map(decode_channel_deposit, Op::ChannelDeposit).parse(input), + opcode::CHANNEL_WITHDRAW => map(decode_channel_withdraw, Op::ChannelWithdraw).parse(input), opcode::SDP_DECLARE => map(decode_sdp_declare, Op::SDPDeclare).parse(input), opcode::SDP_WITHDRAW => map(decode_sdp_withdraw, Op::SDPWithdraw).parse(input), opcode::SDP_ACTIVE => map(decode_sdp_active, Op::SDPActive).parse(input), @@ -138,6 +140,13 @@ fn decode_channel_deposit(input: &[u8]) -> IResult<&[u8], DepositOp> { )) } +fn decode_channel_withdraw(input: &[u8]) -> IResult<&[u8], ChannelWithdrawOp> { + // ChannelWithdraw = ChannelId Amount + let (input, channel_id) = map(decode_hash32, ChannelId::from).parse(input)?; + let (input, amount) = decode_uint64(input)?; + Ok((input, ChannelWithdrawOp { channel_id, amount })) +} + // ============================================================================== // SDP Operation Decoders // ============================================================================== @@ -328,6 +337,11 @@ fn decode_op_proof<'a>(input: &'a [u8], op: &Op) -> IResult<&'a [u8], OpProof> { }) .parse(input), + // ChannelWithdrawProof + Op::ChannelWithdraw(_) => { + map(decode_channel_withdraw_proof, OpProof::ChannelWithdrawProof).parse(input) + } + // None. It's indirectly signed through the Ledger Transaction signature. Op::ChannelDeposit(_) => Ok((input, OpProof::NoProof)), } @@ -379,6 +393,31 @@ fn decode_ed25519_signature(input: &[u8]) -> IResult<&[u8], Ed25519Signature> { .parse(input) } +const fn calculate_channel_withdraw_proof_byte_size( + channel_withdraw_threshold: ChannelKeyIndex, +) -> usize { + (channel_withdraw_threshold as usize) * (ED25519_SIG_BYTES + 4) +} + +fn decode_channel_withdraw_proof(input: &[u8]) -> IResult<&[u8], ChannelWithdrawProof> { + // ChannelWithdrawProof = SignatureCount *WithdrawSignature + // WithdrawSignature = Ed25519Signature Index + let (input, signatures) = length_count( + map(decode_uint16, |n: ChannelKeyIndex| n as usize), + pair(decode_ed25519_signature, decode_uint16), + ) + .parse(input)?; + + let signatures: Vec = signatures + .into_iter() + .map(|(signature, index)| WithdrawSignature::from((index, signature))) + .collect(); + + ChannelWithdrawProof::new(signatures) + .map(|proof| (input, proof)) + .map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::Verify))) +} + fn decode_field_element(input: &[u8]) -> IResult<&[u8], Fr> { // FieldElement = 32BYTE map_res(take(32usize), |bytes: &[u8]| { @@ -404,9 +443,9 @@ fn decode_array(input: &[u8]) -> IResult<&[u8], [u8; N]> { .parse(input) } -fn decode_uint64(input: &[u8]) -> IResult<&[u8], u64> { - // UINT64 = 8BYTE - le_u64(input) +fn decode_uint16(input: &[u8]) -> IResult<&[u8], u16> { + // UINT16 = 2BYTE + le_u16(input) } fn decode_uint32(input: &[u8]) -> IResult<&[u8], u32> { @@ -414,6 +453,11 @@ fn decode_uint32(input: &[u8]) -> IResult<&[u8], u32> { le_u32(input) } +fn decode_uint64(input: &[u8]) -> IResult<&[u8], u64> { + // UINT64 = 8BYTE + le_u64(input) +} + fn decode_byte(input: &[u8]) -> IResult<&[u8], u8> { // Byte = OCTET decode_u8(input) @@ -426,9 +470,16 @@ fn decode_byte(input: &[u8]) -> IResult<&[u8], u8> { use lb_groth16::fr_to_bytes; use super::ops::opcode; +use crate::{ + mantle::{ + ops::channel::{ChannelKeyIndex, withdraw::ChannelWithdrawOp}, + tx::MantleTxGasContext, + }, + proofs::channel_withdraw_proof::{ChannelWithdrawProof, WithdrawSignature}, +}; +// Encode primitives -/// Encode primitives -fn encode_uint64(value: u64) -> Vec { +fn encode_uint16(value: u16) -> Vec { value.to_le_bytes().to_vec() } @@ -436,6 +487,10 @@ fn encode_uint32(value: u32) -> Vec { value.to_le_bytes().to_vec() } +fn encode_uint64(value: u64) -> Vec { + value.to_le_bytes().to_vec() +} + fn encode_byte(value: u8) -> Vec { vec![value] } @@ -471,6 +526,17 @@ fn encode_groth16_proof(proof: &CompressedGroth16Proof) -> Vec { proof.to_bytes().to_vec() } +fn encode_channel_withdraw_proof(proof: &ChannelWithdrawProof) -> Vec { + let mut bytes = Vec::new(); + bytes.extend(encode_uint16(proof.signatures().len() as ChannelKeyIndex)); + bytes.extend(proof.signatures().iter().flat_map(|signature| { + encode_ed25519_signature(&signature.signature) + .into_iter() + .chain(encode_uint16(signature.channel_key_index)) + })); + bytes +} + /// Encode channel operations #[must_use] pub fn encode_channel_inscribe(op: &InscriptionOp) -> Vec { @@ -502,6 +568,13 @@ fn encode_channel_deposit(op: &DepositOp) -> Vec { bytes } +fn encode_channel_withdraw(op: &ChannelWithdrawOp) -> Vec { + let mut bytes = Vec::new(); + bytes.extend(encode_hash32(op.channel_id.as_ref())); + bytes.extend(encode_uint64(op.amount)); + bytes +} + /// Encode SDP operations fn encode_locator(locator: &multiaddr::Multiaddr) -> Vec { let locator_bytes = locator.to_vec(); @@ -614,6 +687,10 @@ pub fn encode_op(op: &Op) -> Vec { bytes.extend(encode_byte(opcode::CHANNEL_DEPOSIT)); bytes.extend(encode_channel_deposit(op)); } + Op::ChannelWithdraw(op) => { + bytes.extend(encode_byte(opcode::CHANNEL_WITHDRAW)); + bytes.extend(encode_channel_withdraw(op)); + } Op::SDPDeclare(op) => { bytes.extend(encode_byte(opcode::SDP_DECLARE)); bytes.extend(encode_sdp_declare(op)); @@ -654,6 +731,9 @@ fn encode_op_proof(proof: &OpProof, op: &Op) -> Vec { encode_ed25519_signature(sig) } (OpProof::NoProof, Op::ChannelDeposit(_)) => Vec::new(), + (OpProof::ChannelWithdrawProof(proof), Op::ChannelWithdraw(_)) => { + encode_channel_withdraw_proof(proof) + } ( OpProof::ZkAndEd25519Sigs { zk_sig, @@ -701,7 +781,7 @@ pub fn encode_signed_mantle_tx(tx: &SignedMantleTx) -> Vec { bytes } -pub(crate) fn predict_signed_mantle_tx_size(tx: &MantleTx) -> usize { +pub(crate) fn predict_signed_mantle_tx_size(tx: &MantleTx, context: &MantleTxGasContext) -> usize { let mantle_tx_size = encode_mantle_tx(tx).len(); let ops_proofs_size = tx @@ -719,6 +799,14 @@ pub(crate) fn predict_signed_mantle_tx_size(tx: &MantleTx) -> usize { GROTH16_BYTES } + // WithdrawProof + Op::ChannelWithdraw(operation) => { + let channel_withdraw_threshold = context.withdraw_threshold(&operation.channel_id).expect( + "Operation should have been verified before reaching this point, so the channel must exist in the context." + ); + calculate_channel_withdraw_proof_byte_size(channel_withdraw_threshold) + } + // None Op::ChannelDeposit(_) => 0, }) @@ -729,6 +817,8 @@ pub(crate) fn predict_signed_mantle_tx_size(tx: &MantleTx) -> usize { #[cfg(test)] mod tests { + use std::collections::HashMap; + use ark_ff::Field as _; use lb_key_management_system_keys::keys::{Ed25519Key, ZkKey}; use num_bigint::BigUint; @@ -950,7 +1040,9 @@ mod tests { let encoded = encode_signed_mantle_tx(&signed_tx); - let predicted_size = predict_signed_mantle_tx_size(&signed_tx.mantle_tx); + let gas_context = MantleTxGasContext::new(HashMap::new()); + let predicted_size = + predict_signed_mantle_tx_size(&signed_tx.mantle_tx, &gas_context); assert_eq!( predicted_size, encoded.len(), @@ -1057,7 +1149,8 @@ mod tests { }; // Predict size - let predicted_size = predict_signed_mantle_tx_size(&mantle_tx); + let gas_context = MantleTxGasContext::new(HashMap::new()); + let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context); // Create a signed tx and encode it to get actual size let signed_tx = SignedMantleTx::new(mantle_tx, vec![]).unwrap(); @@ -1084,7 +1177,8 @@ mod tests { }; // Predict size - let predicted_size = predict_signed_mantle_tx_size(&mantle_tx); + let gas_context = MantleTxGasContext::new(HashMap::new()); + let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context); // Create a signed tx and encode it to get actual size let txhash = mantle_tx.hash(); @@ -1118,7 +1212,8 @@ mod tests { }; // Predict size - let predicted_size = predict_signed_mantle_tx_size(&mantle_tx); + let gas_context = MantleTxGasContext::new(HashMap::new()); + let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context); // Create a signed tx and encode it to get actual size let dummy_ed25519_sig = Ed25519Signature::from_bytes(&[0; 64]); @@ -1163,7 +1258,8 @@ mod tests { }; // Predict size - let predicted_size = predict_signed_mantle_tx_size(&mantle_tx); + let gas_context = MantleTxGasContext::new(HashMap::new()); + let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context); // Create a signed tx and encode it to get actual size let txhash = mantle_tx.hash(); @@ -1200,7 +1296,8 @@ mod tests { let txhash = mantle_tx.hash(); // Predict size - let predicted_size = predict_signed_mantle_tx_size(&mantle_tx); + let gas_context = MantleTxGasContext::new(HashMap::new()); + let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context); // Create a signed tx and encode it to get actual size let signed_tx = SignedMantleTx::new( @@ -1242,7 +1339,8 @@ mod tests { storage_gas_price: 50, }; - let predicted_size = predict_signed_mantle_tx_size(&mantle_tx); + let gas_context = MantleTxGasContext::new(HashMap::new()); + let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context); let txhash = mantle_tx.hash(); let signed_tx = SignedMantleTx::new( @@ -1301,7 +1399,8 @@ mod tests { }; // Predict size - let predicted_size = predict_signed_mantle_tx_size(&mantle_tx); + let gas_context = MantleTxGasContext::new(HashMap::new()); + let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context); let txhash = mantle_tx.hash(); let op_sig = signing_key.sign_payload(&txhash.as_signing_bytes()); @@ -1344,7 +1443,8 @@ mod tests { }; // Predict size - let predicted_size = predict_signed_mantle_tx_size(&mantle_tx); + let gas_context = MantleTxGasContext::new(HashMap::new()); + let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context); // Create a signed tx and encode it to get actual size let signed_tx = SignedMantleTx::new( @@ -1405,7 +1505,8 @@ mod tests { }; // Predict size - let predicted_size = predict_signed_mantle_tx_size(&mantle_tx); + let gas_context = MantleTxGasContext::new(HashMap::new()); + let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context); // Create a signed tx and encode it to get actual size let txhash = mantle_tx.hash(); @@ -1444,7 +1545,8 @@ mod tests { storage_gas_price: 50, }; - let predicted_size = predict_signed_mantle_tx_size(&mantle_tx); + let empty_gas_context = MantleTxGasContext::new(HashMap::new()); + let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &empty_gas_context); let poc_proof = Groth16LeaderClaimProof::new( CompressedGroth16Proof::from_bytes(&[0u8; 128]), @@ -1491,4 +1593,31 @@ mod tests { )) ); } + + #[test] + fn test_encode_decode_channel_withdraw_tx() { + let signing_key = Ed25519Key::from_bytes(&[21u8; 32]); + let mantle_tx = MantleTx { + ops: vec![Op::ChannelWithdraw(ChannelWithdrawOp { + channel_id: ChannelId::from([0xAB; 32]), + amount: 17, + })], + execution_gas_price: 100, + storage_gas_price: 50, + }; + let tx_hash = mantle_tx.hash(); + let proof = ChannelWithdrawProof::new(vec![WithdrawSignature::new( + 0, + signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()), + )]) + .unwrap(); + let signed_tx = + SignedMantleTx::new(mantle_tx, vec![OpProof::ChannelWithdrawProof(proof)]).unwrap(); + + let encoded = encode_signed_mantle_tx(&signed_tx); + let (remaining, decoded_tx) = decode_signed_mantle_tx(&encoded).unwrap(); + + assert!(remaining.is_empty()); + assert_eq!(decoded_tx, signed_tx); + } } diff --git a/core/src/mantle/gas.rs b/core/src/mantle/gas.rs index cf38516fd..a85a40842 100644 --- a/core/src/mantle/gas.rs +++ b/core/src/mantle/gas.rs @@ -1,28 +1,32 @@ pub type Gas = crate::mantle::ledger::Value; pub trait GasCost { + type Context; + /// Returns the gas cost of this operation. - fn total_gas_cost(&self) -> Gas; - fn storage_gas_cost(&self) -> Gas; - fn execution_gas_consumption(&self) -> Gas; - fn storage_gas_consumption(&self) -> Gas; + fn total_gas_cost(&self, context: &Self::Context) -> Gas; + fn storage_gas_cost(&self, context: &Self::Context) -> Gas; + fn execution_gas_consumption(&self, context: &Self::Context) -> Gas; + fn storage_gas_consumption(&self, context: &Self::Context) -> Gas; } impl GasCost for &T { - fn total_gas_cost(&self) -> Gas { - T::total_gas_cost::(self) + type Context = T::Context; + + fn total_gas_cost(&self, context: &Self::Context) -> Gas { + T::total_gas_cost::(self, context) } - fn storage_gas_cost(&self) -> Gas { - T::storage_gas_cost(self) + fn storage_gas_cost(&self, context: &Self::Context) -> Gas { + T::storage_gas_cost(self, context) } - fn execution_gas_consumption(&self) -> Gas { - T::execution_gas_consumption::(self) + fn execution_gas_consumption(&self, context: &Self::Context) -> Gas { + T::execution_gas_consumption::(self, context) } - fn storage_gas_consumption(&self) -> Gas { - T::storage_gas_consumption(self) + fn storage_gas_consumption(&self, context: &Self::Context) -> Gas { + T::storage_gas_consumption(self, context) } } @@ -39,6 +43,9 @@ pub trait GasConstants { /// Verify the deposit signature. const CHANNEL_DEPOSIT: Gas; + /// Verify the withdrawal signature. + const CHANNEL_WITHDRAW: Gas; + /// Verify the proof of ownership. const SDP_DECLARE: Gas; @@ -59,6 +66,7 @@ impl GasConstants for MainnetGasConstants { const CHANNEL_INSCRIBE: Gas = 22; const CHANNEL_SET_KEYS: Gas = 22; const CHANNEL_DEPOSIT: Gas = 0; + const CHANNEL_WITHDRAW: Gas = 22; const SDP_DECLARE: Gas = 2727; const SDP_WITHDRAW: Gas = 2705; const SDP_ACTIVE: Gas = 2705; diff --git a/core/src/mantle/genesis_tx.rs b/core/src/mantle/genesis_tx.rs index 157fc8679..a874e27cb 100644 --- a/core/src/mantle/genesis_tx.rs +++ b/core/src/mantle/genesis_tx.rs @@ -3,6 +3,8 @@ use lb_poseidon2::Digest; use serde::{Deserialize, Serialize}; use super::{OpProof, SignedMantleTx, ops::sdp::SDPDeclareOp}; +#[cfg(feature = "mock")] +use crate::mantle::tx::MantleTxGasContext; use crate::{ crypto::ZkHasher, mantle::{ @@ -74,11 +76,11 @@ impl GenesisTx { #[cfg(feature = "mock")] #[must_use] - pub fn new_mocked() -> Self { + pub fn new_mocked(context: MantleTxGasContext) -> Self { use crate::mantle::tx_builder::MantleTxBuilder; Self(SignedMantleTx::new_unverified( - MantleTxBuilder::new().build(), + MantleTxBuilder::new(context).build(), vec![], )) } @@ -116,22 +118,24 @@ impl Transaction for GenesisTx { } impl GasCost for GenesisTx { - fn total_gas_cost(&self) -> Gas { + type Context = (); + + fn total_gas_cost(&self, _context: &Self::Context) -> Gas { // Genesis transactions have zero gas cost as per spec 0 } - fn storage_gas_cost(&self) -> Gas { + fn storage_gas_cost(&self, _context: &Self::Context) -> Gas { // Genesis transactions have zero gas cost as per spec 0 } - fn execution_gas_consumption(&self) -> Gas { + fn execution_gas_consumption(&self, _context: &Self::Context) -> Gas { // Genesis transactions have zero gas cost as per spec 0 } - fn storage_gas_consumption(&self) -> Gas { + fn storage_gas_consumption(&self, _context: &Self::Context) -> Gas { // Genesis transactions have zero gas cost as per spec 0 } diff --git a/core/src/mantle/mod.rs b/core/src/mantle/mod.rs index b49852183..f0cc51a66 100644 --- a/core/src/mantle/mod.rs +++ b/core/src/mantle/mod.rs @@ -19,9 +19,9 @@ use lb_groth16::Fr; pub use ledger::{Note, NoteId, Utxo, Value}; pub use ops::{Op, OpProof}; use ops::{channel::inscribe::InscriptionOp, sdp::SDPDeclareOp}; -pub use tx::{MantleTx, SignedMantleTx, TxHash}; +pub use tx::{MantleTx, SignedMantleTx, TxHash, VerificationError}; -use crate::mantle::ops::transfer::TransferOp; +use crate::mantle::{gas::Gas, ops::transfer::TransferOp}; pub const MAX_MANTLE_TXS: usize = 1024; @@ -50,6 +50,17 @@ pub trait AuthenticatedMantleTx: Transaction + GasCost + StorageS fn mantle_tx(&self) -> &MantleTx; fn ops_with_proof(&self) -> impl Iterator; + + // Gas Cost functions with context already handled + fn total_gas_cost(&self) -> Gas; + fn storage_gas_cost(&self) -> Gas; + fn execution_gas_consumption(&self) -> Gas; + fn storage_gas_consumption(&self) -> Gas; + + fn verify_ops_proofs_with_helper( + &self, + helper: &impl tx::OperationVerificationHelper, + ) -> Result<(), VerificationError>; } /// A genesis transaction as specified in @@ -84,6 +95,32 @@ impl AuthenticatedMantleTx for &T { fn ops_with_proof(&self) -> impl Iterator { T::ops_with_proof(self) } + + fn total_gas_cost(&self) -> Gas { + ::total_gas_cost::(self) + } + + fn storage_gas_cost(&self) -> Gas { + ::storage_gas_cost(self) + } + + fn execution_gas_consumption(&self) -> Gas { + ::execution_gas_consumption::(self) + } + + fn storage_gas_consumption(&self) -> Gas { + ::storage_gas_consumption(self) + } + + fn verify_ops_proofs_with_helper( + &self, + operation_verification_helper: &impl tx::OperationVerificationHelper, + ) -> Result<(), VerificationError> { + ::verify_ops_proofs_with_helper( + self, + operation_verification_helper, + ) + } } impl GenesisTx for &T { diff --git a/core/src/mantle/ops/channel/mod.rs b/core/src/mantle/ops/channel/mod.rs index 2e930557d..390ea3a18 100644 --- a/core/src/mantle/ops/channel/mod.rs +++ b/core/src/mantle/ops/channel/mod.rs @@ -1,13 +1,25 @@ pub mod deposit; pub mod inscribe; pub mod set_keys; +pub mod withdraw; + +use std::fmt::{Display, Formatter}; use crate::utils::serde_bytes_newtype; +pub type ChannelKeyIndex = u16; + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct ChannelId([u8; 32]); serde_bytes_newtype!(ChannelId, 32); +impl Display for ChannelId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let hex_string = hex::encode(self.0); + write!(f, "{hex_string}") + } +} + /// The id of the previous message in the channel #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct MsgId([u8; 32]); diff --git a/core/src/mantle/ops/channel/withdraw.rs b/core/src/mantle/ops/channel/withdraw.rs new file mode 100644 index 000000000..9252771ce --- /dev/null +++ b/core/src/mantle/ops/channel/withdraw.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +use crate::mantle::{Value, ops::channel::ChannelId}; + +#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct ChannelWithdrawOp { + pub channel_id: ChannelId, + pub amount: Value, +} diff --git a/core/src/mantle/ops/internal.rs b/core/src/mantle/ops/internal.rs index 44b37a298..25dc8b615 100644 --- a/core/src/mantle/ops/internal.rs +++ b/core/src/mantle/ops/internal.rs @@ -5,13 +5,14 @@ use super::{ channel::{deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp}, leader_claim::LeaderClaimOp, opcode::{ - CHANNEL_DEPOSIT, INSCRIBE, LEADER_CLAIM, SDP_ACTIVE, SDP_DECLARE, SDP_WITHDRAW, - SET_CHANNEL_KEYS, TRANSFER, + CHANNEL_DEPOSIT, CHANNEL_WITHDRAW, INSCRIBE, LEADER_CLAIM, SDP_ACTIVE, SDP_DECLARE, + SDP_WITHDRAW, SET_CHANNEL_KEYS, TRANSFER, }, sdp::{SDPActiveOp, SDPDeclareOp, SDPWithdrawOp}, serde_, transfer::TransferOp, }; +use crate::mantle::ops::channel::withdraw::ChannelWithdrawOp; /// Core set of supported Mantle operations and their serialization behaviour. #[derive(Serialize)] @@ -33,6 +34,12 @@ pub enum OpSer<'a> { )] &'a DepositOp, ), + ChannelWithdraw( + #[serde( + serialize_with = "serde_::serialize_op_variant::<{CHANNEL_WITHDRAW}, ChannelWithdrawOp, _>" + )] + &'a ChannelWithdrawOp, + ), SDPDeclare( #[serde(serialize_with = "serde_::serialize_op_variant::<{SDP_DECLARE}, SDPDeclareOp, _>")] &'a SDPDeclareOp, @@ -65,6 +72,7 @@ impl<'a> From<&'a Op> for OpSer<'a> { Op::ChannelInscribe(op) => OpSer::ChannelInscribe(op), Op::ChannelSetKeys(op) => OpSer::ChannelSetKeys(op), Op::ChannelDeposit(op) => OpSer::ChannelDeposit(op), + Op::ChannelWithdraw(op) => OpSer::ChannelWithdraw(op), Op::SDPDeclare(op) => OpSer::SDPDeclare(op), Op::SDPWithdraw(op) => OpSer::SDPWithdraw(op), Op::SDPActive(op) => OpSer::SDPActive(op), @@ -96,6 +104,12 @@ pub enum OpDe { )] DepositOp, ), + ChannelWithdraw( + #[serde( + deserialize_with = "serde_::deserialize_op_variant::<{CHANNEL_WITHDRAW}, ChannelWithdrawOp, _>" + )] + ChannelWithdrawOp, + ), SDPDeclare( #[serde( deserialize_with = "serde_::deserialize_op_variant::<{SDP_DECLARE}, SDPDeclareOp, _>" @@ -132,6 +146,7 @@ impl From for Op { OpDe::ChannelInscribe(inscribe) => Self::ChannelInscribe(inscribe), OpDe::ChannelSetKeys(channel_set_keys) => Self::ChannelSetKeys(channel_set_keys), OpDe::ChannelDeposit(channel_deposit) => Self::ChannelDeposit(channel_deposit), + OpDe::ChannelWithdraw(channel_withdraw) => Self::ChannelWithdraw(channel_withdraw), OpDe::SDPDeclare(sdp_declare) => Self::SDPDeclare(sdp_declare), OpDe::SDPWithdraw(sdp_withdraw) => Self::SDPWithdraw(sdp_withdraw), OpDe::SDPActive(sdp_active) => Self::SDPActive(sdp_active), diff --git a/core/src/mantle/ops/mod.rs b/core/src/mantle/ops/mod.rs index 29e88f8f9..d7ed2612a 100644 --- a/core/src/mantle/ops/mod.rs +++ b/core/src/mantle/ops/mod.rs @@ -6,7 +6,9 @@ pub mod sdp; mod serde_; pub mod transfer; -use channel::{deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp}; +use channel::{ + deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp, withdraw::ChannelWithdrawOp, +}; use lb_key_management_system_keys::keys::{Ed25519Signature, ZkSignature}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -26,11 +28,13 @@ use crate::{ encoding::{decode_op, encode_op}, ops::{ internal::{OpDe, OpSer}, - opcode::CHANNEL_DEPOSIT, + opcode::{CHANNEL_DEPOSIT, CHANNEL_WITHDRAW}, transfer::TransferOp, }, }, - proofs::leader_claim_proof::Groth16LeaderClaimProof, + proofs::{ + channel_withdraw_proof::ChannelWithdrawProof, leader_claim_proof::Groth16LeaderClaimProof, + }, }; /// Core set of supported Mantle operations. @@ -49,6 +53,7 @@ pub enum Op { ChannelInscribe(InscriptionOp), ChannelSetKeys(SetKeysOp), ChannelDeposit(DepositOp), + ChannelWithdraw(ChannelWithdrawOp), SDPDeclare(SDPDeclareOp), SDPWithdraw(SDPWithdrawOp), SDPActive(SDPActiveOp), @@ -66,6 +71,7 @@ pub enum OpProof { ed25519_sig: Ed25519Signature, }, PoC(Groth16LeaderClaimProof), + ChannelWithdrawProof(ChannelWithdrawProof), } /// Delegates serialization through the [`OpInternal`] representation. @@ -117,6 +123,7 @@ impl Op { Self::ChannelInscribe(_) => "ChannelInscribe", Self::ChannelSetKeys(_) => "ChannelSetKeys", Self::ChannelDeposit(_) => "ChannelDeposit", + Self::ChannelWithdraw(_) => "ChannelWithdraw", Self::SDPDeclare(_) => "SDPDeclare", Self::SDPWithdraw(_) => "SDPWithdraw", Self::SDPActive(_) => "SDPActive", @@ -130,6 +137,7 @@ impl Op { Self::ChannelInscribe(_) => INSCRIBE, Self::ChannelSetKeys(_) => SET_CHANNEL_KEYS, Self::ChannelDeposit(_) => CHANNEL_DEPOSIT, + Self::ChannelWithdraw(_) => CHANNEL_WITHDRAW, Self::SDPDeclare(_) => SDP_DECLARE, Self::SDPWithdraw(_) => SDP_WITHDRAW, Self::SDPActive(_) => SDP_ACTIVE, @@ -144,6 +152,7 @@ impl Op { Self::ChannelInscribe(_) => Constants::CHANNEL_INSCRIBE, Self::ChannelSetKeys(_) => Constants::CHANNEL_SET_KEYS, Self::ChannelDeposit(_) => Constants::CHANNEL_DEPOSIT, + Self::ChannelWithdraw(_) => Constants::CHANNEL_WITHDRAW, Self::SDPDeclare(_) => Constants::SDP_DECLARE, Self::SDPWithdraw(_) => Constants::SDP_WITHDRAW, Self::SDPActive(_) => Constants::SDP_ACTIVE, diff --git a/core/src/mantle/ops/opcode.rs b/core/src/mantle/ops/opcode.rs index 196f8f366..1b479370f 100644 --- a/core/src/mantle/ops/opcode.rs +++ b/core/src/mantle/ops/opcode.rs @@ -2,6 +2,7 @@ pub const TRANSFER: u8 = 0x00; pub const SET_CHANNEL_KEYS: u8 = 0x10; pub const INSCRIBE: u8 = 0x11; pub const CHANNEL_DEPOSIT: u8 = 0x12; +pub const CHANNEL_WITHDRAW: u8 = 0x13; pub const SDP_DECLARE: u8 = 0x20; pub const SDP_WITHDRAW: u8 = 0x21; pub const SDP_ACTIVE: u8 = 0x22; diff --git a/core/src/mantle/tx.rs b/core/src/mantle/tx.rs index 1a65caed8..ed182b731 100644 --- a/core/src/mantle/tx.rs +++ b/core/src/mantle/tx.rs @@ -1,7 +1,11 @@ -use std::sync::LazyLock; +use std::{ + collections::{HashMap, HashSet}, + sync::LazyLock, +}; use bytes::Bytes; use lb_groth16::{Fr, fr_from_bytes, fr_from_bytes_unchecked, fr_to_bytes, serde::serde_fr}; +use lb_key_management_system_keys::keys::Ed25519PublicKey; use lb_poseidon2::{Digest, ZkHash}; use num_bigint::BigUint; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -12,9 +16,16 @@ use crate::{ AuthenticatedMantleTx, StorageSize, Transaction, TransactionHasher, encoding::{decode_mantle_tx, encode_mantle_tx, encode_signed_mantle_tx}, gas::{Gas, GasConstants, GasCost}, - ops::{Op, OpProof, transfer::TransferOp}, + ops::{ + Op, OpProof, + channel::{ChannelId, ChannelKeyIndex, withdraw::ChannelWithdrawOp}, + transfer::TransferOp, + }, + }, + proofs::{ + channel_withdraw_proof::ChannelWithdrawProof, + leader_claim_proof::{LeaderClaimProof as _, LeaderClaimPublic}, }, - proofs::leader_claim_proof::{LeaderClaimProof as _, LeaderClaimPublic}, }; /// The hash of a transaction @@ -80,6 +91,25 @@ struct MantleTxDeSerImpl { pub storage_gas_price: Gas, } +#[derive(Debug, Clone)] +pub struct MantleTxGasContext { + withdraw_thresholds: HashMap, +} + +impl MantleTxGasContext { + #[must_use] + pub const fn new(withdraw_thresholds: HashMap) -> Self { + Self { + withdraw_thresholds, + } + } + + #[must_use] + pub fn withdraw_threshold(&self, channel_id: &ChannelId) -> Option { + self.withdraw_thresholds.get(channel_id).copied() + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct MantleTx { pub ops: Vec, @@ -151,32 +181,34 @@ impl<'de> Deserialize<'de> for MantleTx { } impl GasCost for MantleTx { - fn total_gas_cost(&self) -> Gas { - let execution_gas = self.execution_gas_consumption::(); + type Context = MantleTxGasContext; - execution_gas * self.execution_gas_price + self.storage_gas_cost() + fn total_gas_cost(&self, context: &Self::Context) -> Gas { + let execution_gas = self.execution_gas_consumption::(context); + + execution_gas * self.execution_gas_price + self.storage_gas_cost(context) } - fn storage_gas_cost(&self) -> Gas { - self.storage_gas_consumption() * self.storage_gas_price + fn storage_gas_cost(&self, context: &Self::Context) -> Gas { + self.storage_gas_consumption(context) * self.storage_gas_price } - fn execution_gas_consumption(&self) -> Gas { + fn execution_gas_consumption(&self, _context: &Self::Context) -> Gas { self.ops .iter() .map(Op::execution_gas::) .sum::() } - fn storage_gas_consumption(&self) -> Gas { - self.signed_serialized_size() + fn storage_gas_consumption(&self, context: &Self::Context) -> Gas { + self.signed_serialized_size(context) } } impl MantleTx { #[must_use] - pub fn signed_serialized_size(&self) -> u64 { - super::encoding::predict_signed_mantle_tx_size(self) as u64 + pub fn signed_serialized_size(&self, context: &::Context) -> u64 { + super::encoding::predict_signed_mantle_tx_size(self, context) as u64 } #[must_use] @@ -218,6 +250,10 @@ impl From for MantleTx { } } +// Deserializing here is dangerous, as it bypasses the verification without +// confirmation. +// TODO: Split entity into a system that allows for verification in different +// stages. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct SignedMantleTx { pub mantle_tx: MantleTx, @@ -241,11 +277,50 @@ pub enum VerificationError { op_type: &'static str, op_index: usize, }, - #[error("Number of proofs ({proofs_count}) does not match number of operations ({ops_count})")] + #[error( + "The number of proofs ({proofs_count}) does not match the number of operations ({ops_count})" + )] ProofCountMismatch { ops_count: usize, proofs_count: usize, }, + #[error("Channel {channel_id} could not be found")] + ChannelNotFound { channel_id: ChannelId }, + #[error("Key {key_index} could not be found in channel {channel_id}")] + KeyNotFound { + channel_id: ChannelId, + key_index: ChannelKeyIndex, + }, + #[error( + "Not enough signatures in ChannelWithdrawProof at index {op_index}: got {actual}, required {required}" + )] + ChannelWithdrawProofNotEnoughSignatures { + op_index: usize, + actual: usize, + required: ChannelKeyIndex, + }, + #[error("Duplicate signature indices in ChannelWithdrawProof at index {op_index}")] + ChannelWithdrawProofDuplicateIndices { op_index: usize }, + #[error( + "Invalid signature in ChannelWithdrawProof at index {op_index} for signature index {signature_index}" + )] + ChannelWithdrawProofInvalidSignature { + op_index: usize, + signature_index: usize, + }, +} + +pub trait OperationVerificationHelper { + fn get_channel_withdraw_threshold( + &self, + channel_id: &ChannelId, + ) -> Result; + + fn get_key_from_channel_at_index( + &self, + channel_id: &ChannelId, + key_index: &ChannelKeyIndex, + ) -> Result; } impl SignedMantleTx { @@ -319,7 +394,53 @@ impl SignedMantleTx { } } // Other operations are checked by the ledger or don't require verification here - _ => {} + _ => { + // TODO: If the op and proof don't match, we are silently + // delaying the error + // until tx execution. + } + } + } + + Ok(()) + } + + pub fn verify_ops_proofs_with_helper( + &self, + operation_verification_helper: &impl OperationVerificationHelper, + ) -> Result<(), VerificationError> { + let tx_hash = self.hash(); + let tx_hash_bytes = tx_hash.as_signing_bytes(); + + for (idx, (op, proof)) in self + .mantle_tx + .ops + .iter() + .zip(self.ops_proofs.iter()) + .enumerate() + { + #[expect( + clippy::single_match_else, + reason = "Clearer and follows the pattern of verify_ops_proofs." + )] + match (op, proof) { + ( + Op::ChannelWithdraw(channel_withdraw_op), + OpProof::ChannelWithdrawProof(proof), + ) => { + verify_channel_withdraw( + channel_withdraw_op, + proof, + &tx_hash_bytes, + operation_verification_helper, + idx, + )?; + } + // Other operations don't require verification here + _ => { + // TODO: If the op and proof don't match, we are silently + // delaying the error until tx execution. + } } } @@ -331,6 +452,49 @@ impl SignedMantleTx { } } +fn verify_channel_withdraw( + operation: &ChannelWithdrawOp, + proof: &ChannelWithdrawProof, + tx_hash_bytes: &Bytes, + helper: &impl OperationVerificationHelper, + op_index: usize, +) -> Result<(), VerificationError> { + let channel_id = &operation.channel_id; + let withdraw_threshold = helper.get_channel_withdraw_threshold(channel_id)?; + + let signatures = proof.signatures(); + let signatures_len = signatures.len(); + if signatures_len < withdraw_threshold as usize { + return Err(VerificationError::ChannelWithdrawProofNotEnoughSignatures { + op_index, + actual: signatures_len, + required: withdraw_threshold, + }); + } + + let indices_set = signatures + .iter() + .map(|signature| signature.channel_key_index) + .collect::>(); + let indices_set_len = indices_set.len(); + if indices_set_len != signatures_len { + return Err(VerificationError::ChannelWithdrawProofDuplicateIndices { op_index }); + } + + for (i, signature) in signatures.iter().enumerate() { + let public_key = + helper.get_key_from_channel_at_index(channel_id, &signature.channel_key_index)?; + if let Err(_error) = public_key.verify(tx_hash_bytes.as_ref(), &signature.signature) { + return Err(VerificationError::ChannelWithdrawProofInvalidSignature { + op_index, + signature_index: i, + }); + } + } + + Ok(()) +} + impl Transaction for SignedMantleTx { const HASHER: TransactionHasher = |tx| ::digest(&tx.as_signing_frs()).into(); @@ -349,20 +513,46 @@ impl AuthenticatedMantleTx for SignedMantleTx { fn ops_with_proof(&self) -> impl Iterator { self.mantle_tx.ops.iter().zip(self.ops_proofs.iter()) } -} -impl GasCost for SignedMantleTx { fn total_gas_cost(&self) -> Gas { - let execution_gas = self.execution_gas_consumption::(); - - execution_gas * self.mantle_tx.execution_gas_price + self.storage_gas_cost() + GasCost::total_gas_cost::(&self, &()) } fn storage_gas_cost(&self) -> Gas { - self.storage_gas_consumption() * self.mantle_tx.storage_gas_price + GasCost::storage_gas_cost(&self, &()) } fn execution_gas_consumption(&self) -> Gas { + GasCost::execution_gas_consumption::(&self, &()) + } + + fn storage_gas_consumption(&self) -> Gas { + GasCost::storage_gas_consumption(&self, &()) + } + + fn verify_ops_proofs_with_helper( + &self, + operation_verification_helper: &impl OperationVerificationHelper, + ) -> Result<(), VerificationError> { + Self::verify_ops_proofs_with_helper(self, operation_verification_helper) + } +} + +impl GasCost for SignedMantleTx { + type Context = (); + + fn total_gas_cost(&self, context: &Self::Context) -> Gas { + let execution_gas = GasCost::execution_gas_consumption::(&self, context); + let storage_gas = GasCost::storage_gas_consumption(&self, context); + execution_gas * self.mantle_tx.execution_gas_price + storage_gas + } + + fn storage_gas_cost(&self, context: &Self::Context) -> Gas { + let storage_gas = GasCost::storage_gas_consumption(&self, context); + storage_gas * self.mantle_tx.storage_gas_price + } + + fn execution_gas_consumption(&self, _context: &Self::Context) -> Gas { self.mantle_tx .ops .iter() @@ -370,7 +560,7 @@ impl GasCost for SignedMantleTx { .sum::() } - fn storage_gas_consumption(&self) -> Gas { + fn storage_gas_consumption(&self, _context: &Self::Context) -> Gas { self.gas_storage_size() } } @@ -402,7 +592,10 @@ mod tests { use lb_key_management_system_keys::keys::{Ed25519Key, ZkKey}; use super::*; - use crate::mantle::ops::channel::inscribe::InscriptionOp; + use crate::{ + mantle::ops::channel::inscribe::InscriptionOp, + proofs::channel_withdraw_proof::WithdrawSignature, + }; fn create_test_mantle_tx(ops: Vec) -> MantleTx { MantleTx { @@ -421,6 +614,70 @@ mod tests { } } + struct TestOperationVerificationHelper { + thresholds: HashMap, + keys: HashMap<(ChannelId, ChannelKeyIndex), Ed25519PublicKey>, + } + + impl TestOperationVerificationHelper { + fn new( + thresholds: impl IntoIterator, + keys: impl IntoIterator, + ) -> Self { + Self { + thresholds: thresholds.into_iter().collect(), + keys: keys.into_iter().collect(), + } + } + } + + impl OperationVerificationHelper for TestOperationVerificationHelper { + fn get_channel_withdraw_threshold( + &self, + channel_id: &ChannelId, + ) -> Result { + self.thresholds + .get(channel_id) + .copied() + .ok_or(VerificationError::ChannelNotFound { + channel_id: *channel_id, + }) + } + + fn get_key_from_channel_at_index( + &self, + channel_id: &ChannelId, + key_index: &ChannelKeyIndex, + ) -> Result { + self.keys.get(&(*channel_id, *key_index)).copied().ok_or( + VerificationError::KeyNotFound { + channel_id: *channel_id, + key_index: *key_index, + }, + ) + } + } + + fn create_withdraw_tx(channel_id: ChannelId, signing_keys: &[&Ed25519Key]) -> SignedMantleTx { + let mantle_tx = create_test_mantle_tx(vec![Op::ChannelWithdraw(ChannelWithdrawOp { + channel_id, + amount: 5, + })]); + let tx_hash = mantle_tx.hash(); + let signatures = signing_keys + .iter() + .enumerate() + .map(|(index, key)| { + WithdrawSignature::new( + index as ChannelKeyIndex, + key.sign_payload(tx_hash.as_signing_bytes().as_ref()), + ) + }) + .collect(); + let proof = ChannelWithdrawProof::new(signatures).unwrap(); + SignedMantleTx::new(mantle_tx, vec![OpProof::ChannelWithdrawProof(proof)]).unwrap() + } + #[test] fn test_signed_mantle_tx_new_with_valid_inscribe_proof() { let signing_key = Ed25519Key::from_bytes(&[1; 32]); @@ -583,7 +840,7 @@ mod tests { let err_msg = deserialized.unwrap_err().to_string(); assert_eq!( err_msg, - "Number of proofs (0) does not match number of operations (1)" + "The number of proofs (0) does not match the number of operations (1)" ); } @@ -644,4 +901,103 @@ mod tests { }) )); } + + #[test] + fn helper_backed_verification_accepts_valid_channel_withdraw() { + let channel_id = ChannelId::from([8u8; 32]); + let key0 = Ed25519Key::from_bytes(&[8; 32]); + let key1 = Ed25519Key::from_bytes(&[9; 32]); + let signed_tx = create_withdraw_tx(channel_id, &[&key0, &key1]); + + let helper = TestOperationVerificationHelper::new( + [(channel_id, 2)], + [ + ((channel_id, 0), key0.public_key()), + ((channel_id, 1), key1.public_key()), + ], + ); + + assert!(signed_tx.verify_ops_proofs_with_helper(&helper).is_ok()); + } + + #[test] + fn helper_backed_verification_rejects_missing_channel() { + let channel_id = ChannelId::from([10u8; 32]); + let key0 = Ed25519Key::from_bytes(&[0; 32]); + let signed_tx = create_withdraw_tx(channel_id, &[&key0]); + + let helper = TestOperationVerificationHelper::new([], []); + + let verification_result = signed_tx.verify_ops_proofs_with_helper(&helper); + assert_eq!( + verification_result, + Err(VerificationError::ChannelNotFound { channel_id }) + ); + } + + #[test] + fn helper_backed_verification_rejects_missing_key() { + let channel_id = ChannelId::from([10u8; 32]); + let key0 = Ed25519Key::from_bytes(&[0; 32]); + let key1 = Ed25519Key::from_bytes(&[1; 32]); + let signed_tx = create_withdraw_tx(channel_id, &[&key0, &key1]); + + let helper = TestOperationVerificationHelper::new( + [(channel_id, 2)], + [((channel_id, 0), key0.public_key())], + ); + + let verification_result = signed_tx.verify_ops_proofs_with_helper(&helper); + assert_eq!( + verification_result, + Err(VerificationError::KeyNotFound { + channel_id, + key_index: 1 + }) + ); + } + + #[test] + fn helper_backed_verification_rejects_not_enough_signatures() { + let channel_id = ChannelId::from([10u8; 32]); + let key0 = Ed25519Key::from_bytes(&[0; 32]); + let signed_tx = create_withdraw_tx(channel_id, &[&key0]); + + let helper = TestOperationVerificationHelper::new( + [(channel_id, 2)], + [((channel_id, 0), key0.public_key())], + ); + + let verification_result = signed_tx.verify_ops_proofs_with_helper(&helper); + assert_eq!( + verification_result, + Err(VerificationError::ChannelWithdrawProofNotEnoughSignatures { + op_index: 0, + actual: 1, + required: 2 + }) + ); + } + + #[test] + fn helper_backed_verification_rejects_invalid_signature() { + let channel_id = ChannelId::from([10u8; 32]); + let expected_key = Ed25519Key::from_bytes(&[0; 32]); + let wrong_key = Ed25519Key::from_bytes(&[9; 32]); + let signed_tx = create_withdraw_tx(channel_id, &[&wrong_key]); + + let helper = TestOperationVerificationHelper::new( + [(channel_id, 1)], + [((channel_id, 0), expected_key.public_key())], + ); + + let verification_result = signed_tx.verify_ops_proofs_with_helper(&helper); + assert_eq!( + verification_result, + Err(VerificationError::ChannelWithdrawProofInvalidSignature { + op_index: 0, + signature_index: 0 + }) + ); + } } diff --git a/core/src/mantle/tx_builder.rs b/core/src/mantle/tx_builder.rs index e6430c209..02da092d0 100644 --- a/core/src/mantle/tx_builder.rs +++ b/core/src/mantle/tx_builder.rs @@ -1,21 +1,31 @@ -use std::cmp::Ordering; +use std::{cmp::Ordering, collections::HashMap}; use lb_key_management_system_keys::keys::ZkPublicKey; use super::{GasConstants, GasCost as _, MantleTx, Note, Op, Utxo}; -use crate::mantle::{NoteId, ops::transfer::TransferOp}; +use crate::{ + mantle::{ + NoteId, + ops::{channel::withdraw::ChannelWithdrawOp, transfer::TransferOp}, + tx::MantleTxGasContext, + }, + proofs::channel_withdraw_proof::ChannelWithdrawProof, +}; #[derive(Debug, Clone)] pub struct MantleTxBuilder { mantle_tx: MantleTx, ledger_inputs: Vec, pending_transfer: TransferOp, + // Maps a Proof to its Op by the Op Index + channel_withdraw_proofs: HashMap, + context: MantleTxGasContext, } // TODO: refactor to support more than 32 inputs (more than a single transfer) impl MantleTxBuilder { #[must_use] - pub const fn new() -> Self { + pub fn new(context: MantleTxGasContext) -> Self { Self { mantle_tx: MantleTx { ops: vec![], @@ -24,6 +34,8 @@ impl MantleTxBuilder { }, ledger_inputs: vec![], pending_transfer: TransferOp::new(vec![], vec![]), + channel_withdraw_proofs: HashMap::new(), + context, } } @@ -38,6 +50,14 @@ impl MantleTxBuilder { self } + #[must_use] + pub fn push_channel_withdraw(self, op: ChannelWithdrawOp, proof: ChannelWithdrawProof) -> Self { + let mut builder = self.push_op(Op::ChannelWithdraw(op)); + let index = builder.mantle_tx.ops.len() - 1; + builder.channel_withdraw_proofs.insert(index, proof); + builder + } + #[must_use] pub fn add_ledger_input(self, utxo: Utxo) -> Self { self.extend_ledger_inputs([utxo]) @@ -139,7 +159,7 @@ impl MantleTxBuilder { #[must_use] pub fn gas_cost(&self) -> u64 { let build = self.clone().build(); - build.total_gas_cost::() + build.total_gas_cost::(&self.context) } #[must_use] @@ -167,15 +187,14 @@ impl MantleTxBuilder { &self.ledger_inputs } + #[must_use] + pub const fn channel_withdraw_proofs(&self) -> &HashMap { + &self.channel_withdraw_proofs + } + #[must_use] pub fn build(mut self) -> MantleTx { self.mantle_tx.ops.push(Op::Transfer(self.pending_transfer)); self.mantle_tx } } - -impl Default for MantleTxBuilder { - fn default() -> Self { - Self::new() - } -} diff --git a/core/src/proofs/channel_withdraw_proof.rs b/core/src/proofs/channel_withdraw_proof.rs new file mode 100644 index 000000000..cbe8a35e2 --- /dev/null +++ b/core/src/proofs/channel_withdraw_proof.rs @@ -0,0 +1,122 @@ +use std::cmp::Ordering; + +use lb_key_management_system_keys::keys::Ed25519Signature; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::mantle::ops::channel::ChannelKeyIndex; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct WithdrawSignature { + pub channel_key_index: ChannelKeyIndex, /* Using ChannelKeyIndex ensures indices are + * bounded, and MAX provides an upper limit for the + * number of unique signatures (one per index) */ + pub signature: Ed25519Signature, +} + +impl WithdrawSignature { + #[must_use] + pub const fn new(channel_key_index: ChannelKeyIndex, signature: Ed25519Signature) -> Self { + Self { + channel_key_index, + signature, + } + } +} + +impl From<(ChannelKeyIndex, Ed25519Signature)> for WithdrawSignature { + fn from((index, signature): (ChannelKeyIndex, Ed25519Signature)) -> Self { + Self::new(index, signature) + } +} + +impl PartialOrd for WithdrawSignature { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for WithdrawSignature { + fn cmp(&self, other: &Self) -> Ordering { + self.channel_key_index + .cmp(&other.channel_key_index) + .then_with(|| self.signature.to_bytes().cmp(&other.signature.to_bytes())) + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("Duplicate indices found: {0:?}.")] + DuplicateIndices(Vec), + #[error("Too many signatures: got {actual}, maximum allowed is {maximum}.")] + TooManySignatures { actual: usize, maximum: usize }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ChannelWithdrawProof { + // Invariant: signatures are sorted by index (then signature) with no duplicates + signatures: Vec, +} + +impl ChannelWithdrawProof { + pub fn new(signatures: Vec) -> Result { + let signatures = Self::normalize_signatures(signatures); + Self::validate_well_formedness(&signatures)?; + Ok(Self { signatures }) + } + + /// Sorts and removes duplicate signatures. + /// + /// This is required for the Proof to be well-formed, but it's not + /// sufficient for the Proof to be valid. + fn normalize_signatures(mut signatures: Vec) -> Vec { + signatures.sort_unstable(); + signatures.dedup(); + signatures + } + + /// Validates that the proof is structurally well-formed. + /// + /// Must be called after [`Self::normalize_signatures`]. + /// + /// # Checks + /// + /// - No duplicate indices (each index appears at most once) + /// - Signature count doesn't exceed `ChannelKeyIndex::MAX` + /// + /// # Note + /// + /// This validates structural correctness only. Cryptographic validity + /// (e.g.: signature verification, threshold requirements, index-to-key + /// correspondence) must be checked separately. + fn validate_well_formedness(signatures: &[WithdrawSignature]) -> Result<(), Error> { + let unique_indices = signatures + .iter() + .map(|signature| signature.channel_key_index) + .collect::>(); + if unique_indices.len() != signatures.len() { + return Err(Error::DuplicateIndices(unique_indices)); + } + let max_signatures_allowed = usize::from(ChannelKeyIndex::MAX) + 1; + if signatures.len() > max_signatures_allowed { + return Err(Error::TooManySignatures { + actual: signatures.len(), + maximum: max_signatures_allowed, + }); + } + Ok(()) + } + + #[must_use] + pub const fn signatures(&self) -> &Vec { + &self.signatures + } +} + +impl TryFrom> for ChannelWithdrawProof { + type Error = Error; + + fn try_from(value: Vec) -> Result { + Self::new(value) + } +} diff --git a/core/src/proofs/mod.rs b/core/src/proofs/mod.rs index 732cc8e6d..d35326de6 100644 --- a/core/src/proofs/mod.rs +++ b/core/src/proofs/mod.rs @@ -1,3 +1,4 @@ +pub mod channel_withdraw_proof; pub mod leader_claim_proof; pub mod leader_proof; mod merkle; diff --git a/ledger/src/cryptarchia/mod.rs b/ledger/src/cryptarchia/mod.rs index b4e19c38f..841ff3760 100644 --- a/ledger/src/cryptarchia/mod.rs +++ b/ledger/src/cryptarchia/mod.rs @@ -648,8 +648,8 @@ pub mod tests { use lb_core::{ crypto::{Digest as _, Hasher}, mantle::{ - GasCost as _, MantleTx, Note, Op, OpProof::ZkSig, SignedMantleTx, Transaction as _, - gas::MainnetGasConstants, ops::leader_claim::VoucherCm, + AuthenticatedMantleTx, MantleTx, Note, Op, OpProof::ZkSig, SignedMantleTx, + Transaction as _, gas::MainnetGasConstants, ops::leader_claim::VoucherCm, }, sdp::ServiceParameters, }; @@ -1280,7 +1280,7 @@ pub mod tests { let (tx, transfer_op, transfer_sig) = create_tx_with_transfer(&[(¬e_sk, &input_utxo)], vec![output_note1, output_note2]); - let _fees = tx.total_gas_cost::(); + let _fees = AuthenticatedMantleTx::total_gas_cost::(&tx); let (new_state, balance) = ledger_state .try_apply_transfer::<(), MainnetGasConstants>( &locked_notes, @@ -1316,7 +1316,7 @@ pub mod tests { vec![], ); let locked_notes = LockedNotes::new(); - let _fees = tx.total_gas_cost::(); + let _fees = AuthenticatedMantleTx::total_gas_cost::(&tx); let (final_state, final_balance) = new_state .try_apply_transfer::<(), MainnetGasConstants>( &locked_notes, @@ -1444,7 +1444,7 @@ pub mod tests { let (tx, transfer_op, transfer_sig) = create_tx_with_transfer(&[(&input_sk, &input_utxo)], vec![]); - let _fees = tx.total_gas_cost::(); + let _fees = AuthenticatedMantleTx::total_gas_cost::(&tx); let result = ledger_state.try_apply_transfer::<(), MainnetGasConstants>( &locked_notes, &transfer_op, diff --git a/ledger/src/lib.rs b/ledger/src/lib.rs index 7d42e8032..884eb73ae 100644 --- a/ledger/src/lib.rs +++ b/ledger/src/lib.rs @@ -2,7 +2,7 @@ mod config; // The ledger is split into two modules: // - `cryptarchia`: the base functionalities needed by the Cryptarchia consensus // algorithm, including a minimal UTxO model. -// - `mantle_ops` : our extensions in the form of Mantle operations, e.g. SDP. +// - `mantle_ops`: our extensions in the form of Mantle operations, e.g. SDP. pub mod cryptarchia; pub mod mantle; @@ -14,7 +14,7 @@ pub use cryptarchia::{EpochState, UtxoTree}; use lb_core::{ block::BlockNumber, mantle::{ - AuthenticatedMantleTx, GenesisTx, NoteId, Op, OpProof, Utxo, Value, + AuthenticatedMantleTx, GenesisTx, NoteId, Op, OpProof, Utxo, Value, VerificationError, gas::{Gas, GasConstants}, }, proofs::leader_proof, @@ -25,6 +25,8 @@ use lb_groth16::{Field as _, Fr}; use mantle::LedgerState as MantleLedger; use thiserror::Error; +use crate::mantle::helpers::MantleOperationVerificationHelper; + const WINDOW_SIZE: usize = 120; /// Denominator of 1/(`I_max` * `D1_target` * `Delta_t` * `T`) @@ -102,6 +104,8 @@ pub enum LedgerError { TooMuchExecutionGas { gas: Gas, limit: Gas }, #[error("Storage fees aren't equal to the storage fee of the current epoch")] InvalidStoragePrice, + #[error("Verification error: {0}")] + VerificationError(#[from] VerificationError), } #[derive(Clone, Debug, PartialEq)] @@ -328,16 +332,17 @@ impl LedgerState { (self, balance) = self.try_apply_tx::<_, Constants>(config, &tx)?; // Check the transaction is balanced - match balance.cmp(&tx.total_gas_cost::().into()) { + let total_gas_cost = AuthenticatedMantleTx::total_gas_cost::(&tx); + match balance.cmp(&total_gas_cost.into()) { Ordering::Less => return Err(LedgerError::InsufficientBalance), Ordering::Greater => return Err(LedgerError::UnbalancedTransaction), Ordering::Equal => {} // OK! } // Update the total of fee burned and tipped in the block - let tx_fee_burned = tx.execution_gas_consumption::() + let tx_fee_burned = AuthenticatedMantleTx::execution_gas_consumption::(&tx) * self.cryptarchia_ledger.execution_base_fee() - + tx.storage_gas_cost(); + + AuthenticatedMantleTx::storage_gas_cost(&tx); // Check that the transaction at least pays for the base fee if balance < tx_fee_burned.into() { @@ -353,7 +358,8 @@ impl LedgerState { let tx_fee_tip = balance as Gas - tx_fee_burned; total_fee_burned += tx_fee_burned; total_fee_tip += tx_fee_tip; - total_block_execution_gas += &tx.execution_gas_consumption::(); + total_block_execution_gas += + &AuthenticatedMantleTx::execution_gas_consumption::(&tx); // Check that the block is not exceeding the Gas limit if total_block_execution_gas > EXECUTION_GAS_LIMIT { @@ -462,15 +468,47 @@ impl LedgerState { self.mantle_ledger.active_sessions() } + /// Applies a transaction to the ledger state, returning the updated state + /// and the net balance change. + /// + /// # Prerequisites + /// + /// A transaction must not be applied unless all required proofs have been + /// fully verified. + /// + /// Proof verification is currently split across multiple paths depending on + /// the operation: + /// - `SignedMantleTx::verify_ops_proofs`: Invoked during construction + /// (`SignedMantleTx::new`, e.g. on deserialization). Handles: + /// `ChannelInscribe`, `LeaderClaim`. + /// - `SignedMantleTx::verify_ops_proofs_with_helper`: Invoked here before + /// applying the transaction. Handles: `ChannelWithdraw`. + /// - Additional validation: Performed by the ledger or implicitly satisfied + /// by certain operations. + /// + /// This fragmented design means verification may be: + /// - Distributed across different stages, and + /// - Potentially duplicated or missed if assumptions about prior + /// verification are incorrect. + /// + /// Callers are responsible for ensuring that all required proofs have been + /// verified before applying the transaction. + /// + /// TODO: A refactor into a typed state model to enforce verification at + /// compile is planned. fn try_apply_tx( mut self, config: &Config, tx: impl AuthenticatedMantleTx, ) -> Result<(Self, Balance), LedgerError> { + let operation_verification_helper = + MantleOperationVerificationHelper::new(&self.mantle_ledger); + tx.verify_ops_proofs_with_helper(&operation_verification_helper) + .map_err(LedgerError::VerificationError)?; + let mut balance: Balance = 0; let tx_hash = tx.hash(); - let ops = tx.ops_with_proof().map(|(op, proof)| (op, Some(proof))); - for (op, proof) in ops { + for (op, proof) in tx.ops_with_proof() { match (op, proof) { // The signature for channel ops can be verified before reaching this point, // as you only need the signer's public key and tx hash @@ -478,12 +516,12 @@ impl LedgerState { (Op::ChannelInscribe(op), _) => { self.mantle_ledger = self.mantle_ledger.try_apply_channel_inscription(op)?; } - (Op::ChannelSetKeys(op), Some(OpProof::Ed25519Sig(sig))) => { + (Op::ChannelSetKeys(op), OpProof::Ed25519Sig(sig)) => { self.mantle_ledger = self .mantle_ledger .try_apply_channel_set_keys(op, sig, &tx_hash)?; } - (Op::ChannelDeposit(op), Some(OpProof::NoProof)) => { + (Op::ChannelDeposit(op), OpProof::NoProof) => { let deposit_amount; (self.mantle_ledger, deposit_amount) = self.mantle_ledger.try_apply_channel_deposit(op)?; @@ -491,12 +529,20 @@ impl LedgerState { .checked_sub(deposit_amount.into()) .ok_or(LedgerError::BalanceOverflow)?; } + (Op::ChannelWithdraw(op), OpProof::ChannelWithdrawProof(_proof)) => { + let withdraw_amount; + (self.mantle_ledger, withdraw_amount) = + self.mantle_ledger.try_apply_channel_withdraw(op)?; + balance = balance + .checked_add(withdraw_amount.into()) + .ok_or(LedgerError::BalanceOverflow)?; + } ( Op::SDPDeclare(op), - Some(OpProof::ZkAndEd25519Sigs { + OpProof::ZkAndEd25519Sigs { zk_sig, ed25519_sig, - }), + }, ) => { self.mantle_ledger = self.mantle_ledger.try_apply_sdp_declaration( op, @@ -507,17 +553,17 @@ impl LedgerState { config, )?; } - (Op::SDPActive(op), Some(OpProof::ZkSig(sig))) => { + (Op::SDPActive(op), OpProof::ZkSig(sig)) => { self.mantle_ledger = self .mantle_ledger .try_apply_sdp_active(op, sig, tx_hash, config)?; } - (Op::SDPWithdraw(op), Some(OpProof::ZkSig(sig))) => { + (Op::SDPWithdraw(op), OpProof::ZkSig(sig)) => { self.mantle_ledger = self .mantle_ledger .try_apply_sdp_withdraw(op, sig, tx_hash, config)?; } - (Op::LeaderClaim(op), None) => { + (Op::LeaderClaim(op), OpProof::PoC(_)) => { // Correct derivation of the voucher nullifier and membership in the merkle tree // can be verified outside of this function since public inputs are already // available. Callers are expected to validate the proof @@ -528,7 +574,7 @@ impl LedgerState { .checked_add(reward.into()) .ok_or(LedgerError::BalanceOverflow)?; } - (Op::Transfer(op), Some(OpProof::ZkSig(sig))) => { + (Op::Transfer(op), OpProof::ZkSig(sig)) => { let transfer_balance; (self.cryptarchia_ledger, transfer_balance) = self.cryptarchia_ledger.try_apply_transfer::<_, Constants>( @@ -553,15 +599,19 @@ impl LedgerState { #[cfg(test)] mod tests { use cryptarchia::tests::{config, generate_proof, utxo}; - use lb_core::mantle::{ - GasCost as _, MantleTx, Note, SignedMantleTx, Transaction as _, - gas::MainnetGasConstants, - ops::{ - channel::{ - ChannelId, MsgId, deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp, + use lb_core::{ + mantle::{ + MantleTx, Note, SignedMantleTx, Transaction as _, + gas::MainnetGasConstants, + ops::{ + channel::{ + ChannelId, MsgId, deposit::DepositOp, inscribe::InscriptionOp, + set_keys::SetKeysOp, withdraw::ChannelWithdrawOp, + }, + transfer::TransferOp, }, - transfer::TransferOp, }, + proofs::channel_withdraw_proof::{ChannelWithdrawProof, WithdrawSignature}, }; use lb_key_management_system_keys::keys::{Ed25519Key, Ed25519PublicKey, ZkKey, ZkPublicKey}; use num_bigint::BigUint; @@ -613,6 +663,8 @@ mod tests { enum Key { Ed25519(Ed25519Key), Zk(ZkKey), + EmptyZk, + Withdraw(ChannelWithdrawProof), None, } @@ -638,6 +690,8 @@ mod tests { Key::Zk(key) => OpProof::ZkSig( ZkKey::multi_sign(std::slice::from_ref(key), tx_hash.as_ref()).unwrap(), ), + Key::EmptyZk => OpProof::ZkSig(ZkKey::multi_sign(&[], tx_hash.as_ref()).unwrap()), + Key::Withdraw(proof) => OpProof::ChannelWithdrawProof(proof.clone()), Key::None => OpProof::NoProof, }) .collect(); @@ -692,7 +746,7 @@ mod tests { 1, 1, ); - let fees = tx.total_gas_cost::(); + let fees = AuthenticatedMantleTx::total_gas_cost::(&tx); output_note.value = utxo.note.value - fees; let tx = create_tx(vec![utxo.id()], vec![output_note], &[sk], 1, 1); @@ -851,6 +905,205 @@ mod tests { assert_eq!(balance, Balance::from(0)); } + #[test] + fn test_channel_withdraw_operation() { + let test_config = config(); + let (sk, utxo) = utxo_with_sk(); + let mut ledger_state = LedgerState::from_utxos([utxo], &test_config); + let (signing_key, verifying_key) = create_test_keys(); + let channel_id = ChannelId::from([9; 32]); + + ledger_state = create_channel( + ledger_state, + &test_config, + channel_id, + &signing_key, + verifying_key, + ); + + // Deposit some funds into the channel + let deposit = DepositOp { + channel_id, + amount: 10, + metadata: vec![5, 6, 7, 8], + }; + let deposit_ops = vec![ + Op::ChannelDeposit(deposit.clone()), + Op::Transfer(TransferOp { + inputs: vec![utxo.id()], + outputs: vec![Note::new( + utxo.note.value - deposit.amount, + sk.to_public_key(), + )], + }), + ]; + ledger_state = ledger_state + .try_apply_tx::( + &test_config, + create_multi_signed_tx(deposit_ops, vec![&Key::None, &Key::Zk(sk)]), + ) + .unwrap() + .0; + + // Withdraw some funds from the channel + let withdraw = ChannelWithdrawOp { + channel_id, + amount: 6, + }; + let recipient_sk = ZkKey::from(BigUint::from(99u8)); + let recipient_pk = recipient_sk.to_public_key(); + let transfer_op = TransferOp { + inputs: vec![], + outputs: vec![Note::new(withdraw.amount, recipient_pk)], + }; + let withdraw_tx = MantleTx { + ops: vec![ + Op::ChannelWithdraw(withdraw.clone()), + Op::Transfer(transfer_op.clone()), + ], + execution_gas_price: 0, + storage_gas_price: 0, + }; + let withdraw_tx_hash = withdraw_tx.hash(); + let withdraw_proof = ChannelWithdrawProof::new(vec![WithdrawSignature::new( + 0, + signing_key.sign_payload(withdraw_tx_hash.as_signing_bytes().as_ref()), + )]) + .unwrap(); + + let signed_tx = create_multi_signed_tx( + withdraw_tx.ops, + vec![&Key::Withdraw(withdraw_proof), &Key::EmptyZk], + ); + + let result = + ledger_state.try_apply_tx::(&test_config, signed_tx); + assert!(result.is_ok()); + + let (new_state, tx_balance) = result.unwrap(); + assert_eq!(tx_balance, 0); + let channel_balance = new_state + .mantle_ledger() + .channels() + .channels + .get(&channel_id) + .unwrap() + .balance; + assert_eq!(channel_balance, 4); + let output_utxo = transfer_op.utxo_by_index(0).unwrap(); + assert_eq!(output_utxo.note.value, withdraw.amount); + assert_eq!(output_utxo.note.pk, recipient_sk.to_public_key()); + assert!(new_state.latest_utxos().contains(&output_utxo.id())); + } + + #[test] + fn test_channel_withdraw_invalid_helper_backed_proof_fails_on_apply() { + let test_config = config(); + let (sk, utxo) = utxo_with_sk(); + let mut ledger_state = LedgerState::from_utxos([utxo], &test_config); + let (signing_key, verifying_key) = create_test_keys(); + let channel_id = ChannelId::from([10; 32]); + + ledger_state = create_channel( + ledger_state, + &test_config, + channel_id, + &signing_key, + verifying_key, + ); + + // Deposit some funds into the channel + let deposit = DepositOp { + channel_id, + amount: 10, + metadata: vec![], + }; + let deposit_ops = vec![ + Op::ChannelDeposit(deposit.clone()), + Op::Transfer(TransferOp { + inputs: vec![utxo.id()], + outputs: vec![Note::new( + utxo.note.value - deposit.amount, + sk.to_public_key(), + )], + }), + ]; + ledger_state = ledger_state + .try_apply_tx::( + &test_config, + create_multi_signed_tx(deposit_ops, vec![&Key::None, &Key::Zk(sk)]), + ) + .unwrap() + .0; + let channel_balance_after_deposit = ledger_state + .mantle_ledger() + .channels() + .channels + .get(&channel_id) + .unwrap() + .balance; + + // Try to withdraw some funds from the channel, but with an invalid proof + let withdraw = ChannelWithdrawOp { + channel_id, + amount: 6, + }; + let wrong_key = Ed25519Key::from_bytes(&[42; 32]); + let recipient_sk = ZkKey::from(BigUint::from(100u8)); + let recipient_pk = recipient_sk.to_public_key(); + let transfer_op = TransferOp { + inputs: vec![], + outputs: vec![Note::new(withdraw.amount, recipient_pk)], + }; + let withdraw_tx = MantleTx { + ops: vec![ + Op::ChannelWithdraw(withdraw), + Op::Transfer(transfer_op.clone()), + ], + execution_gas_price: 0, + storage_gas_price: 0, + }; + let withdraw_tx_hash = withdraw_tx.hash(); + let invalid_proof = ChannelWithdrawProof::new(vec![WithdrawSignature::new( + 0, + wrong_key.sign_payload(withdraw_tx_hash.as_signing_bytes().as_ref()), + )]) + .unwrap(); + + let signed_tx = create_multi_signed_tx( + withdraw_tx.ops, + vec![&Key::Withdraw(invalid_proof), &Key::EmptyZk], + ); + + let result = ledger_state + .clone() + .try_apply_tx::(&test_config, signed_tx); + assert_eq!( + result, + Err(LedgerError::VerificationError( + VerificationError::ChannelWithdrawProofInvalidSignature { + op_index: 0, + signature_index: 0, + } + )) + ); + + let channel_balance_after_withdraw = ledger_state + .mantle_ledger() + .channels() + .channels + .get(&channel_id) + .unwrap() + .balance; + assert_eq!(channel_balance_after_deposit, 10); + assert_eq!( + channel_balance_after_deposit, + channel_balance_after_withdraw + ); + let recipient_utxo = transfer_op.utxo_by_index(0).unwrap(); + assert!(!ledger_state.latest_utxos().contains(&recipient_utxo.id())); + } + #[test] fn test_invalid_parent_error() { let test_config = config(); @@ -1104,7 +1357,7 @@ mod tests { 1, 0, ); - let fees = tx.total_gas_cost::(); + let fees = AuthenticatedMantleTx::total_gas_cost::(&tx); output_note.value = utxo.note.value - fees; let tx = create_tx(vec![utxo.id()], vec![output_note], &[sk], 1, 0); @@ -1129,7 +1382,7 @@ mod tests { 1, ); // Pays 2925 fees = 2705 execution base fee + 0 execution tip + 220 storage - let fees = tx.total_gas_cost::(); + let fees = AuthenticatedMantleTx::total_gas_cost::(&tx); output_note.value = utxo.note.value - fees; let tx = create_tx(vec![utxo.id()], vec![output_note], &[sk], 1, 1); @@ -1165,7 +1418,7 @@ mod tests { ); // The tx ays 2925 fees = 2705 execution base fee + 0 execution tip + 220 // storage - let fees = tx.total_gas_cost::(); + let fees = AuthenticatedMantleTx::total_gas_cost::(&tx); output_note.value = utxo.note.value - fees; let tx = create_tx( vec![utxo.id()], @@ -1190,7 +1443,7 @@ mod tests { ); // The tx ays 5630 fees = 2705 execution base fee + 2705 execution tip + 220 // storage - let fees = tx.total_gas_cost::(); + let fees = AuthenticatedMantleTx::total_gas_cost::(&tx); output_note.value = utxo.note.value - fees; let tx = create_tx(vec![utxo.id()], vec![output_note], &[sk], 2, 1); let result = ledger diff --git a/ledger/src/mantle/channel.rs b/ledger/src/mantle/channel.rs index 902de1282..9b696772f 100644 --- a/ledger/src/mantle/channel.rs +++ b/ledger/src/mantle/channel.rs @@ -3,9 +3,10 @@ use std::sync::Arc; use lb_core::mantle::{ TxHash, Value, ops::channel::{ - ChannelId, Ed25519PublicKey as PublicKey, MsgId, deposit::DepositOp, - inscribe::InscriptionOp, set_keys::SetKeysOp, + ChannelId, ChannelKeyIndex, Ed25519PublicKey as PublicKey, MsgId, deposit::DepositOp, + inscribe::InscriptionOp, set_keys::SetKeysOp, withdraw::ChannelWithdrawOp, }, + tx::MantleTxGasContext, }; use lb_key_management_system_keys::keys::Ed25519Signature; #[cfg(feature = "serde")] @@ -30,6 +31,8 @@ pub enum Error { EmptyKeys { channel_id: ChannelId }, #[error("Channel {channel_id:?} not found")] ChannelNotFound { channel_id: ChannelId }, + #[error("Insufficient funds")] + InsufficientFunds, #[error("Balance overflow")] BalanceOverflow, } @@ -40,15 +43,31 @@ pub struct Channels { pub channels: rpds::HashTrieMapSync, } +impl From<&Channels> for MantleTxGasContext { + fn from(value: &Channels) -> Self { + let withdraw_thresholds = value + .channels + .iter() + .map(|(channel_id, channel)| (*channel_id, channel.withdraw_threshold)) + .collect(); + Self::new(withdraw_thresholds) + } +} + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct ChannelState { pub tip: MsgId, // avoid cloning the keys every new message - pub keys: Arc<[PublicKey]>, + pub keys: Arc<[PublicKey]>, // keys.len() <= ChannelKeyIndex::MAX pub balance: Value, + // Indicating how many accredited keys are required to withdraw + // funds from the channel. + pub withdraw_threshold: ChannelKeyIndex, } +const DEFAULT_WITHDRAW_THRESHOLD: ChannelKeyIndex = 1; + impl Default for Channels { fn default() -> Self { Self::new() @@ -75,6 +94,7 @@ impl Channels { tip: MsgId::root(), keys: vec![*signer].into(), balance: 0, + withdraw_threshold: DEFAULT_WITHDRAW_THRESHOLD, }); if *parent != channel.tip { @@ -98,11 +118,13 @@ impl Channels { tip: msg, keys: Arc::clone(&channel.keys), balance: channel.balance, + withdraw_threshold: channel.withdraw_threshold, }, ); Ok(self) } + // TODO: Replace with CHANNEL_CONFIG op: https://github.com/logos-blockchain/logos-blockchain/issues/2461 pub fn set_keys( mut self, channel_id: ChannelId, @@ -129,6 +151,9 @@ impl Channels { tip: MsgId::root(), keys: op.keys.clone().into(), balance: 0, + // TODO: Replace with `ChannelConfig.withdraw_threshold` + // once this op is replaced with CHANNEL_CONFIG op: https://github.com/logos-blockchain/logos-blockchain/issues/2461 + withdraw_threshold: DEFAULT_WITHDRAW_THRESHOLD, }, ); } @@ -150,6 +175,20 @@ impl Channels { } } + pub fn withdraw(mut self, op: &ChannelWithdrawOp) -> Result { + if let Some(channel) = self.channels.get_mut(&op.channel_id) { + channel.balance = channel + .balance + .checked_sub(op.amount) + .ok_or(Error::InsufficientFunds)?; + Ok(self) + } else { + Err(Error::ChannelNotFound { + channel_id: op.channel_id, + }) + } + } + #[must_use] pub fn new() -> Self { Self { @@ -162,3 +201,120 @@ impl Channels { self.channels.get(channel_id) } } + +#[cfg(test)] +mod tests { + use lb_key_management_system_keys::keys::Ed25519Key; + + use super::*; + + fn test_public_key(seed: u8) -> PublicKey { + Ed25519Key::from_bytes(&[seed; 32]).public_key() + } + + impl Channels { + #[must_use] + pub fn with_balance(channel_id: ChannelId, balance: Value) -> Self { + Self { + channels: rpds::HashTrieMapSync::new_sync().insert( + channel_id, + ChannelState { + tip: MsgId::root(), + keys: vec![test_public_key(7)].into(), + balance, + withdraw_threshold: 1, + }, + ), + } + } + } + + #[test] + fn channels_to_gas_context_tracks_withdraw_thresholds() { + let first_id = ChannelId::from([1u8; 32]); + let second_id = ChannelId::from([2u8; 32]); + let missing_id = ChannelId::from([0u8; 32]); + + let channels = Channels { + channels: rpds::HashTrieMapSync::new_sync() + .insert( + first_id, + ChannelState { + tip: MsgId::root(), + keys: vec![test_public_key(11)].into(), + balance: 5, + withdraw_threshold: 1, + }, + ) + .insert( + second_id, + ChannelState { + tip: MsgId::root(), + keys: vec![test_public_key(22), test_public_key(23)].into(), + balance: 9, + withdraw_threshold: 2, + }, + ), + }; + + let gas_context = MantleTxGasContext::from(&channels); + + assert_eq!(gas_context.withdraw_threshold(&first_id), Some(1)); + assert_eq!(gas_context.withdraw_threshold(&second_id), Some(2)); + assert_eq!(gas_context.withdraw_threshold(&missing_id), None); + } + + #[test] + fn deposit_increases_channel_balance() { + let channel_id = ChannelId::from([0u8; 32]); + let channels = Channels::with_balance(channel_id, 10); + + let updated = channels + .deposit(&DepositOp { + channel_id, + amount: 6, + metadata: vec![], + }) + .expect("deposit should succeed"); + + assert_eq!(updated.channel_state(&channel_id).unwrap().balance, 16); + } + + #[test] + fn withdraw_decreases_channel_balance() { + let channel_id = ChannelId::from([0u8; 32]); + let channels = Channels::with_balance(channel_id, 10); + + let updated = channels + .withdraw(&ChannelWithdrawOp { + channel_id, + amount: 6, + }) + .expect("withdraw should succeed"); + + assert_eq!(updated.channel_state(&channel_id).unwrap().balance, 4); + } + + #[test] + fn withdraw_fails_with_insufficient_funds() { + let channel_id = ChannelId::from([0u8; 32]); + let channels = Channels::with_balance(channel_id, 3); + + let result = channels.withdraw(&ChannelWithdrawOp { + channel_id, + amount: 6, + }); + + assert!(matches!(result, Err(Error::InsufficientFunds))); + } + + #[test] + fn withdraw_fails_for_missing_channel() { + let result = Channels::new().withdraw(&ChannelWithdrawOp { + channel_id: ChannelId::from([0u8; 32]), + amount: 1, + }); + + assert!(matches!(result, Err(Error::ChannelNotFound { .. }))); + } +} diff --git a/ledger/src/mantle/helpers.rs b/ledger/src/mantle/helpers.rs new file mode 100644 index 000000000..d3b82d557 --- /dev/null +++ b/ledger/src/mantle/helpers.rs @@ -0,0 +1,53 @@ +use lb_core::mantle::{ + ops::channel::{ChannelId, ChannelKeyIndex}, + tx::{OperationVerificationHelper, VerificationError}, +}; +use lb_key_management_system_keys::keys::Ed25519PublicKey; + +use crate::mantle::LedgerState; + +pub struct MantleOperationVerificationHelper<'a> { + ledger_state: &'a LedgerState, +} + +impl<'a> MantleOperationVerificationHelper<'a> { + #[must_use] + pub const fn new(ledger_state: &'a LedgerState) -> Self { + Self { ledger_state } + } +} + +impl OperationVerificationHelper for MantleOperationVerificationHelper<'_> { + fn get_channel_withdraw_threshold( + &self, + channel_id: &ChannelId, + ) -> Result { + self.ledger_state + .channels() + .channel_state(channel_id) + .ok_or(VerificationError::ChannelNotFound { + channel_id: *channel_id, + }) + .map(|channel_state| channel_state.withdraw_threshold) + } + + fn get_key_from_channel_at_index( + &self, + channel_id: &ChannelId, + key_index: &ChannelKeyIndex, + ) -> Result { + self.ledger_state + .channels() + .channel_state(channel_id) + .ok_or(VerificationError::ChannelNotFound { + channel_id: *channel_id, + })? + .keys + .get(*key_index as usize) + .ok_or(VerificationError::KeyNotFound { + channel_id: *channel_id, + key_index: *key_index, + }) + .cloned() + } +} diff --git a/ledger/src/mantle/mod.rs b/ledger/src/mantle/mod.rs index b752124f8..923db18dc 100644 --- a/ledger/src/mantle/mod.rs +++ b/ledger/src/mantle/mod.rs @@ -1,4 +1,5 @@ pub mod channel; +pub mod helpers; pub mod leader; pub mod sdp; @@ -9,7 +10,10 @@ use lb_core::{ mantle::{ GenesisTx, NoteId, TxHash, Utxo, Value, ops::{ - channel::{deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp}, + channel::{ + deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp, + withdraw::ChannelWithdrawOp, + }, leader_claim::{LeaderClaimOp, RewardsRoot, VoucherCm}, sdp::{SDPActiveOp, SDPDeclareOp, SDPWithdrawOp}, }, @@ -184,6 +188,16 @@ impl LedgerState { Ok((self, op.amount)) } + pub fn try_apply_channel_withdraw( + mut self, + op: &ChannelWithdrawOp, + ) -> Result<(Self, Value), Error> { + self.channels = self.channels.withdraw(op).inspect_err( + |err| error!(target: LOG_TARGET, %err, "Failed to apply the Channel Withdraw message."), + )?; + Ok((self, op.amount)) + } + pub fn try_apply_sdp_declaration( mut self, sdp_declare_op: &SDPDeclareOp, diff --git a/nodes/api-common/src/bodies/wallet.rs b/nodes/api-common/src/bodies/wallet.rs index 528fc64ca..251be3435 100644 --- a/nodes/api-common/src/bodies/wallet.rs +++ b/nodes/api-common/src/bodies/wallet.rs @@ -1,9 +1,14 @@ pub mod balance { + use std::collections::HashMap; + use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; - use lb_core::{header::HeaderId, mantle::Value}; + use lb_core::{ + header::HeaderId, + mantle::{NoteId, Value}, + }; use lb_key_management_system_keys::keys::ZkPublicKey; use serde::{Deserialize, Serialize}; use tracing::error; @@ -12,6 +17,7 @@ pub mod balance { pub struct WalletBalanceResponseBody { pub tip: HeaderId, pub balance: Value, + pub notes: HashMap, pub address: ZkPublicKey, } diff --git a/nodes/api-common/src/paths.rs b/nodes/api-common/src/paths.rs index 24e59a321..cfb9a6a20 100644 --- a/nodes/api-common/src/paths.rs +++ b/nodes/api-common/src/paths.rs @@ -7,6 +7,7 @@ pub const CRYPTARCHIA_LIB_STREAM: &str = "/cryptarchia/lib-stream"; pub const NETWORK_INFO: &str = "/network/info"; pub const STORAGE_BLOCK: &str = "/storage/block"; pub const MEMPOOL_ADD_TX: &str = "/mempool/add/tx"; +pub const CHANNEL: &str = "/channel/:id"; pub const CHANNEL_DEPOSIT: &str = "/channel/deposit"; pub const SDP_POST_DECLARATION: &str = "/sdp/declaration"; pub const SDP_POST_ACTIVITY: &str = "/sdp/activity"; diff --git a/nodes/node/binary/src/api/backend.rs b/nodes/node/binary/src/api/backend.rs index 6695bb7ff..9f9cc8027 100644 --- a/nodes/node/binary/src/api/backend.rs +++ b/nodes/node/binary/src/api/backend.rs @@ -47,7 +47,8 @@ use crate::{ WalletService, api::{ handlers::{ - channel_deposit, leader_claim, post_activity, post_declaration, post_withdrawal, + channel, channel_deposit, leader_claim, post_activity, post_declaration, + post_withdrawal, }, openapi::ApiDoc, }, @@ -217,6 +218,7 @@ where paths::MEMPOOL_ADD_TX, routing::post(add_tx::), ) + .route(paths::CHANNEL, routing::get(channel::)) .route( paths::CHANNEL_DEPOSIT, routing::post( diff --git a/nodes/node/binary/src/api/handlers.rs b/nodes/node/binary/src/api/handlers.rs index b54ce561c..495396478 100644 --- a/nodes/node/binary/src/api/handlers.rs +++ b/nodes/node/binary/src/api/handlers.rs @@ -20,7 +20,8 @@ use lb_core::{ block::Block, header::HeaderId, mantle::{ - Op, SignedMantleTx, Transaction, gas::MainnetGasConstants, tx_builder::MantleTxBuilder, + Op, SignedMantleTx, Transaction, gas::MainnetGasConstants, ops::channel::ChannelId, + tx_builder::MantleTxBuilder, }, }; use lb_http_api_common::{ @@ -365,6 +366,25 @@ where >(&handle, tx, Transaction::hash)) } +#[utoipa::path( + get, + path = paths::CHANNEL, + responses( + (status = 200, description = "Channel state"), + (status = 500, description = "Internal server error", body = String), + ) +)] +pub async fn channel( + State(handle): State>, + Path(id): Path, +) -> Response +where + RuntimeServiceId: + Debug + Send + Sync + Display + 'static + AsServiceId>, +{ + make_request_and_return_response!(mantle::channel::(&handle, id)) +} + #[utoipa::path( post, path = paths::CHANNEL_DEPOSIT, @@ -418,7 +438,8 @@ where handle.relay::().await?, ); - let tx_builder = MantleTxBuilder::new() + let gas_context = wallet.get_gas_context(None).await?; + let tx_builder = MantleTxBuilder::new(gas_context) .push_op(Op::ChannelDeposit(req.deposit)) .push_op(Op::Transfer(req.burn)); let lb_wallet_service::TipResponse { @@ -705,7 +726,8 @@ pub mod wallet { response: Some(balance), }) => WalletBalanceResponseBody { tip, - balance, + balance: balance.balance, + notes: balance.notes, address, } .into_response(), diff --git a/services/api/Cargo.toml b/services/api/Cargo.toml index f01a57561..56c966f0e 100644 --- a/services/api/Cargo.toml +++ b/services/api/Cargo.toml @@ -28,6 +28,7 @@ lb-chain-broadcast-service = { workspace = true } lb-chain-leader-service = { workspace = true } lb-chain-service = { features = ["libp2p"], workspace = true } lb-core = { workspace = true } +lb-ledger = { workspace = true } lb-network-service = { workspace = true } lb-sdp-service = { workspace = true } lb-storage-service = { features = ["rocksdb-backend"], workspace = true } diff --git a/services/api/src/http/consensus/cryptarchia.rs b/services/api/src/http/consensus/cryptarchia.rs index 51790e0ef..9a5af384d 100644 --- a/services/api/src/http/consensus/cryptarchia.rs +++ b/services/api/src/http/consensus/cryptarchia.rs @@ -2,6 +2,7 @@ use std::fmt::{Debug, Display}; use lb_chain_service::{ConsensusMsg, CryptarchiaConsensus, CryptarchiaInfo}; use lb_core::{header::HeaderId, mantle::SignedMantleTx}; +use lb_ledger::LedgerState; use lb_storage_service::backends::rocksdb::RocksBackend; use lb_time_service::backends::ntp::NtpTimeBackend; use overwatch::{overwatch::handle::OverwatchHandle, services::AsServiceId}; @@ -51,3 +52,27 @@ where Ok(receiver.await?) } + +pub async fn cryptarchia_ledger_state( + handle: &OverwatchHandle, +) -> Result +where + RuntimeServiceId: + Debug + Send + Sync + Display + 'static + AsServiceId>, +{ + let info = cryptarchia_info(handle).await?; + + let relay = handle.relay().await?; + let (sender, receiver) = oneshot::channel(); + relay + .send(ConsensusMsg::GetLedgerState { + block_id: info.tip, + tx: sender, + }) + .await + .map_err(|(e, _)| e)?; + + receiver + .await? + .ok_or_else(|| "ledger state for tip must exist".into()) +} diff --git a/services/api/src/http/consensus/mod.rs b/services/api/src/http/consensus/mod.rs index f562efc98..d3136c243 100644 --- a/services/api/src/http/consensus/mod.rs +++ b/services/api/src/http/consensus/mod.rs @@ -1,3 +1,4 @@ mod cryptarchia; pub mod leader; +pub(crate) use cryptarchia::cryptarchia_ledger_state; pub use cryptarchia::{Cryptarchia, cryptarchia_headers, cryptarchia_info}; diff --git a/services/api/src/http/mantle.rs b/services/api/src/http/mantle.rs index 2d1694d93..894315b57 100644 --- a/services/api/src/http/mantle.rs +++ b/services/api/src/http/mantle.rs @@ -11,9 +11,10 @@ use lb_chain_service::{ use lb_core::{ block::Block, header::HeaderId, - mantle::{SignedMantleTx, Transaction, TxHash}, + mantle::{SignedMantleTx, Transaction, TxHash, ops::channel::ChannelId}, sdp::Declaration, }; +use lb_ledger::mantle::channel::ChannelState; use lb_storage_service::{ StorageMsg, StorageService, api::{ @@ -31,6 +32,8 @@ use serde::{Serialize, de::DeserializeOwned}; use tokio::sync::oneshot; use tokio_stream::wrappers::BroadcastStream; +use crate::http::consensus::{Cryptarchia, cryptarchia_ledger_state}; + /// A block along with the current chain state (tip and LIB) at the time it was /// processed. This allows clients to track the canonical chain without needing /// to poll /cryptarchia/info. @@ -56,6 +59,23 @@ pub type MempoolService = TxMempoolService< RuntimeServiceId, >; +pub async fn channel( + handle: &overwatch::overwatch::handle::OverwatchHandle, + id: ChannelId, +) -> Result +where + RuntimeServiceId: + Debug + Send + Sync + Display + 'static + AsServiceId>, +{ + let ledger_state = cryptarchia_ledger_state(handle).await?; + ledger_state + .mantle_ledger() + .channels() + .channel_state(&id) + .cloned() + .ok_or_else(|| "channel not found".into()) +} + pub async fn mantle_mempool_metrics( handle: &overwatch::overwatch::handle::OverwatchHandle, ) -> Result @@ -349,16 +369,10 @@ pub async fn get_sdp_declarations( handle: &overwatch::overwatch::handle::OverwatchHandle, ) -> Result, super::DynError> where - RuntimeServiceId: Debug - + Send - + Sync - + Display - + 'static - + AsServiceId>, + RuntimeServiceId: + Debug + Send + Sync + Display + 'static + AsServiceId>, { - let relay = handle - .relay::>() - .await?; + let relay = handle.relay::>().await?; let (sender, receiver) = oneshot::channel(); relay diff --git a/services/chain/chain-leader/src/lib.rs b/services/chain/chain-leader/src/lib.rs index 512efacc7..4a4ae9e18 100644 --- a/services/chain/chain-leader/src/lib.rs +++ b/services/chain/chain-leader/src/lib.rs @@ -81,7 +81,7 @@ pub enum Error { #[error("Failed to create valid block during proposal: {0}")] BlockCreation(#[from] BlockError), #[error("Wallet API error: {0}")] - Wallet(#[from] WalletApiError), + Wallet(#[from] Box), #[error("Leader wallet error: {0}")] LeaderWallet(#[from] LeaderWalletError), #[error("Mempool error: {0}")] @@ -94,6 +94,12 @@ pub enum Error { LedgerStateNotFound(HeaderId), } +impl From for Error { + fn from(error: WalletApiError) -> Self { + Self::Wallet(Box::new(error)) + } +} + #[derive(Debug)] pub enum LeaderMsg { /// Request a new receiver that yields PoL-winning slot information. diff --git a/services/chain/chain-leader/src/wallet.rs b/services/chain/chain-leader/src/wallet.rs index 004f48fdb..90183dff5 100644 --- a/services/chain/chain-leader/src/wallet.rs +++ b/services/chain/chain-leader/src/wallet.rs @@ -32,7 +32,11 @@ where Wallet: WalletServiceData, RuntimeServiceId: Debug + Send + Sync + Display + 'static + AsServiceId, { - let tx_builder = MantleTxBuilder::new().push_op(Op::LeaderClaim(op)); + let gas_context = wallet + .get_gas_context(Some(tip)) + .await + .map_err(|error| LeaderWalletError::WalletApi(Box::new(error)))?; + let tx_builder = MantleTxBuilder::new(gas_context).push_op(Op::LeaderClaim(op)); let funded_tx_builder = wallet .fund_tx( Some(tip), diff --git a/services/sdp/src/lib.rs b/services/sdp/src/lib.rs index 7b8c641e8..b5eaf0f15 100644 --- a/services/sdp/src/lib.rs +++ b/services/sdp/src/lib.rs @@ -13,7 +13,8 @@ use futures::Stream; use lb_chain_service::api::{CryptarchiaServiceApi, CryptarchiaServiceData}; use lb_core::{ block::BlockNumber, - mantle::{NoteId, SignedMantleTx, tx_builder::MantleTxBuilder}, + header::HeaderId, + mantle::{NoteId, SignedMantleTx, tx::MantleTxGasContext, tx_builder::MantleTxBuilder}, sdp::{ ActiveMessage, ActivityMetadata, DeclarationId, DeclarationMessage, Locator, ProviderId, ServiceType, WithdrawMessage, @@ -182,8 +183,13 @@ where SdpMessage::PostActivity { metadata, .. } => { metrics::activity_posts_total(); - self.handle_post_activity(metadata, &wallet_adapter, &mempool_adapter) - .await; + self.handle_post_activity( + metadata, + &wallet_adapter, + &mempool_adapter, + &chain_api, + ) + .await; } SdpMessage::PostDeclaration { declaration, @@ -196,14 +202,20 @@ where &wallet_adapter, &mempool_adapter, reply_channel, + &chain_api, ) .await; } SdpMessage::PostWithdrawal { declaration_id } => { metrics::withdrawals_total(); - self.handle_post_withdrawal(declaration_id, &wallet_adapter, &mempool_adapter) - .await; + self.handle_post_withdrawal( + declaration_id, + &wallet_adapter, + &mempool_adapter, + &chain_api, + ) + .await; } } } @@ -307,8 +319,13 @@ where wallet_adapter: &WalletAdapter, mempool_adapter: &MempoolAdapter, reply_channel: oneshot::Sender>, + chain_api: &CryptarchiaServiceApi, ) { - let tx_builder = MantleTxBuilder::new(); + let Ok(gas_context) = self.get_gas_context(None, chain_api).await else { + tracing::error!("Failed to get gas context for declaration"); + return; + }; + let tx_builder = MantleTxBuilder::new(gas_context); let declaration_id = declaration.id(); let signed_tx = match wallet_adapter @@ -341,6 +358,7 @@ where metadata: ActivityMetadata, wallet_adapter: &WalletAdapter, mempool_adapter: &MempoolAdapter, + chain_api: &CryptarchiaServiceApi, ) { // Check if we have a declaration_id let Some(ref declaration) = self.current_declaration else { @@ -354,7 +372,11 @@ where metadata, }; - let tx_builder = MantleTxBuilder::new(); + let Ok(gas_context) = self.get_gas_context(None, chain_api).await else { + tracing::error!("Failed to get gas context for activity"); + return; + }; + let tx_builder = MantleTxBuilder::new(gas_context); let signed_tx = match wallet_adapter .active_tx(tx_builder, active_message, &self.wallet_config) @@ -381,6 +403,7 @@ where declaration_id: DeclarationId, wallet_adapter: &WalletAdapter, mempool_adapter: &MempoolAdapter, + chain_api: &CryptarchiaServiceApi, ) { if let Err(e) = self.validate_withdrawal(&declaration_id) { tracing::error!("{}", e); @@ -395,7 +418,11 @@ where nonce: self.bump_nonce(), }; - let tx_builder = MantleTxBuilder::new(); + let Ok(gas_context) = self.get_gas_context(None, chain_api).await else { + tracing::error!("Failed to get gas context for withdrawal"); + return; + }; + let tx_builder = MantleTxBuilder::new(gas_context); let signed_tx = match wallet_adapter .withdraw_tx(tx_builder, withdraw_message, &self.wallet_config) @@ -437,6 +464,31 @@ where Ok(()) } + async fn block_id_or_tip( + &self, + block_id: Option, + chain_api: &CryptarchiaServiceApi, + ) -> Result { + if let Some(block_id) = block_id { + Ok(block_id) + } else { + let info = chain_api.info().await?; + Ok(info.tip) + } + } + + async fn get_gas_context( + &self, + block_id: Option, + chain_api: &CryptarchiaServiceApi, + ) -> Result { + let block_id = self.block_id_or_tip(block_id, chain_api).await?; + let Some(ledger_state) = chain_api.get_ledger_state(block_id).await? else { + return Err(format!("Ledger state not found for block {block_id:?}").into()); + }; + Ok(ledger_state.mantle_ledger().channels().into()) + } + /// Increments the nonce of the current declaration, and returns the /// incremented nonce. /// diff --git a/services/wallet/src/api.rs b/services/wallet/src/api.rs index 7fbf32e74..2d35c4492 100644 --- a/services/wallet/src/api.rs +++ b/services/wallet/src/api.rs @@ -1,10 +1,12 @@ use lb_core::{ header::HeaderId, mantle::{ - Note, SignedMantleTx, Value, ops::leader_claim::VoucherCm, tx_builder::MantleTxBuilder, + Note, SignedMantleTx, Value, ops::leader_claim::VoucherCm, tx::MantleTxGasContext, + tx_builder::MantleTxBuilder, }, }; use lb_key_management_system_service::keys::ZkPublicKey; +use lb_wallet::WalletBalance; use overwatch::{ overwatch::OverwatchHandle, services::{ @@ -24,7 +26,7 @@ pub enum WalletApiError { #[error("Failed to relay message with wallet:{relay_error:?}, msg={msg:?}")] RelaySend { relay_error: RelayError, - msg: WalletMsg, + msg: Box, }, #[error("Failed to recv message from wallet: {0}")] RelayRecv(#[from] RecvError), @@ -34,6 +36,7 @@ pub enum WalletApiError { impl From<(RelayError, WalletMsg)> for WalletApiError { fn from((relay_error, msg): (RelayError, WalletMsg)) -> Self { + let msg = Box::new(msg); Self::RelaySend { relay_error, msg } } } @@ -95,7 +98,7 @@ where &self, tip: Option, pk: ZkPublicKey, - ) -> Result>, WalletApiError> { + ) -> Result>, WalletApiError> { let (resp_tx, rx) = oneshot::channel(); self.relay @@ -127,6 +130,17 @@ where Ok(rx.await??) } + pub async fn get_gas_context( + &self, + block_id: Option, + ) -> Result { + let (resp_tx, rx) = oneshot::channel(); + self.relay + .send(WalletMsg::GetGasContext { block_id, resp_tx }) + .await?; + Ok(rx.await??) + } + pub async fn transfer_funds( &self, tip: Option, @@ -135,8 +149,9 @@ where recipient_pk: ZkPublicKey, amount: Value, ) -> Result, WalletApiError> { + let context = self.get_gas_context(tip).await?; let mantle_tx_builder = - MantleTxBuilder::new().add_ledger_output(Note::new(amount, recipient_pk)); + MantleTxBuilder::new(context).add_ledger_output(Note::new(amount, recipient_pk)); let funded_tx_builder = self .fund_tx(tip, mantle_tx_builder, change_pk, funding_pks) .await?; @@ -193,3 +208,80 @@ where Ok(rx.await??) } } + +#[cfg(test)] +mod tests { + use std::fmt::{self, Display, Formatter}; + + use lb_core::mantle::ops::channel::{ChannelId, ChannelKeyIndex}; + use overwatch::services::state::{NoOperator, NoState}; + use tokio::sync::mpsc; + + use super::*; + + struct DummyWallet; + + impl ServiceData for DummyWallet { + type Settings = WalletServiceSettings; + type State = NoState; + type StateOperator = NoOperator; + type Message = WalletMsg; + } + + impl WalletServiceData for DummyWallet { + type Kms = (); + type Cryptarchia = (); + type Tx = (); + type Storage = (); + } + + #[derive(Debug)] + struct TestRuntimeServiceId; + + impl AsServiceId for TestRuntimeServiceId { + const SERVICE_ID: Self = Self; + } + + impl Display for TestRuntimeServiceId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "TestRuntimeServiceId") + } + } + + #[tokio::test] + async fn get_gas_context_round_trips_through_wallet_api() { + let expected_block_id = HeaderId::from([7u8; 32]); + let expected_channel_id = ChannelId::from([9u8; 32]); + let expected_threshold: ChannelKeyIndex = 2; + + let (msg_sender, mut msg_receiver) = mpsc::channel(1); + tokio::spawn(async move { + while let Some(msg) = msg_receiver.recv().await { + if let WalletMsg::GetGasContext { block_id, resp_tx } = msg { + assert_eq!(block_id, Some(expected_block_id)); + let context = MantleTxGasContext::new( + std::iter::once((expected_channel_id, expected_threshold)).collect(), + ); + drop(resp_tx.send(Ok(context))); + break; + } + } + }); + + let api = + WalletApi::::new(OutboundRelay::new(msg_sender)); + let context = api + .get_gas_context(Some(expected_block_id)) + .await + .expect("gas context should round-trip through the wallet API"); + + assert_eq!( + context.withdraw_threshold(&expected_channel_id), + Some(expected_threshold) + ); + assert_eq!( + context.withdraw_threshold(&ChannelId::from([1u8; 32])), + None + ); + } +} diff --git a/services/wallet/src/lib.rs b/services/wallet/src/lib.rs index ae5843d9d..d1ed780cf 100644 --- a/services/wallet/src/lib.rs +++ b/services/wallet/src/lib.rs @@ -15,7 +15,7 @@ use lb_core::{ block::Block, header::HeaderId, mantle::{ - AuthenticatedMantleTx, Op, OpProof, SignedMantleTx, Transaction as _, TxHash, Utxo, Value, + AuthenticatedMantleTx, Op, OpProof, SignedMantleTx, Transaction as _, TxHash, Utxo, gas::MainnetGasConstants, ops::{ channel::{ChannelId, inscribe::InscriptionOp, set_keys::SetKeysOp}, @@ -24,6 +24,7 @@ use lb_core::{ }, sdp::{SDPActiveOp, SDPDeclareOp, SDPWithdrawOp}, }, + tx::MantleTxGasContext, tx_builder::MantleTxBuilder, }, proofs::leader_claim_proof::{Groth16LeaderClaimProof, LeaderClaimPrivate, LeaderClaimPublic}, @@ -45,7 +46,7 @@ use lb_services_utils::{ }; use lb_storage_service::{api::chain::StorageChainApi, backends::StorageBackend}; use lb_utxotree::MerklePath; -use lb_wallet::{WalletBlock, WalletError}; +use lb_wallet::{WalletBalance, WalletBlock, WalletError}; use overwatch::{ DynError, OpaqueServiceResourcesHandle, services::{AsServiceId, ServiceCore, ServiceData}, @@ -105,6 +106,9 @@ pub enum WalletServiceError { #[error("blocking task failed: {0}")] TaskJoin(#[from] JoinError), + + #[error("Failed to fetch Channel Withdraw proof for op index {0} from the TxBuilder")] + ChannelWithdrawProofNotFound(usize), } #[derive(Debug)] @@ -112,7 +116,7 @@ pub enum WalletMsg { GetBalance { tip: Option, pk: ZkPublicKey, - resp_tx: Sender>, WalletServiceError>>, + resp_tx: Sender>, WalletServiceError>>, }, FundTx { tip: Option, @@ -141,6 +145,10 @@ pub enum WalletMsg { GetKnownAddresses { resp_tx: Sender, WalletServiceError>>, }, + GetGasContext { + block_id: Option, + resp_tx: Sender>, + }, } #[derive(Debug)] @@ -171,7 +179,8 @@ impl WalletMsg { | Self::FundTx { tip, .. } | Self::SignTx { tip, .. } | Self::GetLeaderAgedNotes { tip, .. } - | Self::GetClaimableVoucher { tip, .. } => *tip, + | Self::GetClaimableVoucher { tip, .. } + | Self::GetGasContext { block_id: tip, .. } => *tip, Self::GenerateNewVoucherSecret { .. } | Self::GetKnownAddresses { .. } => None, } } @@ -367,6 +376,7 @@ where } } + #[expect(clippy::too_many_lines, reason = "TODO: Address this at some point.")] async fn handle_wallet_message( msg: WalletMsg, state: &mut ServiceState<'_>, @@ -477,13 +487,16 @@ where WalletMsg::GetKnownAddresses { resp_tx } => { Self::get_known_addresses(state.wallet(), resp_tx); } + WalletMsg::GetGasContext { block_id, resp_tx } => { + Self::get_gas_context(block_id, resp_tx, cryptarchia).await; + } } } async fn handle_get_balance( tip: Option, pk: ZkPublicKey, - resp_tx: Sender>, WalletServiceError>>, + resp_tx: Sender>, WalletServiceError>>, wallet: &Wallet, cryptarchia: &CryptarchiaServiceApi, ) { @@ -508,7 +521,7 @@ where } } - async fn sign_insciption( + async fn sign_inscription( tx_hash: TxHash, inscribe_op: &InscriptionOp, kms: &KmsServiceApi, @@ -673,19 +686,26 @@ where .map(|utxo| utxo.note.pk) .collect(); + let mut channel_withdraw_proofs = tx_builder.channel_withdraw_proofs().clone(); let mantle_tx = tx_builder.build(); let tx_hash = mantle_tx.hash(); let mut ops_proofs = Vec::new(); - for op in &mantle_tx.ops { + for (i, op) in mantle_tx.ops.iter().enumerate() { let proof = match op { Op::ChannelInscribe(inscribe_op) => { - Self::sign_insciption(tx_hash, inscribe_op, kms).await? + Self::sign_inscription(tx_hash, inscribe_op, kms).await? } Op::ChannelSetKeys(set_keys_op) => { Self::sign_channel_set_key(tx_hash, set_keys_op, &ledger, kms).await? } Op::ChannelDeposit(_deposit_op) => OpProof::NoProof, + Op::ChannelWithdraw(_channel_withdraw_op) => { + let proof = channel_withdraw_proofs + .remove(&i) + .ok_or(WalletServiceError::ChannelWithdrawProofNotFound(i))?; + OpProof::ChannelWithdrawProof(proof) + } Op::SDPDeclare(declare_op) => { Self::sign_sdp_declare(tx_hash, declare_op, &ledger, kms).await? } @@ -1093,4 +1113,35 @@ where error!(err = ?e, "Failed to send known addresses response"); } } + + async fn get_gas_context( + block_id: Option, + resp_tx: Sender>, + cryptarchia: &CryptarchiaServiceApi, + ) { + let block_id = match Self::msg_tip_or_latest(block_id, cryptarchia).await { + Ok(block_id) => block_id, + Err(error) => { + Self::send_err(resp_tx, error); + return; + } + }; + + let ledger_state = match cryptarchia.get_ledger_state(block_id).await { + Ok(Some(ledger_state)) => ledger_state, + Ok(None) => { + Self::send_err(resp_tx, WalletServiceError::LedgerStateNotFound(block_id)); + return; + } + Err(err) => { + Self::send_err(resp_tx, WalletServiceError::from(err)); + return; + } + }; + + let gas_context = ledger_state.mantle_ledger().channels().into(); + if let Err(e) = resp_tx.send(Ok(gas_context)) { + error!(err = ?e, "Failed to send gas context response"); + } + } } diff --git a/tests/src/cucumber/steps/manual_transactions/utils.rs b/tests/src/cucumber/steps/manual_transactions/utils.rs index 32e7f2433..5ac6c073c 100644 --- a/tests/src/cucumber/steps/manual_transactions/utils.rs +++ b/tests/src/cucumber/steps/manual_transactions/utils.rs @@ -43,7 +43,7 @@ impl Display for WalletStateType { use std::str::FromStr; -use lb_core::mantle::OpProof; +use lb_core::mantle::{OpProof, tx::MantleTxGasContext}; use lb_http_api_common::bodies::wallet::transfer_funds::WalletTransferFundsRequestBody; use crate::cucumber::{ @@ -83,7 +83,8 @@ pub async fn create_and_submit_transaction( ref wallet_account, .. } => { let wallet_state = wallet_state_from_utxos(available_utxos); - let mut tx_builder = MantleTxBuilder::new(); + let empty_context = MantleTxGasContext::new(HashMap::new()); + let mut tx_builder = MantleTxBuilder::new(empty_context); for (receiver_pk, value) in receivers { tx_builder = tx_builder.add_ledger_output(Note::new(*value, *receiver_pk)); } diff --git a/tests/testing_framework/src/workloads/inscription/workload.rs b/tests/testing_framework/src/workloads/inscription/workload.rs index cd58bfc1e..ce79a5e0d 100644 --- a/tests/testing_framework/src/workloads/inscription/workload.rs +++ b/tests/testing_framework/src/workloads/inscription/workload.rs @@ -240,7 +240,6 @@ impl<'a, E: LbcScenarioEnv + LbcBlockFeedEnv> InscriptionRunner<'a, E> { let Some(channel) = self.channels.get_mut(channel_idx) else { return Ok(()); }; - let (tx, msg_id, tx_hash) = build_inscription_transaction(channel, self.payload_bytes)?; submit_transaction_via_cluster(self.ctx, Arc::new(tx)).await?; diff --git a/tests/testing_framework/src/workloads/transaction/workload.rs b/tests/testing_framework/src/workloads/transaction/workload.rs index eaa66edc9..0925acc61 100644 --- a/tests/testing_framework/src/workloads/transaction/workload.rs +++ b/tests/testing_framework/src/workloads/transaction/workload.rs @@ -9,7 +9,7 @@ use std::{ use async_trait::async_trait; use lb_core::mantle::{ - GenesisTx as _, Note, OpProof, SignedMantleTx, Transaction as _, Utxo, + GenesisTx as _, Note, OpProof, SignedMantleTx, Transaction as _, Utxo, tx::MantleTxGasContext, tx_builder::MantleTxBuilder, }; use lb_key_management_system_service::keys::{ZkKey, ZkPublicKey}; @@ -184,13 +184,13 @@ impl<'a, E: LbcScenarioEnv> Submission<'a, E> { } async fn execute(mut self) -> Result<(), DynError> { + let gas_context = MantleTxGasContext::new(HashMap::new()); while let Some(input) = self.plan.pop_front() { - submit_wallet_transaction(self.ctx, &input).await?; + submit_wallet_transaction(self.ctx, &input, gas_context.clone()).await?; if !self.interval.is_zero() { sleep(self.interval).await; } } - Ok(()) } } @@ -198,8 +198,9 @@ impl<'a, E: LbcScenarioEnv> Submission<'a, E> { async fn submit_wallet_transaction( ctx: &RunContext, input: &WalletInput, + gas_context: MantleTxGasContext, ) -> Result<(), DynError> { - let signed_tx = Arc::new(build_wallet_transaction(input)?); + let signed_tx = Arc::new(build_wallet_transaction(input, gas_context)?); submit_transaction_via_cluster(ctx, signed_tx).await } @@ -264,8 +265,11 @@ fn cluster_client_exhausted_error() -> DynError { TxWorkloadError::ClusterClientExhausted.into() } -fn build_wallet_transaction(input: &WalletInput) -> Result { - let tx = MantleTxBuilder::new() +fn build_wallet_transaction( + input: &WalletInput, + gas_context: MantleTxGasContext, +) -> Result { + let tx = MantleTxBuilder::new(gas_context) .add_ledger_input(input.utxo) .add_ledger_output(Note::new(input.utxo.note.value, input.account.public_key())) .build(); diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 60c237801..e8f28e64d 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -140,13 +140,17 @@ impl WalletState { } #[must_use] - pub fn balance(&self, pk: ZkPublicKey) -> Option { - let balance = self - .pk_index - .get(&pk)? - .iter() - .map(|id| self.utxos[id].note.value) - .sum(); + pub fn balance(&self, pk: ZkPublicKey) -> Option { + let mut balance = WalletBalance { + balance: 0, + notes: HashMap::new(), + }; + + self.pk_index.get(&pk)?.iter().for_each(|id| { + let value = self.utxos[id].note.value; + balance.balance += value; + balance.notes.insert(*id, value); + }); Some(balance) } @@ -269,7 +273,11 @@ where Ok(()) } - pub fn balance(&self, tip: HeaderId, pk: ZkPublicKey) -> Result, WalletError> { + pub fn balance( + &self, + tip: HeaderId, + pk: ZkPublicKey, + ) -> Result, WalletError> { Ok(self.wallet_state_at(tip)?.balance(pk)) } @@ -326,6 +334,12 @@ where } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WalletBalance { + pub balance: Value, + pub notes: HashMap, +} + #[cfg(test)] mod tests { use std::{ @@ -340,6 +354,7 @@ mod tests { Note, Op, TxHash, gas::MainnetGasConstants as Gas, ops::channel::{ChannelId, MsgId, inscribe::InscriptionOp}, + tx::MantleTxGasContext, }, sdp::{MinStake, ServiceParameters, ServiceType}, }; @@ -404,7 +419,10 @@ mod tests { genesis, &ledger, ); - assert_eq!(wallet.balance(genesis, alice).unwrap(), Some(104)); + assert_eq!( + wallet.balance(genesis, alice).unwrap().unwrap().balance, + 104 + ); assert_eq!(wallet.balance(genesis, bob).unwrap(), None); assert_eq!( wallet.vouchers().get(&voucher_cm), @@ -414,7 +432,7 @@ mod tests { let wallet = Wallet::<_, TestVoucherId>::from_lib([(bob, 2)], Vouchers::default(), genesis, &ledger); assert_eq!(wallet.balance(genesis, alice).unwrap(), None); - assert_eq!(wallet.balance(genesis, bob).unwrap(), Some(20)); + assert_eq!(wallet.balance(genesis, bob).unwrap().unwrap().balance, 20); let wallet = Wallet::<_, TestVoucherId>::from_lib( [(alice, 1), (bob, 2)], @@ -422,8 +440,11 @@ mod tests { genesis, &ledger, ); - assert_eq!(wallet.balance(genesis, alice).unwrap(), Some(104)); - assert_eq!(wallet.balance(genesis, bob).unwrap(), Some(20)); + assert_eq!( + wallet.balance(genesis, alice).unwrap().unwrap().balance, + 104 + ); + assert_eq!(wallet.balance(genesis, bob).unwrap().unwrap().balance, 20); } #[test] @@ -475,24 +496,33 @@ mod tests { assert_eq!(wallet.balance(genesis, alice).unwrap(), None); assert_eq!(wallet.balance(genesis, bob).unwrap(), None); - assert_eq!(wallet.balance(block_1.id, alice).unwrap(), Some(104)); + assert_eq!( + wallet.balance(block_1.id, alice).unwrap().unwrap().balance, + 104 + ); assert_eq!(wallet.balance(block_1.id, bob).unwrap(), None); - assert_eq!(wallet.balance(block_2.id, alice).unwrap(), Some(84)); - assert_eq!(wallet.balance(block_2.id, bob).unwrap(), Some(20)); + assert_eq!( + wallet.balance(block_2.id, alice).unwrap().unwrap().balance, + 84 + ); + assert_eq!( + wallet.balance(block_2.id, bob).unwrap().unwrap().balance, + 20 + ); } #[test] fn test_fund_tx_with_change() { let alice = pk(1); let alice_utxo = Utxo::new(tx_hash(0), 0, Note::new(5000, alice)); + let ledger_state = LedgerState::from_utxos([alice_utxo], &ledger_config()); - let wallet_state = WalletState::from_ledger( - &HashMap::from_iter([(alice, 1)]), - &LedgerState::from_utxos([alice_utxo], &ledger_config()), - ); + let wallet_state = + WalletState::from_ledger(&HashMap::from_iter([(alice, 1)]), &ledger_state); - let tx_builder = MantleTxBuilder::new() + let context: MantleTxGasContext = ledger_state.mantle_ledger().channels().into(); + let tx_builder = MantleTxBuilder::new(context) .set_execution_gas_price(1) .set_storage_gas_price(1); @@ -526,21 +556,20 @@ mod tests { #[test] fn test_fund_tx_insufficient_funds() { let alice = pk(1); - - let wallet_state = WalletState::from_ledger( - &HashMap::from_iter([(alice, 1)]), - &LedgerState::from_utxos( - [ - Utxo::new(tx_hash(0), 0, Note::new(100, alice)), - Utxo::new(tx_hash(0), 1, Note::new(100, alice)), - Utxo::new(tx_hash(0), 2, Note::new(100, alice)), - Utxo::new(tx_hash(0), 3, Note::new(100, alice)), - ], - &ledger_config(), - ), + let ledger_state = LedgerState::from_utxos( + [ + Utxo::new(tx_hash(0), 0, Note::new(100, alice)), + Utxo::new(tx_hash(0), 1, Note::new(100, alice)), + Utxo::new(tx_hash(0), 2, Note::new(100, alice)), + Utxo::new(tx_hash(0), 3, Note::new(100, alice)), + ], + &ledger_config(), ); - let mut tx_builder = MantleTxBuilder::new() + let wallet_state = + WalletState::from_ledger(&HashMap::from_iter([(alice, 1)]), &ledger_state); + let context: MantleTxGasContext = ledger_state.mantle_ledger().channels().into(); + let mut tx_builder = MantleTxBuilder::new(context) .set_execution_gas_price(1) .set_storage_gas_price(1); @@ -567,13 +596,13 @@ mod tests { #[test] fn test_fund_tx_zero_funds() { let alice = pk(1); + let ledger_state = LedgerState::from_utxos([], &ledger_config()); - let wallet_state = WalletState::from_ledger( - &HashMap::from_iter([(alice, 1)]), - &LedgerState::from_utxos([], &ledger_config()), - ); + let wallet_state = + WalletState::from_ledger(&HashMap::from_iter([(alice, 1)]), &ledger_state); - let tx_builder = MantleTxBuilder::new() + let context: MantleTxGasContext = ledger_state.mantle_ledger().channels().into(); + let tx_builder = MantleTxBuilder::new(context) .set_execution_gas_price(1) .set_storage_gas_price(1); @@ -589,16 +618,16 @@ mod tests { fn test_fund_tx_respects_pk_list() { let alice = pk(1); let bob = pk(2); - - let wallet_state = WalletState::from_ledger( - &HashMap::from_iter([(alice, 1), (bob, 2)]), - &LedgerState::from_utxos( - [Utxo::new(tx_hash(0), 0, Note::new(1_000_000, bob))], - &ledger_config(), - ), + let ledger_state = LedgerState::from_utxos( + [Utxo::new(tx_hash(0), 0, Note::new(1_000_000, bob))], + &ledger_config(), ); - let tx_builder = MantleTxBuilder::new() + let wallet_state = + WalletState::from_ledger(&HashMap::from_iter([(alice, 1), (bob, 2)]), &ledger_state); + + let context: MantleTxGasContext = ledger_state.mantle_ledger().channels().into(); + let tx_builder = MantleTxBuilder::new(context) .set_execution_gas_price(1) .set_storage_gas_price(1); @@ -620,7 +649,8 @@ mod tests { fn test_fund_tx_unfundable_region() { let alice = pk(1); - let tx_builder = MantleTxBuilder::new() + let context = MantleTxGasContext::new(HashMap::new()); + let tx_builder = MantleTxBuilder::new(context) .set_execution_gas_price(1) .set_storage_gas_price(1);