diff --git a/.gitignore b/.gitignore index 1bfca715..256c1085 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,7 @@ *.vscode /chain/assets /types/pkg +/storage/rocksdb +.idea -targets \ No newline at end of file +targets diff --git a/Cargo.lock b/Cargo.lock index 28d57788..80e1b67e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,13 @@ dependencies = [ "memchr", ] +[[package]] +name = "alto-actions" +version = "0.1.0" +dependencies = [ + "alto-types", +] + [[package]] name = "alto-chain" version = "0.0.6" @@ -59,11 +66,15 @@ dependencies = [ "futures", "governor", "prometheus-client", - "rand", + "rand 0.8.5", "serde", + "serde_bytes", "serde_yaml", "sysinfo", "thiserror 2.0.12", + "tokio", + "tokio-tungstenite 0.26.2", + "tower 0.4.13", "tracing", "tracing-subscriber", "uuid", @@ -78,11 +89,15 @@ dependencies = [ "commonware-cryptography", "commonware-utils", "futures", - "rand", + "rand 0.8.5", "reqwest", + "serde", + "serde_bytes", + "serde_yaml", "thiserror 2.0.12", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.17.2", + "url", ] [[package]] @@ -96,28 +111,52 @@ dependencies = [ "commonware-cryptography", "commonware-utils", "futures", - "rand", + "rand 0.8.5", "thiserror 2.0.12", "tokio", "tracing", "tracing-subscriber", ] +[[package]] +name = "alto-storage" +version = "0.1.0" +dependencies = [ + "alto-types", + "bytes", + "commonware-codec 0.0.43", + "commonware-cryptography", + "rand 0.8.5", + "rocksdb", + "tempfile", +] + [[package]] name = "alto-types" -version = "0.0.6" +version = "0.0.4" dependencies = [ "bytes", + "commonware-codec 0.0.40", "commonware-cryptography", "commonware-utils", "getrandom 0.2.15", - "rand", + "more-asserts", + "rand 0.8.5", "serde", "serde-wasm-bindgen", "thiserror 2.0.12", "wasm-bindgen", ] +[[package]] +name = "alto-vm" +version = "0.1.0" +dependencies = [ + "alto-actions", + "alto-storage", + "alto-types", +] + [[package]] name = "anstream" version = "0.6.18" @@ -532,11 +571,13 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" dependencies = [ "axum-core", + "axum-macros", + "base64 0.22.1", "bytes", "form_urlencoded", "futures-util", @@ -556,9 +597,11 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", + "sha1", "sync_wrapper", "tokio", - "tower", + "tokio-tungstenite 0.26.2", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", @@ -566,12 +609,12 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", - "futures-util", + "futures-core", "http 1.2.0", "http-body 1.0.1", "http-body-util", @@ -584,6 +627,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -645,6 +699,26 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.99", +] + [[package]] name = "bitflags" version = "2.9.0" @@ -721,6 +795,16 @@ dependencies = [ "either", ] +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cc" version = "1.2.16" @@ -732,6 +816,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -773,6 +866,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.31" @@ -823,11 +927,33 @@ dependencies = [ "prometheus-client", "prost", "prost-build", - "rand", + "rand 0.8.5", "thiserror 2.0.12", "tracing", ] +[[package]] +name = "commonware-codec" +version = "0.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea2a8daeca3c1346a30a4c92af7791b1c37e669fad9e1907203e2cc6f92eec7" +dependencies = [ + "bytes", + "paste", + "thiserror 2.0.12", +] + +[[package]] +name = "commonware-codec" +version = "0.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ebb5fc2fbf7d3fbdaf8d3dd3afdb80f03ee02a7d5a9fefc979235a2c06569a" +dependencies = [ + "bytes", + "paste", + "thiserror 2.0.12", +] + [[package]] name = "commonware-consensus" version = "0.0.43" @@ -847,7 +973,7 @@ dependencies = [ "prometheus-client", "prost", "prost-build", - "rand", + "rand 0.8.5", "rand_distr", "thiserror 2.0.12", "tracing", @@ -866,7 +992,7 @@ dependencies = [ "getrandom 0.2.15", "p256", "prost-build", - "rand", + "rand 0.8.5", "rayon", "sha2 0.10.8", "thiserror 2.0.12", @@ -926,7 +1052,7 @@ dependencies = [ "prometheus-client", "prost", "prost-build", - "rand", + "rand 0.8.5", "rand_distr", "thiserror 2.0.12", "tracing", @@ -952,7 +1078,7 @@ dependencies = [ "prometheus-client", "prost", "prost-build", - "rand", + "rand 0.8.5", "thiserror 2.0.12", "tracing", ] @@ -972,7 +1098,7 @@ dependencies = [ "getrandom 0.2.15", "governor", "prometheus-client", - "rand", + "rand 0.8.5", "sha2 0.10.8", "thiserror 2.0.12", "tokio", @@ -1015,7 +1141,7 @@ dependencies = [ "futures", "prost", "prost-build", - "rand", + "rand 0.8.5", "thiserror 2.0.12", "x25519-dalek", ] @@ -1104,7 +1230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1116,7 +1242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -1154,7 +1280,7 @@ checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" dependencies = [ "byteorder", "digest 0.9.0", - "rand_core", + "rand_core 0.6.4", "subtle-ng", "zeroize", ] @@ -1172,6 +1298,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + [[package]] name = "der" version = "0.7.9" @@ -1252,7 +1384,7 @@ checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" dependencies = [ "curve25519-dalek-ng", "hex", - "rand_core", + "rand_core 0.6.4", "serde", "sha2 0.9.9", "thiserror 1.0.69", @@ -1279,7 +1411,7 @@ dependencies = [ "group", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -1322,7 +1454,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1532,7 +1664,7 @@ dependencies = [ "parking_lot", "portable-atomic", "quanta", - "rand", + "rand 0.8.5", "smallvec", "spinning_top", ] @@ -1544,7 +1676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1560,7 +1692,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.7.1", "slab", "tokio", "tokio-util", @@ -1579,13 +1711,19 @@ dependencies = [ "futures-core", "futures-sink", "http 1.2.0", - "indexmap", + "indexmap 2.7.1", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1598,6 +1736,16 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "byteorder", + "num-traits", +] + [[package]] name = "heck" version = "0.5.0" @@ -1945,6 +2093,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.7.1" @@ -1976,6 +2134,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2016,18 +2183,60 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets", +] + [[package]] name = "libm" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "librocksdb-sys" +version = "0.17.1+9.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7869a512ae9982f4d46ba482c2a304f1efd80c6412a3d4bf57bb79a619679f" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "libc", + "libz-sys", + "lz4-sys", + "zstd-sys", +] + +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.9.2" @@ -2056,6 +2265,16 @@ version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "matchit" version = "0.8.4" @@ -2074,6 +2293,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.5" @@ -2094,6 +2319,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + [[package]] name = "multimap" version = "0.10.0" @@ -2123,6 +2354,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nonzero_ext" version = "0.3.0" @@ -2295,6 +2536,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2317,7 +2564,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.7.1", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", ] [[package]] @@ -2377,7 +2644,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -2448,7 +2715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck", - "itertools", + "itertools 0.14.0", "log", "multimap", "once_cell", @@ -2468,7 +2735,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.99", @@ -2520,8 +2787,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy 0.8.24", ] [[package]] @@ -2531,7 +2809,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -2543,6 +2831,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.1", +] + [[package]] name = "rand_distr" version = "0.4.3" @@ -2550,7 +2847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -2667,7 +2964,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tower", + "tower 0.5.2", "tower-service", "url", "wasm-bindgen", @@ -2700,12 +2997,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rocksdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec73b20525cb235bad420f911473b69f9fe27cc856c5461bccd7e4af037f43" +dependencies = [ + "libc", + "librocksdb-sys", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2910,6 +3223,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -2961,7 +3283,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.7.1", "itoa", "ryu", "serde", @@ -2979,6 +3301,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.9.9" @@ -3034,7 +3367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -3371,7 +3704,19 @@ dependencies = [ "native-tls", "tokio", "tokio-native-tls", - "tungstenite", + "tungstenite 0.17.3", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.26.2", ] [[package]] @@ -3387,6 +3732,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "hdrhistogram", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -3505,13 +3871,30 @@ dependencies = [ "httparse", "log", "native-tls", - "rand", + "rand 0.8.5", "sha-1", "thiserror 1.0.69", "url", "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http 1.2.0", + "httparse", + "log", + "rand 0.9.0", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + [[package]] name = "typenum" version = "1.18.0" @@ -3949,7 +4332,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core", + "rand_core 0.6.4", "serde", "zeroize", ] @@ -3991,7 +4374,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", ] [[package]] @@ -4005,6 +4397,17 @@ dependencies = [ "syn 2.0.99", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.99", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 334ede8e..ac14ebfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,21 @@ [workspace] -members = [ +members = [ "actions", "chain", "client", "inspector", + "storage", "types", + "vm", ] resolver = "2" [workspace.dependencies] alto-client = { version = "0.0.6", path = "client" } -alto-types = { version = "0.0.6", path = "types" } +alto-types = { version = "0.0.4", path = "types" } +alto-actions = { version = "0.1.0", path = "actions"} +alto-storage = { version = "0.1.0", path = "storage"} +alto-chain = { version = "0.0.6", path = "chain"} +alto-vm = { version = "0.1.0", path = "vm"} commonware-broadcast = { version = "0.0.43" } commonware-consensus = { version = "0.0.43" } commonware-cryptography = { version = "0.0.43" } @@ -21,6 +27,7 @@ commonware-runtime = { version = "0.0.43" } commonware-storage = { version = "0.0.43" } commonware-stream = { version = "0.0.43" } commonware-utils = { version = "0.0.43" } +commonware-codec = { version = "0.0.40"} thiserror = "2.0.12" bytes = "1.7.1" rand = "0.8.5" @@ -33,6 +40,7 @@ tracing-subscriber = "0.3.19" governor = "0.6.3" prometheus-client = "0.22.3" clap = "4.5.18" +more-asserts = "0.3.1" [profile.bench] # Because we enable overflow checks in "release," we should benchmark with them. diff --git a/actions/Cargo.toml b/actions/Cargo.toml new file mode 100644 index 00000000..60a2bf23 --- /dev/null +++ b/actions/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "alto-actions" +version = "0.1.0" +edition = "2021" + +[dependencies] +alto-types = { workspace = true } diff --git a/actions/src/lib.rs b/actions/src/lib.rs new file mode 100644 index 00000000..6b3f5403 --- /dev/null +++ b/actions/src/lib.rs @@ -0,0 +1,2 @@ +pub mod msg; +pub mod transfer; \ No newline at end of file diff --git a/actions/src/msg.rs b/actions/src/msg.rs new file mode 100644 index 00000000..ac211393 --- /dev/null +++ b/actions/src/msg.rs @@ -0,0 +1,22 @@ +use alto_types::address::Address; + +pub struct SequencerMsg { + pub chain_id: Vec, + pub data: Vec, + pub from_address: Address, + pub relayer_id: u64, +} + +impl SequencerMsg { + pub fn new() -> Self { + Self { + chain_id: vec![], + data: vec![], + from_address: Address::empty(), + relayer_id: 0, + } + } + pub fn get_type_id(&self) -> u8 { + 0 + } +} \ No newline at end of file diff --git a/actions/src/transfer.rs b/actions/src/transfer.rs new file mode 100644 index 00000000..9d026c33 --- /dev/null +++ b/actions/src/transfer.rs @@ -0,0 +1,55 @@ +use alto_types::address::Address; + +const MAX_MEMO_SIZE: usize = 256; + +#[derive(Debug)] +pub struct Transfer { + pub from_address: Address, + pub to_address: Address, + pub value: u64, + pub memo: Vec, +} + +#[derive(Debug)] +pub enum TransferError { + DuplicateAddress, + InvalidToAddress, + InvalidFromAddress, + InsufficientFunds, + TooMuchFunds, + InvalidMemoSize, + StorageError, +} + +impl Transfer { + pub fn new(from_address: Address, to_address: Address, value: u64) -> Result { + let empty_memo = b"".to_vec(); + Self::new_with_memo(from_address, to_address, value, empty_memo) + } + + pub fn new_with_memo(from: Address, to: Address, value: u64, memo: Vec) -> Result { + if value == 0 { + Err(TransferError::InsufficientFunds) + } + else if memo.len() > MAX_MEMO_SIZE { + Err(TransferError::InvalidMemoSize) + } + else if from.is_empty() { + Err(TransferError::InvalidFromAddress) + } + else if to.is_empty() { + Err(TransferError::InvalidToAddress) + } + else if from == to { + Err(TransferError::DuplicateAddress) + } + else { + Ok(Self { + from_address: from, + to_address: to, + value, + memo, + }) + } + } +} \ No newline at end of file diff --git a/chain/Cargo.toml b/chain/Cargo.toml index 127d3fde..2bf7d701 100644 --- a/chain/Cargo.toml +++ b/chain/Cargo.toml @@ -34,10 +34,14 @@ governor = { workspace = true } prometheus-client = { workspace = true } clap = { workspace = true } sysinfo = "0.33.1" -axum = "0.8.1" +axum = { version = "0.8.1", features = ["macros", "ws"] } uuid = "1.15.1" serde = { version = "1.0.218", features = ["derive"] } serde_yaml = "0.9.34" +serde_bytes = "0.11.17" +tokio-tungstenite = "0.26.2" +tokio = "1.44.0" +tower = { version = "0.4", features = ["full"] } [[bin]] name = "validator" diff --git a/chain/src/actors/application/actor.rs b/chain/src/actors/application/actor.rs index b3c7a091..adbaefec 100644 --- a/chain/src/actors/application/actor.rs +++ b/chain/src/actors/application/actor.rs @@ -3,10 +3,10 @@ use super::{ supervisor::Supervisor, Config, }; -use crate::actors::syncer; +use crate::actors::{net, syncer}; use alto_types::{Block, Finalization, Notarization, Seed}; use commonware_consensus::threshold_simplex::Prover; -use commonware_cryptography::{sha256::Digest, Hasher, Sha256}; +use commonware_cryptography::{hash, sha256::{self, Digest}, Hasher, Sha256}; use commonware_macros::select; use commonware_runtime::{Clock, Handle, Metrics, Spawner}; use commonware_utils::SystemTimeExt; @@ -85,10 +85,11 @@ impl Actor { // Compute genesis digest self.hasher.update(GENESIS); let genesis_parent = self.hasher.finalize(); - let genesis = Block::new(genesis_parent, 0, 0); + let genesis = Block::new(genesis_parent, 0, 0, vec![], sha256::hash(&[0; 32])); let genesis_digest = genesis.digest(); let built: Option = None; let built = Arc::new(Mutex::new(built)); + while let Some(message) = self.mailbox.next().await { match message { Message::Genesis { response } => { @@ -124,7 +125,7 @@ impl Actor { if current <= parent.timestamp { current = parent.timestamp + 1; } - let block = Block::new(parent.digest(), parent.height+1, current); + let block = Block::new(parent.digest(), parent.height+1, current, vec![], sha256::hash(&[0; 32])); let digest = block.digest(); { let mut built = built.lock().unwrap(); diff --git a/chain/src/actors/application/mod.rs b/chain/src/actors/application/mod.rs index 70f2dcbc..54ff87d8 100644 --- a/chain/src/actors/application/mod.rs +++ b/chain/src/actors/application/mod.rs @@ -10,6 +10,7 @@ pub use actor::Actor; mod ingress; pub use ingress::Mailbox; mod supervisor; + pub use supervisor::Supervisor; /// Configuration for the application. diff --git a/chain/src/actors/mempool/actor.rs b/chain/src/actors/mempool/actor.rs index ae47cd76..6ee7066b 100644 --- a/chain/src/actors/mempool/actor.rs +++ b/chain/src/actors/mempool/actor.rs @@ -1,6 +1,6 @@ use super::{ ingress::{Mailbox, Message}, mempool}; use commonware_broadcast::Broadcaster; -use commonware_cryptography::Digest; +use commonware_cryptography::{Digest, Hasher}; use commonware_utils::Array; use futures::{ channel::mpsc, @@ -9,19 +9,19 @@ use futures::{ use tracing::{error, warn, debug}; -pub struct Actor { - mailbox: mpsc::Receiver>, +pub struct Actor { + mailbox: mpsc::Receiver>, } -impl Actor { - pub fn new() -> (Self, Mailbox) { +impl Actor { + pub fn new() -> (Self, Mailbox) { let (sender, receiver) = mpsc::channel(1024); (Actor { mailbox: receiver }, Mailbox::new(sender)) } pub async fn run(mut self, - mut engine: impl Broadcaster, - mut mempool: mempool::Mailbox + mut engine: impl Broadcaster, + mut mempool: mempool::Mailbox ) { // it passes msgs in the mailbox of the actor to the engine mailbox while let Some(msg) = self.mailbox.next().await { diff --git a/chain/src/actors/mempool/collector.rs b/chain/src/actors/mempool/collector.rs index 9b746887..21347b41 100644 --- a/chain/src/actors/mempool/collector.rs +++ b/chain/src/actors/mempool/collector.rs @@ -1,5 +1,5 @@ use commonware_broadcast::{linked::Prover, Collector as Z, Proof, }; -use commonware_cryptography::{bls12381::primitives::group, Digest, Scheme}; +use commonware_cryptography::{bls12381::primitives::group, Digest, Hasher, Scheme}; use futures::{ channel::mpsc, SinkExt, StreamExt, @@ -8,13 +8,13 @@ use tracing::error; use super::mempool; -enum Message { - Acknowledged(Proof, D), +enum Message { + Acknowledged(Proof, H::Digest), _Phantom(C::PublicKey), } -pub struct Collector { - mailbox: mpsc::Receiver>, +pub struct Collector { + mailbox: mpsc::Receiver>, // Application namespace namespace: Vec, @@ -23,8 +23,8 @@ pub struct Collector { public: group::Public, } -impl Collector { - pub fn new(namespace: &[u8], public: group::Public) -> (Self, Mailbox) { +impl Collector { + pub fn new(namespace: &[u8], public: group::Public) -> (Self, Mailbox) { let (sender, receiver) = mpsc::channel(1024); ( Collector { @@ -36,13 +36,13 @@ impl Collector { ) } - pub async fn run(mut self, mut mempool: mempool::Mailbox) { + pub async fn run(mut self, mut mempool: mempool::Mailbox) { while let Some(msg) = self.mailbox.next().await { match msg { Message::Acknowledged(proof, payload) => { // Check proof. // The prover checks the validity of the threshold signature when deserializing - let prover = Prover::::new(&self.namespace, self.public); + let prover = Prover::::new(&self.namespace, self.public); let _ = match prover.deserialize_threshold(proof) { Some((context, _payload, _epoch, _threshold)) => context, None => { @@ -64,12 +64,12 @@ impl Collector { } #[derive(Clone)] -pub struct Mailbox { - sender: mpsc::Sender>, +pub struct Mailbox { + sender: mpsc::Sender>, } -impl Z for Mailbox { - type Digest = D; +impl Z for Mailbox { + type Digest = H::Digest; async fn acknowledged(&mut self, proof: Proof, payload: Self::Digest) { self.sender .send(Message::Acknowledged(proof, payload)) diff --git a/chain/src/actors/mempool/handler.rs b/chain/src/actors/mempool/handler.rs index e0f014e2..9b8221f1 100644 --- a/chain/src/actors/mempool/handler.rs +++ b/chain/src/actors/mempool/handler.rs @@ -1,5 +1,6 @@ use super::key::MultiIndex; use bytes::Bytes; +use commonware_cryptography::{Digest, Hasher}; use commonware_resolver::{p2p::Producer, Consumer}; use futures::{ channel::{mpsc, oneshot}, @@ -7,32 +8,32 @@ use futures::{ }; use tracing::warn; -pub enum Message { +pub enum Message { Deliver { - key: MultiIndex, + key: MultiIndex, value: Bytes, response: oneshot::Sender, }, Produce { - key: MultiIndex, + key: MultiIndex, response: oneshot::Sender, }, } /// Mailbox for resolver #[derive(Clone)] -pub struct Handler { - sender: mpsc::Sender, +pub struct Handler { + sender: mpsc::Sender>, } -impl Handler { - pub(super) fn new(sender: mpsc::Sender) -> Self { +impl Handler { + pub(super) fn new(sender: mpsc::Sender>) -> Self { Self { sender } } } -impl Consumer for Handler { - type Key = MultiIndex; +impl Consumer for Handler { + type Key = MultiIndex; type Value = Bytes; type Failure = (); @@ -54,8 +55,8 @@ impl Consumer for Handler { } } -impl Producer for Handler { - type Key = MultiIndex; +impl Producer for Handler { + type Key = MultiIndex; async fn produce(&mut self, key: Self::Key) -> oneshot::Receiver { let (response, receiver) = oneshot::channel(); diff --git a/chain/src/actors/mempool/ingress.rs b/chain/src/actors/mempool/ingress.rs index e8dd1def..c02adc15 100644 --- a/chain/src/actors/mempool/ingress.rs +++ b/chain/src/actors/mempool/ingress.rs @@ -1,5 +1,7 @@ +use std::hash::Hash; + use commonware_utils::Array; -use commonware_cryptography::Digest; +use commonware_cryptography::{Digest, Hasher}; use commonware_broadcast::{linked::Context, Application as A}; use futures::{ channel::{mpsc, oneshot}, SinkExt}; @@ -8,31 +10,31 @@ pub struct Payload { data: Vec } -pub enum Message { - Broadcast(D), - Verify(Context

, D, oneshot::Sender), +pub enum Message { + Broadcast(H::Digest), + Verify(Context

, H::Digest, oneshot::Sender), } #[derive(Clone)] -pub struct Mailbox { - sender: mpsc::Sender>, +pub struct Mailbox { + sender: mpsc::Sender>, } -impl Mailbox { - pub(super) fn new(sender: mpsc::Sender>) -> Self { +impl Mailbox { + pub(super) fn new(sender: mpsc::Sender>) -> Self { Self { sender } } - pub async fn broadcast(&mut self, payload: D) { + pub async fn broadcast(&mut self, payload: H::Digest) { let _ = self.sender.send(Message::Broadcast(payload)).await; } } -impl A for Mailbox { +impl A for Mailbox { type Context = Context

; - type Digest = D; + type Digest = H::Digest; async fn verify( &mut self, diff --git a/chain/src/actors/mempool/key.rs b/chain/src/actors/mempool/key.rs index d6ed6b98..b4cb79bd 100644 --- a/chain/src/actors/mempool/key.rs +++ b/chain/src/actors/mempool/key.rs @@ -1,14 +1,14 @@ -use commonware_cryptography::sha256::Digest; +use commonware_cryptography::Digest; use commonware_utils::{Array, SizedSerialize}; use std::{ - cmp::{Ord, PartialOrd}, - fmt::{Debug, Display}, - hash::Hash, - ops::Deref, + cmp::{Ord, PartialOrd}, fmt::{Debug, Display}, hash::Hash, marker::PhantomData, ops::Deref }; use thiserror::Error; -const SERIALIZED_LEN: usize = 1 + Digest::SERIALIZED_LEN; +// to resolve issue of https://github.com/rust-lang/rust/issues/76560 +// the first byte of MultiIndex indicates the index type +// the rest bytes stores key for that type, e.g. a sha256 index would be [0 | digest(32) | rest(31)] +const SERIALIZED_LEN: usize = 64; #[derive(Error, Debug, PartialEq)] pub enum Error { @@ -16,31 +16,42 @@ pub enum Error { InvalidLength, } -pub enum Value { - Digest(Digest), +pub enum Value { + Digest(D), } #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[repr(transparent)] -pub struct MultiIndex([u8; SERIALIZED_LEN]); +pub struct MultiIndex { + index: [u8; SERIALIZED_LEN], -impl MultiIndex { - pub fn new(value: Value) -> Self { + _marker: PhantomData +} + + +impl MultiIndex { + const DIGEST_LENGTH: usize = D::SERIALIZED_LEN; + + pub fn new(value: Value) -> Self { let mut bytes = [0; SERIALIZED_LEN]; match value { Value::Digest(digest) => { bytes[0] = 0; - bytes[1..].copy_from_slice(&digest); + bytes[1..(1+D::SERIALIZED_LEN)].copy_from_slice(&digest); } } - Self(bytes) + Self { + index: bytes, + + _marker: PhantomData + } } - pub fn to_value(&self) -> Value { - match self.0[0] { + pub fn to_value(&self) -> Value { + match self.index[0] { 0 => { - let bytes: [u8; Digest::SERIALIZED_LEN] = self.0[1..].try_into().unwrap(); - let digest = Digest::from(bytes); + let bytes: Vec = self.index[1..(1+Self::DIGEST_LENGTH)].to_vec(); + let digest = D::try_from(bytes).unwrap(); Value::Digest(digest) } _ => unreachable!(), @@ -48,34 +59,42 @@ impl MultiIndex { } } -impl Array for MultiIndex { +impl Array for MultiIndex { type Error = Error; } -impl SizedSerialize for MultiIndex { +impl SizedSerialize for MultiIndex { const SERIALIZED_LEN: usize = SERIALIZED_LEN; } -impl From<[u8; MultiIndex::SERIALIZED_LEN]> for MultiIndex { - fn from(value: [u8; MultiIndex::SERIALIZED_LEN]) -> Self { - Self(value) +impl From<[u8; SERIALIZED_LEN]> for MultiIndex { + fn from(value: [u8; SERIALIZED_LEN]) -> Self { + + Self { + index: value, + + _marker: PhantomData + } } } -impl TryFrom<&[u8]> for MultiIndex { +impl TryFrom<&[u8]> for MultiIndex { type Error = Error; fn try_from(value: &[u8]) -> Result { - if value.len() != MultiIndex::SERIALIZED_LEN { + if value.len() != SERIALIZED_LEN { return Err(Error::InvalidLength); } - let array: [u8; MultiIndex::SERIALIZED_LEN] = + let array: [u8; SERIALIZED_LEN] = value.try_into().map_err(|_| Error::InvalidLength)?; - Ok(Self(array)) + Ok(Self{ + index: array, + _marker: PhantomData + }) } } -impl TryFrom<&Vec> for MultiIndex { +impl TryFrom<&Vec> for MultiIndex { type Error = Error; fn try_from(value: &Vec) -> Result { @@ -83,49 +102,52 @@ impl TryFrom<&Vec> for MultiIndex { } } -impl TryFrom> for MultiIndex { +impl TryFrom> for MultiIndex { type Error = Error; fn try_from(value: Vec) -> Result { - if value.len() != MultiIndex::SERIALIZED_LEN { + if value.len() != SERIALIZED_LEN { return Err(Error::InvalidLength); } // If the length is correct, we can safely convert the vector into a boxed slice without any // copies. let boxed_slice = value.into_boxed_slice(); - let boxed_array: Box<[u8; MultiIndex::SERIALIZED_LEN]> = + let boxed_array: Box<[u8; SERIALIZED_LEN]> = boxed_slice.try_into().map_err(|_| Error::InvalidLength)?; - Ok(Self(*boxed_array)) + Ok(Self { + index: *boxed_array, + _marker: PhantomData + }) } } -impl AsRef<[u8]> for MultiIndex { +impl AsRef<[u8]> for MultiIndex { fn as_ref(&self) -> &[u8] { - &self.0 + &self.index } } -impl Deref for MultiIndex { +impl Deref for MultiIndex { type Target = [u8]; fn deref(&self) -> &[u8] { - &self.0 + &self.index } } -impl Debug for MultiIndex { +impl Debug for MultiIndex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.0[0] { + match self.index[0] { 0 => { - let bytes: [u8; Digest::SERIALIZED_LEN] = self.0[1..].try_into().unwrap(); - write!(f, "digest({})", Digest::from(bytes)) + let bytes: Vec = self.index[1..(1+D::SERIALIZED_LEN)].to_vec(); + write!(f, "digest({})", D::try_from(bytes).unwrap()) } _ => unreachable!(), } } } -impl Display for MultiIndex { +impl Display for MultiIndex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Debug::fmt(self, f) } diff --git a/chain/src/actors/mempool/mempool.rs b/chain/src/actors/mempool/mempool.rs index a743977e..ce28b5d4 100644 --- a/chain/src/actors/mempool/mempool.rs +++ b/chain/src/actors/mempool/mempool.rs @@ -1,4 +1,4 @@ -use std::{collections::{HashMap}, time::{Duration, SystemTime}}; +use std::{collections::HashMap, hash::Hash, time::{Duration, SystemTime}}; use bytes::{BufMut, Bytes}; use commonware_cryptography::{ed25519::PublicKey, sha256, Digest, Hasher, Sha256}; @@ -17,175 +17,56 @@ use rand::Rng; use tracing::{debug, warn, info}; use governor::clock::Clock as GClock; use super::{handler::{Handler, self}, key::{self, MultiIndex, Value}, ingress, coordinator::Coordinator, archive::Wrapped}; -use crate::maybe_delay_between; +use crate::{actors::net, maybe_delay_between}; +use alto_types::{signed_tx::SignedTx, tx::Tx, Batch}; -#[derive(Clone, Debug)] -pub struct Batch { - pub timestamp: SystemTime, - pub txs: Vec>, - pub digest: D, -} - -impl Batch - where Sha256: Hasher -{ - fn compute_digest(txs: &Vec>) -> D { - let mut hasher = Sha256::new(); - - for tx in txs.into_iter() { - hasher.update(tx.raw.as_ref()); - } - - hasher.finalize() - } - - pub fn new(txs: Vec>, timestamp: SystemTime) -> Self { - let digest = Self::compute_digest(&txs); - - Self { - txs, - digest, - timestamp - } - } - - pub fn serialize(&self) -> Vec { - let mut bytes = Vec::new(); - bytes.put_u64(self.timestamp.epoch_millis()); - bytes.put_u64(self.txs.len() as u64); - for tx in self.txs.iter() { - bytes.put_u64(tx.size()); - bytes.extend_from_slice(&tx.raw); - } - bytes - } - - pub fn deserialize(mut bytes: &[u8]) -> Option { - use bytes::Buf; - // We expect at least 18 bytes for the header - if bytes.remaining() < 18 { - return None; - } - let timestamp = bytes.get_u64(); - let timestamp = SystemTime::UNIX_EPOCH + Duration::from_millis(timestamp); - - let tx_count = bytes.get_u64(); - let mut txs = Vec::with_capacity(tx_count as usize); - for _ in 0..tx_count { - // For each transaction, first read the size (u64). - if bytes.remaining() < 8 { - return None; - } - let tx_size = bytes.get_u64() as usize; - // Ensure there are enough bytes left. - if bytes.remaining() < tx_size { - return None; - } - // Extract tx_size bytes. - let tx_bytes = bytes.copy_to_bytes(tx_size); - txs.push(RawTransaction::new(tx_bytes)); - } - // Compute the digest from the transactions. - let digest = Self::compute_digest(&txs); - // Since serialize did not include accepted and timestamp, we set accepted to false - // and set timestamp to the current time. - Some(Self { - timestamp, - txs, - digest, - }) - } - - pub fn contain_tx(&self, digest: &D) -> bool { - self.txs.iter().any(|tx| &tx.digest == digest) - } - - pub fn tx(&self, digest: &D) -> Option> { - self.txs.iter().find(|tx| &tx.digest == digest).cloned() - } -} - -#[derive(Clone, Debug)] -pub struct RawTransaction { - pub raw: Bytes, - - pub digest: D -} - -impl RawTransaction - where Sha256: Hasher -{ - fn compute_digest(raw: &Bytes) -> D { - let mut hasher = Sha256::new(); - hasher.update(&raw); - hasher.finalize() - } - - pub fn new(raw: Bytes) -> Self { - let digest = Self::compute_digest(&raw); - Self { - raw, - digest - } - } - - pub fn validate(&self) -> bool { - // TODO: implement validate here - true - } - - pub fn size(&self) -> u64 { - self.raw.len() as u64 - } -} - -pub enum Message { +pub enum Message { // mark batch as accepted by the netowrk through the broadcast protocol BatchAcknowledged { - digest: D, + digest: H::Digest, response: oneshot::Sender }, // from rpc or websocket - SubmitTx { - payload: RawTransaction, - response: oneshot::Sender + SubmitTxs { + payload: Vec>, + response: oneshot::Sender> }, BatchConsumed { - digests: Vec, + digests: Vec, block_number: u64, response: oneshot::Sender, }, // proposer consume batches to produce a block ConsumeBatches { - response: oneshot::Sender>> + response: oneshot::Sender>> }, GetTx { - digest: D, - response: oneshot::Sender>> + digest: H::Digest, + response: oneshot::Sender>> }, GetBatch { - digest: D, - response: oneshot::Sender>> + digest: H::Digest, + response: oneshot::Sender>> }, GetBatchContainTx { - digest: D, - response: oneshot::Sender>> + digest: H::Digest, + response: oneshot::Sender>> } } #[derive(Clone)] -pub struct Mailbox { - sender: mpsc::Sender> +pub struct Mailbox { + sender: mpsc::Sender> } -impl Mailbox { - pub fn new(sender: mpsc::Sender>) -> Self { +impl Mailbox { + pub fn new(sender: mpsc::Sender>) -> Self { Self { sender } } - pub async fn acknowledge_batch(&mut self, digest: D) -> bool { + pub async fn acknowledge_batch(&mut self, digest: H::Digest) -> bool { let (response, receiver) = oneshot::channel(); self.sender .send(Message::BatchAcknowledged { digest, response}) @@ -195,17 +76,17 @@ impl Mailbox { receiver.await.expect("failed to receive batch acknowledge") } - pub async fn issue_tx(&mut self, tx: RawTransaction) -> bool { + pub async fn submit_txs(&mut self, payload: Vec>) -> Vec { let (response, receiver) = oneshot::channel(); self.sender - .send(Message::SubmitTx { payload: tx, response }) + .send(Message::SubmitTxs { payload, response }) .await .expect("failed to issue tx"); receiver.await.expect("failed to receive tx issue status") } - pub async fn consume_batches(&mut self) -> Vec> { + pub async fn consume_batches(&mut self) -> Vec> { let (response, receiver) = oneshot::channel(); self.sender .send(Message::ConsumeBatches { response }) @@ -215,7 +96,7 @@ impl Mailbox { receiver.await.expect("failed to receive batches") } - pub async fn consumed_batches(&mut self, digests: Vec, block_number: u64) -> bool { + pub async fn consumed_batches(&mut self, digests: Vec, block_number: u64) -> bool { let (response, receiver) = oneshot::channel(); self.sender .send(Message::BatchConsumed { digests, block_number, response }) @@ -225,7 +106,7 @@ impl Mailbox { receiver.await.expect("failed to mark batches as consumed") } - pub async fn get_tx(&mut self, digest: D) -> Option> { + pub async fn get_tx(&mut self, digest: H::Digest) -> Option> { let (response, receiver) = oneshot::channel(); self.sender .send(Message::GetTx { digest, response }) @@ -235,7 +116,7 @@ impl Mailbox { receiver.await.expect("failed to receive tx") } - pub async fn get_batch(&mut self, digest: D) -> Option> { + pub async fn get_batch(&mut self, digest: H::Digest) -> Option> { let (response, receiver) = oneshot::channel(); self.sender .send(Message::GetBatch { digest, response }) @@ -245,7 +126,7 @@ impl Mailbox { receiver.await.expect("failed to receive batch") } - pub async fn get_batch_contain_tx(&mut self, digest: D) -> Option> { + pub async fn get_batch_contain_tx(&mut self, digest: H::Digest) -> Option> { let (response, receiver) = oneshot::channel(); self.sender .send(Message::GetBatchContainTx { digest, response }) @@ -269,24 +150,24 @@ pub struct Config { pub struct Mempool< B: Blob, R: Rng + Clock + GClock + Spawner + Metrics + Storage, - D: Digest + Into + From + H: Hasher > { context: R, public_key: PublicKey, - batches: HashMap>, + batches: HashMap>, - acknowledged: Vec, + acknowledged: Vec, //TODO: replace the following two - accepted: Archive, - consumed: Archive, + accepted: Archive, + consumed: Archive, - txs: Vec>, + txs: Vec>, - mailbox: mpsc::Receiver>, + mailbox: mpsc::Receiver>, mailbox_size: usize, block_height_seen: u64, @@ -299,12 +180,9 @@ pub struct Mempool< impl< B: Blob, R: Rng + Clock + GClock + Spawner + Metrics + Storage, - D: Digest + Into + From -> Mempool - where - Sha256: Hasher, -{ - pub async fn init(context: R, cfg: Config) -> (Self, Mailbox) { + H: Hasher, +> Mempool { + pub async fn init(context: R, cfg: Config) -> (Self, Mailbox) { let accepted_journal = Journal::init( context.with_label("accepted_journal"), journal::variable::Config { @@ -378,7 +256,7 @@ impl< impl Receiver, ), coordinator: Coordinator, - app_mailbox: ingress::Mailbox + app_mailbox: ingress::Mailbox ) -> Handle<()> { self.context.spawn_ref()(self.run(batch_network, backfill_network, coordinator, app_mailbox)) } @@ -394,10 +272,10 @@ impl< impl Receiver, ), coordinator: Coordinator, - mut app_mailbox: ingress::Mailbox + mut app_mailbox: ingress::Mailbox ) { let (handler_sender, mut handler_receiver) = mpsc::channel(self.mailbox_size); - let handler = Handler::new(handler_sender); + let handler = Handler::::new(handler_sender); let (resolver_engine, mut resolver) = p2p::Engine::new( self.context.with_label("resolver"), p2p::Config { @@ -418,7 +296,7 @@ impl< ); resolver_engine.start(backfill_network); - let mut waiters: HashMap>>>> = HashMap::new(); + let mut waiters: HashMap>>>> = HashMap::new(); let mut propose_timeout = self.context.current() + self.batch_propose_interval; let accepted = Wrapped::new(self.accepted); let consumed = Wrapped::new(self.consumed); @@ -436,14 +314,19 @@ impl< return; }; match message { - Message::SubmitTx { payload, response } => { - if !payload.validate() { - let _ = response.send(false); - return; + Message::SubmitTxs { payload, response } => { + // TODO: add timestamp verification here + + let mut result = Vec::with_capacity(payload.len()); + for tx in payload.into_iter() { + if !tx.validate() { + result.push(false); + continue + } + self.txs.push(tx); + result.push(true); } - - self.txs.push(payload); - let _ = response.send(true); + let _ = response.send(result); }, // batch ackowledged by the network Message::BatchAcknowledged { digest, response } => { @@ -474,7 +357,7 @@ impl< // update the seen height self.block_height_seen = block_number; - let consumed_keys: Vec = self.batches.iter() + let consumed_keys: Vec = self.batches.iter() .filter_map(|(digest, batch)| { if digests.contains(&batch.digest) { Some(digest.clone()) @@ -491,7 +374,7 @@ impl< } // remove digests and batches - let consumed_batches: Vec> = consumed_keys.into_iter() + let consumed_batches: Vec> = consumed_keys.into_iter() .filter_map(|key| self.batches.remove(&key)) .collect(); @@ -553,7 +436,7 @@ impl< let mut size = 0; let mut txs_cnt = 0; for tx in self.txs.iter() { - size += tx.size(); + size += tx.size() as u64; txs_cnt += 1; if size > self.batch_size_limit { @@ -581,7 +464,7 @@ impl< }, batch_message = batch_network.1.recv() => { let (sender, message) = batch_message.expect("Batch broadcast closed"); - let Some(batch) = Batch::deserialize(&message) else { + let Ok(batch) = Batch::deserialize(&message) else { warn!(?sender, "failed to deserialize batch"); continue; }; @@ -597,18 +480,19 @@ impl< handler::Message::Produce { key, response } => { match key.to_value() { key::Value::Digest(digest) => { - if let Some(batch) = self.batches.get(&D::from(digest)).cloned() { + if let Some(batch) = self.batches.get(&digest).cloned() { let _ = response.send(batch.serialize().into()); continue; }; - let consumed = consumed.get(Identifier::Key(&D::from(digest))) - .await - .expect("Failed to get accepted batch"); - if let Some(consumed) = consumed { - let _ = response.send(consumed); - continue; - } + // TODO: replace with new key-value db + // let consumed = consumed.get(Identifier::Key(&H::Digest::from(digest))) + // .await + // .expect("Failed to get accepted batch"); + // if let Some(consumed) = consumed { + // let _ = response.send(consumed); + // continue; + // } debug!(?digest, "missing batch"); } } @@ -617,7 +501,7 @@ impl< match key.to_value() { key::Value::Digest(digest) => { let batch = Batch::deserialize(&value).expect("Failed to deserialize batch"); - if batch.digest.into() != digest { + if batch.digest != digest { let _ = response.send(false); continue; } diff --git a/chain/src/actors/mempool/mod.rs b/chain/src/actors/mempool/mod.rs index 0b73aece..bb80906c 100644 --- a/chain/src/actors/mempool/mod.rs +++ b/chain/src/actors/mempool/mod.rs @@ -11,19 +11,20 @@ pub mod archive; mod tests { use core::panic; use std::{collections::{BTreeMap, HashMap}, num::NonZeroU32, sync::{Arc, Mutex}, time::Duration}; + use alto_types::{signed_tx::SignedTx, tx::Tx}; use bytes::Bytes; use commonware_broadcast::linked::{Config, Engine}; use governor::Quota; use tracing::{debug, info, warn}; - use commonware_cryptography::{bls12381::{dkg, primitives::{group::Share, poly}}, ed25519::PublicKey, sha256, Ed25519, Scheme}; + use commonware_cryptography::{bls12381::{dkg, primitives::{group::Share, poly}}, ed25519::PublicKey, sha256, Ed25519, Scheme, Sha256}; use commonware_macros::test_traced; use commonware_p2p::simulated::{Oracle, Receiver, Sender, Link, Network}; use commonware_runtime::{deterministic::{Context, Executor}, Clock, Metrics, Runner, Spawner}; use futures::channel::mpsc; - use super::{ingress, mempool::{self, Mempool, RawTransaction}}; + use super::{ingress, mempool::{self, Mempool}}; type Registrations

= HashMap, Receiver

), @@ -142,10 +143,10 @@ mod tests { pks: &[PublicKey], validators: &[(PublicKey, Ed25519, Share)], registrations: &mut Registrations, - collectors: &mut BTreeMap>, + collectors: &mut BTreeMap>, refresh_epoch_timeout: Duration, rebroadcast_timeout: Duration, - ) -> BTreeMap> { + ) -> BTreeMap> { let mut mailboxes = BTreeMap::new(); let namespace = b"my testing namespace"; for (validator, scheme, share) in validators.iter() { @@ -170,11 +171,11 @@ mod tests { coordinator.set_view(111); let (app, app_mailbox) = - super::actor::Actor::::new(); + super::actor::Actor::::new(); let collector_mempool_mailbox = mempool_mailbox.clone(); let (collector, collector_mailbox) = - super::collector::Collector::::new( + super::collector::Collector::::new( namespace, *poly::public(&identity), ); @@ -215,9 +216,9 @@ mod tests { pks: &[PublicKey], validators: &[(PublicKey, Ed25519, Share)], registrations: &mut Registrations, - app_mailbox: &mut ingress::Mailbox, + app_mailbox: &mut ingress::Mailbox, // mailboxes: &mut BTreeMap>, - ) -> BTreeMap> { + ) -> BTreeMap> { let mut mailboxes= BTreeMap::new(); for (validator, _, share) in validators.iter() { let context = context.with_label(&validator.to_string()); @@ -250,7 +251,7 @@ mod tests { async fn spawn_tx_issuer_and_wait( context: Context, - mailboxes: Arc>>>, + mailboxes: Arc>>>, num_txs: u32, wait_batch_acknowlegement: bool, consume_batch: bool, @@ -259,7 +260,7 @@ mod tests { .clone() .with_label("tx issuer") .spawn(move |context| async move { - let mut mailbox_vec: Vec> = { + let mut mailbox_vec: Vec> = { let guard = mailboxes.lock().unwrap(); guard.values().cloned().collect() }; @@ -276,9 +277,9 @@ mod tests { // issue tx to the first validator let mut digests = Vec::new(); for i in 0..num_txs { - let tx = RawTransaction::new(Bytes::from(format!("tx-{}", i))); - let submission_res = mailbox.issue_tx(tx.clone()).await; - if !submission_res { + let tx = SignedTx::random(); + let submission_res = mailbox.submit_txs(vec![tx.clone()]).await; + if !submission_res[0] { warn!(?tx.digest, "failed to submit tx"); continue; } @@ -339,7 +340,7 @@ mod tests { context.with_label("simulation"), num_validators, &mut shares_vec).await; - let mut collectors = BTreeMap::>::new(); + let mut collectors = BTreeMap::>::new(); let mailboxes = spawn_validator_engines( context.with_label("validator"), identity.clone(), diff --git a/chain/src/actors/mod.rs b/chain/src/actors/mod.rs index 3bac1080..4d8ed485 100644 --- a/chain/src/actors/mod.rs +++ b/chain/src/actors/mod.rs @@ -1,3 +1,4 @@ pub mod application; pub mod syncer; pub mod mempool; +pub mod net; \ No newline at end of file diff --git a/chain/src/actors/net/actor.rs b/chain/src/actors/net/actor.rs new file mode 100644 index 00000000..a2abcf6f --- /dev/null +++ b/chain/src/actors/net/actor.rs @@ -0,0 +1,299 @@ +use std::{ + collections::{HashMap, HashSet}, hash::Hash, io, ops::Deref, sync::{Arc, RwLock} +}; +use alto_client::client_types::{WebsocketClientMessage}; +use alto_types::signed_tx::SignedTx; +use axum::response::IntoResponse; +use axum::{ + routing::get, + extract::{Path, State, ws::{ + WebSocket, WebSocketUpgrade, Message as WSMessage + }}, +}; + +use bytes::Bytes; +use commonware_cryptography::{sha256, Digest, Hasher}; +use commonware_runtime::{Clock, Handle, Metrics, Spawner}; +use futures::{channel::{mpsc, oneshot}, lock::Mutex, SinkExt, StreamExt}; +use rand::Rng; +use serde::Deserialize; +use tokio::net::TcpListener; +use tracing::{debug, event, Level, error}; + +use crate::actors::mempool::mempool; + +use super::ingress::{Mailbox, Message}; +#[derive(Deserialize)] +pub struct DummyTransaction { + #[serde(with = "serde_bytes")] + pub payload: Vec, +} + +type ClientSender = mpsc::Sender>; +type ClientID = String; +type Clients = Arc>>; + +type SharedState = Arc>>; + +#[derive(Clone)] +struct AppState { + context: R, + clients: Clients, + mempool: mempool::Mailbox, + block_listeners: Arc>>, + tx_listeners: Arc>> +} + +pub struct Config { + pub port: u16, + + pub mempool: mempool::Mailbox +} + +pub struct Actor { + context: R, + port: u16, + listener: Option, + pub router: Option, + is_active: bool, + + state: SharedState +} + +impl Actor { + pub const WEBSOCKET_PREFIX: &'static str = "/ws"; + pub const RPC_PREFIX: &'static str = "/api"; + pub const PATH_SUBMIT_TX: &'static str = "/mempool/submit"; + + pub fn new(context: R, cfg: Config) -> (Self, Mailbox) { + if cfg.port == 0 { + panic!("Invalid port number"); + } + + let (sender, mut receiver) = mpsc::channel(1024); + let mailbox = Mailbox::new(sender); + + let state = AppState:: { + context: context.with_label("app_state"), + mempool: cfg.mempool, + clients: Arc::new(RwLock::new(HashMap::new())), + block_listeners: Arc::new(RwLock::new(HashSet::new())), + tx_listeners: Arc::new(RwLock::new(HashSet::new())), + }; + let state = Arc::new(RwLock::new(state)); + let receiver_state = state.clone(); + + context.with_label("receiver").spawn(async move |_| { + println!("starting receiving mailbox messages"); + while let Some(msg) = receiver.next().await { + Self::handle_message(receiver_state.clone(), msg).await; + } + }); + + + let mut router = Actor { + context, + port: cfg.port, + listener: None, + router: None, + is_active: false, + state + }; + router.init_router(); + + ( + router, + mailbox + ) + } + + pub fn start(mut self) -> Handle<()> { + self.context.spawn_ref()(self.run()) + } + + pub fn stop(&self) { + if !self.is_active { + return + } + + event!(Level::INFO, "stopped router service"); + } + + async fn init_listener(&mut self) -> io::Result { + let listener = TcpListener::bind(format!("127.0.0.1:{}", self.port)).await?; + Ok(listener) + } + + /// handles messages from other services within a node such as block messages + async fn handle_message(state: SharedState, msg: Message) { + debug!("handling msg {:?}", msg); + let block_listeners = state.read().unwrap().block_listeners.clone(); + let clients = state.read().unwrap().clients.clone(); + + match msg { + Message::PublishBlock { block } => { + let msg = Arc::new(Message::PublishBlock { block }); + let listeners: Vec<_> = { + block_listeners.read().unwrap().iter().cloned().collect() + }; + + for listener in listeners { + if let Some(tx) = { + let guard = clients.read().unwrap(); + guard.get(&listener).cloned() + } { + let _ = tx.clone().send(msg.clone()).await; + } + } + } + } + } + + async fn ws_handler( + ws: WebSocketUpgrade, + State(state): State>, + ) -> impl IntoResponse { + let client_id = uuid::Uuid::new_v4().to_string(); + ws.on_upgrade(move |socket| Self::handle_socket(socket, client_id, state)) + } + + async fn handle_socket(mut socket: WebSocket, client_id: ClientID, state: SharedState) { + let (mut socket_sender, mut socket_receiver) = socket.split(); + + let (tx, mut rx) = mpsc::channel::>(1024); + + // Insert the sender into the shared state + { + let state = state.write().unwrap(); + state.clients.write().unwrap().insert(client_id.clone(), tx); + print!("inserting client {}\n", client_id); + + state.context.with_label(format!("client-{}", client_id).deref()).spawn(async move |_| { + println!("starting client rx listener"); + while let Some(msg) = rx.next().await { + print!("received message from client receiver chan: {:?}", msg); + let raw = msg.serialize(); + socket_sender.send(WSMessage::Binary(Bytes::from(raw))).await.unwrap(); + } + }); + } + + + while let Some(msg) = socket_receiver.next().await { + match msg { + Ok(WSMessage::Text(text)) => { + debug!(?text, "receiving text"); + } + Ok(WSMessage::Binary(bin)) => { + match WebsocketClientMessage::deserialize(bin.deref()) { + Ok(msg) => { + debug!(?msg, "received msg from client"); + match msg { + WebsocketClientMessage::RegisterBlock => { + println!("adding block listener {}", client_id); + let state = state.write().unwrap(); + state.block_listeners.write().unwrap().insert(client_id.clone()); + }, + WebsocketClientMessage::RegisterTx => { + let state = state.write().unwrap(); + state.tx_listeners.write().unwrap().insert(client_id.clone()); + }, + WebsocketClientMessage::SubmitTxs(txs) => { + let mut mempool = { + let state = state.write().unwrap(); + state.mempool.clone() + }; + let submission_res = mempool.submit_txs(txs).await; + debug!(?submission_res, "txs submission result") + } + } + }, + Err(err) => { + // TODO: possibly terminate the connection as malicious message is sent? + error!(?err, "received unsupported message") + } + } + } + Ok(WSMessage::Close(_)) => { + let state = state.write().unwrap(); + state.clients.write().unwrap().remove(&client_id); + state.block_listeners.write().unwrap().remove(&client_id); + state.tx_listeners.write().unwrap().remove(&client_id); + break; + } + _ => {} + } + } + + { + let state = state.write().unwrap(); + state.clients.write().unwrap().remove(&client_id); + print!("removing client {}\n", client_id); + } + } + + async fn handle_submit_tx( + State(state): State>, + payload: Bytes, + ) -> impl IntoResponse { + let mut mempool = { + state.read().unwrap().mempool.clone() + }; + match SignedTx::::deserialize(&payload) { + Ok(tx) => { + let success = mempool.submit_txs(vec![tx]).await[0]; + if success { + "submitted".to_string() + } else { + "failed to submit tx".to_string() + } + }, + Err(err) => { + format!("failed to submit tx {}", err) + } + } + } + + + fn init_router(&mut self) { + let router = axum::Router::new() + .route( + Self::PATH_SUBMIT_TX, + get(Self::handle_submit_tx).with_state(Arc::clone(&self.state)) + ) + .route( + Self::WEBSOCKET_PREFIX, + get(Self::ws_handler).with_state(Arc::clone(&self.state)) + ); + self.router = Some(router) + } + + async fn serve(&mut self) -> Result<(), Box> { + let listener = self.listener.take().ok_or("serve failed because listener is None"); + let router = self.router.take().ok_or("serve failed because router is None"); + axum::serve(listener.unwrap(), router.unwrap()).await?; + Ok(()) + } + + async fn run(mut self) { + event!(Level::INFO, "starting router service"); + + println!("init listener"); + let listener_res = self.init_listener(); + match listener_res.await { + Ok(value) => self.listener = Some(value), + Err(error) => { + println!("Error during listener init: {}", error); + return + }, + } + + println!("init router & serve"); + self.init_router(); + self.serve().await.unwrap(); + self.is_active = true; + + event!(Level::INFO, "server stopping..."); + + } +} \ No newline at end of file diff --git a/chain/src/actors/net/ingress.rs b/chain/src/actors/net/ingress.rs new file mode 100644 index 00000000..9b23a395 --- /dev/null +++ b/chain/src/actors/net/ingress.rs @@ -0,0 +1,59 @@ +use std::vec; + +use alto_types::Block; +use bytes::{Buf, BufMut, Bytes}; +use commonware_cryptography::sha256::Digest; +use futures::{channel::{mpsc, oneshot}, stream::SelectNextSome, SinkExt}; + +// message from other services +#[derive(Debug)] +pub enum Message { + PublishBlock { + block: Block, + }, +} + +impl Message { + pub fn serialize(&self) -> Vec { + match self { + Self::PublishBlock { block } => { + let raw_block = block.serialize(); + let mut raw = Vec::with_capacity(1 + raw_block.len()); + raw.push(0); + raw.extend_from_slice(&raw_block); + raw + } + } + } + + pub fn deserialize(raw: &[u8]) -> Result { + if raw.is_empty() { + return Err("Empty payload provided".into()); + } + // The first byte indicates the message type. + match raw[0] { + 0 => { + let Some(block) = Block::deserialize(&raw[1..]) else { + return Err(format!("unable to deserialize into block")); + }; + Ok(Message::PublishBlock { block }) + } + other => Err(format!("Unsupported message type: {}", other)), + } + } +} + +#[derive(Clone)] +pub struct Mailbox { + sender: mpsc::Sender +} + +impl Mailbox { + pub fn new(sender: mpsc::Sender) -> Self { + Self { sender } + } + + pub async fn broadcast_block(&mut self, block: Block) { + let _ = self.sender.send(Message::PublishBlock { block }).await; + } +} \ No newline at end of file diff --git a/chain/src/actors/net/mod.rs b/chain/src/actors/net/mod.rs new file mode 100644 index 00000000..9dd0919c --- /dev/null +++ b/chain/src/actors/net/mod.rs @@ -0,0 +1,174 @@ +pub use ingress::{Mailbox, Message}; +pub use actor::{Actor, Config}; + +pub mod actor; +pub mod ingress; + +#[cfg(test)] +mod tests { + use core::panic; + use std::{ops::Deref, str, time::Duration}; + use alto_types::{signed_tx::SignedTx, Block}; + use axum::{ + body::{to_bytes, Body}, + http::{Request, StatusCode}, Router + }; + use commonware_cryptography::{sha256, Sha256}; + use commonware_macros::{test_async, test_traced}; + use commonware_runtime::{tokio::{self, Context, Executor}, Clock, Handle, Metrics, Runner, Spawner}; + use futures::{channel::mpsc, future::{join_all, try_join_all}, SinkExt, StreamExt}; + use tokio_tungstenite::{connect_async, tungstenite::{client, Message as WsClientMessage}}; + use tower::{ServiceExt}; + use alto_client::client_types::{WebsocketClientMessage}; + + use crate::actors::{mempool::mempool}; + + + use super::{actor::Actor, ingress::Message, actor::{self}}; + use tracing::debug; + + fn spawn_mempool(context: Context) -> (Handle<()>, Router) { + let (mempool_sender, mut mempool_receiver) = mpsc::channel(1024); + let mempool_mailbox: mempool::Mailbox = mempool::Mailbox::new(mempool_sender); + let (actor, _) = Actor::new(context.with_label("router"), actor::Config { + port: 7890, + mempool: mempool_mailbox + }); + + let Some(router) = actor.router else { + panic!("router not initalized"); + }; + + let handler = context.with_label("mock_mempool").spawn(async move |_| { + while let Some(msg) = mempool_receiver.next().await { + match msg { + mempool::Message::SubmitTxs { payload, response } => { + print!("received txs from rpc: {:?}", payload); + let _ = response.send(vec![true; payload.len()]); + return; + }, + _ => unreachable!() + } + } + }); + + (handler, router) + } + + #[test_traced] + fn test_submit_tx() { + let (runner, context) = Executor::init(tokio::Config::default()); + runner.start(async move { + let (mempool_handler, router) = spawn_mempool(context); + // Construct a GET request. + // Note: the handler expects a payload (a String). Since GET requests normally have no body, + // you might decide to pass the payload as a query parameter or in the body if that's what you intend. + // Here, we'll assume the payload is extracted from the request body. + let tx = SignedTx::::random(); + let payload = tx.payload(); + let request = Request::builder() + .method("GET") + .uri("/mempool/submit") + .body(Body::from(payload)) + .unwrap(); + + // Send the request to the app. + let response = router.oneshot(request).await.unwrap(); + + // Check that the response status is OK. + assert_eq!(response.status(), StatusCode::OK); + let _ = try_join_all(vec![mempool_handler]).await; + }) + } + + #[test_traced] + fn test_submit_tx_wrong_format() { + let (runner, context) = Executor::init(tokio::Config::default()); + runner.start(async move { + let (mempool_handler, router) = spawn_mempool(context); + // Construct a GET request. + // Note: the handler expects a payload (a String). Since GET requests normally have no body, + // you might decide to pass the payload as a query parameter or in the body if that's what you intend. + // Here, we'll assume the payload is extracted from the request body. + let tx = b"test-tx"; + let request = Request::builder() + .method("GET") + .uri("/mempool/submit") + .body(Body::from(tx.to_vec())) + .unwrap(); + + // Send the request to the app. + let response = router.oneshot(request).await.unwrap(); + + // Check that the response status is OK. + assert_eq!(response.status(), StatusCode::OK); + let body = response.into_body(); + let body = to_bytes(body, 2*1024*1024).await.unwrap(); + let result = String::from_utf8(body.to_vec()).unwrap(); + print!("submission result {}\n", result); + + assert!(result.contains("failed to submit tx")); + + let _ = try_join_all(vec![mempool_handler]).await; + }) + } + + + #[test_traced] + fn test_ws() { + let (runner, mut context) = Executor::default(); + runner.start(async move { + let (mempool_sender, mempool_receiver) = mpsc::channel(1024); + let mempool_mailbox: mempool::Mailbox = mempool::Mailbox::new(mempool_sender); + let (actor, mut mailbox) = Actor::new(context.with_label("router"), actor::Config { + port: 7890, + mempool: mempool_mailbox + }); + + println!("starting router"); + let app_handler = actor.start(); + + println!("launching ws client"); + // instantiate websocket client listening block + let url = format!("ws://127.0.0.1:7890/ws"); + let (ws_stream, response) = connect_async(url).await.expect("Failed to connect"); + assert_eq!(response.status(), StatusCode::SWITCHING_PROTOCOLS); + + let (mut write, mut read) = ws_stream.split(); + // register block + let _ = write.send(WsClientMessage::binary(WebsocketClientMessage::::RegisterBlock.serialize())).await; + + // listening block + let client_handler = context.with_label("ws_client").spawn(async move |_| { + while let Ok(msg) = read.next().await.unwrap() { + match msg { + WsClientMessage::Binary(bin) => { + let msg = Message::deserialize(&bin).unwrap(); + match msg { + Message::PublishBlock { block } => { + println!("received a block from server: {:?}", block); + return; + } + } + }, + _ => { + debug!("unknown message") + } + } + }; + }); + + // send a dummy block + println!("mock sending dummy block from another service"); + let parent_digest = sha256::hash(&[0; 32]); + let height = 0; + let timestamp = 1; + let block = Block::new(parent_digest, height, timestamp, vec![], sha256::hash(&[0; 32])); + mailbox.broadcast_block(block).await; + + context.sleep(Duration::from_millis(1000)).await; + + join_all(vec![client_handler]).await; + }) + } +} \ No newline at end of file diff --git a/chain/src/actors/syncer/actor.rs b/chain/src/actors/syncer/actor.rs index 6ae3feab..b51f66ab 100644 --- a/chain/src/actors/syncer/actor.rs +++ b/chain/src/actors/syncer/actor.rs @@ -7,10 +7,10 @@ use super::{ Config, }; use crate::{ - actors::syncer::{ + actors::{net, syncer::{ handler, key::{self, MultiIndex, Value}, - }, + }}, Indexer, }; use alto_types::{Block, Finalization, Finalized, Notarized}; @@ -236,8 +236,9 @@ impl, I: Index impl Sender, impl Receiver, ), + mut net: net::Mailbox, ) -> Handle<()> { - self.context.spawn_ref()(self.run(broadcast_network, backfill_network)) + self.context.spawn_ref()(self.run(broadcast_network, backfill_network, net)) } /// Run the application actor. @@ -251,6 +252,7 @@ impl, I: Index impl Sender, impl Receiver, ), + mut net: net::Mailbox, ) { // Initialize resolver let coordinator = Coordinator::new(self.participants.clone()); @@ -321,6 +323,7 @@ impl, I: Index // In an application that maintains state, you would compute the state transition function here. + // Cancel any outstanding requests (by height and by digest) resolver .cancel(MultiIndex::new(Value::Finalized(next))) @@ -331,6 +334,9 @@ impl, I: Index .cancel(MultiIndex::new(Value::Digest(block.digest()))) .await; + // send block to net actor and try to broadcast block to any subscribers + net.broadcast_block(block).await; + // Update the latest indexed self.contiguous_height.set(next as i64); last_indexed = next; diff --git a/chain/src/bin/validator.rs b/chain/src/bin/validator.rs index d1d730c4..874c6318 100644 --- a/chain/src/bin/validator.rs +++ b/chain/src/bin/validator.rs @@ -1,4 +1,7 @@ -use alto_chain::{actors::mempool::{self, mempool::Mempool}, engine, Config}; +use alto_chain::{actors::{ + mempool::{self, mempool::Mempool}, + net +}, engine, Config}; use alto_client::Client; use alto_types::P2P_NAMESPACE; use axum::{routing::get, serve, Extension, Router}; @@ -8,7 +11,7 @@ use commonware_cryptography::{ bls12381::primitives::{ group::{self, Element}, poly, - }, ed25519::{PrivateKey, PublicKey}, sha256, Ed25519, Scheme + }, ed25519::{PrivateKey, PublicKey}, sha256, Ed25519, Scheme, Sha256 }; use commonware_deployer::ec2::Peers; use commonware_p2p::authenticated; @@ -30,6 +33,7 @@ use sysinfo::{Disks, System}; use tracing::{error, info, Level}; const SYSTEM_METRICS_REFRESH: Duration = Duration::from_secs(5); +const RPC_PORT: u16 = 7890; // const METRICS_PORT: u16 = 9090; const VOTER_CHANNEL: u32 = 0; @@ -241,9 +245,9 @@ fn main() { // Create mempool/broadcast/Proof of Availability engine let mempool_namespace = b"mempool"; - let (mempool_application, mempool_app_mailbox) = mempool::actor::Actor::::new(); + let (mempool_application, mempool_app_mailbox) = mempool::actor::Actor::::new(); let broadcast_coordinator = mempool::coordinator::Coordinator::new(identity.clone(), peer_keys.clone(), share); - let (_, collector_mailbox) = mempool::collector::Collector::::new(mempool_namespace, identity_public); + let (_, collector_mailbox) = mempool::collector::Collector::::new(mempool_namespace, identity_public); let (broadcast_engine, broadcast_mailbox) = linked::Engine::new(context.with_label("broadcast_engine"), linked::Config { crypto: signer.clone(), coordinator: broadcast_coordinator.clone(), @@ -276,8 +280,17 @@ fn main() { let broadcast_engine = broadcast_engine.start(mempool_broadcaster, mempool_ack_broadcaster); + let broadcaster_mempool_mailbox = mempool_mailbox.clone(); let mempool_handler = mempool.start(mempool_batch_broadcaster, mempool_backfill_broadcaster, broadcast_coordinator, mempool_app_mailbox); - let mempool_broadcast_app_handler = context.with_label("mempool_app").spawn(|_| mempool_application.run(broadcast_mailbox, mempool_mailbox)); + let mempool_broadcast_app_handler = context.with_label("mempool_app").spawn(|_| mempool_application.run(broadcast_mailbox, broadcaster_mempool_mailbox)); + + // Create net + let (net, net_mailbox) = net::Actor::new(context.with_label("net"), net::Config { + port: RPC_PORT, + mempool: mempool_mailbox.clone() + }); + + let net_handler = net.start(); // Create engine let config = engine::Config { @@ -304,7 +317,7 @@ fn main() { let engine = engine::Engine::new(context.with_label("engine"), config).await; // Start engine - let engine = engine.start(voter, resolver, broadcaster, backfiller); + let engine = engine.start(voter, resolver, broadcaster, backfiller, net_mailbox); // Start system metrics collector let system = context.with_label("system").spawn(|context| async move { @@ -377,7 +390,7 @@ fn main() { }); // Wait for any task to error - if let Err(e) = try_join_all(vec![p2p, engine, broadcast_engine, system, metrics, mempool_handler, mempool_broadcast_app_handler]).await { + if let Err(e) = try_join_all(vec![p2p, engine, broadcast_engine, system, metrics, mempool_handler, mempool_broadcast_app_handler, net_handler]).await { error!(?e, "task failed"); } }); diff --git a/chain/src/engine.rs b/chain/src/engine.rs index b54d5df2..eae0ee45 100644 --- a/chain/src/engine.rs +++ b/chain/src/engine.rs @@ -1,14 +1,14 @@ use crate::{ - actors::{application, syncer}, + actors::{application, net, syncer}, Indexer, }; use alto_types::NAMESPACE; use commonware_consensus::threshold_simplex::{self, Engine as Consensus, Prover}; use commonware_cryptography::{ - bls12381::primitives::{group, poly::public, poly::Poly}, + bls12381::primitives::{group, poly::{public, Poly}}, ed25519::PublicKey, sha256::Digest, - Ed25519, Scheme, + Ed25519, Scheme, Sha256, }; use commonware_p2p::{Receiver, Sender}; use commonware_runtime::{Blob, Clock, Handle, Metrics, Spawner, Storage}; @@ -164,6 +164,7 @@ impl + Metri impl Sender, impl Receiver, ), + net: net::Mailbox, ) -> Handle<()> { self.context.clone().spawn(|_| { self.run( @@ -171,6 +172,7 @@ impl + Metri resolver_network, broadcast_network, backfill_network, + net ) }) } @@ -193,16 +195,20 @@ impl + Metri impl Sender, impl Receiver, ), + net: net::Mailbox, ) { // Start the application let application_handle = self.application.start(self.syncer_mailbox); // Start the syncer - let syncer_handle = self.syncer.start(broadcast_network, backfill_network); + let syncer_handle = self.syncer.start(broadcast_network, backfill_network, net); // Start consensus let consensus_handle = self.consensus.start(voter_network, resolver_network); + // Start the router + // let router_config = RouterConfig::default_config(); + // Wait for any actor to finish if let Err(e) = try_join_all(vec![application_handle, syncer_handle, consensus_handle]).await diff --git a/chain/src/lib.rs b/chain/src/lib.rs index 02d3a20b..3f575aac 100644 --- a/chain/src/lib.rs +++ b/chain/src/lib.rs @@ -83,20 +83,23 @@ pub struct Config { #[cfg(test)] mod tests { + use crate::actors::{mempool::mempool, net}; + use super::*; use alto_types::{Finalized, Notarized, Seed}; use bls12381::primitives::poly; - use commonware_cryptography::{bls12381::dkg::ops, ed25519::PublicKey, Ed25519, Scheme}; + use commonware_cryptography::{bls12381::dkg::ops, ed25519::PublicKey, Ed25519, Scheme, Sha256}; use commonware_macros::test_traced; use commonware_p2p::simulated::{self, Link, Network, Oracle, Receiver, Sender}; use commonware_runtime::{ - deterministic::{self, Executor}, + deterministic::{self, Context, Executor}, Clock, Metrics, Runner, Spawner, }; use commonware_utils::quorum; use engine::{Config, Engine}; use governor::Quota; use rand::{rngs::StdRng, Rng, SeedableRng}; + use futures::{channel::mpsc, StreamExt}; use std::{ collections::{HashMap, HashSet}, num::NonZeroU32, @@ -185,6 +188,23 @@ mod tests { registrations } + async fn spawn_net( + context: Context, + ) -> net::Mailbox { + let (net_sender, mut net_receiver) = mpsc::channel(1024); + context.with_label("mock_net").spawn(async move |_| { + while let Some(msg) = net_receiver.next().await { + match msg { + net::Message::PublishBlock { block } => { + info!(?block, "received block from syncer") + } + } + } + }); + + net::Mailbox::new(net_sender) + } + /// Links (or unlinks) validators using the oracle. /// /// The `action` parameter determines the action (e.g. link, unlink) to take. @@ -296,8 +316,11 @@ mod tests { let (voter, resolver, broadcast, backfill) = registrations.remove(&public_key).unwrap(); + // Spawn mock net actor + let net = spawn_net(context.with_label("net")).await; + // Start engine - engine.start(voter, resolver, broadcast, backfill); + engine.start(voter, resolver, broadcast, backfill, net); } // Poll metrics @@ -455,8 +478,11 @@ mod tests { let (voter, resolver, broadcast, backfill) = registrations.remove(&public_key).unwrap(); + // Spawn mock net actor + let net = spawn_net(context.with_label("net")).await; + // Start engine - engine.start(voter, resolver, broadcast, backfill); + engine.start(voter, resolver, broadcast, backfill, net); } // Poll metrics @@ -538,8 +564,11 @@ mod tests { // Get networking let (voter, resolver, broadcast, backfill) = registrations.remove(&public_key).unwrap(); + // Spawn mock net actor + let net = spawn_net(context.with_label("net")).await; + // Start engine - engine.start(voter, resolver, broadcast, backfill); + engine.start(voter, resolver, broadcast, backfill, net); // Poll metrics loop { @@ -673,8 +702,10 @@ mod tests { let (voter, resolver, broadcast, backfill) = registrations.remove(&public_key).unwrap(); + // Spawn mock net actor + let net = spawn_net(context.with_label("net")).await; // Start engine - engine.start(voter, resolver, broadcast, backfill); + engine.start(voter, resolver, broadcast, backfill, net); } // Poll metrics @@ -818,8 +849,10 @@ mod tests { let (voter, resolver, broadcast, backfill) = registrations.remove(&public_key).unwrap(); + // Spawn mock net actor + let net = spawn_net(context.with_label("net")).await; // Start engine - engine.start(voter, resolver, broadcast, backfill); + engine.start(voter, resolver, broadcast, backfill, net); } // Poll metrics diff --git a/client/Cargo.toml b/client/Cargo.toml index 90aa40ac..fb84b3a1 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -21,3 +21,7 @@ futures = { workspace = true } reqwest = "0.12.12" tokio-tungstenite = { version = "0.17", features = ["native-tls"] } tokio = { version = "1.40.0", features = ["full"] } +serde = { version = "1.0.218", features = ["derive"] } +serde_yaml = "0.9.34" +serde_bytes = "0.11.17" +url = "2.5.4" \ No newline at end of file diff --git a/client/src/client.rs b/client/src/client.rs new file mode 100644 index 00000000..81c106e2 --- /dev/null +++ b/client/src/client.rs @@ -0,0 +1,83 @@ +use std::error::Error; +use commonware_cryptography::Sha256; +use reqwest::{Client, Url}; +use super::client_types::{ClientRpcMessageResp, ClientRpcMessage}; +use bytes::Bytes; +use serde::Deserialize; +use alto_types::tx::{Tx}; +pub const WEBSOCKET_PREFIX: &'static str = "/ws"; +pub const RPC_PREFIX: &'static str = "/api"; +// TODO: Update the below endpoints when we know what they are +pub const PATH_SUBMIT_TX: &'static str = "/mempool/submit"; +pub const PATH_GET_BLOCK: &'static str = "/api/get_block"; +pub const PATH_GET_BLOCK_HEIGHT: &'static str = "/api/get_block_height"; + +#[derive(Debug)] +pub struct JSONRPCClient { + http_client: Client, + base_url: String, + chain_id: String, +} + +impl JSONRPCClient { + pub fn new(mut uri: String, chain_id: String) -> Self { + if uri.ends_with('/') { + uri.pop(); + } + let final_url = format!("{}/jsonrpc", uri); + + Self { + http_client: Client::new(), + base_url: final_url, + chain_id, + } + } + //todo implement methods needed to communicate with server + pub async fn submit_tx(&self, mut tx: Tx) -> Result> { + let encoded_tx_bytes = tx.encode(); + let mut submit_request = ClientRpcMessage::SubmitTx { + payload: encoded_tx_bytes.into(), + }; + + let full_url = Url::parse(&self.base_url) + .and_then(|base| base.join(PATH_SUBMIT_TX)) + .expect("Invalid base_url or path for submit tx"); + + todo!() + // self.send_request(full_url.to_string(), submit_request.into()).await + } + + pub async fn get_block(&self, height: u64) -> Result> { + let mut get_block_req = ClientRpcMessage::GetBlock { + height + }; + + let full_url = Url::parse(&self.base_url) + .and_then(|base| base.join(PATH_GET_BLOCK)) + .expect("Invalid base_url or path for get block"); + + todo!() + // self.send_request(full_url.to_string(), get_block_req.into()).await + } + + pub async fn get_block_height(&self) -> Result> { + let mut get_block_height_req = ClientRpcMessage::GetBlockHeight {}; + + let full_url = Url::parse(&self.base_url) + .and_then(|base| base.join(PATH_GET_BLOCK_HEIGHT)) + .expect("Invalid base_url or path for get block"); + + todo!() + // self.send_request(full_url.to_string(), get_block_height_req.into()).await + } + + async fn send_request(&self, uri: String, data: Vec) -> Result> { + let resp = self.http_client.post(uri) + .body(data) + .send() + .await?; + todo!() + // Ok(resp) + } + +} diff --git a/client/src/client_types.rs b/client/src/client_types.rs new file mode 100644 index 00000000..6761bcf5 --- /dev/null +++ b/client/src/client_types.rs @@ -0,0 +1,154 @@ +use std::fmt::Debug; + +use alto_types::signed_tx::SignedTx; +use bytes::{BufMut, Bytes}; +use commonware_cryptography::{sha256::Digest, Hasher, Sha256}; +use serde::{Deserialize, Serialize}; + +/// Messages sent from client +pub enum ClientMessage { + WSMessage(WebsocketClientMessage), + RpcMessage(ClientRpcMessage) +} + +/// Websocket Message sent from client +#[repr(u8)] +pub enum WebsocketClientMessage { + RegisterBlock = 0, + RegisterTx, + SubmitTxs(Vec>) +} + +impl Debug for WebsocketClientMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + todo!() + } +} + + +impl WebsocketClientMessage { + // TODO: cache the serialization result + pub fn serialize(&self) -> Vec { + match self { + Self::RegisterBlock => vec![0], + Self::RegisterTx => vec![1], + Self::SubmitTxs(txs) => { + todo!() + // let mut raw = vec![2]; // first byte indicates the message type + // raw.put_u64(txs.len() as u64); + // for tx in txs.into_iter() { + // raw.put_u64(tx.size() as u64); + // raw.put(tx.payload().into()); + // } + // raw + } + } + } + + pub fn deserialize(raw: &[u8]) -> Result, String> { + use bytes::Buf; + + if raw.is_empty() { + return Err("empty payload provided".into()); + } + + // Create a mutable buffer view over the input slice + let mut buf = raw; + + // Read the message type byte. + let msg_type = buf.get_u8(); + match msg_type { + 0 => Ok(WebsocketClientMessage::RegisterBlock), + 1 => Ok(WebsocketClientMessage::RegisterTx), + 2 => { + todo!() + // // Ensure there are enough bytes for the number of transactions. + // if buf.remaining() < 8 { + // return Err("payload too short for number of transactions".into()); + // } + // let num_txs = buf.get_u64(); + + // let mut txs = Vec::with_capacity(num_txs as usize); + // // Loop over each transaction. + // for _ in 0..num_txs { + // if buf.remaining() < 8 { + // return Err("payload too short for transaction length".into()); + // } + // let tx_len = buf.get_u64() as usize; + // if buf.remaining() < tx_len { + // return Err("payload too short for transaction data".into()); + // } + // // Extract the transaction bytes. + // let tx_data = buf.copy_to_bytes(tx_len); + // txs.push(tx_data); + // } + // Ok(WebsocketClientMessage::SubmitTxs(txs)) + } + other => Err(format!("unsupported message type: {}", other)), + } + } +} + +#[derive(Debug)] +pub enum ClientRpcMessage { + // for rpc + SubmitTx { + payload: Bytes, + }, + GetBlockHeight { + }, + GetBlock { + height: u64, + }, +} + +impl Serialize for ClientRpcMessage { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer { + todo!() + } +} + +impl<'de> Deserialize<'de> for ClientRpcMessage { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de> { + todo!() + } +} + +#[derive(Debug)] +pub enum ClientRpcMessageResp { + // for rpc + SubmitTxResp { + ok: bool, + digest: Vec, + err: String, + }, + GetBlockHeightResp { + chain_id: Vec, + height: u64, + err: String, + }, + GetBlockResp { + height: u64, + err: String, + }, +} + +impl Serialize for ClientRpcMessageResp { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer { + todo!() + } +} + +impl<'de> Deserialize<'de> for ClientRpcMessageResp { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de> { + todo!() + } +} diff --git a/client/src/lib.rs b/client/src/lib.rs index f2068b13..01425f5a 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -6,6 +6,8 @@ use thiserror::Error; pub mod consensus; pub mod utils; +mod client; +pub mod client_types; const LATEST: &str = "latest"; diff --git a/storage/Cargo.toml b/storage/Cargo.toml new file mode 100644 index 00000000..cba09b57 --- /dev/null +++ b/storage/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "alto-storage" +version = "0.1.0" +edition = "2021" + +[dependencies] +alto-types = { workspace = true } +commonware-cryptography = { workspace = true } +rand = "0.8.5" +rocksdb = "0.23.0" +commonware-codec = { version = "0.0.43" } +bytes = "1.7.1" +tempfile = "3.18.0" \ No newline at end of file diff --git a/storage/src/database.rs b/storage/src/database.rs new file mode 100644 index 00000000..032938f4 --- /dev/null +++ b/storage/src/database.rs @@ -0,0 +1,14 @@ +use alto_types::account::{Balance, Account}; +use std::error::Error; +use bytes::Bytes; +use commonware_codec::{Codec, ReadBuffer, WriteBuffer}; +use alto_types::address::Address; + +// Define database interface that will be used for all impls +pub trait Database { + fn put(&mut self, key: &[u8], value: &[u8]) -> Result<(), Box>; + + fn get(&self, key: &[u8]) -> Result, Box>; + + fn delete(&mut self, key: &[u8]) -> Result<(), Box>; +} \ No newline at end of file diff --git a/storage/src/hashmap_db.rs b/storage/src/hashmap_db.rs new file mode 100644 index 00000000..d3396498 --- /dev/null +++ b/storage/src/hashmap_db.rs @@ -0,0 +1,53 @@ +use std::collections::HashMap; +use std::error::Error; +use crate::database::Database; + +pub struct HashmapDatabase { + data: HashMap, +} + +impl HashmapDatabase { + pub fn new() -> Self { + Self { + data: HashMap::new(), + } + } +} + +impl Database for HashmapDatabase { + fn put(&mut self, key: &[u8], value: &[u8]) -> Result<(), Box> { + let key_value: String = String::from_utf8(key.into())?; + let str_value: String = String::from_utf8(value.into())?; + + self.data.insert(key_value, str_value); + Ok(()) + } + + fn get(&self, key: &[u8]) -> Result>, Box> { + let str_key: String = String::from_utf8(key.into()).unwrap(); + self.data.get(&str_key).map_or( + Ok(None), + |v| Ok(Some(v.clone().into()))) + } + + fn delete(&mut self, key: &[u8]) -> Result<(), Box> { + let key_value: String = String::from_utf8(key.into())?; + self.data.remove(&key_value); + Ok(()) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_hashmap_db() { + let mut db = HashmapDatabase::new(); + let key = b"key1"; + let value = b"value1"; + db.put(key, value).unwrap(); + let retrieved = db.get(key).unwrap().unwrap(); + assert_eq!(retrieved.as_slice(), value); + } +} diff --git a/storage/src/lib.rs b/storage/src/lib.rs new file mode 100644 index 00000000..ae0cda68 --- /dev/null +++ b/storage/src/lib.rs @@ -0,0 +1,5 @@ +pub mod database; +pub mod hashmap_db; +mod rocks_db; +mod tx_state_view; +mod state_db; \ No newline at end of file diff --git a/storage/src/rocks_db.rs b/storage/src/rocks_db.rs new file mode 100644 index 00000000..5e39d5d7 --- /dev/null +++ b/storage/src/rocks_db.rs @@ -0,0 +1,66 @@ +use std::error::Error; +use rocksdb::{DB, Options}; +use commonware_codec::{Codec}; +use crate::database::Database; +use std::path::Path; +use bytes::{BufMut}; +use tempfile::TempDir; + +const SAL_ROCKS_DB_PATH: &str = "rocksdb"; + +pub struct RocksDbDatabase { + db: DB, +} + +impl RocksDbDatabase { + pub fn new() -> Result> { + Self::new_with_path(SAL_ROCKS_DB_PATH) + } + + pub fn new_with_path(path: &str) -> Result> { + let mut opts = Options::default(); + opts.create_if_missing(true); + + let db_path = Path::new(path); + let db = DB::open(&opts, &db_path)?; + Ok(RocksDbDatabase { db }) + } + + pub fn new_tmp_db() -> Result> { + let temp_dir = TempDir::new()?; + let db_path = temp_dir.path().join("testdb"); + Self::new_with_path(db_path.to_str().unwrap()) + } + +} + +impl Database for RocksDbDatabase { + fn put(&mut self, key: &[u8], value: &[u8]) -> Result<(), Box> { + self.db.put(key, value)?; + Ok(()) + } + + fn get(&self, key: &[u8]) -> Result>, Box> { + let result = self.db.get(key)?; + Ok(result) + } + + fn delete(&mut self, key: &[u8]) -> Result<(), Box> { + self.db.delete(key)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_rocks_db_basic() { + let mut db = RocksDbDatabase::new().expect("db could not be created"); + let key = b"key1"; + let value = b"value1"; + db.put(key, value).unwrap(); + let retrieved = db.get(key).unwrap().unwrap(); + assert_eq!(retrieved.as_slice(), value); + } +} diff --git a/storage/src/state_db.rs b/storage/src/state_db.rs new file mode 100644 index 00000000..ac72a58a --- /dev/null +++ b/storage/src/state_db.rs @@ -0,0 +1,122 @@ +use crate::database::Database; +use alto_types::account::{Account, Balance}; +use alto_types::address::Address; +use bytes::Bytes; +use commonware_codec::{Codec, ReadBuffer, WriteBuffer}; +use std::error::Error; +use crate::rocks_db::RocksDbDatabase; + +const ACCOUNTS_PREFIX: &[u8] = b"sal_accounts"; +const DB_WRITE_BUFFER_CAPACITY: usize = 500; + +pub struct StateDb { + db: Box, +} +// can use like a redis from Arcadia like get and set for diff types? +impl StateDb { + pub fn new(db: Box) -> StateDb { + StateDb { db } + } + + pub fn get_account(&self, address: &Address) -> Result, Box> { + let key = Self::key_accounts(address); + let result = self.db.get(key.as_slice())?; + match result { + None => Ok(None), + Some(value) => { + let bytes = Bytes::copy_from_slice(&value); + let mut read_buf = ReadBuffer::new(bytes); + let acc = Account::read(&mut read_buf)?; + Ok(Some(acc)) + } + } + } + + pub fn set_account(&mut self, acc: &Account) -> Result<(), Box> { + let key = Self::key_accounts(&acc.address); + let mut write_buf = WriteBuffer::new(DB_WRITE_BUFFER_CAPACITY); + acc.write(&mut write_buf); + self.db.put(&key, write_buf.as_ref()).expect("TODO: panic message"); + Ok(()) + } + + pub fn get_balance(&self, address: &Address) -> Option { + let result = self.get_account(address).and_then(|acc| match acc { + Some(acc) => Ok(acc.balance), + None => Ok(0), + }); + match result { + Ok(balance) => Some(balance), + _ => None, + } + } + + pub fn set_balance(&mut self, address: &Address, amt: Balance) -> bool { + let result = self.get_account(address); + match result { + Ok(Some(mut acc)) => { + acc.balance = amt; + let result = self.set_account(&acc); + result.is_ok() + } + _ => false, + } + } + + fn key_accounts(addr: &Address) -> Vec { + Self::make_multi_key(ACCOUNTS_PREFIX, addr.as_slice()) + } + fn make_multi_key(prefix: &[u8], sub_id: &[u8]) -> Vec { + let mut key = Vec::with_capacity(prefix.len() + sub_id.len() + 1); + key.extend_from_slice(prefix); + key.push(b':'); + key.extend_from_slice(sub_id); + key + } +} + +impl Database for StateDb { + fn put(&mut self, key: &[u8], value: &[u8]) -> Result<(), Box> { + self.db.put(key, value) + } + + fn get(&self, key: &[u8]) -> Result>, Box> { + self.db.get(key) + } + + fn delete(&mut self, key: &[u8]) -> Result<(), Box> { + self.db.delete(key) + } +} + +#[cfg(test)] +mod tests { + use alto_types::address::Address; + use alto_types::account::Account; + use alto_types::address::Address; + use super::*; + + #[test] + fn test_rocks_db_accounts() { + let db = RocksDbDatabase::new_tmp_db().expect("db could not be created"); + let mut state_db = StateDb::new(Box::new(db)); + + let mut account = Account::new(); + let test_address = Address::new(b"0xBEEF"); + account.address = test_address.clone(); + account.balance = 100; + + // get account for test address is empty + let empty_result = state_db.get_account(&test_address); + empty_result.unwrap().is_none(); + + // set account + state_db.set_account(&account).unwrap(); + + let acct_result = state_db.get_account(&test_address).unwrap(); + assert!(acct_result.is_some()); + let account = acct_result.unwrap(); + assert_eq!(account.address, test_address); + assert_eq!(account.balance, 100); + } +} \ No newline at end of file diff --git a/storage/src/tx_state_view.rs b/storage/src/tx_state_view.rs new file mode 100644 index 00000000..6a171111 --- /dev/null +++ b/storage/src/tx_state_view.rs @@ -0,0 +1,110 @@ +use std::collections::HashMap; +use std::error::Error; +use alto_types::address::Address; +use crate::database::Database; +use alto_types::state::{State}; +use crate::state_db::StateDb; + +const ACCOUNT_KEY_TYPE: u8 = 0; + +type UnitKey<'a> = alto_types::state::UnitKey<'a>; // 1st byte denotes the type of key. 0b for account key, 1b for others. + +pub fn decode_unit_key(key: UnitKey) -> (u8, Address) { + let key_type: u8 = key[0]; + let address_bytes: &[u8] = &key[1..]; + (key_type, address_bytes.into()) +} + +pub enum OpAction { + Read, // key was read + Create, // key was created + Update, // key was updated + Delete, // key got deleted +} + +pub struct Op<'a>{ + pub action: OpAction, + pub key: UnitKey<'a>, + pub value: Vec, +} + +pub trait TxStateViewTrait: State { + fn init_cache(&mut self, cache: HashMap>); // initialize the cache with an already available hashmap of key-value pairs. + fn get_from_cache(&self, key: UnitKey) -> Result>, Box>; // get a key from the cache. If the key is not in the cache, it will return an error. + fn get_from_state(&self, key: UnitKey) -> Result>, Box>; // get a key from the underlying storage. If the key is not in the storage, it will return an error. +} + +pub struct TxStateView<'a> { + pub cache: HashMap, Vec>, // key-value, state view cache before tx execution. This is not an exhaustive list of all state keys read/write during tx. If cache misses occur, the state view will read from the underlying storage. + pub ops: Vec>, // list of state ops applied. + pub touched: HashMap, Vec>, // key-value pairs that were changed during tx execution. This is a subset of the cache. + pub state_db: StateDb, // underlying state storage, to use when cache misses occur. +} + +impl<'a> TxStateView<'a> { + pub fn new(state_db: StateDb) -> Self { + Self{ + cache: HashMap::new(), + ops: Vec::new(), + touched: HashMap::new(), + state_db, + } + } + // initialize the cache with an already available hashmap of key-value pairs. + pub fn init_cache(&mut self, cache: HashMap>) { + self.cache = cache; + } + + pub fn get_from_cache(&self, key: UnitKey) -> Result>, Box> { + self.cache.get(&key).map_or( + Ok(None), + |v| Ok(Some(v.clone().into()))) + } + + pub fn get_from_state(&self, key: UnitKey) -> Result>, Box> { + let (key_type, address) + match key[0] { + ACCOUNT_KEY_TYPE => { + self.get_from_state(key) + }, + _ => Err(format!("invalid state key {:?}", key[0]).into()) + } + } + + pub fn get(&self, key: UnitKey) -> Result>, Box>{ + } + + pub fn get_multi_key(&self, key: UnitKey) -> Result>, Box> { + todo!() + } + pub fn update(&mut self, key: UnitKey, value: Vec) -> Result<(), Box>{ + todo!() + } + pub fn delete(&mut self, key: UnitKey) -> Result<(), Box> { + todo!() + } + pub fn commit(&mut self) -> Result<(), Box> { + todo!() + } + pub fn rollback(&mut self) -> Result<(), Box> { + todo!() + } + + pub fn process_get_action(&mut self, cmd_type: u8, key: &UnitKey) -> Result>, Box> { + match cmd_type { + ACCOUNT_KEY_TYPE => { + self.state_db.get(key) + } + _ => Err(format!("invalid state key {:?}", key).into()) + } + } + + pub fn process_put_action(&mut self, cmd_type: u8, key: &UnitKey) -> Result>, Box> { + match cmd_type { + ACCOUNT_KEY_TYPE => { + self.state_db.get(key) + } + _ => Err(format!("invalid state key {:?}", key).into()) + } + } +} \ No newline at end of file diff --git a/types/Cargo.toml b/types/Cargo.toml index 1b8b2958..d58be7c7 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alto-types" -version = "0.0.6" +version = "0.0.4" publish = true edition = "2021" license = "MIT OR Apache-2.0" @@ -16,14 +16,16 @@ crate-type = ["rlib", "cdylib"] [dependencies] commonware-cryptography = { workspace = true } commonware-utils = { workspace = true } +commonware-codec = { workspace = true} bytes = { workspace = true } rand = { workspace = true } thiserror = { workspace = true } wasm-bindgen = "0.2.100" serde = { version = "1.0.219", features = ["derive"] } serde-wasm-bindgen = "0.6.5" +more-asserts = "0.3.1" # Enable "js" feature when WASM is target [target.'cfg(target_arch = "wasm32")'.dependencies.getrandom] version = "0.2.15" -features = ["js"] +features = ["js"] \ No newline at end of file diff --git a/types/src/account.rs b/types/src/account.rs new file mode 100644 index 00000000..bcdb85f0 --- /dev/null +++ b/types/src/account.rs @@ -0,0 +1,53 @@ +use crate::address::Address; +use commonware_codec::{Codec, Error, Reader, Writer}; + +pub type Balance = u64; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Account { + pub address: Address, + pub balance: Balance, +} + +impl Default for Account { + fn default() -> Self { + Self::new() + } +} + +impl Account { + pub fn new() -> Self { + Self { + address: Address::empty(), + balance: 0, + } + } + + pub fn from_address(address: Address) -> Self { + Self { + address, + balance: 0, + } + } +} + +impl Codec for Account { + fn write(&self, writer: &mut impl Writer) { + // @rikoeldon I think we don't need to write account address into the state. + // account address is part of the key. + // todo: might not need to write address in since address is already part of the key? + // writer.write_bytes(self.address.0.as_slice()); + self.balance.write(writer); + } + + fn read(reader: &mut impl Reader) -> Result { + let addr_bytes = <[u8; 33]>::read(reader)?; + let address = Address::from_bytes(&addr_bytes[1..]).unwrap(); + let balance = ::read(reader)?; + Ok(Self { address, balance }) + } + + fn len_encoded(&self) -> usize { + Codec::len_encoded(&self.address.0) + Codec::len_encoded(&self.balance) + } +} \ No newline at end of file diff --git a/types/src/address.rs b/types/src/address.rs new file mode 100644 index 00000000..fdd837c3 --- /dev/null +++ b/types/src/address.rs @@ -0,0 +1,53 @@ +use crate::{PublicKey, ADDRESSLEN}; +use more_asserts::assert_le; +use rand::Rng; + +#[derive(Hash, Eq, PartialEq, Clone, Debug)] +pub struct Address(pub [u8; ADDRESSLEN]); + +impl Address { + pub fn new(slice: &[u8]) -> Self { + assert_le!(slice.len(), ADDRESSLEN, "address slice is too large"); + let mut arr = [0u8; ADDRESSLEN]; + arr[..slice.len()].copy_from_slice(slice); + Address(arr) + } + + pub fn create_random_address() -> Self { + let mut arr = [0u8; ADDRESSLEN]; + rand::thread_rng().fill(&mut arr); + Address(arr) + } + + pub fn from_pub_key(pub_key: &PublicKey) -> Self { + // @todo implement a hasher to derive the address from the public key. + assert_le!(pub_key.len(), ADDRESSLEN, "public key is too large"); + let mut arr = [0u8; ADDRESSLEN]; + arr[..pub_key.len()].copy_from_slice(pub_key.as_ref()); + Address(arr) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 32 { + return Err("Address must be 32 bytes."); + } + + Ok(Address(<[u8; 32]>::try_from(bytes).unwrap())) + } + + pub fn empty() -> Self { + Self([0; ADDRESSLEN]) + } + + pub fn is_empty(&self) -> bool { + self.0 == Self::empty().0 + } + + pub fn as_slice(&self) -> &[u8] { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8; ADDRESSLEN] { + &self.0 + } +} \ No newline at end of file diff --git a/types/src/batch.rs b/types/src/batch.rs new file mode 100644 index 00000000..08e0db95 --- /dev/null +++ b/types/src/batch.rs @@ -0,0 +1,141 @@ +use std::hash::Hash; +use std::time::{Duration, SystemTime}; + +use bytes::BufMut; +use commonware_cryptography::Hasher; +use commonware_utils::{SizedSerialize, SystemTimeExt}; +use bytes::Buf; + +use crate::signed_tx::SignedTx; + +#[derive(Clone, Debug)] +pub struct Batch { + pub timestamp: SystemTime, + // TODO: store real transactions not just raws + pub txs: Vec>, + pub digest: H::Digest, +} + +impl SizedSerialize for Batch { + const SERIALIZED_LEN: usize = size_of::()*2; +} + +impl Batch { + fn compute_digest(txs: &Vec>) -> H::Digest { + let mut hasher = H::new(); + + for tx in txs.iter() { + hasher.update(&tx.digest()); + } + + hasher.finalize() + } + + pub fn new(txs: Vec>, timestamp: SystemTime) -> Self { + let digest = Self::compute_digest(&txs); + + Self { + txs, + digest, + timestamp + } + } + + pub fn serialize(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.put_u64(self.timestamp.epoch_millis()); + bytes.put_u64(self.txs.len() as u64); + for tx in self.txs.iter() { + bytes.put_u64(tx.size() as u64); + bytes.extend_from_slice(&tx.payload()); + } + bytes + } + + pub fn deserialize(mut bytes: &[u8]) -> Result { + if bytes.remaining() < Self::SERIALIZED_LEN { + return Err(format!("not enough bytes for header")); + } + let timestamp = bytes.get_u64(); + let timestamp = SystemTime::UNIX_EPOCH + Duration::from_millis(timestamp); + + let tx_count = bytes.get_u64(); + let mut txs = Vec::with_capacity(tx_count as usize); + for _ in 0..tx_count { + // For each transaction, first read the size (u64). + if bytes.remaining() < size_of::() { + return Err("not enough bytes for tx size".to_string()); + } + let tx_size = bytes.get_u64() as usize; + // Ensure there are enough bytes left. + if bytes.remaining() < tx_size { + return Err(format!("not enough bytes for tx payload, needed: {}, actual: {}", tx_size, bytes.remaining())); + } + // Extract tx_size bytes. + let tx_bytes = bytes.copy_to_bytes(tx_size); + txs.push(SignedTx::deserialize(&tx_bytes)?); + } + if bytes.remaining() != 0 { + return Err(format!("left residue after decoding all the txs: {}", bytes.remaining())); + } + + // Compute the digest from the transactions. + let digest = Self::compute_digest(&txs); + // Since serialize did not include accepted and timestamp, we set accepted to false + // and set timestamp to the current time. + Ok(Self { + timestamp, + txs, + digest, + }) + } + + pub fn contain_tx(&self, digest: &H::Digest) -> bool { + self.txs.iter().any(|tx| &tx.digest == digest) + } + + pub fn tx(&self, digest: &H::Digest) -> Option> { + self.txs.iter().find(|tx| &tx.digest == digest).map_or(None, |tx| Some(tx.clone())) + } +} + +#[cfg(test)] +mod tests { + use std::time::SystemTime; + + use commonware_cryptography::Sha256; + use commonware_utils::SystemTimeExt; + + use crate::signed_tx::SignedTx; + + use super::Batch; + + #[test] + fn test_encode_decode() { + let tx = SignedTx::::random(); + let batch = Batch::new(vec![tx], SystemTime::now()); + let payload = batch.serialize(); + + let batch_recover = Batch::::deserialize(&payload).unwrap(); + + assert_eq!(batch.timestamp.epoch_millis(), batch_recover.timestamp.epoch_millis()); + assert_eq!(batch.txs.len(), batch_recover.txs.len()); + assert_eq!(batch.txs[0].digest, batch_recover.txs[0].digest); + assert_eq!(batch.txs[0].payload(), batch_recover.txs[0].payload()); + assert_eq!(batch.digest, batch_recover.digest); + } + + #[test] + fn test_residue() { + let tx = SignedTx::::random(); + let batch = Batch::new(vec![tx], SystemTime::now()); + let mut payload = batch.serialize(); + payload.push(10); + + let decode_result = Batch::::deserialize(&payload); + + let err_str = decode_result.map_err(|e| e.to_string()).err().unwrap(); + print!("{}\n", err_str); + assert!(err_str.contains("left residue after decoding all the txs")); + } +} \ No newline at end of file diff --git a/types/src/block.rs b/types/src/block.rs index c247541f..1c082dea 100644 --- a/types/src/block.rs +++ b/types/src/block.rs @@ -1,12 +1,17 @@ -use crate::{Finalization, Notarization}; +use std::hash::Hash; + +use crate::signed_tx::{pack_signed_txs, unpack_signed_txs, SignedTx}; +use crate::{batch, Batch, Finalization, Notarization}; use bytes::{Buf, BufMut}; -use commonware_cryptography::{bls12381::PublicKey, sha256::Digest, Hasher, Sha256}; +use commonware_cryptography::{bls12381::PublicKey, sha256, sha256::Digest as Sha256Digest, Hasher, Sha256}; use commonware_utils::{Array, SizedSerialize}; -#[derive(Clone, Debug, PartialEq, Eq)] +// @todo add state root, fee manager and results to the block struct. +// what method of state root generation should be used? +#[derive(Clone, Debug)] pub struct Block { /// The parent block's digest. - pub parent: Digest, + pub parent: Sha256Digest, /// The height of the block in the blockchain. pub height: u64, @@ -14,25 +19,61 @@ pub struct Block { /// The timestamp of the block (in milliseconds since the Unix epoch). pub timestamp: u64, + /// The state root of the block. + pub state_root: Sha256Digest, + + pub batches: Vec, + + _batches: Vec>, + /// Pre-computed digest of the block. - digest: Digest, + digest: Sha256Digest, +} + +impl SizedSerialize for Block { + // parent + height + timestamp + state_root + len(batches) + const SERIALIZED_LEN: usize = + Sha256Digest::SERIALIZED_LEN + u64::SERIALIZED_LEN + u64::SERIALIZED_LEN + Sha256Digest::SERIALIZED_LEN + u64::SERIALIZED_LEN; } impl Block { - fn compute_digest(parent: &Digest, height: u64, timestamp: u64) -> Digest { + fn compute_digest( + parent: &Sha256Digest, + height: u64, + timestamp: u64, + batch_digests: &Vec, + state_root: &Sha256Digest, + ) -> Sha256Digest { let mut hasher = Sha256::new(); hasher.update(parent); hasher.update(&height.to_be_bytes()); hasher.update(×tamp.to_be_bytes()); + for digest in batch_digests.iter() { + hasher.update(digest); + } + hasher.update(state_root); hasher.finalize() } - pub fn new(parent: Digest, height: u64, timestamp: u64) -> Self { - let digest = Self::compute_digest(&parent, height, timestamp); + pub fn new( + parent: Sha256Digest, + height: u64, + timestamp: u64, + batches: Vec>, + state_root: Sha256Digest, + ) -> Self { + // let mut txs = txs; + // @todo this is packing txs in a block. + let batch_digests = batches.iter().map(|batch| batch.digest).collect(); + + let digest = Self::compute_digest(&parent, height, timestamp, &batch_digests, &state_root); Self { parent, height, timestamp, + state_root, + batches: batch_digests, + _batches: batches, digest, } } @@ -42,38 +83,56 @@ impl Block { bytes.extend_from_slice(&self.parent); bytes.put_u64(self.height); bytes.put_u64(self.timestamp); + bytes.extend_from_slice(&self.state_root); + bytes.put_u64(self.batches.len() as u64); + for digest in self.batches.iter() { + bytes.extend_from_slice(&digest); + } bytes } pub fn deserialize(mut bytes: &[u8]) -> Option { // Parse the block - if bytes.len() != Self::SERIALIZED_LEN { + if bytes.len() < Self::SERIALIZED_LEN { return None; } - let parent = Digest::read_from(&mut bytes).ok()?; + let parent = Sha256Digest::read_from(&mut bytes).ok()?; let height = bytes.get_u64(); let timestamp = bytes.get_u64(); + let state_root = Sha256Digest::read_from(&mut bytes).ok()?; + let num_batches = bytes.get_u64(); + let mut batch_digests = Vec::with_capacity(num_batches as usize); + for _ in 0..num_batches { + if bytes.remaining() < Sha256Digest::SERIALIZED_LEN { + return None; + } + let batch_digest = Sha256Digest::read_from(&mut bytes).ok()?; + batch_digests.push(batch_digest); + } + + if bytes.remaining() != 0 { + return None; + } + + let digest = Self::compute_digest(&parent, height, timestamp, &batch_digests, &state_root); // Return block - let digest = Self::compute_digest(&parent, height, timestamp); Some(Self { parent, height, timestamp, + state_root, + batches: batch_digests, + _batches: vec![], digest, }) } - pub fn digest(&self) -> Digest { - self.digest + pub fn digest(&self) -> Sha256Digest { + self.digest.clone() } } -impl SizedSerialize for Block { - const SERIALIZED_LEN: usize = - Digest::SERIALIZED_LEN + u64::SERIALIZED_LEN + u64::SERIALIZED_LEN; -} - pub struct Notarized { pub proof: Notarization, pub block: Block, @@ -136,4 +195,4 @@ impl Finalized { } Some(Self { proof, block }) } -} +} \ No newline at end of file diff --git a/types/src/consensus.rs b/types/src/consensus.rs index 5fca5214..6ae6a5ed 100644 --- a/types/src/consensus.rs +++ b/types/src/consensus.rs @@ -292,4 +292,4 @@ impl SizedSerialize for Finalization { pub fn leader_index(seed: &[u8], participants: usize) -> usize { modulo(seed, participants as u64) as usize -} +} \ No newline at end of file diff --git a/types/src/lib.rs b/types/src/lib.rs index 9ffac637..e4014763 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -1,23 +1,77 @@ //! Common types used throughout `alto`. mod block; +mod batch; + pub use block::{Block, Finalized, Notarized}; +pub use batch::Batch; +use commonware_cryptography::{Ed25519, Scheme}; +use commonware_utils::SystemTimeExt; +use std::time::SystemTime; mod consensus; pub use consensus::{leader_index, Finalization, Kind, Notarization, Nullification, Seed}; +pub mod account; +pub mod address; +pub mod null_error; +pub mod signed_tx; +pub mod state_view; +pub mod tx; +pub mod units; +pub mod wallet; pub mod wasm; +use rand::rngs::OsRng; +use crate::address::Address; + // We don't use functions here to guard against silent changes. pub const NAMESPACE: &[u8] = b"_ALTO"; +pub const TX_NAMESPACE: &[u8] = b"_tx_namespace_"; pub const P2P_NAMESPACE: &[u8] = b"_ALTO_P2P"; pub const SEED_NAMESPACE: &[u8] = b"_ALTO_SEED"; pub const NOTARIZE_NAMESPACE: &[u8] = b"_ALTO_NOTARIZE"; pub const NULLIFY_NAMESPACE: &[u8] = b"_ALTO_NULLIFY"; pub const FINALIZE_NAMESPACE: &[u8] = b"_ALTO_FINALIZE"; +const ADDRESSLEN: usize = 32; + +type PublicKey = commonware_cryptography::ed25519::PublicKey; +type PrivateKey = commonware_cryptography::ed25519::PrivateKey; +type Signature = commonware_cryptography::ed25519::Signature; + + +pub fn create_test_keypair() -> (PublicKey, PrivateKey) { + let mut rng = OsRng; + // generates keypair using random number generator + let keypair = Ed25519::new(&mut rng); + + let public_key = keypair.public_key(); + let private_key = keypair.private_key(); + + (public_key, private_key) +} + +// pub fn empty_pub_key() -> PublicKey { +// PublicKey::try_from(&[0; 33]).unwrap() +// } + +// pub fn curr_timestamp() -> u64 { +// SystemTime::now().epoch_millis() +// } + +// pub fn empty_signature() -> Signature { +// Signature::try_from("").unwrap() +// } + +// pub fn random_signature() -> Signature { +// let addr = Address::create_random_address(); +// Signature::try_from(addr).unwrap() +// } + #[cfg(test)] mod tests { use super::*; use commonware_cryptography::{hash, Bls12381, Scheme}; + use commonware_utils::SizedSerialize; use rand::{rngs::StdRng, SeedableRng}; #[test] @@ -117,7 +171,7 @@ mod tests { let parent_digest = hash(&[0; 32]); let height = 0; let timestamp = 1; - let block = Block::new(parent_digest, height, timestamp); + let block = Block::new(parent_digest, height, timestamp, Vec::new(), [0; 32].into()); let block_digest = block.digest(); // Check block serialization @@ -127,6 +181,7 @@ mod tests { assert_eq!(block.parent, deserialized.parent); assert_eq!(block.height, deserialized.height); assert_eq!(block.timestamp, deserialized.timestamp); + // @todo add deserialization checks for signed transactions. // Create notarization let view = 0; @@ -168,7 +223,7 @@ mod tests { let parent_digest = hash(&[0; 32]); let height = 0; let timestamp = 1; - let block = Block::new(parent_digest, height, timestamp); + let block = Block::new(parent_digest, height, timestamp, Vec::new(), [0; 32].into()); // Create notarization let view = 0; @@ -206,4 +261,34 @@ mod tests { let result = Finalization::deserialize(None, &serialized); assert!(result.is_some()); } -} + + #[test] + fn test_block_residue() { + // Create network key + let mut rng = StdRng::seed_from_u64(0); + // Create block + let parent_digest = hash(&[0; 32]); + let height = 0; + let timestamp = 1; + let block = Block::new(parent_digest, height, timestamp, Vec::new(), [0; 32].into()); + let mut payload = block.serialize(); + payload.push(10); + + let block_recover = Block::deserialize(&payload); + assert!(block_recover.is_none()); + } + + #[test] + fn test_block_below_serialize_len() { + // Create block + let parent_digest = hash(&[0; 32]); + let height = 0; + let timestamp = 1; + let block = Block::new(parent_digest, height, timestamp, Vec::new(), [0; 32].into()); + let mut payload = block.serialize(); + payload.push(10); + + let block_recover = Block::deserialize(&payload[0..Block::SERIALIZED_LEN-1]); + assert!(block_recover.is_none()); + } +} \ No newline at end of file diff --git a/types/src/null_error.rs b/types/src/null_error.rs new file mode 100644 index 00000000..5e3050bc --- /dev/null +++ b/types/src/null_error.rs @@ -0,0 +1,15 @@ +use std::{ + error::Error, + fmt::{Display, Formatter, Result}, +}; + +#[derive(Debug)] +pub struct NullError; + +impl Display for NullError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "NoError") + } +} + +impl Error for NullError {} \ No newline at end of file diff --git a/types/src/signed_tx.rs b/types/src/signed_tx.rs new file mode 100644 index 00000000..f7298b3d --- /dev/null +++ b/types/src/signed_tx.rs @@ -0,0 +1,350 @@ +use std::fmt::Debug; +use std::hash::Hash; +use std::ops::Deref; +use std::sync::OnceLock; +use bytes::{Buf, BufMut}; +use commonware_codec::Codec; +use commonware_utils::SizedSerialize; +use crate::address::Address; +use crate::tx::{Tx}; +use crate::wallet::{Wallet, WalletMethods}; +use crate::{create_test_keypair, PublicKey, Signature, TX_NAMESPACE}; +use commonware_cryptography::{Ed25519, Hasher, Scheme, Sha256}; +use std::cell::OnceCell; +use rand::rngs::OsRng; +use crate::units::msg::SequencerMsg; +use crate::units::transfer::Transfer; +use crate::units::Unit; + +// this is sent by the user to the validators. +#[derive(Clone)] +pub struct SignedTx { + pub_key: PublicKey, + signature: Signature, + pub tx: Tx, + + pub digest: H::Digest, + // cached is encode of SignedTx + // todo use OnceCell since payload encode is set once or use RefCell for mutable access? + cached_payload: OnceLock>, +} + +impl SizedSerialize for SignedTx { + // Pubkey + Sig + TxLen + Sizeof(Tx) + const SERIALIZED_LEN: usize = PublicKey::SERIALIZED_LEN + Signature::SERIALIZED_LEN + size_of::(); +} + +impl Debug for SignedTx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // todo! do any of these need to be hex encoded? + f.debug_struct("SignedTx") + .field("tx", &self.tx) + .field("digest", &self.digest) + // .field("address", &self.address) + .field("signature", &self.signature) + .finish() + } +} + + +impl SignedTx { + pub fn digest(&self) -> H::Digest { + self.digest + } + + pub fn payload(&self) -> Vec { + if self.cached_payload.get().is_none() { + self.cached_payload.set(self.encode()).expect("could not set cache payload"); + } + self.cached_payload.get().unwrap().to_vec() + } + + pub fn size(&self) -> usize { + self.payload().len() + } + + pub fn serialize(&self) -> Vec { + self.payload() + } + + pub fn deserialize(raw: &[u8]) -> Result { + Self::decode(raw) + } + + pub fn validate(&self) -> bool { + let tx_data = self.tx.encode(); + let signature = self.signature.clone(); + if signature.is_empty() { + return false; + } + let sender_pk = self.pub_key.clone(); + Ed25519::verify(Some(TX_NAMESPACE), &tx_data, &sender_pk, &signature) + } + + pub fn random() -> Self { + // create a tx + let tx = Tx::random(); + // generate keypair for sk usage + let (_, sk) = create_test_keypair(); + // create wallet + let wallet = Wallet::load(sk.as_ref()); + // sign tx to create a signed tx + Self::sign(tx, wallet) + } + + fn new(tx: Tx, pub_key: PublicKey, signature: Vec) -> Self { + let mut hasher = H::new(); + let digest = hasher.finalize(); + Self { + tx, + pub_key: pub_key.clone(), + signature: Signature::try_from(signature.clone()).unwrap(), + cached_payload: OnceLock::new(), + digest + } + } + + fn verify(&self) -> bool { + let tx_data = self.tx.encode(); + let signature = self.signature.clone(); + if signature.is_empty() { + return false; + } + Ed25519::verify(Some(TX_NAMESPACE), &tx_data, &self.pub_key, &signature) + } + + fn signature(&self) -> Signature { + self.signature.clone() + } + + fn public_key(&self) -> PublicKey { + self.pub_key.clone() + } + + // @todo add syntactic checks. + pub fn encode(&self) -> Vec { + let mut bytes = Vec::new(); + + bytes.extend_from_slice(&self.pub_key); + bytes.extend_from_slice(&self.signature); + + let raw_tx = self.tx.payload(); + bytes.put_u64(raw_tx.len() as u64); + bytes.extend_from_slice(&raw_tx); + bytes + } + + // @todo add syntactic checks and use methods consume. + fn decode(mut bytes: &[u8]) -> Result { + let payload = bytes.to_vec(); + + if bytes.len() < Self::SERIALIZED_LEN { + return Err(format!("bytes len: {} below min size: {}", bytes.len(), Self::SERIALIZED_LEN)) + } + + let pub_key = PublicKey::try_from(bytes.copy_to_bytes(PublicKey::SERIALIZED_LEN).deref()).map_err(|e| stringify!(e))?; + let signature = Signature::try_from(bytes.copy_to_bytes(Signature::SERIALIZED_LEN).deref()).map_err(|e| stringify!(e))?; + + let raw_tx_len = bytes.get_u64(); + if bytes.remaining() != raw_tx_len as usize { + return Err(format!("remaining bytes length not equal to tx len, wanted: {}, actual: {}", raw_tx_len, bytes.remaining())) + } + + let raw_tx = bytes.copy_to_bytes(raw_tx_len as usize); + let tx = Tx::decode(&raw_tx)?; + + let mut hasher = H::new(); + hasher.update(&raw_tx); + let digest = hasher.finalize(); + + Ok(SignedTx { + tx, + pub_key, + signature, + digest, + cached_payload: OnceLock::from(payload), + }) + } + + pub fn sign(mut tx: Tx, mut wallet: Wallet) -> SignedTx { + let tx_data = tx.encode(); + + let mut hasher = H::new(); + hasher.update(&tx_data); + let digest = hasher.finalize(); + + SignedTx { + tx: tx.clone(), + signature: Signature::try_from(wallet.sign(&tx_data)).unwrap(), + pub_key: wallet.public_key(), + digest, + cached_payload: OnceLock::new(), + } + } +} + +pub fn pack_signed_txs(signed_txs: Vec>) -> Vec { + let mut bytes = Vec::new(); + bytes.extend((signed_txs.len() as u64).to_be_bytes()); + for signed_tx in signed_txs { + // @todo improvise + let mut signed_tx = signed_tx; + let signed_tx_bytes = signed_tx.encode(); + bytes.extend((signed_tx_bytes.len() as u64).to_be_bytes()); + bytes.extend_from_slice(&signed_tx_bytes); + } + bytes +} + +pub fn unpack_signed_txs(bytes: Vec) -> Vec> { + let signed_txs_len = u64::from_be_bytes(bytes[0..8].try_into().unwrap()); + let mut signed_txs = Vec::with_capacity(signed_txs_len as usize); + let mut offset = 8; + for _ in 0..signed_txs_len { + let signed_tx_len = u64::from_be_bytes(bytes[offset..offset + 8].try_into().unwrap()); + offset += 8; + let signed_tx_bytes = &bytes[offset..offset + signed_tx_len as usize]; + offset += signed_tx_len as usize; + let signed_tx = SignedTx::decode(signed_tx_bytes); + if signed_tx.is_err() { + panic!("Failed to unpack signed tx: {}", signed_tx.unwrap_err()); + } + signed_txs.push(signed_tx.unwrap()); + } + signed_txs +} + +#[cfg(test)] +mod tests { + use std::default; + use std::error::Error; + use std::hash::Hash; + use std::time::SystemTime; + + use super::*; + use crate::units::transfer::Transfer; + use crate::units::Unit; + use crate::{create_test_keypair}; + use commonware_cryptography::sha256::{self, Digest}; + use commonware_cryptography::Sha256; + use commonware_utils::SystemTimeExt; + use more_asserts::assert_gt; + + #[test] + fn test_encode_decode() -> Result<(), Box> { + let timestamp = SystemTime::now().epoch_millis(); + let max_fee = 100; + let priority_fee = 75; + let chain_id = 45205; + let transfer = Transfer::default(); + let units: Vec> = vec![Box::new(transfer)]; + let (_, sk) = create_test_keypair(); + // TODO: the .encode call on next line gave error and said origin_msg needed to be mut? but why? + // shouldn't encode be able to encode without changing the msg? + let tx = Tx::::new( + timestamp, + max_fee, + priority_fee, + chain_id, + Address::empty(), + units, + ); + let origin_msg = SignedTx::sign(tx, Wallet::load(&sk)); + let encoded_bytes = origin_msg.encode(); + assert_gt!(encoded_bytes.len(), 0); + let decoded_msg = SignedTx::::decode(&encoded_bytes)?; + assert_eq!(origin_msg.pub_key, decoded_msg.pub_key); + assert_eq!(origin_msg.signature, decoded_msg.signature); + // @todo make helper to compare fields in tx and units. same issue when testing in tx.rs file. + Ok(()) + } + + #[test] + fn test_insufficient_bytes() { + let timestamp = SystemTime::now().epoch_millis(); + let max_fee = 100; + let priority_fee = 75; + let chain_id = 45205; + let transfer = Transfer::default(); + let units: Vec> = vec![Box::new(transfer)]; + let (_, sk) = create_test_keypair(); + // TODO: the .encode call on next line gave error and said origin_msg needed to be mut? but why? + // shouldn't encode be able to encode without changing the msg? + let tx = Tx::::new( + timestamp, + max_fee, + priority_fee, + chain_id, + Address::empty(), + units, + ); + let origin_msg = SignedTx::sign(tx, Wallet::load(&sk)); + let encoded_bytes = origin_msg.encode(); + assert_gt!(encoded_bytes.len(), 0); + let decode_result = SignedTx::::decode(&encoded_bytes[0..encoded_bytes.len()-10]); + + let err_str = decode_result.map_err(|e| e.to_string()).err().unwrap(); + print!("{}\n", err_str); + assert!(err_str.contains("remaining bytes length not equal to tx len")); + } + + #[test] + fn test_below_serialize_len() { + let timestamp = SystemTime::now().epoch_millis(); + let max_fee = 100; + let priority_fee = 75; + let chain_id = 45205; + let transfer = Transfer::default(); + let units: Vec> = vec![Box::new(transfer)]; + let (_, sk) = create_test_keypair(); + // TODO: the .encode call on next line gave error and said origin_msg needed to be mut? but why? + // shouldn't encode be able to encode without changing the msg? + let tx = Tx::::new( + timestamp, + max_fee, + priority_fee, + chain_id, + Address::empty(), + units, + ); + let origin_msg = SignedTx::sign(tx, Wallet::load(&sk)); + let encoded_bytes = origin_msg.encode(); + assert_gt!(encoded_bytes.len(), 0); + let decode_result = SignedTx::::decode(&encoded_bytes[0..SignedTx::::SERIALIZED_LEN-1]); + + let err_str = decode_result.map_err(|e| e.to_string()).err().unwrap(); + print!("{}\n", err_str); + assert!(err_str.contains("below min size")); + } + + #[test] + fn test_residue() { + let timestamp = SystemTime::now().epoch_millis(); + let max_fee = 100; + let priority_fee = 75; + let chain_id = 45205; + let transfer = Transfer::default(); + let units: Vec> = vec![Box::new(transfer)]; + let (_, sk) = create_test_keypair(); + // TODO: the .encode call on next line gave error and said origin_msg needed to be mut? but why? + // shouldn't encode be able to encode without changing the msg? + let tx = Tx::::new( + timestamp, + max_fee, + priority_fee, + chain_id, + Address::empty(), + units, + ); + let origin_msg = SignedTx::sign(tx, Wallet::load(&sk)); + let mut encoded_bytes = origin_msg.encode(); + encoded_bytes.push(10); + + assert_gt!(encoded_bytes.len(), 0); + let decode_result = SignedTx::::decode(&encoded_bytes); + + let err_str = decode_result.map_err(|e| e.to_string()).err().unwrap(); + print!("{}\n", err_str); + assert!(err_str.contains("remaining bytes length not equal to tx len")); + } +} \ No newline at end of file diff --git a/types/src/state_view.rs b/types/src/state_view.rs new file mode 100644 index 00000000..2574b168 --- /dev/null +++ b/types/src/state_view.rs @@ -0,0 +1,10 @@ +use crate::account::{Account, Balance}; +use crate::address::Address; +use std::error::Error; + +pub trait StateView { + fn get_account(&mut self, address: &Address) -> Result, Box>; + fn set_account(&mut self, acc: &Account) -> Result<(), Box>; + fn get_balance(&mut self, address: &Address) -> Option; + fn set_balance(&mut self, address: &Address, amt: Balance) -> bool; +} \ No newline at end of file diff --git a/types/src/tx.rs b/types/src/tx.rs new file mode 100644 index 00000000..f4151dd0 --- /dev/null +++ b/types/src/tx.rs @@ -0,0 +1,389 @@ +use bytes::{Buf, BufMut}; +use commonware_cryptography::{hash, sha256, Hasher, Scheme}; +use commonware_cryptography::sha256::Digest; +use std::any::Any; +use std::cell::OnceCell; +use std::error::Error; +use std::fmt::Debug; +use std::ops::Add; +use std::sync::OnceLock; + +use crate::address::Address; +use crate::signed_tx::SignedTx; +use crate::state_view::StateView; +use crate::units::{self, decode_units, encode_units, transfer, Unit, UnitType}; +use crate::wallet::Wallet; +use commonware_utils::{SizedSerialize, SystemTimeExt}; +use std::time::SystemTime; +use commonware_cryptography::ed25519::PublicKey; +use crate::units::msg::SequencerMsg; +use crate::units::transfer::Transfer; + +// TODO: add a commonware_cryptography::Hasher trait for Tx, and the digest should be labeled as H::Digest +#[derive(Clone)] +pub struct Tx { + /// timestamp of the tx creation. set by the user. + /// will be verified if the tx is in the valid window once received by validators. + /// if the timestamp is not in the valid window, the tx will be rejected. + /// if tx is in a valid window it is added to mempool. + /// timestamp is used to prevent replay attacks. and counter infinite spam attacks as Tx does not have nonce. + pub timestamp: u64, + /// max fee is the maximum fee the user is willing to pay for the tx. + pub max_fee: u64, + /// priority fee is the fee the user is willing to pay for the tx to be included in the next block. + pub priority_fee: u64, + /// chain id is the id of the chain the tx is intended for. + pub chain_id: u64, + /// units are fundamental unit of a tx. similar to actions. + pub units: Vec>, + + /// id is the transaction id. It is the hash of payload. + pub id: H::Digest, + /// payload is encoded tx. + pub payload: OnceLock>, + + // TODO: add a payload referenced by OnceCell here possibly to avoid repeated serialization/deserialization +} + +/// The minimal length for a tx +impl SizedSerialize for Tx { + // timestamp + max_fee + priority_fee + chain_id + len(units) + sizeof(Units) + const SERIALIZED_LEN: usize = size_of::()*5; +} + +impl Debug for Tx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // todo! do any of these need to be hex encoded? + f.debug_struct("SignedTx") + .field("timestamp", &self.timestamp) + .field("max_fee", &self.max_fee) + .field("priority_fee", &self.priority_fee) + .field("chain_id", &self.chain_id) + .field("units", &self.units) + .field("id", &self.id) + .field("payload", &self.payload) + .finish() + } +} + +impl Tx { + fn compute_digest(&self) -> H::Digest { + if self.payload.get().is_none() { + let _ = self.serialize(); + } + + let payload = self.payload.get().expect("payload nil"); + let mut hasher = H::new(); + hasher.update(&payload); + hasher.finalize() + } + + pub fn digest(&mut self) -> H::Digest { + self.id + } + + pub fn validate(&self) -> bool { + todo!() + } + + pub fn payload(&self) -> Vec { + self.encode() + } + + pub fn serialize(&self) -> Vec { + self.encode() + } + + pub fn deserialize(raw: &[u8]) -> Result { + Self::decode(raw) + } + + // size of the payload + pub fn size(&mut self) -> usize { + self.payload().len() + } + + pub fn random() -> Self { + // create a tx + let timestamp = SystemTime::now().epoch_millis(); + let max_fee = 100; + let priority_fee = 75; + let chain_id = 45205; + let transfer = Transfer::new(Address::empty(), 100, vec![34,10,43]); + let msg = SequencerMsg::new(10, Address::empty(), vec![1, 2, 3]); + let units: Vec> = vec![Box::new(transfer), Box::new(msg)]; + + Tx::new( + timestamp, + max_fee, + priority_fee, + chain_id, + Address::empty(), + units.clone(), + ) + } + + pub fn new( + timestamp: u64, + max_fee: u64, + priority_fee: u64, + chain_id: u64, + sender: Address, + units: Vec>, + ) -> Self { + let mut hasher = H::new(); + hasher.update(&[0; 32]); + + let mut tx = Self { + id: hasher.finalize(), + timestamp, + max_fee, + priority_fee, + chain_id, + units, + payload: OnceLock::new(), + }; + tx.id = tx.compute_digest(); + + tx + } + + fn set_fee(&mut self, max_fee: u64, priority_fee: u64) { + self.max_fee = max_fee; + self.priority_fee = priority_fee; + } + + fn sign(&mut self, wallet: Wallet) -> SignedTx { + SignedTx::sign(self.clone(), wallet) + } + + fn new_from_params( + timestamp: u64, + units: Vec>, + priority_fee: u64, + max_fee: u64, + chain_id: u64, + sender: Address, + ) -> Self { + let mut tx = Self::default(); + tx.timestamp = timestamp; + tx.units = units; + tx.max_fee = max_fee; + tx.priority_fee = priority_fee; + tx.chain_id = chain_id; + tx.encode(); + tx + } + + pub fn encode(&self) -> Vec { + if let Some(payload) = self.payload.get() { + // TODO: use ref counter instead of copying? + return payload.to_vec(); + } + let mut payload: Vec = Vec::new(); + // pack tx timestamp. + payload.put_u64(self.timestamp); + // pack max fee + payload.put_u64(self.max_fee); + // pack priority fee + payload.put_u64(self.priority_fee); + // pack chain id + payload.put_u64(self.chain_id); + // pack # of units. + let units_raw = encode_units(&self.units); + payload.extend_from_slice(&units_raw); + + // cache the payload + self.payload.set(payload).expect("cannot set payload"); + self.payload.get().expect("unable to get payload").to_vec() + } + + pub fn decode(mut bytes: &[u8]) -> Result { + if bytes.len() < Self::SERIALIZED_LEN { + return Err(format!("bytes length: {} below min: {}", bytes.len(), Self::SERIALIZED_LEN).into()); + } + + // Store the payload the compute digest + let mut tx = Self::default(); + tx.payload = OnceLock::from(bytes.to_vec()); + let digest = tx.compute_digest(); + tx.id = digest; + + tx.timestamp = bytes.get_u64(); + tx.max_fee = bytes.get_u64(); + tx.priority_fee = bytes.get_u64(); + tx.chain_id = bytes.get_u64(); + + let units = decode_units(bytes).map_err(|e| e.to_string())?; + tx.units = units; + + Ok(tx) + } +} + +impl Default for Tx { + fn default() -> Self { + let mut hasher = H::new(); + hasher.update(&[0; 32]); + + Self { + timestamp: 0, + units: vec![], + max_fee: 0, + priority_fee: 0, + chain_id: 19517, + id: hasher.finalize(), + payload: OnceLock::new(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::units::msg::SequencerMsg; + use crate::units::transfer::Transfer; + use commonware_cryptography::Sha256; + use more_asserts::assert_gt; + use std::error::Error; + use std::vec; + + #[test] + fn test_encode_decode() -> Result<(), Box> { + let timestamp = SystemTime::now().epoch_millis(); + let max_fee = 100; + let priority_fee = 75; + let chain_id = 45205; + let transfer = Transfer::new(Address::empty(), 100, vec![34,10,43]); + let msg = SequencerMsg::new(10, Address::empty(), vec![1, 2, 3]); + let units: Vec> = vec![Box::new(transfer), Box::new(msg)]; + // TODO: the .encode call on next line gave error and said origin_msg needed to be mut? but why? + // shouldn't encode be able to encode without changing the msg? + let mut tx = Tx::::new( + timestamp, + max_fee, + priority_fee, + chain_id, + Address::empty(), + units.clone(), + ); + let encoded_bytes = tx.encode(); + print!("encoded tx length: {}\n", encoded_bytes.len()); + let decoded_msg = Tx::::decode(&encoded_bytes)?; + + assert_eq!(decoded_msg.payload().len(), encoded_bytes.len()); + + let origin_transfer = tx.units[0] + .as_ref() + .as_any() + .downcast_ref::() + .expect("Failed to downcast to Transfer"); + + let decode_transfer = decoded_msg.units[0] + .as_ref() + .as_any() + .downcast_ref::() + .expect("Failed to downcast to Transfer"); + + assert_eq!(tx.timestamp, decoded_msg.timestamp); + assert_eq!(tx.max_fee, decoded_msg.max_fee); + assert_eq!(tx.priority_fee, decoded_msg.priority_fee); + assert_eq!(tx.chain_id, decoded_msg.chain_id); + assert_eq!(tx.id, decoded_msg.id); + assert_eq!(tx.payload, decoded_msg.payload); + + // units + assert_eq!(origin_transfer.to, decode_transfer.to); + assert_eq!(origin_transfer.value, decode_transfer.value); + assert_eq!(origin_transfer.memo, decode_transfer.memo); + Ok(()) + } + + #[test] + fn test_insufficient_bytes() { + let timestamp = SystemTime::now().epoch_millis(); + let max_fee = 100; + let priority_fee = 75; + let chain_id = 45205; + let transfer = Transfer::new(Address::empty(), 100, vec![34,10,43]); + let msg = SequencerMsg::new(10, Address::empty(), vec![1, 2, 3]); + let units: Vec> = vec![Box::new(transfer), Box::new(msg)]; + // TODO: the .encode call on next line gave error and said origin_msg needed to be mut? but why? + // shouldn't encode be able to encode without changing the msg? + let mut tx = Tx::::new( + timestamp, + max_fee, + priority_fee, + chain_id, + Address::empty(), + units.clone(), + ); + let encoded_bytes = tx.encode(); + print!("encoded tx length: {}\n", encoded_bytes.len()); + let decode_result = Tx::::decode(&encoded_bytes[0..&encoded_bytes.len() - 10]); + + let err_str = decode_result.map_err(|e| e.to_string()).err().unwrap(); + print!("{}\n", err_str); + assert!(err_str.contains("remaining bytes invalid to decode a unit")); + } + + #[test] + fn test_excessive_bytes() { + let timestamp = SystemTime::now().epoch_millis(); + let max_fee = 100; + let priority_fee = 75; + let chain_id = 45205; + let transfer = Transfer::new(Address::empty(), 100, vec![34,10,43]); + let msg = SequencerMsg::new(10, Address::empty(), vec![1, 2, 3]); + let units: Vec> = vec![Box::new(transfer), Box::new(msg)]; + // TODO: the .encode call on next line gave error and said origin_msg needed to be mut? but why? + // shouldn't encode be able to encode without changing the msg? + let mut tx = Tx::::new( + timestamp, + max_fee, + priority_fee, + chain_id, + Address::empty(), + units.clone(), + ); + let mut encoded_bytes = tx.encode(); + encoded_bytes.push(10); + print!("encoded tx length: {}\n", encoded_bytes.len()); + let decode_result = Tx::::decode(&encoded_bytes); + + let err_str = decode_result.map_err(|e| e.to_string()).err().unwrap(); + print!("{}\n", err_str); + assert!(err_str.contains("left residue after decoding all the units")); + } + + + #[test] + fn test_below_serialize_len() { + let timestamp = SystemTime::now().epoch_millis(); + let max_fee = 100; + let priority_fee = 75; + let chain_id = 45205; + let transfer = Transfer::new(Address::empty(), 100, vec![34,10,43]); + let msg = SequencerMsg::new(10, Address::empty(), vec![1, 2, 3]); + let units: Vec> = vec![Box::new(transfer), Box::new(msg)]; + // TODO: the .encode call on next line gave error and said origin_msg needed to be mut? but why? + // shouldn't encode be able to encode without changing the msg? + let mut tx = Tx::::new( + timestamp, + max_fee, + priority_fee, + chain_id, + Address::empty(), + units.clone(), + ); + let mut encoded_bytes = tx.encode(); + encoded_bytes.push(10); + print!("encoded tx length: {}\n", encoded_bytes.len()); + let decode_result = Tx::::decode(&encoded_bytes[0..Tx::::SERIALIZED_LEN-1]); + + let err_str = decode_result.map_err(|e| e.to_string()).err().unwrap(); + print!("{}\n", err_str); + assert!(err_str.contains("below min")); + } + +} \ No newline at end of file diff --git a/types/src/units/mod.rs b/types/src/units/mod.rs new file mode 100644 index 00000000..154cd443 --- /dev/null +++ b/types/src/units/mod.rs @@ -0,0 +1,218 @@ +pub(crate) mod msg; +pub(crate) mod transfer; + +use std::any::Any; +use std::collections::HashMap; +use std::error::Error; +use std::sync::OnceLock; + +use bytes::{Buf, BufMut}; +use msg::SequencerMsg; +use transfer::Transfer; + +use crate::address::Address; +use crate::state_view::StateView; + +// A registry mapping each UnitType to its decode function. +static UNIT_DECODERS: OnceLock Result, Box>>> = OnceLock::new(); + +fn init_registry() -> HashMap Result, Box>> { + let mut m: HashMap Result, Box>> = HashMap::new(); + m.insert(UnitType::Transfer, Transfer::decode_box); + m.insert(UnitType::SequencerMsg, SequencerMsg::decode_box); + // register additional units as needed... + m +} + +pub fn decode_unit(unit_type: UnitType, data: &[u8]) -> Result, Box> { + let registry = UNIT_DECODERS.get_or_init(init_registry); + let Some(decode_fn) = registry.get(&unit_type) else { + return Err(format!("unsupported unit type {:?}", unit_type).into()) + }; + + decode_fn(data) +} + +pub fn encode_units(units: &Vec>) -> Vec { + let mut raw = Vec::new(); + raw.put_u64(units.len() as u64); + for unit in units.iter() { + // put unit length + unit type + unit data + let unit_raw = unit.encode(); + raw.put_u8(unit.unit_type() as u8); + raw.put_u64(unit_raw.len() as u64); + raw.extend_from_slice(&unit_raw); + } + + raw +} + +pub fn decode_units(mut raw: T) -> Result>, Box> { + if raw.remaining() < size_of::() { + return Err(format!("invalid raw units size: {}", raw.remaining()).into()) + } + + let num_units = raw.get_u64(); + let mut units = Vec::with_capacity(num_units as usize); + for _ in 0..num_units { + if raw.remaining() < size_of::() + size_of::() { + return Err(format!("remaining bytes invalid to form a unit header: {}", raw.remaining()).into()) + } + + let unit_type = raw.get_u8(); + let raw_len = raw.get_u64(); + if raw.remaining() < raw_len as usize { + return Err(format!("remaining bytes invalid to decode a unit, wanted: {}, actual: {}", raw_len, raw.remaining()).into()) + } + let unit_raw = raw.copy_to_bytes(raw_len as usize); + let unit = decode_unit(unit_type.try_into()?, &unit_raw)?; + units.push(unit); + } + + if raw.remaining() != 0 { + return Err(format!("left residue after decoding all the units: {} ", raw.remaining()).into()) + } + + Ok(units) +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum UnitType { + Transfer = 1, + SequencerMsg, +} + +impl TryFrom for UnitType { + type Error = String; + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(UnitType::Transfer), + 2 => Ok(UnitType::SequencerMsg), + _ => Err(format!("unknown unit type: {}", value)), + } + } +} + +pub struct UnitContext { + // timestamp of the tx. + pub timestamp: u64, + // chain id of the tx. + pub chain_id: u64, + // sender of the tx. + pub sender: Address, +} + +pub trait UnitClone { + fn clone_box(&self) -> Box; +} + +impl UnitClone for T +where + T: 'static + Unit + Clone, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +// unit need to be simple and easy to be packed in the tx and executed by the vm. +pub trait Unit: UnitClone + Send + Sync + std::fmt::Debug { + fn unit_type(&self) -> UnitType; + fn encode(&self) -> Vec; + + fn apply( + &self, + context: &UnitContext, + state: &mut Box<&mut dyn StateView>, + ) -> Result>, Box>; + + fn as_any(&self) -> &dyn Any; +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.clone_box() + } +} + +#[cfg(test)] +mod tests { + use commonware_utils::SizedSerialize; + + use crate::address::Address; + + use super::{decode_units, encode_units, msg::SequencerMsg, transfer::Transfer, Unit}; + + #[test] + fn test_encode_decode() { + let mut units: Vec> = Vec::new(); + + let transfer = Transfer::new(Address::empty(), 100, vec![0, 1, 2, 3]); + let msg = SequencerMsg::new(0, Address::empty(), vec![3, 4, 5, 6]); + + units.push(Box::new(transfer)); + units.push(Box::new(msg)); + + let units_raw = encode_units(&units); + let decoded_untis = decode_units(units_raw.as_slice()).unwrap(); + + assert_eq!(units.len(), decoded_untis.len()) + } + + #[test] + fn test_insufficient_bytes() { + let mut units: Vec> = Vec::new(); + + let transfer = Transfer::new(Address::empty(), 100, vec![0, 1, 2, 3]); + let msg = SequencerMsg::new(0, Address::empty(), vec![3, 4, 5, 6]); + + units.push(Box::new(transfer)); + units.push(Box::new(msg)); + + let mut units_raw = encode_units(&units); + + let decode_result = decode_units(&units_raw[0..units_raw.len() - 10]); + + let err_str = decode_result.map_err(|e| e.to_string()).err().unwrap(); + assert!(err_str.contains("remaining bytes invalid to decode a unit")); + } + + + #[test] + fn test_excessive_bytes() { + let mut units: Vec> = Vec::new(); + + let transfer = Transfer::new(Address::empty(), 100, vec![0, 1, 2, 3]); + let msg = SequencerMsg::new(0, Address::empty(), vec![3, 4, 5, 6]); + + units.push(Box::new(transfer)); + units.push(Box::new(msg)); + + let mut units_raw = encode_units(&units); + units_raw.append(&mut [0 as u8; 32].to_vec()); + + let decode_result = decode_units(units_raw.as_slice()); + + let err_str = decode_result.map_err(|e| e.to_string()).err().unwrap(); + assert!(err_str.contains("left residue after decoding all the units")); + } + + #[test] + fn test_unit_header_truncated() { + let mut units: Vec> = Vec::new(); + + let transfer = Transfer::new(Address::empty(), 100, vec![0, 1, 2, 3]); + let msg = SequencerMsg::new(0, Address::empty(), vec![3, 4, 5, 6]); + + units.push(Box::new(transfer)); + units.push(Box::new(msg)); + + let units_raw = encode_units(&units); + let decode_result = decode_units(&units_raw[0..units_raw.len()-Transfer::SERIALIZED_LEN-5]); + + let err_str = decode_result.map_err(|e| e.to_string()).err().unwrap(); + print!("{}\n", err_str); + assert!(err_str.contains("remaining bytes invalid to form a unit header")); + } +} \ No newline at end of file diff --git a/types/src/units/msg.rs b/types/src/units/msg.rs new file mode 100644 index 00000000..4e99e491 --- /dev/null +++ b/types/src/units/msg.rs @@ -0,0 +1,126 @@ +use bytes::{Buf, BufMut}; +use commonware_utils::SizedSerialize; + +use crate::{ + address::Address, + state_view::StateView, ADDRESSLEN, +}; +use std::{any::Any, ops::Add}; +use std::error::Error; + +use super::{Unit, UnitContext, UnitType}; + +// @todo couple SequencerMsg with DA. +// and skip execution no-op. +#[derive(Clone, Debug)] +pub struct SequencerMsg { + pub chain_id: u64, + pub from: Address, + pub data: Vec, +} + +impl SizedSerialize for SequencerMsg { + const SERIALIZED_LEN: usize = ADDRESSLEN + size_of::() * 2; +} + +impl SequencerMsg { + pub fn new(chain_id: u64, from: Address, data: Vec) -> SequencerMsg { + Self { + chain_id, + data, + from, + } + } + + // @todo introduce syntactic checks. + pub fn decode(mut bytes: &[u8]) -> Result> { + // ChainID + DataLen + AddressLen + + if bytes.len() < Self::SERIALIZED_LEN { + return Err(format!("Not enough data to decode sequencer message, wanted: >{}, actual: {}", Self::SERIALIZED_LEN, bytes.len()).into()); + } + + let chain_id = bytes.get_u64(); + let from= Address::from_bytes(&bytes.copy_to_bytes(ADDRESSLEN))?; + let data_len = bytes.get_u64() as usize; + if bytes.remaining() != data_len { + return Err(format!("Incorrect data length, wanted: {}, actual: {}", data_len, bytes.remaining()).into()); + } + let data = bytes.copy_to_bytes(data_len).to_vec(); + + Ok(SequencerMsg { chain_id, data, from }) + } + + pub fn decode_box(mut bytes: &[u8]) -> Result, Box> { + let msg = Self::decode(bytes)?; + Ok(Box::new(msg)) + } +} + +impl Unit for SequencerMsg { + fn unit_type(&self) -> UnitType { + UnitType::SequencerMsg + } + + fn encode(&self) -> Vec { + let mut bytes: Vec = Vec::new(); + // chain id length is 8 bytes.n store chain id. + bytes.put_u64(self.chain_id); + // address length is 32. store address. + bytes.extend_from_slice(self.from.as_slice()); + // store data length. + bytes.put_u64(self.data.len() as u64); + // store data. + bytes.extend_from_slice(&self.data); + + bytes + } + + fn apply( + &self, + _: &UnitContext, + _: &mut Box<&mut dyn StateView>, + ) -> Result>, Box> { + Ok(None) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl Default for SequencerMsg { + fn default() -> Self { + Self { + chain_id: 0, + data: vec![], + from: Address::empty(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use more_asserts::assert_gt; + use std::error::Error; + + #[test] + fn test_encode_decode() -> Result<(), Box> { + let chain_id = 4502; + let data = vec![0xDE, 0xAD, 0xBE, 0xEF]; + let from = Address::create_random_address(); + let origin_msg = SequencerMsg { + chain_id, + data, + from, + }; + let encoded_bytes = origin_msg.encode(); + assert_gt!(encoded_bytes.len(), 0); + let decoded_msg = SequencerMsg::decode(&encoded_bytes)?; + assert_eq!(origin_msg.chain_id, decoded_msg.chain_id); + assert_eq!(origin_msg.data.len(), decoded_msg.data.len()); + assert_eq!(origin_msg.data, decoded_msg.data); + assert_eq!(origin_msg.from, decoded_msg.from); + Ok(()) + } +} \ No newline at end of file diff --git a/types/src/units/transfer.rs b/types/src/units/transfer.rs new file mode 100644 index 00000000..dd441530 --- /dev/null +++ b/types/src/units/transfer.rs @@ -0,0 +1,158 @@ +use bytes::{Buf, BufMut}; +use commonware_utils::SizedSerialize; + +use crate::{address::Address, ADDRESSLEN}; +use crate::state_view::StateView; +use std::ops::Add; +use std::{any::Any, error::Error, fmt::Display}; + +use super::{Unit, UnitContext, UnitType}; + +const MAX_MEMO_SIZE: usize = 256; + +#[derive(Debug, Clone)] +pub struct Transfer { + pub to: Address, + pub value: u64, + pub memo: Vec, +} + +impl SizedSerialize for Transfer { + const SERIALIZED_LEN: usize = ADDRESSLEN + size_of::() * 2; +} + +impl Transfer { + pub fn new(to: Address, value: u64, memo: Vec) -> Transfer { + Self { + to, + value, + memo, + } + } + + pub fn decode(mut bytes: &[u8]) -> Result> { + // Value + MemoLen + AddressLen + + if bytes.len() < Self::SERIALIZED_LEN { + return Err("Not enough data to decode transfer".into()); + } + + let to = Address::from_bytes(&bytes.copy_to_bytes(ADDRESSLEN))?; + let value = bytes.get_u64(); + let memo_len = bytes.get_u64() as usize; + if bytes.remaining() != memo_len { + return Err(format!("Incorrect memo length, wanted: {}, actual: {}", memo_len, bytes.remaining()).into()); + } + let memo = bytes.copy_to_bytes(memo_len).to_vec(); + Ok( Self { to, value, memo }) + } + + // @todo introduce syntactic checks. + pub fn decode_box(mut bytes: &[u8]) -> Result, Box> { + let transfer = Self::decode(bytes)?; + Ok(Box::new(transfer)) + } +} + +#[derive(Debug)] +pub enum TransferError { + SenderAccountNotFound, + InsufficientFunds, + InvalidMemoSize, + StorageError, +} + +impl Display for TransferError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TransferError::SenderAccountNotFound => write!(f, "Sender account not found"), + TransferError::InsufficientFunds => write!(f, "Insufficient funds"), + TransferError::InvalidMemoSize => write!(f, "Invalid memo size"), + TransferError::StorageError => write!(f, "Storage error"), + } + } +} + +impl Error for TransferError {} + +impl Unit for Transfer { + fn unit_type(&self) -> UnitType { + UnitType::Transfer + } + + fn encode(&self) -> Vec { + let mut bytes = Vec::new(); + + bytes.extend_from_slice(self.to.as_slice()); + bytes.extend(self.value.to_be_bytes()); + bytes.put_u64(self.memo.len() as u64); + bytes.extend_from_slice(&self.memo); + bytes + } + + fn apply( + &self, + context: &UnitContext, + state: &mut Box<&mut dyn StateView>, + ) -> Result>, Box> { + if self.memo.len() > MAX_MEMO_SIZE { + return Err(TransferError::InvalidMemoSize.into()); + } + + if let Some(bal) = state.get_balance(&context.sender) { + if bal < self.value { + return Err(TransferError::InsufficientFunds.into()); + } + let receiver_bal = state.get_balance(&self.to).unwrap_or(0); + + if !state.set_balance(&context.sender, bal - self.value) + || !state.set_balance(&self.to, receiver_bal + self.value) + { + return Err(TransferError::StorageError.into()); + } + } else { + return Err(TransferError::SenderAccountNotFound.into()); + } + + Ok(None) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl Default for Transfer { + fn default() -> Self { + Self { + to: Address::empty(), + value: 0, + memo: vec![], + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use more_asserts::assert_gt; + use std::error::Error; + + #[test] + fn test_encode_decode() -> Result<(), Box> { + let to = Address::create_random_address(); + let value = 5; + let memo = vec![0xDE, 0xAD, 0xBE, 0xEF]; + let origin_msg = Transfer { + to, + value, + memo, + }; + let encoded_bytes = origin_msg.encode(); + assert_gt!(encoded_bytes.len(), 0); + let decoded_msg = Transfer::decode(&encoded_bytes)?; + assert_eq!(origin_msg.to, decoded_msg.to); + assert_eq!(origin_msg.value, decoded_msg.value); + assert_eq!(origin_msg.memo, decoded_msg.memo); + Ok(()) + } +} \ No newline at end of file diff --git a/types/src/wallet.rs b/types/src/wallet.rs new file mode 100644 index 00000000..0aba3844 --- /dev/null +++ b/types/src/wallet.rs @@ -0,0 +1,128 @@ +use crate::address::Address; +use crate::{PrivateKey, PublicKey, Signature, TX_NAMESPACE}; +use commonware_cryptography::ed25519::Ed25519; +use commonware_cryptography::Scheme; +use rand::{CryptoRng, Rng}; +use std::fmt::Error; + +#[derive(Clone, Debug)] +pub enum AuthTypes { + ED25519, +} + +/// auth should have a method to verify signatures. +/// also batch signature verification. +pub trait Auth { + // returns the public key of the signer. + fn public_key(&self) -> PublicKey; + // returns the account address of the signer. + fn address(&self) -> Address; + // verifys the signature. + fn verify(&self, data: &[u8], signature: &[u8]) -> bool; + // batch verify signatures. returns false if batch verification fails. + fn batch_verify(&self, data: &[u8], signatures: Vec<&[u8]>) -> bool; +} + +/// Wallet is the module used by the user to sign transactions. Wallet uses Ed25519 signature scheme. +pub struct Wallet { + // Private key + priv_key: PrivateKey, + // Public key + pub_key: PublicKey, + // Account Address, is derived from the public key. + address: Address, + // Signer + signer: Ed25519, +} + +// wallet generation, management, and signing should be functions of the wallet. +pub trait WalletMethods { + // create a new wallet using the given randomness. + fn generate(r: &mut R) -> Self; + // load signer from bytes rep of a private key and initialize the wallet. + fn load(priv_key: &[u8]) -> Self; + // sign the given arbitary data with the private key of the wallet. + fn sign(&mut self, data: &[u8]) -> Vec; + // verify the signature of the given data with the public key of the wallet. + fn verify(&self, data: &[u8], signature: &[u8]) + -> Result; + // return corresponding wallet's address. + fn address(&self) -> Address; + // return corresponding wallet's public key. + fn public_key(&self) -> PublicKey; + // return corresponding wallet's private key. + fn private_key(&self) -> Vec; + // store the private key at the given path. + fn store_private_key(&self, path: &str) -> Result<(), Error>; + // @todo remove this? + fn init_address(&mut self); +} + +impl WalletMethods for Wallet { + fn generate(r: &mut R) -> Self { + let signer = Ed25519::new(r); + let pub_key = signer.public_key(); + let address = Address::from_pub_key(&pub_key); + Self { + priv_key: signer.private_key(), + pub_key: signer.public_key(), + address, + signer, + } + } + + fn load(priv_key: &[u8]) -> Self { + let private_key = PrivateKey::try_from(priv_key).expect("Invalid private key"); + let signer = ::from(private_key).unwrap(); + Self { + priv_key: signer.private_key(), + pub_key: signer.public_key(), + address: Address::from_pub_key(&signer.public_key()), + signer, + } + } + + fn sign(&mut self, data: &[u8]) -> Vec { + self.signer.sign(Some(TX_NAMESPACE), data).as_ref().to_vec() + } + + fn verify( + &self, + data: &[u8], + signature: &[u8], + ) -> Result { + let signature = Signature::try_from(signature); + if let Err(e) = signature { + return Err(e); + } + + let signature = signature.unwrap(); + let pub_key = self.signer.public_key(); + Ok(Ed25519::verify( + Some(TX_NAMESPACE), + data, + &pub_key, + &signature, + )) + } + + fn address(&self) -> Address { + self.address.clone() + } + + fn public_key(&self) -> PublicKey { + self.pub_key.clone() + } + + fn private_key(&self) -> Vec { + self.priv_key.as_ref().to_vec() + } + + fn store_private_key(&self, _path: &str) -> Result<(), Error> { + todo!() + } + + fn init_address(&mut self) { + todo!() + } +} \ No newline at end of file diff --git a/types/src/wasm.rs b/types/src/wasm.rs index f9626fca..36c23dc7 100644 --- a/types/src/wasm.rs +++ b/types/src/wasm.rs @@ -130,4 +130,4 @@ pub fn parse_block(bytes: Vec) -> JsValue { #[wasm_bindgen] pub fn leader_index(seed: Vec, participants: usize) -> usize { compute_leader_index(&seed, participants) -} +} \ No newline at end of file diff --git a/vm/Cargo.toml b/vm/Cargo.toml new file mode 100644 index 00000000..3ef612cc --- /dev/null +++ b/vm/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "alto-vm" +version = "0.1.0" +edition = "2021" + +[dependencies] +alto-storage = { workspace = true } +alto-actions = { workspace = true } +alto-types = { workspace = true } diff --git a/vm/src/lib.rs b/vm/src/lib.rs new file mode 100644 index 00000000..77e9423e --- /dev/null +++ b/vm/src/lib.rs @@ -0,0 +1 @@ +pub mod vm; \ No newline at end of file diff --git a/vm/src/vm.rs b/vm/src/vm.rs new file mode 100644 index 00000000..e6d65bb1 --- /dev/null +++ b/vm/src/vm.rs @@ -0,0 +1,119 @@ +use alto_actions::transfer::{Transfer, TransferError}; +use alto_storage::database::Database; +use alto_storage::hashmap_db::HashmapDatabase; +use alto_types::account::Balance; +use alto_types::Address; + +const TEST_FAUCET_ADDRESS: &[u8] = b"0x0123456789abcdef0123456789abcd"; +const TEST_FAUCET_BALANCE: Balance = 10_000_000; + +struct VM { + state_db: Box, +} + +impl VM { + pub fn new() -> Self { + Self { + state_db: Box::new(HashmapDatabase::new()), + } + } + + pub fn new_test_vm() -> Self { + let mut state_db = Box::new(HashmapDatabase::new()); + state_db.set_balance(&Self::test_faucet_address(), TEST_FAUCET_BALANCE); + + Self { + state_db + } + } + + // TODO: + // make check for sending funds > 0 so reject any negative values or balances. + // make sure address isn't a duplicate of self. + // need from balance to be greater than or equal to the transfer amount + // need the to_balance to not overflow + pub fn execute(&mut self, msg: Transfer) -> Result<(), TransferError> { + let from_balance = self.state_db.get_balance(&msg.from_address); + if from_balance.is_none() { + return Err(TransferError::InvalidFromAddress); + } + + + let mut to_balance = self.state_db.get_balance(&msg.to_address); + if to_balance.is_none() { + to_balance = Some(0) + } + + let updated_from_balance = from_balance.unwrap().checked_sub(msg.value); + if updated_from_balance.is_none() { + return Err(TransferError::InsufficientFunds); + } + + let updated_to_balance = to_balance.unwrap().checked_add(msg.value); + if updated_to_balance.is_none() { + return Err(TransferError::TooMuchFunds); + } + + // TODO: Below doesn't rollback cases. Fix later. + if !self.state_db.set_balance(&msg.from_address, updated_from_balance.unwrap()) { + return Err(TransferError::StorageError); + } + if !self.state_db.set_balance(&msg.to_address, updated_to_balance.unwrap()) { + return Err(TransferError::StorageError); + } + + Ok(()) + } + + pub fn query_balance(&self, address: &Address) -> Option { + self.state_db.get_balance(address) + } + + pub fn test_faucet_address() -> Address { + Address::new(TEST_FAUCET_ADDRESS) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic() -> Result<(), String> { + let from_address = Address::new(b"0x10000"); + let to_address = Address::new(b"0x20000"); + let faucet_address = VM::test_faucet_address(); + + let mut test_vm = VM::new_test_vm(); + + // should be empty + let from_balance1 = test_vm.query_balance(&from_address); + assert!(from_balance1.is_none()); + + let faucet_balance = test_vm.query_balance(&VM::test_faucet_address()); + assert_eq!(faucet_balance, Some(TEST_FAUCET_BALANCE)); + + // process faucet transfers to addresses + let transfer1 = Transfer::new(faucet_address.clone(), from_address.clone(), 100); + test_vm.execute(transfer1.unwrap()).unwrap(); + let transfer2 = Transfer::new(faucet_address.clone(), to_address.clone(), 200); + test_vm.execute(transfer2.unwrap()).unwrap(); + + // check balances have been updated from faucet transfers + let from_balance2 = test_vm.query_balance(&from_address); + assert_eq!(from_balance2, Some(100)); + let from_balance3 = test_vm.query_balance(&to_address); + assert_eq!(from_balance3, Some(200)); + + let transfer3 = Transfer::new(from_address.clone(), to_address.clone(), 50); + test_vm.execute(transfer3.unwrap()).unwrap(); + + // check updated balances + let from_balance2 = test_vm.query_balance(&from_address); + assert_eq!(from_balance2, Some(50)); + let from_balance3 = test_vm.query_balance(&to_address); + assert_eq!(from_balance3, Some(250)); + + Ok(()) + } +}