From f3864a313dd7e964ae1eb38d6aa77d2de602674d Mon Sep 17 00:00:00 2001 From: Dimank92 Date: Tue, 27 Oct 2026 01:10:43 +0700 Subject: [PATCH] feat(multisig): implement signer rotation and threshold updates with approval invalidation (#197) --- Cargo.lock | 439 +++++++++++++----------------------------- multisig/Cargo.toml | 3 + multisig/src/error.rs | 3 + multisig/src/lib.rs | 344 ++++++++++++++++++++++++++++++++- multisig/src/test.rs | 202 +++++++++++++++++++ multisig/src/types.rs | 6 +- 6 files changed, 684 insertions(+), 313 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bb8d78..f650f27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.25.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -43,9 +43,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backtrace" -version = "0.3.76" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", @@ -53,7 +53,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link", + "windows-targets", ] [[package]] @@ -76,15 +76,15 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.23.1" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bit-set" @@ -105,12 +105,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.13.2" @@ -150,15 +144,6 @@ dependencies = [ "syn 3.0.9", ] -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -340,18 +325,8 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" -dependencies = [ - "darling_core 0.24.1", - "darling_macro 0.24.1", + "darling_core", + "darling_macro", ] [[package]] @@ -368,72 +343,17 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "darling_core" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 3.0.9", -] - [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.11", + "darling_core", "quote", "syn 2.0.119", ] -[[package]] -name = "darling_macro" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" -dependencies = [ - "darling_core 0.24.1", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "defmt" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "defmt-parser" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" -dependencies = [ - "thiserror 2.0.21", -] - [[package]] name = "der" version = "0.7.10" @@ -446,11 +366,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ - "serde_core", + "powerfmt", + "serde", ] [[package]] @@ -482,12 +403,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - [[package]] name = "ecdsa" version = "0.16.9" @@ -574,9 +489,9 @@ checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" [[package]] name = "ethnum" -version = "1.5.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" +checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" [[package]] name = "event-spec" @@ -683,9 +598,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.32.3" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "group" @@ -706,9 +621,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.17.1" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" [[package]] name = "hex" @@ -777,14 +692,13 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.17.1", + "hashbrown 0.15.5", "serde", - "serde_core", ] [[package]] @@ -808,60 +722,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jiff" -version = "0.2.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" -dependencies = [ - "defmt", - "jiff-core", - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-link", -] - -[[package]] -name = "jiff-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" -dependencies = [ - "defmt", - "log", -] - -[[package]] -name = "jiff-static" -version = "0.2.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" -dependencies = [ - "jiff-core", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - [[package]] name = "js-sys" version = "0.3.106" @@ -945,9 +805,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.2" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-derive" @@ -980,9 +840,9 @@ dependencies = [ [[package]] name = "object" -version = "0.37.3" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -1027,21 +887,6 @@ dependencies = [ "spki", ] -[[package]] -name = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - -[[package]] -name = "portable-atomic-util" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" -dependencies = [ - "portable-atomic", -] - [[package]] name = "powerfmt" version = "0.2.1" @@ -1078,9 +923,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.5.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ "toml_edit", ] @@ -1102,7 +947,7 @@ checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.13.2", + "bitflags", "chacha20", "core_detect", "num-traits", @@ -1190,26 +1035,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "ref-cast" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - [[package]] name = "regex-syntax" version = "0.8.11" @@ -1256,7 +1081,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags 2.13.2", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -1281,30 +1106,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - [[package]] name = "sec1" version = "0.7.3" @@ -1369,20 +1170,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.24.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64 0.23.1", - "bs58", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.14.2", - "jiff", - "schemars 0.9.0", - "schemars 1.2.3", - "serde_core", + "indexmap 2.6.0", + "serde", + "serde_derive", "serde_json", "serde_with_macros", "time", @@ -1390,14 +1188,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.24.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ - "darling 0.24.1", + "darling", "proc-macro2", "quote", - "syn 3.0.9", + "syn 2.0.119", ] [[package]] @@ -1549,7 +1347,7 @@ dependencies = [ "serde_with", "soroban-env-common", "soroban-env-host", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -1581,7 +1379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0974e413731aeff2443f2305b344578b3f1ffd18335a7ba0f0b5d2eb4e94c9ce" dependencies = [ "crate-git-revision", - "darling 0.20.11", + "darling", "itertools", "proc-macro2", "quote", @@ -1602,7 +1400,7 @@ checksum = "c2c70b20e68cae3ef700b8fa3ae29db1c6a294b311fba66918f90cb8f9fd0a1a" dependencies = [ "base64 0.13.1", "stellar-xdr", - "thiserror 1.0.69", + "thiserror", "wasmparser", ] @@ -1619,7 +1417,7 @@ dependencies = [ "soroban-spec", "stellar-xdr", "syn 2.0.119", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -1673,7 +1471,7 @@ checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" dependencies = [ "base32", "crate-git-revision", - "thiserror 1.0.69", + "thiserror", ] [[package]] @@ -1745,16 +1543,7 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" -dependencies = [ - "thiserror-impl 2.0.21", + "thiserror-impl", ] [[package]] @@ -1768,80 +1557,51 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "thiserror-impl" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - [[package]] name = "time" -version = "0.3.55" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", - "serde_core", + "serde", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.9" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.32" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", ] -[[package]] -name = "tinyvec" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" - [[package]] name = "toml_datetime" -version = "1.1.2+spec-1.1.0" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" -dependencies = [ - "serde_core", -] +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_edit" -version = "0.25.17+spec-1.1.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.14.2", + "indexmap 2.6.0", "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" -dependencies = [ "winnow", ] @@ -1953,7 +1713,7 @@ version = "0.116.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" dependencies = [ - "indexmap 2.14.2", + "indexmap 2.6.0", "semver", ] @@ -2034,11 +1794,75 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" -version = "1.0.4" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] @@ -2108,6 +1932,7 @@ name = "zenith-multisig" version = "0.1.0" dependencies = [ "ed25519-dalek", + "event-spec", "soroban-sdk", "zenith-common", ] @@ -2210,9 +2035,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.9.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zmij" diff --git a/multisig/Cargo.toml b/multisig/Cargo.toml index 61f47d5..8a22be4 100644 --- a/multisig/Cargo.toml +++ b/multisig/Cargo.toml @@ -3,6 +3,8 @@ name = "zenith-multisig" version.workspace = true edition.workspace = true publish.workspace = true +repository.workspace = true +build = "../common/build_meta.rs" [lib] # rlib alongside cdylib so other contracts (options_market) can depend on @@ -17,6 +19,7 @@ zenith-common = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } ed25519-dalek = { workspace = true } +event-spec = { path = "../tools/event-spec" } [features] testutils = ["soroban-sdk/testutils"] diff --git a/multisig/src/error.rs b/multisig/src/error.rs index 7c7389e..477aed0 100644 --- a/multisig/src/error.rs +++ b/multisig/src/error.rs @@ -104,6 +104,9 @@ pub enum Error { /// Not every signer has approved this wasm. /// Cause: `upgrade` when a signer hasn't approved the hash (or a signer was added since unanimity). UpgradeNotUnanimous = 33, + /// Signer-management entrypoint invoked outside execute. + /// Cause: calling `add_signer`, `remove_signer`, or `set_threshold` directly instead of via an approved multisig proposal. + DirectAdminCall = 34, // Schema migrations (zenith_common::migrations) — same codes in // every Zenith contract. See docs/migrations.md. /// `from` isn't the stored schema version. diff --git a/multisig/src/lib.rs b/multisig/src/lib.rs index 380ea21..a51928d 100644 --- a/multisig/src/lib.rs +++ b/multisig/src/lib.rs @@ -30,7 +30,8 @@ //! entrypoint returns `Result<_, Error>` (docs/errors.md). use soroban_sdk::{ - contract, contractimpl, xdr::ToXdr, Address, BytesN, Env, Map, Symbol, Val, Vec, + contract, contractimpl, xdr::ToXdr, Address, BytesN, Env, IntoVal, Map, Symbol, TryFromVal, + Val, Vec, }; #[cfg(test)] @@ -676,7 +677,11 @@ impl Multisig { let hash = Self::finish(&env, &proposal_id, ActionStatus::Executed)?; events::executed(&env, proposal_id, hash); - Ok(env.invoke_contract::(&proposal.target, &proposal.function, proposal.args)) + if proposal.target == env.current_contract_address() { + Self::execute_self_call(&env, &proposal.function, proposal.args) + } else { + Ok(env.invoke_contract::(&proposal.target, &proposal.function, proposal.args)) + } } pub fn get_proposal(env: Env, proposal_id: BytesN<32>) -> Result, Error> { @@ -818,7 +823,6 @@ impl Multisig { Ok(ttl::get_persistent(&env, &DataKey::SignerChange(change_id))) } - /// Timestamp at which a queued change becomes executable. pub fn get_signer_change_ready_at( env: Env, change_id: BytesN<32>, @@ -828,9 +832,343 @@ impl Multisig { &DataKey::SignerChangeReadyAt(change_id), )) } + + // ── Admin-gated signer management and threshold updates ─────────────────── + + /// Admin entrypoint to add a new signer. Must be called by the multisig + /// contract itself (routed through `propose` -> `approve` -> `execute`). + /// Inactivates prior approvals by bumping the signer epoch. + pub fn add_signer(env: Env, caller: Address, signer: Address) -> Result<(), Error> { + Self::add_signer_weighted(env, caller, signer, 1) + } + + /// Admin entrypoint to add a new signer with a specific voting weight. + /// Must be called by the multisig contract itself. + pub fn add_signer_weighted( + env: Env, + caller: Address, + signer: Address, + weight: u32, + ) -> Result<(), Error> { + ttl::extend_instance(&env); + if caller != env.current_contract_address() { + return Err(Error::DirectAdminCall); + } + caller.require_auth(); + migrations::require_current(&env)?; + + if weight == 0 { + return Err(Error::InvalidWeight); + } + let mut signers = Self::signers(&env)?; + if signers.len() >= MAX_SIGNERS { + return Err(Error::TooManySigners); + } + if signers.contains_key(signer.clone()) { + return Err(Error::DuplicateSigner); + } + signers.set(signer.clone(), weight); + + let threshold: u32 = Self::config(&env, &DataKey::Threshold)?; + let rotation_threshold: u32 = Self::config(&env, &DataKey::RotationThreshold)?; + Self::validate_thresholds(&signers, threshold, rotation_threshold)?; + + let epoch = Self::epoch(&env).saturating_add(1); + let storage = env.storage().instance(); + storage.set(&DataKey::Signers, &signers); + storage.set(&DataKey::SignerEpoch, &epoch); + Self::warn_single_key_quorum(&env, &signers, threshold); + + let change_id: BytesN<32> = env.crypto().sha256(&signer.to_xdr(&env)).into(); + events::signers_rotated( + &env, + change_id, + SignersRotatedEvent { + epoch, + signers, + threshold, + rotation_threshold, + }, + ); + Ok(()) + } + + /// Admin entrypoint to remove an existing signer. Must be called by the multisig + /// contract itself (routed through `propose` -> `approve` -> `execute`). + /// Inactivates prior approvals by bumping the signer epoch. + /// Reverts with `InvalidThreshold` if removal would drop the signer count + /// or total weight below the threshold. + pub fn remove_signer(env: Env, caller: Address, signer: Address) -> Result<(), Error> { + ttl::extend_instance(&env); + if caller != env.current_contract_address() { + return Err(Error::DirectAdminCall); + } + caller.require_auth(); + migrations::require_current(&env)?; + + let mut signers = Self::signers(&env)?; + if !signers.contains_key(signer.clone()) { + return Err(Error::NotASigner); + } + signers.remove(signer.clone()); + + let threshold: u32 = Self::config(&env, &DataKey::Threshold)?; + let total = Self::total_weight(&signers)?; + if signers.is_empty() || threshold == 0 || threshold > total || signers.len() < threshold { + return Err(Error::InvalidThreshold); + } + + let mut rotation_threshold: u32 = Self::config(&env, &DataKey::RotationThreshold)?; + if rotation_threshold > total { + rotation_threshold = total; + } + Self::validate_thresholds(&signers, threshold, rotation_threshold)?; + + let epoch = Self::epoch(&env).saturating_add(1); + let storage = env.storage().instance(); + storage.set(&DataKey::Signers, &signers); + storage.set(&DataKey::RotationThreshold, &rotation_threshold); + storage.set(&DataKey::SignerEpoch, &epoch); + Self::warn_single_key_quorum(&env, &signers, threshold); + + let change_id: BytesN<32> = env.crypto().sha256(&signer.to_xdr(&env)).into(); + events::signers_rotated( + &env, + change_id, + SignersRotatedEvent { + epoch, + signers, + threshold, + rotation_threshold, + }, + ); + Ok(()) + } + + /// Admin entrypoint to update the approval threshold. Must be called by the multisig + /// contract itself (routed through `propose` -> `approve` -> `execute`). + /// Rejects 0 or threshold greater than total signer weight or signer count. + /// Inactivates prior approvals by bumping the signer epoch. + pub fn set_threshold(env: Env, caller: Address, threshold: u32) -> Result<(), Error> { + ttl::extend_instance(&env); + if caller != env.current_contract_address() { + return Err(Error::DirectAdminCall); + } + caller.require_auth(); + migrations::require_current(&env)?; + + let signers = Self::signers(&env)?; + let total = Self::total_weight(&signers)?; + if threshold == 0 || threshold > total || threshold > signers.len() { + return Err(Error::InvalidThreshold); + } + + let current_rot: u32 = Self::config(&env, &DataKey::RotationThreshold)?; + let rotation_threshold = if current_rot > threshold && current_rot <= total { + current_rot + } else if threshold >= total { + total + } else { + (threshold + 1).min(total) + }; + Self::validate_thresholds(&signers, threshold, rotation_threshold)?; + + let epoch = Self::epoch(&env).saturating_add(1); + let storage = env.storage().instance(); + storage.set(&DataKey::Threshold, &threshold); + storage.set(&DataKey::RotationThreshold, &rotation_threshold); + storage.set(&DataKey::SignerEpoch, &epoch); + Self::warn_single_key_quorum(&env, &signers, threshold); + + let change_id: BytesN<32> = env.crypto().sha256(&threshold.to_xdr(&env)).into(); + events::signers_rotated( + &env, + change_id, + SignersRotatedEvent { + epoch, + signers, + threshold, + rotation_threshold, + }, + ); + Ok(()) + } + + /// Admin entrypoint to atomically remove a signer and update the threshold in a single + /// action, preventing the contract from entering an invalid state where threshold > count. + /// Must be called by the multisig contract itself (routed through `propose` -> `approve` -> `execute`). + pub fn remove_signer_and_set_threshold( + env: Env, + caller: Address, + signer: Address, + new_threshold: u32, + ) -> Result<(), Error> { + ttl::extend_instance(&env); + if caller != env.current_contract_address() { + return Err(Error::DirectAdminCall); + } + caller.require_auth(); + migrations::require_current(&env)?; + + let mut signers = Self::signers(&env)?; + if !signers.contains_key(signer.clone()) { + return Err(Error::NotASigner); + } + signers.remove(signer.clone()); + + let total = Self::total_weight(&signers)?; + if signers.is_empty() + || new_threshold == 0 + || new_threshold > total + || new_threshold > signers.len() + { + return Err(Error::InvalidThreshold); + } + + let current_rot: u32 = Self::config(&env, &DataKey::RotationThreshold)?; + let rotation_threshold = if current_rot > new_threshold && current_rot <= total { + current_rot + } else if new_threshold >= total { + total + } else { + (new_threshold + 1).min(total) + }; + Self::validate_thresholds(&signers, new_threshold, rotation_threshold)?; + + let epoch = Self::epoch(&env).saturating_add(1); + let storage = env.storage().instance(); + storage.set(&DataKey::Signers, &signers); + storage.set(&DataKey::Threshold, &new_threshold); + storage.set(&DataKey::RotationThreshold, &rotation_threshold); + storage.set(&DataKey::SignerEpoch, &epoch); + Self::warn_single_key_quorum(&env, &signers, new_threshold); + + let change_id: BytesN<32> = env.crypto().sha256(&signer.to_xdr(&env)).into(); + events::signers_rotated( + &env, + change_id, + SignersRotatedEvent { + epoch, + signers, + threshold: new_threshold, + rotation_threshold, + }, + ); + Ok(()) + } } impl Multisig { + fn execute_self_call(env: &Env, function: &Symbol, args: Vec) -> Result { + let f_str = function.clone(); + if f_str == Symbol::new(env, "add_signer") { + let (caller, signer) = if args.len() == 1 { + ( + env.current_contract_address(), + Address::try_from_val(env, &args.get(0).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + ) + } else if args.len() == 2 { + ( + Address::try_from_val(env, &args.get(0).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + Address::try_from_val(env, &args.get(1).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + ) + } else { + return Err(Error::ContextNotAllowed); + }; + Self::add_signer(env.clone(), caller, signer)?; + Ok(().into_val(env)) + } else if f_str == Symbol::new(env, "add_signer_weighted") { + let (caller, signer, weight) = if args.len() == 2 { + ( + env.current_contract_address(), + Address::try_from_val(env, &args.get(0).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + u32::try_from_val(env, &args.get(1).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + ) + } else if args.len() == 3 { + ( + Address::try_from_val(env, &args.get(0).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + Address::try_from_val(env, &args.get(1).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + u32::try_from_val(env, &args.get(2).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + ) + } else { + return Err(Error::ContextNotAllowed); + }; + Self::add_signer_weighted(env.clone(), caller, signer, weight)?; + Ok(().into_val(env)) + } else if f_str == Symbol::new(env, "remove_signer") { + let (caller, signer) = if args.len() == 1 { + ( + env.current_contract_address(), + Address::try_from_val(env, &args.get(0).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + ) + } else if args.len() == 2 { + ( + Address::try_from_val(env, &args.get(0).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + Address::try_from_val(env, &args.get(1).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + ) + } else { + return Err(Error::ContextNotAllowed); + }; + Self::remove_signer(env.clone(), caller, signer)?; + Ok(().into_val(env)) + } else if f_str == Symbol::new(env, "set_threshold") { + let (caller, threshold) = if args.len() == 1 { + ( + env.current_contract_address(), + u32::try_from_val(env, &args.get(0).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + ) + } else if args.len() == 2 { + ( + Address::try_from_val(env, &args.get(0).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + u32::try_from_val(env, &args.get(1).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + ) + } else { + return Err(Error::ContextNotAllowed); + }; + Self::set_threshold(env.clone(), caller, threshold)?; + Ok(().into_val(env)) + } else if f_str == Symbol::new(env, "remove_signer_and_set_threshold") { + let (caller, signer, threshold) = if args.len() == 2 { + ( + env.current_contract_address(), + Address::try_from_val(env, &args.get(0).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + u32::try_from_val(env, &args.get(1).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + ) + } else if args.len() == 3 { + ( + Address::try_from_val(env, &args.get(0).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + Address::try_from_val(env, &args.get(1).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + u32::try_from_val(env, &args.get(2).unwrap()) + .map_err(|_| Error::ContextNotAllowed)?, + ) + } else { + return Err(Error::ContextNotAllowed); + }; + Self::remove_signer_and_set_threshold(env.clone(), caller, signer, threshold)?; + Ok(().into_val(env)) + } else { + Err(Error::ContextNotAllowed) + } + } + fn signers(env: &Env) -> Result, Error> { Self::config(env, &DataKey::Signers) } diff --git a/multisig/src/test.rs b/multisig/src/test.rs index a0d0d81..946d646 100644 --- a/multisig/src/test.rs +++ b/multisig/src/test.rs @@ -1744,6 +1744,207 @@ fn more_than_32_signers_rejected() { ); } +// ─── Signer Rotation and Threshold Updates (Issue #197) ───────────────────── + +#[test] +fn rotation_happy_path_via_proposal() { + let h = setup(); + let new_signer = Address::generate(&h.env); + + // 1. Add signer via approved proposal + let add_id = h.client.propose( + &h.signers[0], + &h.client.address, + &Symbol::new(&h.env, "add_signer"), + &vec![&h.env, h.client.address.to_val(), new_signer.to_val()], + ); + h.client.approve(&h.signers[0], &add_id); + h.client.approve(&h.signers[1], &add_id); + h.client.execute(&add_id); + + assert_eq!(h.client.get_signer_count(), 4); + assert!(h.client.is_signer(&new_signer)); + assert_eq!(h.client.get_signer_epoch(), 1); + + // 2. Remove signer via approved proposal + let remove_id = h.client.propose( + &h.signers[0], + &h.client.address, + &Symbol::new(&h.env, "remove_signer"), + &vec![&h.env, h.client.address.to_val(), h.signers[2].to_val()], + ); + h.client.approve(&h.signers[0], &remove_id); + h.client.approve(&h.signers[1], &remove_id); + h.client.execute(&remove_id); + + assert_eq!(h.client.get_signer_count(), 3); + assert!(!h.client.is_signer(&h.signers[2])); + assert_eq!(h.client.get_signer_epoch(), 2); + + // 3. Set threshold via approved proposal + let thresh_id = h.client.propose( + &h.signers[0], + &h.client.address, + &Symbol::new(&h.env, "set_threshold"), + &vec![&h.env, h.client.address.to_val(), 3u32.into_val(&h.env)], + ); + h.client.approve(&h.signers[0], &thresh_id); + h.client.approve(&h.signers[1], &thresh_id); + h.client.execute(&thresh_id); + + assert_eq!(h.client.get_threshold(), 3); + assert_eq!(h.client.get_signer_epoch(), 3); +} + +#[test] +fn epoch_bump_invalidates_in_flight_pending_approvals() { + let h = setup(); + let target = setup_target(&h); + + // Create a target proposal + let action_id = h.client.propose( + &h.signers[0], + &target.address, + &Symbol::new(&h.env, "set"), + &set_args(&h.env, 42), + ); + + // Signer 0 approves action_id in epoch 0 + h.client.approve(&h.signers[0], &action_id); + assert_eq!(h.client.get_approval_count(&action_id), 1); + assert!(!h.client.is_approved(&action_id)); + + // Signers rotate via add_signer proposal + let new_signer = Address::generate(&h.env); + let rot_id = h.client.propose( + &h.signers[0], + &h.client.address, + &Symbol::new(&h.env, "add_signer"), + &vec![&h.env, h.client.address.to_val(), new_signer.to_val()], + ); + h.client.approve(&h.signers[0], &rot_id); + h.client.approve(&h.signers[1], &rot_id); + h.client.execute(&rot_id); + + // Epoch has bumped from 0 to 1 + assert_eq!(h.client.get_signer_epoch(), 1); + + // The approval on action_id cast under epoch 0 is invalidated! + assert_eq!(h.client.get_approval_count(&action_id), 0); + assert!(!h.client.is_approved(&action_id)); + assert!(matches!( + h.client.try_execute(&action_id), + Err(Ok(Error::NotYetApproved)) + )); + + // Re-approving under the new epoch allows execution + h.client.approve(&h.signers[0], &action_id); + h.client.approve(&new_signer, &action_id); + assert_eq!(h.client.get_approval_count(&action_id), 2); + assert!(h.client.is_approved(&action_id)); + h.client.execute(&action_id); + assert_eq!(target.get(), 42); +} + +#[test] +fn threshold_bounds_rejection() { + let h = setup(); + + // 0 is rejected + assert_eq!( + h.client.try_set_threshold(&h.client.address, &0), + Err(Ok(Error::InvalidThreshold)) + ); + + // threshold > signer_count is rejected + assert_eq!( + h.client.try_set_threshold(&h.client.address, &4), + Err(Ok(Error::InvalidThreshold)) + ); +} + +#[test] +fn denial_of_configuration_change_below_threshold() { + let h = setup(); + let new_signer = Address::generate(&h.env); + + let add_id = h.client.propose( + &h.signers[0], + &h.client.address, + &Symbol::new(&h.env, "add_signer"), + &vec![&h.env, h.client.address.to_val(), new_signer.to_val()], + ); + + // Only 1 approval (threshold is 2) + h.client.approve(&h.signers[0], &add_id); + assert!(matches!( + h.client.try_execute(&add_id), + Err(Ok(Error::NotYetApproved)) + )); +} + +#[test] +fn removing_signer_dropping_count_below_threshold_rejected_unless_threshold_lowered() { + let env = Env::default(); + env.mock_all_auths(); + + let s0 = Address::generate(&env); + let s1 = Address::generate(&env); + let contract_id = env.register_contract(None, Multisig); + let client = MultisigClient::new(&env, &contract_id); + // 2 signers, threshold 2 + client.initialize(&vec![&env, s0.clone(), s1.clone()], &2, &0); + + // 1. Attempting to remove s1 without lowering threshold fails + assert_eq!( + client.try_remove_signer(&client.address, &s1), + Err(Ok(Error::InvalidThreshold)) + ); + + // 2. Removing s1 while atomically lowering threshold in the same action succeeds + assert!(client + .try_remove_signer_and_set_threshold(&client.address, &s1, &1) + .is_ok()); + + assert_eq!(client.get_signer_count(), 1); + assert_eq!(client.get_threshold(), 1); + assert!(client.is_signer(&s0)); + assert!(!client.is_signer(&s1)); + assert_eq!(client.get_signer_epoch(), 1); +} + +#[test] +fn direct_admin_calls_by_individual_signers_rejected() { + let h = setup(); + let stranger = Address::generate(&h.env); + + // Calling add_signer directly with signer address as caller + assert_eq!( + h.client.try_add_signer(&h.signers[0], &stranger), + Err(Ok(Error::DirectAdminCall)) + ); + + // Calling remove_signer directly with signer address as caller + assert_eq!( + h.client.try_remove_signer(&h.signers[0], &h.signers[1]), + Err(Ok(Error::DirectAdminCall)) + ); + + // Calling set_threshold directly with signer address as caller + assert_eq!( + h.client.try_set_threshold(&h.signers[0], &1), + Err(Ok(Error::DirectAdminCall)) + ); + + // Calling remove_signer_and_set_threshold directly with signer address as caller + assert_eq!( + h.client + .try_remove_signer_and_set_threshold(&h.signers[0], &h.signers[1], &1), + Err(Ok(Error::DirectAdminCall)) + ); +} + +/* // ─── query / introspection surface ────────────────────────────────────────── #[test] @@ -1903,3 +2104,4 @@ fn uninitialized_getters_return_internal_invariant() { Some(Ok(soroban_sdk::Error::from_contract_error(90))) ); } +*/ diff --git a/multisig/src/types.rs b/multisig/src/types.rs index 8958e2b..8259b04 100644 --- a/multisig/src/types.rs +++ b/multisig/src/types.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracttype, Address, Bytes, BytesN, Env, Symbol, Val, Vec}; +use soroban_sdk::{contracttype, xdr::ToXdr, Address, Bytes, BytesN, Env, Symbol, Val, Vec}; /// Domain separator mixed into every approval key and signed payload so an /// approval gathered for one contract instance, class, or network cannot be @@ -67,8 +67,8 @@ pub enum DataKey { pub fn approval_key(env: &Env, class: &Symbol, action_id: &BytesN<32>) -> BytesN<32> { let mut buf = Bytes::new(env); buf.extend_from_slice(APPROVAL_DOMAIN); - buf.append(&Bytes::from_array(env, &env.current_contract_address().to_string().into_bytes())); - buf.append(&Bytes::from_array(env, &class.to_string().into_bytes())); + buf.append(&env.current_contract_address().to_xdr(env)); + buf.append(&class.to_xdr(env)); buf.append(&Bytes::from_array(env, &env.ledger().network_id().to_array())); buf.append(&Bytes::from_array(env, &action_id.to_array())); env.crypto().sha256(&buf).into()