diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 1d27a09f..8439ca4e 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -1,2 +1,6 @@ [advisories] -ignore = ["RUSTSEC-2023-0071", "RUSTSEC-2026-0002"] +ignore = [ + "RUSTSEC-2023-0071", + "RUSTSEC-2026-0002", + "RUSTSEC-2025-0141", # bincode unmaintained — transitive dep from f1r3node, not fixable in embers +] diff --git a/.github/workflows/actions/setup-rust/action.yaml b/.github/workflows/actions/setup-rust/action.yaml index b697e796..3a025432 100644 --- a/.github/workflows/actions/setup-rust/action.yaml +++ b/.github/workflows/actions/setup-rust/action.yaml @@ -6,7 +6,6 @@ runs: id: setup-rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: nightly, stable components: rustfmt, clippy cache: true diff --git a/.github/workflows/embers.yaml b/.github/workflows/embers.yaml index e184ea04..fef3a9fc 100644 --- a/.github/workflows/embers.yaml +++ b/.github/workflows/embers.yaml @@ -27,6 +27,8 @@ on: jobs: build: runs-on: ubuntu-latest + env: + RUSTFLAGS: "-C target-feature=+aes,+sse2" steps: - name: Checkout code uses: actions/checkout@v6 diff --git a/.gitignore b/.gitignore index aa65e277..df2373a2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ index.scip .DS_Store *.pyc schema.json +.pgmcp.toml docker/.env docker/**/default.conf +docker/docker-compose.override.yaml diff --git a/Cargo.lock b/Cargo.lock index 2e88bef4..ff58a234 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -139,6 +151,12 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -197,6 +215,16 @@ dependencies = [ "winnow", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-compression" version = "0.4.39" @@ -336,9 +364,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.4" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -346,9 +374,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.1" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" dependencies = [ "cc", "cmake", @@ -364,10 +392,13 @@ checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", "http-body-util", + "hyper", + "hyper-util", "itoa", "matchit", "memchr", @@ -375,10 +406,15 @@ dependencies = [ "percent-encoding", "pin-project-lite", "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper", + "tokio", "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -397,6 +433,7 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -453,6 +490,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bcder" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7c42c9913f68cf9390a225e81ad56a5c515347287eb98baa710090ca1de86d" +dependencies = [ + "bytes", + "smallvec", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.72.1" @@ -473,6 +529,36 @@ dependencies = [ "syn 2.0.116", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitcode" version = "0.6.9" @@ -518,6 +604,21 @@ name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] [[package]] name = "blake2" @@ -528,6 +629,20 @@ dependencies = [ "digest", ] +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -604,6 +719,12 @@ version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -814,6 +935,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" version = "0.10.0" @@ -858,6 +985,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "counter" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d458e66999348f56fd3ffcfbb7f7951542075ca8359687c703de6500c1ddccd" +dependencies = [ + "num-traits", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -900,6 +1036,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -909,12 +1055,65 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto" +version = "0.1.0" +source = "git+https://github.com/F1R3FLY-io/f1r3node-rust.git?rev=91b5c70a0740f91c3ba2a414af3e29fe830263c3#91b5c70a0740f91c3ba2a414af3e29fe830263c3" +dependencies = [ + "async-trait", + "base64", + "bincode", + "blake2", + "byteorder", + "ed25519-dalek", + "eyre", + "ff", + "getrandom 0.2.17", + "hex", + "k256", + "nanorand", + "num-bigint", + "openssl", + "p256", + "pem", + "pkcs8", + "prost", + "rand 0.8.5", + "rand_core 0.6.4", + "rcgen", + "serde", + "sha2", + "shared", + "thiserror 2.0.18", + "time", + "tiny-keccak", + "tracing", + "typenum", + "x509-certificate", + "yasna", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1115,9 +1314,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.116", + "syn 1.0.109", +] + +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", ] +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "der" version = "0.7.10" @@ -1184,12 +1401,27 @@ dependencies = [ "syn 2.0.116", ] +[[package]] +name = "doxygen-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" +dependencies = [ + "phf", +] + [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" version = "0.16.9" @@ -1222,6 +1454,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -1286,6 +1519,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "tracing-test", "uuid", ] @@ -1359,6 +1593,22 @@ dependencies = [ "warp", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fast-slice-utils" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6f7d9b6886c61a9db24b28fe7990090621c7e8c4e3df3fed3a831fe29e15f4" + [[package]] name = "fastrand" version = "2.3.0" @@ -1371,6 +1621,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ + "bitvec", "rand_core 0.6.4", "subtle", ] @@ -1419,6 +1670,7 @@ dependencies = [ "firefly-client-macros", "futures", "hex", + "models", "prost", "reqwest 0.13.2", "scopeguard", @@ -1432,9 +1684,9 @@ dependencies = [ "tokio-tungstenite 0.28.0", "tonic", "tonic-prost", - "tonic-prost-build", "tracing", "uuid", + "wiremock", "zbase32", ] @@ -1476,6 +1728,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1491,6 +1758,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.32" @@ -1562,6 +1835,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.32" @@ -1579,6 +1858,19 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "generic-array" version = "0.14.9" @@ -1703,6 +1995,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "gxhash" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ce1bab7aa741d4e7042b2aae415b78741f267a98a7271ea226cd5ba6c43d7d" +dependencies = [ + "rustversion", +] + [[package]] name = "h2" version = "0.4.13" @@ -1775,6 +2076,50 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "heed" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad82d6598ccf1dac15c8b758a1bd282b755b6776be600429176757190a1b0202" +dependencies = [ + "bitflags", + "byteorder", + "heed-traits", + "heed-types", + "libc", + "lmdb-master-sys", + "once_cell", + "page_size", + "serde", + "synchronoise", + "url", +] + +[[package]] +name = "heed-traits" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" + +[[package]] +name = "heed-types" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" +dependencies = [ + "bincode", + "byteorder", + "heed-traits", + "serde", + "serde_json", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -2068,6 +2413,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "indexmap" version = "2.13.0" @@ -2217,6 +2568,20 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + [[package]] name = "keccak" version = "0.1.6" @@ -2272,6 +2637,16 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libz-ng-sys" +version = "1.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b6bd0c289b22f4973012fa91dfed9d5f4b633567bb26f9454fbf437817b499" +dependencies = [ + "cmake", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -2285,20 +2660,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] -name = "lock_api" -version = "0.4.14" +name = "lmdb-master-sys" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "aaeb9bd22e73bd1babffff614994b341e9b2008de7bb73bf1f7e9154f1978f8b" dependencies = [ - "scopeguard", + "cc", + "doxygen-rs", + "libc", ] [[package]] -name = "log" +name = "local-or-heap" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb8e2f86f48bd28f953e5b0eb54a8434b3747809c31fa5a3e7bb112c69d3006" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lru" version = "0.12.5" @@ -2314,6 +2721,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lz4_flex" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" +dependencies = [ + "twox-hash", +] + [[package]] name = "match-lookup" version = "0.1.2" @@ -2340,12 +2756,28 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-dangling" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59dbb09ed53f8e4f314e353dc6c1853ae5b4c480a668a422657804a544ea9f65" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "metrics" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045b4193fbdc5b5681f32f11070da9be3609f189a79f3390706d42587f46bb5" +dependencies = [ + "ahash", + "portable-atomic", +] + [[package]] name = "mime" version = "0.3.17" @@ -2389,6 +2821,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "models" +version = "0.1.0" +source = "git+https://github.com/F1R3FLY-io/f1r3node-rust.git?rev=91b5c70a0740f91c3ba2a414af3e29fe830263c3#91b5c70a0740f91c3ba2a414af3e29fe830263c3" +dependencies = [ + "crypto", + "hex", + "itertools 0.14.0", + "pathmap", + "proptest", + "proptest-derive", + "prost", + "rand 0.9.2", + "rspace_plus_plus", + "serde", + "serde_json", + "shared", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tracing", + "utoipa", + "uuid", +] + [[package]] name = "moka" version = "0.12.13" @@ -2409,6 +2866,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "monadic" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1124b597894c234120e409b3144406fb2a7dba1bd08cb61d704ce00ce325db8" + [[package]] name = "multer" version = "3.1.0" @@ -2456,6 +2919,18 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "multiset" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8738c9ddd350996cb8b8b718192851df960803764bcdaa3afb44a63b1ddb5c" + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" + [[package]] name = "nix" version = "0.30.1" @@ -2478,6 +2953,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty-collections" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d127e00b37b19c36c28ac00e874240a61faeea14c7b759b1d9c77efa048322a" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2549,6 +3030,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2567,12 +3058,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "p256" version = "0.13.2" @@ -2597,6 +3126,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking" version = "2.2.1" @@ -2626,6 +3165,37 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pathmap" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8d2a6bf5df2e172d398b5e24284cabc6557702102d461118827d02ef0f8f6c" +dependencies = [ + "arrayvec", + "dyn-clone", + "fast-slice-utils", + "gxhash", + "libz-ng-sys", + "local-or-heap", + "maybe-dangling", + "num-traits", + "pathmap-derive", + "reusing-vec", + "smallvec", + "xxhash-rust", +] + +[[package]] +name = "pathmap-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804da4fe149a69b8b63f3870dcfee222a2c6217daf944c8dd072e20aa96c178c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "pear" version = "0.2.9" @@ -2685,6 +3255,48 @@ dependencies = [ "indexmap", ] +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -2936,6 +3548,36 @@ dependencies = [ "yansi", ] +[[package]] +name = "proptest" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "prost" version = "0.14.3" @@ -3009,6 +3651,12 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quick-xml" version = "0.36.2" @@ -3041,9 +3689,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", @@ -3090,6 +3738,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -3149,6 +3803,48 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -3187,6 +3883,12 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.12.28" @@ -3259,6 +3961,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reusing-vec" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c42681e1efa6833b045b1e162399929f478b5dfaa532b8e8e73f3bdc54c68" +dependencies = [ + "smallvec", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -3278,6 +3989,42 @@ dependencies = [ "uncased", ] +[[package]] +name = "rholang-parser" +version = "0.1.0" +source = "git+https://github.com/F1R3FLY-io/rholang-rs?rev=c163755#c1637550a844253f8fe781fc07e57fe5e5aabe7c" +dependencies = [ + "bitvec", + "nonempty-collections", + "rholang-tree-sitter", + "rholang-tree-sitter-proc-macro", + "smallvec", + "tree-sitter", + "typed-arena", + "validated", +] + +[[package]] +name = "rholang-tree-sitter" +version = "0.1.2" +source = "git+https://github.com/F1R3FLY-io/rholang-rs?rev=c163755#c1637550a844253f8fe781fc07e57fe5e5aabe7c" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "rholang-tree-sitter-proc-macro" +version = "0.1.2" +source = "git+https://github.com/F1R3FLY-io/rholang-rs?rev=c163755#c1637550a844253f8fe781fc07e57fe5e5aabe7c" +dependencies = [ + "anyhow", + "quote", + "rholang-tree-sitter", + "syn 2.0.116", + "tree-sitter", +] + [[package]] name = "ring" version = "0.17.14" @@ -3312,6 +4059,76 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rspace_plus_plus" +version = "0.1.0" +source = "git+https://github.com/F1R3FLY-io/f1r3node-rust.git?rev=91b5c70a0740f91c3ba2a414af3e29fe830263c3#91b5c70a0740f91c3ba2a414af3e29fe830263c3" +dependencies = [ + "async-trait", + "bincode", + "bit-set 0.5.3", + "blake2", + "blake3", + "bytes", + "chrono", + "counter", + "dashmap", + "futures", + "heed", + "hex", + "itertools 0.14.0", + "lazy_static", + "metrics", + "monadic", + "multiset", + "once_cell", + "proptest", + "proptest-derive", + "prost", + "rand 0.8.5", + "rayon", + "rholang-parser", + "rstest", + "serde", + "serde_json", + "shared", + "state", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "validated", +] + +[[package]] +name = "rstest" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.116", + "unicode-ident", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -3405,9 +4222,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -3421,6 +4238,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.23" @@ -3586,16 +4415,28 @@ dependencies = [ ] [[package]] -name = "serde_json" -version = "1.0.149" +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", - "memchr", "serde", "serde_core", - "zmij", ] [[package]] @@ -3664,6 +4505,36 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared" +version = "0.1.0" +source = "git+https://github.com/F1R3FLY-io/f1r3node-rust.git?rev=91b5c70a0740f91c3ba2a414af3e29fe830263c3#91b5c70a0740f91c3ba2a414af3e29fe830263c3" +dependencies = [ + "axum", + "bincode", + "bytes", + "futures", + "heed", + "hex", + "http", + "http-body-util", + "indexmap", + "lz4_flex", + "metrics", + "proptest", + "prost", + "rand 0.9.2", + "serde", + "serde_bytes", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tonic", + "tower", + "tracing", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3708,6 +4579,12 @@ dependencies = [ "time", ] +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "slab" version = "0.4.12" @@ -3752,6 +4629,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "state" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" +dependencies = [ + "loom", +] + [[package]] name = "state-sync" version = "0.1.0" @@ -3767,6 +4653,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + [[package]] name = "strsim" version = "0.10.0" @@ -3837,6 +4729,15 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synchronoise" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" +dependencies = [ + "crossbeam-queue", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -3875,6 +4776,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.25.0" @@ -3968,6 +4875,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -4002,6 +4918,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -4275,6 +5192,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.22" @@ -4285,12 +5212,36 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", +] + +[[package]] +name = "tracing-test" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" +dependencies = [ + "quote", + "syn 2.0.116", ] [[package]] @@ -4304,6 +5255,26 @@ dependencies = [ "syn 2.0.116", ] +[[package]] +name = "tree-sitter" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + [[package]] name = "try-lock" version = "0.2.5" @@ -4369,12 +5340,30 @@ dependencies = [ "utf-8", ] +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "uncased" version = "0.9.10" @@ -4466,6 +5455,29 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "uuid" version = "1.21.0" @@ -4474,22 +5486,47 @@ checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ "getrandom 0.4.1", "js-sys", + "rand 0.9.2", "serde_core", "wasm-bindgen", ] +[[package]] +name = "validated" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944c1c11796b4873eb4a0a9a18673ca81346a038d08fbbd7d8ee396c872c2930" +dependencies = [ + "nonempty-collections", +] + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -4691,6 +5728,22 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29333c3ea1ba8b17211763463ff24ee84e41c78224c16b001cd907e663a38c68" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -4700,6 +5753,21 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -4821,6 +5889,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -4860,6 +5943,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4878,6 +5967,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4896,6 +5991,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4926,6 +6027,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4944,6 +6051,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4962,6 +6075,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4980,6 +6099,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5001,6 +6126,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -5095,12 +6243,56 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x509-certificate" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66534846dec7a11d7c50a74b7cdb208b9a581cad890b7866430d438455847c85" +dependencies = [ + "bcder", + "bytes", + "chrono", + "der", + "hex", + "pem", + "ring", + "signature", + "spki", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "num-bigint", + "time", +] + [[package]] name = "yoke" version = "0.8.1" @@ -5176,6 +6368,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] [[package]] name = "zerotrie" diff --git a/Makefile.toml b/Makefile.toml index b42dbf5d..73902f56 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -2,7 +2,6 @@ args = ["clippy", "--workspace", "--all-targets", "--all-features"] command = "cargo" install_crate = "clippy" -toolchain = "nightly" workspace = false [tasks.generate-schema] @@ -20,7 +19,6 @@ workspace = false args = ["fmt", "--all"] command = "cargo" install_crate = "rustfmt" -toolchain = "nightly" workspace = false [tasks.fmt] @@ -30,37 +28,26 @@ alias = "format" args = ["fmt", "--all", "--check"] command = "cargo" install_crate = "rustfmt" -toolchain = "nightly" workspace = false [tasks.docker-up] -args = [ - "compose", - "--file", - "docker/docker-compose.yaml", - "--project-name", - "embers", - "${@}", - "up", - "--build", - "--force-recreate", - "--detach", - "--wait", -] -command = "docker" +script = ''' +COMPOSE_FILES="--file docker/docker-compose.yaml" +if [ -f docker/docker-compose.override.yaml ]; then + COMPOSE_FILES="$COMPOSE_FILES --file docker/docker-compose.override.yaml" +fi +docker compose $COMPOSE_FILES --project-name embers "$@" up --build --force-recreate --detach --wait +''' workspace = false [tasks.docker-down] -args = [ - "compose", - "--file", - "docker/docker-compose.yaml", - "--project-name", - "embers", - "${@}", - "down", -] -command = "docker" +script = ''' +COMPOSE_FILES="--file docker/docker-compose.yaml" +if [ -f docker/docker-compose.override.yaml ]; then + COMPOSE_FILES="$COMPOSE_FILES --file docker/docker-compose.override.yaml" +fi +docker compose $COMPOSE_FILES --project-name embers "$@" down +''' workspace = false [tasks.embers] diff --git a/docker/certs/node.certificate.pem b/docker/certs/node.certificate.pem index 34bfa445..4499b329 100644 --- a/docker/certs/node.certificate.pem +++ b/docker/certs/node.certificate.pem @@ -1,10 +1,13 @@ -----BEGIN CERTIFICATE----- -MIIBXjCCAQKgAwIBAgIIRgmpOyKqJiIwDAYIKoZIzj0EAwIFADAzMTEwLwYDVQQD -EyhlYmZmZDQxOWRlYTYwMjIwNzM0Y2NlYTg4NzVlODZkODdiYWMxMGE3MB4XDTE5 -MTExNjE0MTcwMVoXDTIwMTExNTE0MTcwMVowMzExMC8GA1UEAxMoZWJmZmQ0MTlk -ZWE2MDIyMDczNGNjZWE4ODc1ZTg2ZDg3YmFjMTBhNzBZMBMGByqGSM49AgEGCCqG -SM49AwEHA0IABOMsflowoPMdm4WV5E/sjWVUwQZ0TcKBJNqbMzBwFTHIeTtXfjkz -+OkeMZa1gK7tNm+9XkTa2eaoCiGF8lsgXQkwDAYIKoZIzj0EAwIFAANIADBFAiBO -38RcQxpi0UZ+UlEJGbjiBNMkwOENmP0vKxF54+4skAIhAIHr7dMrvev5Fd/tESmi -VMI7KJh06qdcafM0sx8MSYLr ------END CERTIFICATE----- \ No newline at end of file +MIIB8jCCAZigAwIBAgIUa7Ird8cr9+9U3VYkXdmle81VteYwCgYIKoZIzj0EAwIw +MzExMC8GA1UEAwwoZWJmZmQ0MTlkZWE2MDIyMDczNGNjZWE4ODc1ZTg2ZDg3YmFj +MTBhNzAeFw0yNjAzMTcwMTE4MjBaFw0zNjAzMTQwMTE4MjBaMDMxMTAvBgNVBAMM +KGViZmZkNDE5ZGVhNjAyMjA3MzRjY2VhODg3NWU4NmQ4N2JhYzEwYTcwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAATjLH5aMKDzHZuFleRP7I1lVMEGdE3CgSTamzMw +cBUxyHk7V345M/jpHjGWtYCu7TZvvV5E2tnmqAohhfJbIF0Jo4GJMIGGMB0GA1Ud +DgQWBBTJ3Yi4Eu781N0asoTtZUilI7aIoDAfBgNVHSMEGDAWgBTJ3Yi4Eu781N0a +soTtZUilI7aIoDAPBgNVHRMBAf8EBTADAQH/MDMGA1UdEQQsMCqCKGViZmZkNDE5 +ZGVhNjAyMjA3MzRjY2VhODg3NWU4NmQ4N2JhYzEwYTcwCgYIKoZIzj0EAwIDSAAw +RQIgQGRNTdQHJ+D+Q7mqBG0OfzrF3qqb3zelo16GV4zz7OsCIQD6GvlwqbAmYBmx +5F88i5cS5wYruJVjUpEPGVRGvVJhrg== +-----END CERTIFICATE----- diff --git a/docker/docker-compose-staging.yaml b/docker/docker-compose-staging.yaml index f95bf162..e488ac53 100644 --- a/docker/docker-compose-staging.yaml +++ b/docker/docker-compose-staging.yaml @@ -1,6 +1,6 @@ services: firefly: - image: f1r3flyindustries/f1r3fly-scala-node:latest + image: f1r3flyindustries/f1r3fly-rust-node:latest pull_policy: always user: root env_file: @@ -25,7 +25,7 @@ services: - mainnet firefly-read: - image: f1r3flyindustries/f1r3fly-scala-node:latest + image: f1r3flyindustries/f1r3fly-rust-node:latest pull_policy: always user: root command: @@ -38,7 +38,7 @@ services: - mainnet firefly-testnet: - image: f1r3flyindustries/f1r3fly-scala-node:latest + image: f1r3flyindustries/f1r3fly-rust-node:latest pull_policy: always user: root volumes: @@ -61,7 +61,7 @@ services: - testnet firefly-read-testnet: - image: f1r3flyindustries/f1r3fly-scala-node:latest + image: f1r3flyindustries/f1r3fly-rust-node:latest pull_policy: always user: root command: diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 5c42a295..76b732f7 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,18 +1,23 @@ services: firefly: - image: f1r3flyindustries/f1r3fly-scala-node:latest + image: f1r3flyindustries/f1r3fly-rust-node:latest pull_policy: always user: root env_file: - .env ports: - 14401:40401 # deploy service - - 14402:40402 # propose service - 14403:40403 # rest api volumes: - ./mainnet/genesis:/var/lib/rnode/genesis - ./certs/node.key.pem:/var/lib/rnode/node.key.pem:ro - ./certs/node.certificate.pem:/var/lib/rnode/node.certificate.pem:ro + healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:40403/status || exit 1"] + interval: 30s + timeout: 10s + start_period: 40s + retries: 3 command: - run - -s @@ -21,36 +26,52 @@ services: - --no-upnp - --allow-private-addresses - --synchrony-constraint-threshold=0.0 + - --heartbeat-enabled + - --fault-tolerance-threshold=0.0 - --protocol-port=40400 - --discovery-port=40404 - --tls-key-path=/var/lib/rnode/node.key.pem - --tls-certificate-path=/var/lib/rnode/node.certificate.pem firefly-read: - image: f1r3flyindustries/f1r3fly-scala-node:latest + image: f1r3flyindustries/f1r3fly-rust-node:latest pull_policy: always user: root ports: - 14413:40403 # rest api + healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:40403/status || exit 1"] + interval: 30s + timeout: 10s + start_period: 40s + retries: 3 command: - run - --bootstrap=rnode://ebffd419dea60220734ccea8875e86d87bac10a7@firefly?protocol=40400&discovery=40404 - --host=firefly-read - --no-upnp - --allow-private-addresses + - --synchrony-constraint-threshold=0.0 + - --fork-choice-stale-threshold=30s + - --fork-choice-check-if-stale-interval=10s firefly-testnet: - image: f1r3flyindustries/f1r3fly-scala-node:latest + image: f1r3flyindustries/f1r3fly-rust-node:latest pull_policy: always user: root ports: - 15401:40401 # deploy service - - 15402:40402 # propose service - 15403:40403 # rest api volumes: - ./testnet/genesis:/var/lib/rnode/genesis - ./certs/node.key.pem:/var/lib/rnode/node.key.pem:ro - ./certs/node.certificate.pem:/var/lib/rnode/node.certificate.pem:ro + healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:40403/status || exit 1"] + interval: 30s + timeout: 10s + start_period: 40s + retries: 3 command: - run - -s @@ -59,23 +80,34 @@ services: - --no-upnp - --allow-private-addresses - --synchrony-constraint-threshold=0.0 + - --heartbeat-enabled + - --fault-tolerance-threshold=0.0 - --protocol-port=40400 - --discovery-port=40404 - --tls-key-path=/var/lib/rnode/node.key.pem - --tls-certificate-path=/var/lib/rnode/node.certificate.pem firefly-read-testnet: - image: f1r3flyindustries/f1r3fly-scala-node:latest + image: f1r3flyindustries/f1r3fly-rust-node:latest pull_policy: always user: root ports: - 15413:40403 # rest api + healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:40403/status || exit 1"] + interval: 30s + timeout: 10s + start_period: 40s + retries: 3 command: - run - --bootstrap=rnode://ebffd419dea60220734ccea8875e86d87bac10a7@firefly-testnet?protocol=40400&discovery=40404 - --host=firefly-read-testnet - --no-upnp - --allow-private-addresses + - --synchrony-constraint-threshold=0.0 + - --fork-choice-stale-threshold=30s + - --fork-choice-check-if-stale-interval=10s state-sync-init: build: @@ -92,7 +124,7 @@ services: command: - --wallet-key=232DADA5BBAFC0799D5F370DA04AF70CE438F69F954512B26D6FB5B560B81DFE - --deploy-service-url=http://firefly:40401 - - --propose-service-url=http://firefly:40402 + - --service-id=docker-pds - init @@ -112,7 +144,7 @@ services: command: - --wallet-key=232DADA5BBAFC0799D5F370DA04AF70CE438F69F954512B26D6FB5B560B81DFE - --deploy-service-url=http://firefly:40401 - - --propose-service-url=http://firefly:40402 + - --service-id=docker-pds - upload - --db-url=postgresql://postgres@postgresql:5432 @@ -131,7 +163,7 @@ services: command: - --wallet-key=232DADA5BBAFC0799D5F370DA04AF70CE438F69F954512B26D6FB5B560B81DFE - --deploy-service-url=http://firefly:40401 - - --propose-service-url=http://firefly:40402 + - --service-id=docker-pds - init @@ -153,7 +185,7 @@ services: command: - --wallet-key=232DADA5BBAFC0799D5F370DA04AF70CE438F69F954512B26D6FB5B560B81DFE - --deploy-service-url=http://firefly:40401 - - --propose-service-url=http://firefly:40402 + - --service-id=docker-pds - listen - --communication-service-api-addr=0.0.0.0:8082 @@ -177,7 +209,7 @@ services: command: - --wallet-key=232DADA5BBAFC0799D5F370DA04AF70CE438F69F954512B26D6FB5B560B81DFE - --deploy-service-url=http://firefly:40401 - - --propose-service-url=http://firefly:40402 + - --service-id=docker-pds - push - --events-source-url=ws://host.docker.internal:2583/xrpc/com.atproto.sync.subscribeRepos diff --git a/packages/embers/Cargo.toml b/packages/embers/Cargo.toml index bb3805a7..2d57b780 100644 --- a/packages/embers/Cargo.toml +++ b/packages/embers/Cargo.toml @@ -12,6 +12,7 @@ path = "src/generate_schema.rs" name = "embers" path = "src/main.rs" + [dependencies] aes-gcm = { version = "0.10", features = ["std", "zeroize"] } anyhow = { version = "1.0", features = ["std"] } @@ -42,6 +43,10 @@ tracing = { version = "0.1" } tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.20", features = ["serde", "v7"] } +[dev-dependencies] +tokio = { version = "1.49", features = ["macros", "rt-multi-thread", "time", "test-util"] } +tracing-test = { version = "0.2" } + [lints.clippy] cast_possible_wrap = "allow" cast_sign_loss = "allow" diff --git a/packages/embers/Makefile.toml b/packages/embers/Makefile.toml index c19fdc10..74f6074d 100644 --- a/packages/embers/Makefile.toml +++ b/packages/embers/Makefile.toml @@ -10,7 +10,6 @@ env.EMBERS__MAINNET__DEPLOY_SERVICE_URL = "http://localhost:14401" env.EMBERS__MAINNET__OBSERVER_URL = "http://localhost:14413" env.EMBERS__MAINNET__OBSERVER_WS_API_URL = "ws://localhost:14413" env.EMBERS__MAINNET__OSLFS_ENV_KEY = "E6441631C4E164BF13A0532BF6775606965089CE3750E5ED39AAA9EC0DF81E67" -env.EMBERS__MAINNET__PROPOSE_SERVICE_URL = "http://localhost:14402" env.EMBERS__MAINNET__SERVICE_KEY = "232DADA5BBAFC0799D5F370DA04AF70CE438F69F954512B26D6FB5B560B81DFE" env.EMBERS__MAINNET__VALIDATOR_WS_API_URL = "ws://localhost:14403" env.EMBERS__MAINNET__WALLETS_ENV_KEY = "8BDC54B5551812C43428EB172A2079ABBEF13B5370BB7535F78807CDEBA3E7B3" @@ -19,7 +18,6 @@ env.EMBERS__TESTNET__DEPLOY_SERVICE_URL = "http://localhost:15401" env.EMBERS__TESTNET__ENV_KEY = "D1BD29C232D11142E852EEE23482B239AF5494DFA10D64E82A72A8CDF82D5127" env.EMBERS__TESTNET__OBSERVER_URL = "http://localhost:15413" env.EMBERS__TESTNET__OBSERVER_WS_API_URL = "ws://localhost:15413" -env.EMBERS__TESTNET__PROPOSE_SERVICE_URL = "http://localhost:15402" env.EMBERS__TESTNET__SERVICE_KEY = "732240A471E12931D858F147165BA1B52C011B92B9E8CD7959AADF06D7ACE622" env.EMBERS__TESTNET__VALIDATOR_WS_API_URL = "ws://localhost:15403" env.RUST_BACKTRACE = "full" diff --git a/packages/embers/poetry.lock b/packages/embers/poetry.lock index 596cddb2..b3f80816 100644 --- a/packages/embers/poetry.lock +++ b/packages/embers/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "base58" @@ -445,6 +445,57 @@ files = [ {file = "protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c"}, ] +[[package]] +name = "pycryptodome" +version = "3.23.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +files = [ + {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, + {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, +] + [[package]] name = "pygments" version = "2.19.2" @@ -642,4 +693,4 @@ test = ["pytest", "websockets"] [metadata] lock-version = "2.1" python-versions = ">=3.13,<4" -content-hash = "52e0457d518799bcb850a24d798e6b31af75983ba44361592fa593768b1acec1" +content-hash = "99ebedded1d8a8ca92443909eac5e3fae46788dd49ed1742e5e592bccedf839f" diff --git a/packages/embers/pyproject.toml b/packages/embers/pyproject.toml index 08f69c77..0094c60b 100644 --- a/packages/embers/pyproject.toml +++ b/packages/embers/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "python-zbase32 (>=0.1,<0.2.0)", "requests (>=2.32,<3.0.0)", "websocket-client (>=1.9,<2.0.0)", + "pycryptodome (>=3.23.0,<4.0.0)", ] name = "embers-tests" requires-python = ">=3.13,<4" diff --git a/packages/embers/src/api/wallets/endpoints.rs b/packages/embers/src/api/wallets/endpoints.rs index 7ae33326..c05cb600 100644 --- a/packages/embers/src/api/wallets/endpoints.rs +++ b/packages/embers/src/api/wallets/endpoints.rs @@ -113,8 +113,7 @@ impl WalletsApi { let msg = DeployEvent::from(msg).to_json_string(); future::ok(websocket::Message::Text(msg)) }); - wallets.subscribe_to_deploys(address.0, sink); - future::ready(()) + wallets.subscribe_to_deploys(address.0, sink) }) .boxed() } diff --git a/packages/embers/src/blockchain/agents/models.rs b/packages/embers/src/blockchain/agents/models.rs index 69f2d766..a00ee6b3 100644 --- a/packages/embers/src/blockchain/agents/models.rs +++ b/packages/embers/src/blockchain/agents/models.rs @@ -34,5 +34,13 @@ pub struct Agent { pub description: Option, pub shard: Option, pub logo: Option, + // Reverse the Rholang string escaping the tuplespace preserves — otherwise + // code like `@Nil!("foo")` reads back as `@Nil!(\"foo\")`. This is also the + // form the agent-deploy path re-embeds as a Rholang term, so it must be + // unescaped here. + #[serde( + default, + deserialize_with = "crate::blockchain::common::deserialize_unescaped_opt" + )] pub code: Option, } diff --git a/packages/embers/src/blockchain/agents_teams/models.rs b/packages/embers/src/blockchain/agents_teams/models.rs index b3e4ef3a..8f41b97b 100644 --- a/packages/embers/src/blockchain/agents_teams/models.rs +++ b/packages/embers/src/blockchain/agents_teams/models.rs @@ -33,10 +33,21 @@ impl<'de> Deserialize<'de> for Graph { where D: serde::Deserializer<'de>, { - let graphl = String::deserialize(deserializer)?; - models::Graph::new(graphl) - .map(Self) - .map_err(de::Error::custom) + let raw = String::deserialize(deserializer)?; + // The Rholang tuplespace stores string content without unescaping + // escape sequences. Undo the escape_rho_string() level that was added + // when embedding the graphl string in Rholang source code. + let graphl = crate::blockchain::common::unescape_rho_string(&raw); + models::Graph::new(graphl.clone()).map(Self).map_err(|e| { + tracing::error!( + error = %e, + graphl_len = graphl.len(), + graphl_prefix = %&graphl[..graphl.len().min(200)], + raw_prefix = %&raw[..raw.len().min(200)], + "DIAG: graphl_parser::parse_to_ast failed during Graph deserialization" + ); + de::Error::custom(e) + }) } } @@ -69,3 +80,141 @@ pub struct EncryptedMsg { pub ciphertext: Hex, pub nonce: Hex, } + +#[cfg(test)] +mod tests { + use crate::domain::agents_teams::models::Graph; + + /// Simulate `escape_rho_string` from `firefly_client::rendering`. + fn escape_rho_string(s: &str) -> String { + s.replace('\\', "\\\\").replace('"', "\\\"") + } + + /// Simulate the unescape performed by the Graph deserializer. + /// This reverses `escape_rho_string` — the same operation the + /// deserializer applies to strings returned from the tuplespace. + fn undo_escape_rho_string(s: &str) -> String { + s.replace("\\\"", "\"").replace("\\\\", "\\") + } + + #[test] + fn graphl_round_trip_simple() { + // A minimal graphl context annotation (no JSON, no escapes). + let original = r#"context "foo" for f in 0"#; + let ast = Graph::new(original.to_owned()).expect("original should parse"); + + let graphl = ast.graphl(); + let re_parsed = Graph::new(graphl.clone()).expect("serialized form should re-parse"); + let re_serialized = re_parsed.graphl(); + assert_eq!( + graphl, re_serialized, + "graphl round-trip must be idempotent" + ); + } + + #[test] + fn graphl_round_trip_through_rho_escaping() { + // 1. Start with a graphl string containing a context annotation with JSON. + // The graphl format uses `\"` inside string literals, so the raw graphl + // looks like: context "{\"type\":\"deploy-container\"}" for f in 0 + let original = r#"context "{\"type\":\"deploy-container\"}" for f in 0"#; + let ast = Graph::new(original.to_owned()).expect("original should parse"); + + // 2. Serialize back to graphl (simulates Graph::graphl()). + let graphl = ast.graphl(); + + // 3. Apply escape_rho_string (simulates Rholang template embedding). + // This is what gets stored in the tuplespace — Rholang does NOT + // unescape, so the stored string retains the extra escaping. + let stored = escape_rho_string(&graphl); + + // 4. The stored string should NOT parse directly (it has extra escaping). + assert!( + Graph::new(stored.clone()).is_err(), + "escaped string should not parse without unescaping" + ); + + // 5. Apply undo_escape_rho_string (what the deserializer does). + let unescaped = undo_escape_rho_string(&stored); + + // 6. Verify the unescaped string equals the original graphl. + assert_eq!( + graphl, unescaped, + "undo_escape_rho_string should recover the original graphl" + ); + + // 7. Re-parse the unescaped string. + let re_parsed = Graph::new(unescaped).expect("unescaped string should parse"); + + // 8. Verify AST equality via re-serialization. + let re_serialized = re_parsed.graphl(); + assert_eq!( + graphl, re_serialized, + "ASTs should produce identical graphl" + ); + } + + #[test] + fn graphl_round_trip_complex_json_context() { + // A more complex context annotation with nested JSON. + let original = + r#"context "{\"agents\":[{\"name\":\"bot\",\"model\":\"gpt-4\"}]}" for x in 0"#; + let ast = Graph::new(original.to_owned()).expect("original should parse"); + let graphl = ast.graphl(); + + // Stored in tuplespace with extra escaping (Rholang does NOT unescape). + let stored = escape_rho_string(&graphl); + let unescaped = undo_escape_rho_string(&stored); + + assert_eq!(graphl, unescaped); + + let re_parsed = Graph::new(unescaped).expect("unescaped string should parse"); + assert_eq!(graphl, re_parsed.graphl()); + } + + #[test] + fn graphl_round_trip_with_vertex_and_edge() { + // A graph with vertices, bindings, edges, AND a context annotation. + let original = r#"< a > | { context "{\"k\":\"v\"}" for a in 0 }"#; + let ast = Graph::new(original.to_owned()).expect("original should parse"); + let graphl = ast.graphl(); + + // Stored in tuplespace with extra escaping (Rholang does NOT unescape). + let stored = escape_rho_string(&graphl); + let unescaped = undo_escape_rho_string(&stored); + + assert_eq!(graphl, unescaped); + let re_parsed = Graph::new(unescaped).expect("unescaped string should parse"); + assert_eq!(graphl, re_parsed.graphl()); + } + + #[test] + fn graphl_parser_is_thread_safe_under_mutex() { + // The C parser uses global state; this test verifies the mutex + // serializes concurrent access correctly. + let inputs = [ + r#"context "foo" for f in 0"#, + r#"context "{\"type\":\"deploy-container\"}" for x in 0"#, + r#"< a > | { context "{\"k\":\"v\"}" for a in 0 }"#, + r#"context "{\"agents\":[{\"name\":\"bot\"}]}" for y in 0"#, + ]; + + std::thread::scope(|s| { + let mut handles = Vec::new(); + for _ in 0..4 { + for input in &inputs { + handles.push(s.spawn(|| { + let ast = Graph::new(input.to_string()).expect("parse should succeed"); + let graphl = ast.graphl(); + let re_parsed = + Graph::new(graphl.clone()).expect("re-parse should succeed"); + assert_eq!(graphl, re_parsed.graphl()); + })); + } + } + for handle in handles { + handle.join().expect("thread should not panic"); + } + }); + } +} diff --git a/packages/embers/src/blockchain/common.rs b/packages/embers/src/blockchain/common.rs index c93c2694..c81836ae 100644 --- a/packages/embers/src/blockchain/common.rs +++ b/packages/embers/src/blockchain/common.rs @@ -32,6 +32,30 @@ impl<'de> Deserialize<'de> for Uri { } } +/// Reverse of `firefly_client::rendering::escape_rho_string`. The f1r3node +/// tuplespace stores Rholang string literals verbatim — escape sequences are +/// not processed by the parser (verified: `ch!("a\"b")` reads back as `a\"b`). +/// So user strings embers embedded into Rholang source via `escape_rho_string` +/// (`\` → `\\`, `"` → `\"`) come back still-escaped. Undo that one level, at the +/// point the user-facing value is materialised. Order is the reverse of the +/// escape (`\"` before `\\`), matching the `Graph` deserializer. +pub fn unescape_rho_string(s: &str) -> String { + s.replace("\\\"", "\"").replace("\\\\", "\\") +} + +/// serde helper: deserialize an `Option` and reverse the Rholang string +/// escaping (see [`unescape_rho_string`]). Used for user-authored string fields +/// (`code`, `query`, …) that are read back out of the tuplespace for display or +/// re-deploy. +pub fn deserialize_unescaped_opt<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + Ok(Option::::deserialize(deserializer)? + .as_deref() + .map(unescape_rho_string)) +} + #[derive(Debug, Clone, Into)] pub struct Hex(Vec); diff --git a/packages/embers/src/blockchain/oslfs/models.rs b/packages/embers/src/blockchain/oslfs/models.rs index 589b7c46..064676a9 100644 --- a/packages/embers/src/blockchain/oslfs/models.rs +++ b/packages/embers/src/blockchain/oslfs/models.rs @@ -22,5 +22,11 @@ pub struct Oslf { pub created_at: DateTime, pub name: String, pub description: Option, + // Reverse the Rholang string escaping the tuplespace preserves (see + // `blockchain::common::unescape_rho_string`). + #[serde( + default, + deserialize_with = "crate::blockchain::common::deserialize_unescaped_opt" + )] pub query: Option, } diff --git a/packages/embers/src/configuration.rs b/packages/embers/src/configuration.rs index 0804feba..b42e00c9 100644 --- a/packages/embers/src/configuration.rs +++ b/packages/embers/src/configuration.rs @@ -7,7 +7,6 @@ use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] pub struct MainNet { pub deploy_service_url: String, - pub propose_service_url: String, pub validator_ws_api_url: String, pub observer_url: String, pub observer_ws_api_url: String, @@ -21,7 +20,6 @@ pub struct MainNet { #[derive(Debug, Clone, Deserialize)] pub struct TestNet { pub deploy_service_url: String, - pub propose_service_url: String, pub validator_ws_api_url: String, pub observer_url: String, pub observer_ws_api_url: String, diff --git a/packages/embers/src/domain.rs b/packages/embers/src/domain.rs index 3746e9a3..99fb91eb 100644 --- a/packages/embers/src/domain.rs +++ b/packages/embers/src/domain.rs @@ -4,3 +4,6 @@ pub mod common; pub mod oslfs; pub mod testnet; pub mod wallets; + +#[cfg(test)] +pub(crate) mod test_helpers; diff --git a/packages/embers/src/domain/agents.rs b/packages/embers/src/domain/agents.rs index b5b44735..0001621c 100644 --- a/packages/embers/src/domain/agents.rs +++ b/packages/embers/src/domain/agents.rs @@ -1,8 +1,15 @@ use anyhow::Context; use firefly_client::helpers::insert_signed_signature; -use firefly_client::models::{DeployData, Uri}; +use firefly_client::models::{DeployData, DeployId, Uri}; use firefly_client::rendering::Render; -use firefly_client::{ReadNodeClient, WriteNodeClient}; +use firefly_client::{ + NodeEventSource, + NodeEvents, + ReadNode, + ReadNodeClient, + WriteNode, + WriteNodeClient, +}; use secp256k1::{PublicKey, Secp256k1, SecretKey}; mod create; @@ -15,10 +22,15 @@ pub mod models; mod save; #[derive(Clone)] -pub struct AgentsService { +pub struct AgentsService< + R: ReadNode = ReadNodeClient, + W: WriteNode = WriteNodeClient, + N: NodeEventSource = NodeEvents, +> { pub uri: Uri, - pub write_client: WriteNodeClient, - pub read_client: ReadNodeClient, + pub write_client: W, + pub read_client: R, + pub observer_node_events: N, } #[allow(unused)] @@ -32,14 +44,15 @@ struct InitAgentsEnv { } #[allow(unused)] -impl AgentsService { +impl AgentsService { #[tracing::instrument(level = "info", skip_all, err(Debug))] pub async fn bootstrap( - mut write_client: WriteNodeClient, - read_client: ReadNodeClient, + mut write_client: W, + read_client: R, + observer_node_events: N, deployer_key: &SecretKey, env_key: &SecretKey, - ) -> anyhow::Result { + ) -> anyhow::Result<(Self, DeployId)> { let secp = Secp256k1::new(); let env_public_key = PublicKey::from_secret_key(&secp, env_key); let deployer_public_key = PublicKey::from_secret_key(&secp, deployer_key); @@ -61,15 +74,51 @@ impl AgentsService { let deploy_data = DeployData::builder(code).timestamp(timestamp).build(); - write_client + let deploy_id = write_client .deploy(deployer_key, deploy_data) .await .context("failed to deploy agents env")?; - Ok(Self { - uri: env_uri, - write_client, - read_client, - }) + Ok(( + Self { + uri: env_uri, + write_client, + read_client, + observer_node_events, + }, + deploy_id, + )) + } +} + +#[cfg(test)] +mod tests { + use firefly_client::rendering::Render; + + use super::*; + + #[test] + fn test_init_template_renders_valid_rholang() { + let secp = secp256k1::Secp256k1::new(); + let sk = secp256k1::SecretKey::from_byte_array([1u8; 32]).expect("valid key"); + let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk); + let env_uri: Uri = pk.into(); + + let code = InitAgentsEnv { + env_uri: env_uri.clone(), + version: 0, + public_key: pk.serialize_uncompressed().into(), + sig: vec![1, 2, 3, 4], + } + .render() + .expect("template should render"); + + assert!(!code.is_empty(), "rendered code should not be empty"); + assert!( + code.contains("rho:registry:insertSigned:secp256k1"), + "should contain registry insert pattern" + ); + let uri_str: &str = env_uri.as_ref(); + assert!(code.contains(uri_str), "should contain the env URI"); } } diff --git a/packages/embers/src/domain/agents/create.rs b/packages/embers/src/domain/agents/create.rs index 5d711c7a..acc96b7a 100644 --- a/packages/embers/src/domain/agents/create.rs +++ b/packages/embers/src/domain/agents/create.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use firefly_client::models::{DeployId, SignedCode, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use uuid::Uuid; use crate::domain::agents::AgentsService; @@ -21,7 +22,7 @@ struct Create { code: Option, } -impl AgentsService { +impl AgentsService { #[tracing::instrument( level = "info", skip_all, @@ -72,7 +73,6 @@ impl AgentsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; Ok(deploy_id) } diff --git a/packages/embers/src/domain/agents/delete.rs b/packages/embers/src/domain/agents/delete.rs index e8e89b8e..4d6141cd 100644 --- a/packages/embers/src/domain/agents/delete.rs +++ b/packages/embers/src/domain/agents/delete.rs @@ -1,5 +1,6 @@ use firefly_client::models::{DeployId, SignedCode, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::domain::agents::AgentsService; use crate::domain::agents::models::DeleteResp; @@ -12,7 +13,7 @@ struct Delete { id: String, } -impl AgentsService { +impl AgentsService { #[tracing::instrument(level = "info", skip(self), err(Debug), ret(Debug, level = "trace"))] pub async fn prepare_delete_contract(&self, id: String) -> anyhow::Result { let contract = Delete { @@ -43,8 +44,6 @@ impl AgentsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; - Ok(deploy_id) } } diff --git a/packages/embers/src/domain/agents/deploy.rs b/packages/embers/src/domain/agents/deploy.rs index 3130cc63..ffd74125 100644 --- a/packages/embers/src/domain/agents/deploy.rs +++ b/packages/embers/src/domain/agents/deploy.rs @@ -1,7 +1,10 @@ +use std::time::Duration; + use anyhow::Context; use chrono::{DateTime, Utc}; use firefly_client::models::{DeployId, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::domain::agents::AgentsService; use crate::domain::agents::models::{DeployReq, DeployResp, DeploySignedReq}; @@ -16,7 +19,7 @@ struct UpdateLastDeploy { last_deploy: DateTime, } -impl AgentsService { +impl AgentsService { #[tracing::instrument( level = "info", skip_all, @@ -35,12 +38,17 @@ impl AgentsService { address, phlo_limit, } => { - let code = self - .get(address, id.clone(), version.clone()) + let agent = self + .get_with_retry( + address, + id.clone(), + version.clone(), + 30, + Duration::from_secs(1), + ) .await? - .context("agent not found")? - .code - .context("agent has no code")?; + .context("agent not found")?; + let code = agent.code.context("agent has no code")?; let system_code = UpdateLastDeploy { env_uri: self.uri.clone(), @@ -94,7 +102,63 @@ impl AgentsService { write_client.deploy_signed_contract(system).await?; } - write_client.propose().await?; Ok(deploy_id) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::domain::test_helpers::*; + + fn make_service() -> AgentsService { + AgentsService { + uri: test_uri(), + write_client: MockWriteNode::new() + .with_deploy_response(Ok(test_deploy_id())) + .with_deploy_response(Ok(test_deploy_id())), + read_client: MockReadNode::new(), + observer_node_events: MockNodeEventSource::new(), + } + } + + #[tokio::test] + async fn test_deploy_signed_deploys_both() { + let service = make_service(); + let request = DeploySignedReq { + contract: test_signed_code(), + system: Some(test_signed_code()), + }; + + let result = service.deploy_signed_deploy(request).await; + assert!(result.is_ok(), "expected Ok, got: {result:?}"); + + let deployed = service.write_client.deployed_contracts(); + assert_eq!( + deployed.len(), + 2, + "expected both contract and system to be deployed, but got {} deploys", + deployed.len() + ); + } + + #[tokio::test] + async fn test_deploy_signed_main_only() { + let service = make_service(); + let request = DeploySignedReq { + contract: test_signed_code(), + system: None, + }; + + let result = service.deploy_signed_deploy(request).await; + assert!(result.is_ok(), "expected Ok, got: {result:?}"); + + let deployed = service.write_client.deployed_contracts(); + assert_eq!( + deployed.len(), + 1, + "expected only the main contract to be deployed, but got {} deploys", + deployed.len() + ); + } +} diff --git a/packages/embers/src/domain/agents/get.rs b/packages/embers/src/domain/agents/get.rs index cfe39ef8..239859ad 100644 --- a/packages/embers/src/domain/agents/get.rs +++ b/packages/embers/src/domain/agents/get.rs @@ -1,5 +1,8 @@ +use std::time::Duration; + use firefly_client::models::{Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::blockchain::agents::models; use crate::domain::agents::AgentsService; @@ -15,7 +18,7 @@ struct Get { version: String, } -impl AgentsService { +impl AgentsService { #[tracing::instrument( level = "info", skip_all, @@ -39,7 +42,41 @@ impl AgentsService { } .render()?; - let agent: Option = self.read_client.get_data(code).await?; + let agent: Option = self.read_client.get_data_or_none(code).await?.flatten(); + Ok(agent.map(Into::into)) + } + + /// Like `get`, but retries on empty explore-deploy results to handle observer tuplespace lag. + #[tracing::instrument( + level = "info", + skip_all, + fields(address, id, version), + err(Debug), + ret(Debug, level = "trace") + )] + pub async fn get_with_retry( + &self, + address: WalletAddress, + id: String, + version: String, + max_retries: u32, + delay: Duration, + ) -> anyhow::Result> { + record_trace!(address, id, version); + + let code = Get { + env_uri: self.uri.clone(), + address, + id, + version, + } + .render()?; + + let agent: Option = self + .read_client + .get_data_or_none_with_retry(code, max_retries, delay) + .await? + .flatten(); Ok(agent.map(Into::into)) } } diff --git a/packages/embers/src/domain/agents/list.rs b/packages/embers/src/domain/agents/list.rs index 1f842956..72d5032a 100644 --- a/packages/embers/src/domain/agents/list.rs +++ b/packages/embers/src/domain/agents/list.rs @@ -1,5 +1,6 @@ use firefly_client::models::{Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::blockchain::agents::models; use crate::domain::agents::AgentsService; @@ -13,7 +14,7 @@ struct List { address: WalletAddress, } -impl AgentsService { +impl AgentsService { #[tracing::instrument( level = "info", skip_all, @@ -29,12 +30,13 @@ impl AgentsService { address, } .render()?; - self.read_client - .get_data(code) - .await - .map(|agents: Vec| Agents { - agents: agents.into_iter().map(Into::into).collect(), - }) - .map_err(Into::into) + let agents: Vec = self + .read_client + .get_data_or_none(code) + .await? + .unwrap_or_default(); + Ok(Agents { + agents: agents.into_iter().map(Into::into).collect(), + }) } } diff --git a/packages/embers/src/domain/agents/list_versions.rs b/packages/embers/src/domain/agents/list_versions.rs index 2458dda5..ef6f0db7 100644 --- a/packages/embers/src/domain/agents/list_versions.rs +++ b/packages/embers/src/domain/agents/list_versions.rs @@ -1,5 +1,6 @@ use firefly_client::models::{Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::blockchain::agents::models; use crate::domain::agents::AgentsService; @@ -14,7 +15,7 @@ struct ListVersions { id: String, } -impl AgentsService { +impl AgentsService { #[tracing::instrument( level = "info", skip_all, @@ -36,7 +37,8 @@ impl AgentsService { } .render()?; - let agents: Option> = self.read_client.get_data(code).await?; + let agents: Option> = + self.read_client.get_data_or_none(code).await?.flatten(); Ok(agents.map(|mut agents| { agents.sort_by(|l, r| l.version.cmp(&r.version)); Agents { diff --git a/packages/embers/src/domain/agents/save.rs b/packages/embers/src/domain/agents/save.rs index 2adf71a8..91e86628 100644 --- a/packages/embers/src/domain/agents/save.rs +++ b/packages/embers/src/domain/agents/save.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use firefly_client::models::{DeployId, SignedCode, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use uuid::Uuid; use crate::domain::agents::AgentsService; @@ -21,7 +22,7 @@ struct Save { code: Option, } -impl AgentsService { +impl AgentsService { #[tracing::instrument( level = "info", skip_all, @@ -74,7 +75,7 @@ impl AgentsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; + Ok(deploy_id) } } diff --git a/packages/embers/src/domain/agents_teams.rs b/packages/embers/src/domain/agents_teams.rs index 821c4ee2..a95d3be0 100644 --- a/packages/embers/src/domain/agents_teams.rs +++ b/packages/embers/src/domain/agents_teams.rs @@ -5,9 +5,16 @@ use anyhow::{Context, anyhow}; use dashmap::DashMap; use firefly_client::errors::ReadNodeError; use firefly_client::helpers::insert_signed_signature; -use firefly_client::models::{DeployData, Uri}; +use firefly_client::models::{DeployData, DeployId, Uri}; use firefly_client::rendering::Render; -use firefly_client::{NodeEvents, ReadNodeClient, WriteNodeClient}; +use firefly_client::{ + NodeEventSource, + NodeEvents, + ReadNode, + ReadNodeClient, + WriteNode, + WriteNodeClient, +}; use secp256k1::{PublicKey, Secp256k1, SecretKey}; use crate::blockchain; @@ -28,11 +35,15 @@ mod run_on_firesky; mod save; #[derive(Clone)] -pub struct AgentsTeamsService { +pub struct AgentsTeamsService< + R: ReadNode = ReadNodeClient, + W: WriteNode = WriteNodeClient, + N: NodeEventSource = NodeEvents, +> { pub uri: Uri, - pub write_client: WriteNodeClient, - pub read_client: ReadNodeClient, - pub observer_node_events: NodeEvents, + pub write_client: W, + pub read_client: R, + pub observer_node_events: N, pub aes_encryption_key: Key, pub firesky_accounts: Arc>, } @@ -55,16 +66,16 @@ struct GetFireskyTokens { } #[allow(unused)] -impl AgentsTeamsService { +impl AgentsTeamsService { #[tracing::instrument(level = "info", skip_all, err(Debug))] pub async fn bootstrap( - mut write_client: WriteNodeClient, - read_client: ReadNodeClient, - observer_node_events: NodeEvents, + mut write_client: W, + read_client: R, + observer_node_events: N, deployer_key: &SecretKey, env_key: &SecretKey, aes_encryption_key: Key, - ) -> anyhow::Result { + ) -> anyhow::Result<(Self, DeployId)> { let secp = Secp256k1::new(); let env_public_key = PublicKey::from_secret_key(&secp, env_key); let deployer_public_key = PublicKey::from_secret_key(&secp, deployer_key); @@ -82,11 +93,11 @@ impl AgentsTeamsService { } .render()?; - tracing::debug!("code = {code}"); + tracing::info!("agents_teams init deploy code:\n{code}"); let deploy_data = DeployData::builder(code).timestamp(timestamp).build(); - write_client + let deploy_id = write_client .deploy(deployer_key, deploy_data) .await .context("failed to deploy agents teams env")?; @@ -130,13 +141,63 @@ impl AgentsTeamsService { Err(err) => return Err(anyhow!(err)), }; - Ok(Self { - uri: env_uri, - write_client, - read_client, - observer_node_events, - aes_encryption_key, - firesky_accounts: Arc::new(firesky_accounts), - }) + Ok(( + Self { + uri: env_uri, + write_client, + read_client, + observer_node_events, + aes_encryption_key, + firesky_accounts: Arc::new(firesky_accounts), + }, + deploy_id, + )) + } +} + +#[cfg(test)] +mod template_tests { + use firefly_client::rendering::Render; + + use super::*; + + #[test] + fn test_init_template_renders_valid_rholang() { + let secp = secp256k1::Secp256k1::new(); + let sk = secp256k1::SecretKey::from_byte_array([2u8; 32]).expect("valid key"); + let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk); + let env_uri: Uri = pk.into(); + + let code = InitAgentsTeamsEnv { + env_uri: env_uri.clone(), + version: 0, + public_key: pk.serialize_uncompressed().into(), + sig: vec![1, 2, 3, 4], + } + .render() + .expect("template should render"); + + assert!(!code.is_empty()); + assert!(code.contains("rho:registry:insertSigned:secp256k1")); + let uri_str: &str = env_uri.as_ref(); + assert!(code.contains(uri_str)); + } + + #[test] + fn test_get_firesky_tokens_template_renders() { + let secp = secp256k1::Secp256k1::new(); + let sk = secp256k1::SecretKey::from_byte_array([3u8; 32]).expect("valid key"); + let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk); + let env_uri: Uri = pk.into(); + + let code = GetFireskyTokens { + env_uri: env_uri.clone(), + } + .render() + .expect("template should render"); + + assert!(!code.is_empty()); + let uri_str: &str = env_uri.as_ref(); + assert!(code.contains(uri_str)); } } diff --git a/packages/embers/src/domain/agents_teams/create.rs b/packages/embers/src/domain/agents_teams/create.rs index 4737773c..35fe02bc 100644 --- a/packages/embers/src/domain/agents_teams/create.rs +++ b/packages/embers/src/domain/agents_teams/create.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use firefly_client::models::{DeployId, SignedCode, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use uuid::Uuid; use crate::domain::agents_teams::AgentsTeamsService; @@ -21,7 +22,7 @@ struct Create { graph: Option, } -impl AgentsTeamsService { +impl AgentsTeamsService { #[tracing::instrument( level = "info", skip_all, @@ -72,7 +73,7 @@ impl AgentsTeamsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; + Ok(deploy_id) } } diff --git a/packages/embers/src/domain/agents_teams/delete.rs b/packages/embers/src/domain/agents_teams/delete.rs index b3e53645..0fb861c9 100644 --- a/packages/embers/src/domain/agents_teams/delete.rs +++ b/packages/embers/src/domain/agents_teams/delete.rs @@ -1,5 +1,6 @@ use firefly_client::models::{DeployId, SignedCode, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::domain::agents_teams::AgentsTeamsService; use crate::domain::agents_teams::models::DeleteResp; @@ -12,7 +13,7 @@ struct Delete { id: String, } -impl AgentsTeamsService { +impl AgentsTeamsService { #[tracing::instrument(level = "info", skip(self), err(Debug), ret(Debug, level = "trace"))] pub async fn prepare_delete_contract(&self, id: String) -> anyhow::Result { let contract = Delete { @@ -43,7 +44,6 @@ impl AgentsTeamsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; Ok(deploy_id) } } diff --git a/packages/embers/src/domain/agents_teams/deploy.rs b/packages/embers/src/domain/agents_teams/deploy.rs index 3ad535ed..87280a3b 100644 --- a/packages/embers/src/domain/agents_teams/deploy.rs +++ b/packages/embers/src/domain/agents_teams/deploy.rs @@ -1,7 +1,11 @@ +use std::time::Duration; + use anyhow::Context; use chrono::{DateTime, Utc}; +use firefly_client::errors::ReadNodeError; use firefly_client::models::{DeployId, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::domain::agents_teams::AgentsTeamsService; use crate::domain::agents_teams::compilation::{parse, render}; @@ -18,7 +22,7 @@ struct RecordDeploy { uri: Uri, } -impl AgentsTeamsService { +impl AgentsTeamsService { #[tracing::instrument( level = "info", skip_all, @@ -29,6 +33,83 @@ impl AgentsTeamsService { pub async fn prepare_deploy_contract(&self, request: DeployReq) -> anyhow::Result { record_trace!(request); + // Diagnostic: check if registry entry is still accessible before deploy + let env_uri_str: &str = self.uri.as_ref(); + let probe_code = + format!(r#"new ret, rl(`rho:registry:lookup`) in {{ rl!(`{env_uri_str}`, *ret) }}"#,); + match self + .read_client + .get_data::(probe_code) + .await + { + Ok(value) => tracing::info!( + value = %value, + "pre-deploy registry probe: env entry accessible" + ), + Err(ReadNodeError::ReturnValueMissing) => tracing::error!( + env_uri = %env_uri_str, + "pre-deploy registry probe: env entry NOT FOUND — registry lookup returned empty" + ), + Err(err) => tracing::error!( + error = %err, + "pre-deploy registry probe: failed" + ), + } + + // Control probe: check a well-known system URI + let system_probe_code = + r#"new ret, rl(`rho:registry:lookup`) in { rl!(`rho:lang:treeHashMap`, *ret) }"# + .to_string(); + match self + .read_client + .get_data::(system_probe_code) + .await + { + Ok(value) => tracing::info!( + value = %value, + "pre-deploy registry probe: system URI (treeHashMap) accessible" + ), + Err(ReadNodeError::ReturnValueMissing) => tracing::error!( + "pre-deploy registry probe: system URI (treeHashMap) NOT FOUND — entire registry may be broken" + ), + Err(err) => tracing::error!( + error = %err, + "pre-deploy registry probe: system URI check failed" + ), + } + + // Diagnostic probe: test whether the contract handler actually fires. + // "list" should return data (possibly empty list) if the persistent + // continuation for @agentsTeams is still reachable. If this returns + // empty expr, the handler continuation is lost in the trie. + let handler_probe_code = format!( + r#"new ret, rl(`rho:registry:lookup`), agentsTeamsCh in {{ + rl!(`{env_uri_str}`, *agentsTeamsCh) | + for(@(_, agentsTeams) <- agentsTeamsCh) {{ + @agentsTeams!("list", "diag_probe_address", *ret) + }} +}}"#, + ); + match self + .read_client + .get_data::(handler_probe_code) + .await + { + Ok(value) => tracing::info!( + value = %value, + "pre-deploy handler probe: contract handler FIRES — 'list' returned data" + ), + Err(ReadNodeError::ReturnValueMissing) => tracing::error!( + env_uri = %env_uri_str, + "pre-deploy handler probe: contract handler BLOCKED — 'list' returned empty expr. \ + Persistent continuation for @agentsTeams may be lost in the trie." + ), + Err(err) => tracing::error!( + error = %err, + "pre-deploy handler probe: failed" + ), + } + let valid_after = self.write_client.clone().get_head_block_index().await?; let (graph, phlo_limit, deploy, system) = match request { DeployReq::AgentsTeam { @@ -39,7 +120,13 @@ impl AgentsTeamsService { deploy, } => { let agents_team = self - .get(address, id.clone(), version.clone()) + .get_with_retry( + address, + id.clone(), + version.clone(), + 30, + Duration::from_secs(1), + ) .await? .context("agents team not found")?; let graph = agents_team.graph.context("agents team has no graph")?; @@ -108,7 +195,201 @@ impl AgentsTeamsService { write_client.deploy_signed_contract(system).await?; } - write_client.propose().await?; Ok(deploy_id) } } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use aes_gcm::{Aes256Gcm, Key}; + use dashmap::DashMap; + use firefly_client::models::SignedCode; + + use super::*; + use crate::domain::agents_teams::AgentsTeamsService; + use crate::domain::agents_teams::models::DeploySignedReq; + use crate::domain::test_helpers::*; + + /// Build a minimal `AgentsTeamsService` wired to mock backends. + fn make_service( + read: MockReadNode, + write: MockWriteNode, + ) -> AgentsTeamsService { + AgentsTeamsService { + uri: test_uri(), + write_client: write, + read_client: read, + observer_node_events: MockNodeEventSource::new(), + aes_encryption_key: *Key::::from_slice(&[42u8; 32]), + firesky_accounts: Arc::new(DashMap::new()), + } + } + + fn dummy_signed_code(tag: u8) -> SignedCode { + SignedCode { + contract: vec![tag; 16], + sig: vec![0; 64], + sig_algorithm: "secp256k1".into(), + deployer: vec![0; 65], + } + } + + // ------------------------------------------------------- + // deploy_signed_deploy tests + // ------------------------------------------------------- + + #[tokio::test] + async fn test_deploy_signed_forwards_to_write_client() { + let write = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Ok(test_deploy_id())); + let read = MockReadNode::new(); + + let service = make_service(read, write.clone()); + + let contract = dummy_signed_code(0xAA); + let request = DeploySignedReq { + contract: contract.clone(), + system: None, + }; + + let deploy_id = service + .deploy_signed_deploy(request) + .await + .expect("deploy_signed_deploy should succeed"); + + assert_eq!(deploy_id, test_deploy_id()); + + // Exactly one contract should have been deployed + let deployed = write.deployed_contracts(); + assert_eq!(deployed.len(), 1, "expected exactly 1 deployed contract"); + assert_eq!( + deployed[0], + vec![0xAA; 16], + "deployed contract bytes should match the main contract" + ); + } + + #[tokio::test] + async fn test_deploy_signed_deploys_both_when_system_present() { + let main_id = DeployId::from("main-deploy-id".to_owned()); + let system_id = DeployId::from("system-deploy-id".to_owned()); + + let write = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Ok(main_id.clone())) + .with_deploy_response(Ok(system_id)); + let read = MockReadNode::new(); + + let service = make_service(read, write.clone()); + + let main_contract = dummy_signed_code(0xBB); + let system_contract = dummy_signed_code(0xCC); + + let request = DeploySignedReq { + contract: main_contract.clone(), + system: Some(system_contract.clone()), + }; + + let deploy_id = service + .deploy_signed_deploy(request) + .await + .expect("deploy_signed_deploy should succeed"); + + // The returned deploy ID should be from the first (main) deploy + assert_eq!(deploy_id, main_id); + + // Both contracts should have been deployed in order + let deployed = write.deployed_contracts(); + assert_eq!(deployed.len(), 2, "expected 2 deployed contracts"); + assert_eq!( + deployed[0], + vec![0xBB; 16], + "first deployed contract should be the main contract" + ); + assert_eq!( + deployed[1], + vec![0xCC; 16], + "second deployed contract should be the system contract" + ); + } + + #[tokio::test] + async fn test_deploy_signed_propagates_write_error() { + let write = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Err(anyhow::anyhow!("node unavailable"))); + let read = MockReadNode::new(); + + let service = make_service(read, write); + + let request = DeploySignedReq { + contract: dummy_signed_code(0x01), + system: None, + }; + + let result = service.deploy_signed_deploy(request).await; + assert!(result.is_err(), "should propagate write client error"); + assert!( + result.unwrap_err().to_string().contains("node unavailable"), + "error message should contain the original cause" + ); + } + + #[tokio::test] + async fn test_deploy_signed_system_error_propagates() { + // Main deploy succeeds but system deploy fails + let write = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Ok(test_deploy_id())) + .with_deploy_response(Err(anyhow::anyhow!("system deploy failed"))); + let read = MockReadNode::new(); + + let service = make_service(read, write); + + let request = DeploySignedReq { + contract: dummy_signed_code(0x01), + system: Some(dummy_signed_code(0x02)), + }; + + let result = service.deploy_signed_deploy(request).await; + assert!(result.is_err(), "should propagate system deploy error"); + assert!( + result + .unwrap_err() + .to_string() + .contains("system deploy failed"), + "error message should contain the system deploy failure cause" + ); + } + + #[tokio::test] + async fn test_deploy_signed_no_system_does_not_deploy_extra() { + // Enqueue two responses but only one should be consumed + let write = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Ok(test_deploy_id())); + let read = MockReadNode::new(); + + let service = make_service(read, write.clone()); + + let request = DeploySignedReq { + contract: dummy_signed_code(0xDD), + system: None, + }; + + service + .deploy_signed_deploy(request) + .await + .expect("deploy should succeed"); + + let deployed = write.deployed_contracts(); + assert_eq!( + deployed.len(), + 1, + "with system=None, only the main contract should be deployed" + ); + } +} diff --git a/packages/embers/src/domain/agents_teams/get.rs b/packages/embers/src/domain/agents_teams/get.rs index 2661269e..b71b2bf0 100644 --- a/packages/embers/src/domain/agents_teams/get.rs +++ b/packages/embers/src/domain/agents_teams/get.rs @@ -1,5 +1,8 @@ +use std::time::Duration; + use firefly_client::models::{Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::blockchain::agents_teams::models; use crate::domain::agents_teams::AgentsTeamsService; @@ -15,7 +18,7 @@ struct Get { version: String, } -impl AgentsTeamsService { +impl AgentsTeamsService { #[tracing::instrument( level = "info", skip_all, @@ -39,7 +42,42 @@ impl AgentsTeamsService { } .render()?; - let agents_team: Option = self.read_client.get_data(code).await?; + let agents_team: Option = + self.read_client.get_data_or_none(code).await?.flatten(); + Ok(agents_team.map(Into::into)) + } + + /// Like `get`, but retries on empty explore-deploy results to handle observer tuplespace lag. + #[tracing::instrument( + level = "info", + skip_all, + fields(address, id, version), + err(Debug), + ret(Debug, level = "trace") + )] + pub async fn get_with_retry( + &self, + address: WalletAddress, + id: String, + version: String, + max_retries: u32, + delay: Duration, + ) -> anyhow::Result> { + record_trace!(address, id, version); + + let code = Get { + env_uri: self.uri.clone(), + address, + id, + version, + } + .render()?; + + let agents_team: Option = self + .read_client + .get_data_or_none_with_retry(code, max_retries, delay) + .await? + .flatten(); Ok(agents_team.map(Into::into)) } } diff --git a/packages/embers/src/domain/agents_teams/list.rs b/packages/embers/src/domain/agents_teams/list.rs index 3c88b520..b712643b 100644 --- a/packages/embers/src/domain/agents_teams/list.rs +++ b/packages/embers/src/domain/agents_teams/list.rs @@ -1,5 +1,6 @@ use firefly_client::models::{Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::blockchain::agents_teams::models; use crate::domain::agents_teams::AgentsTeamsService; @@ -13,7 +14,7 @@ struct List { address: WalletAddress, } -impl AgentsTeamsService { +impl AgentsTeamsService { #[tracing::instrument( level = "info", skip_all, @@ -29,12 +30,13 @@ impl AgentsTeamsService { address, } .render()?; - self.read_client - .get_data(code) - .await - .map(|agents_teams: Vec| AgentsTeams { - agents_teams: agents_teams.into_iter().map(Into::into).collect(), - }) - .map_err(Into::into) + let agents_teams: Vec = self + .read_client + .get_data_or_none(code) + .await? + .unwrap_or_default(); + Ok(AgentsTeams { + agents_teams: agents_teams.into_iter().map(Into::into).collect(), + }) } } diff --git a/packages/embers/src/domain/agents_teams/list_versions.rs b/packages/embers/src/domain/agents_teams/list_versions.rs index a93d2f1b..41c35a38 100644 --- a/packages/embers/src/domain/agents_teams/list_versions.rs +++ b/packages/embers/src/domain/agents_teams/list_versions.rs @@ -1,5 +1,6 @@ use firefly_client::models::{Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::blockchain::agents_teams::models; use crate::domain::agents_teams::AgentsTeamsService; @@ -14,7 +15,7 @@ struct ListVersions { id: String, } -impl AgentsTeamsService { +impl AgentsTeamsService { #[tracing::instrument( level = "info", skip_all, @@ -37,7 +38,7 @@ impl AgentsTeamsService { .render()?; let agents_teams: Option> = - self.read_client.get_data(code).await?; + self.read_client.get_data_or_none(code).await?.flatten(); Ok(agents_teams.map(|mut agents_teams| { agents_teams.sort_by(|l, r| l.version.cmp(&r.version)); AgentsTeams { diff --git a/packages/embers/src/domain/agents_teams/models.rs b/packages/embers/src/domain/agents_teams/models.rs index d2c8cb1b..c526deed 100644 --- a/packages/embers/src/domain/agents_teams/models.rs +++ b/packages/embers/src/domain/agents_teams/models.rs @@ -1,4 +1,5 @@ use std::convert::Infallible; +use std::sync::Mutex; use chrono::{DateTime, Utc}; use firefly_client::models::{SignedCode, Uri, WalletAddress}; @@ -23,16 +24,30 @@ pub struct AgentsTeamHeader { pub logo: Option, } +/// Mutex serializing all access to the graphl_parser C FFI. +/// +/// The underlying Flex/Bison parser and C printer use global mutable state +/// (`buf_`, `cur_`, `buf_size`, `_n_`, and the Flex scanner buffer). +/// Concurrent calls from different threads corrupt this state, causing +/// intermittent `InvalidGraphL` / `InvalidCString` parse failures. +static GRAPHL_PARSER_LOCK: Mutex<()> = Mutex::new(()); + #[derive(Debug, Hash, Clone)] pub struct Graph(graphl_parser::ast::Graph); impl Graph { pub fn new(graphl: String) -> Result { + let _guard = GRAPHL_PARSER_LOCK + .lock() + .expect("GRAPHL_PARSER_LOCK poisoned"); graphl_parser::parse_to_ast(graphl).map(Self) } pub fn graphl(self) -> String { - graphl_parser::ast_to_graphl(self.0).unwrap() + let _guard = GRAPHL_PARSER_LOCK + .lock() + .expect("GRAPHL_PARSER_LOCK poisoned"); + graphl_parser::ast_to_graphl(self.0).expect("ast_to_graphl failed") } pub fn visit<'a, V, C>(&'a self, state: C, visitor: V) -> C diff --git a/packages/embers/src/domain/agents_teams/publish_to_firesky.rs b/packages/embers/src/domain/agents_teams/publish_to_firesky.rs index 82389faf..3f711909 100644 --- a/packages/embers/src/domain/agents_teams/publish_to_firesky.rs +++ b/packages/embers/src/domain/agents_teams/publish_to_firesky.rs @@ -12,6 +12,7 @@ use atrium_api::types::{Collection, TryIntoUnknown}; use atrium_xrpc_client::reqwest::ReqwestClient; use firefly_client::models::{DeployId, SignedCode, Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use futures::FutureExt; use futures::future::OptionFuture; use serde::{Deserialize, Serialize}; @@ -41,7 +42,7 @@ struct SaveFireskyToken { ciphertext: Vec, } -impl AgentsTeamsService { +impl AgentsTeamsService { #[tracing::instrument(level = "info", skip_all, err(Debug), ret(Debug, level = "trace"))] pub async fn prepare_publish_to_firesky_contract( &self, @@ -221,7 +222,6 @@ impl AgentsTeamsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; Ok(deploy_id) } } diff --git a/packages/embers/src/domain/agents_teams/run_agents_team.rs b/packages/embers/src/domain/agents_teams/run_agents_team.rs index 20694a39..f0b2e00b 100644 --- a/packages/embers/src/domain/agents_teams/run_agents_team.rs +++ b/packages/embers/src/domain/agents_teams/run_agents_team.rs @@ -3,7 +3,7 @@ use std::time::Duration; use anyhow::anyhow; use firefly_client::models::{DeployId, SignedCode, Uri}; use firefly_client::rendering::Render; -use futures::FutureExt; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::domain::agents_teams::AgentsTeamsService; use crate::domain::agents_teams::models::{RunReq, RunResp}; @@ -22,7 +22,7 @@ struct GetAgentsTeamResult { deploy_id: DeployId, } -impl AgentsTeamsService { +impl AgentsTeamsService { #[tracing::instrument( level = "info", skip_all, @@ -69,16 +69,80 @@ impl AgentsTeamsService { let deploy_id = write_client.deploy_signed_contract(contract).await?; - let deploy_waiter = self + let result = self .observer_node_events - .wait_for_deploy(&deploy_id, Duration::from_mins(1)); - let (_, finalized) = tokio::try_join!(write_client.propose(), deploy_waiter.map(Ok))?; + .wait_for_deploy(&deploy_id, Duration::from_mins(1)) + .await; - if !finalized { - return Err(anyhow!("block is not finalized")); + match result { + Some(true) => return Err(anyhow!("deploy {deploy_id} errored on chain")), + None => return Err(anyhow!("block is not finalized")), + Some(false) => {} } let code = GetAgentsTeamResult { deploy_id }.render()?; - self.read_client.get_data(code).await.map_err(Into::into) + self.read_client + .get_data_with_retry(code, 5, Duration::from_millis(500)) + .await + .map_err(Into::into) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use aes_gcm::{Aes256Gcm, Key}; + use dashmap::DashMap; + + use super::*; + use crate::domain::test_helpers::*; + + fn make_service( + event_source: MockNodeEventSource, + ) -> AgentsTeamsService { + AgentsTeamsService { + uri: test_uri(), + write_client: MockWriteNode::new().with_deploy_response(Ok(test_deploy_id())), + read_client: MockReadNode::new(), + observer_node_events: event_source, + aes_encryption_key: *Key::::from_slice(&[42u8; 32]), + firesky_accounts: Arc::new(DashMap::new()), + } + } + + #[tokio::test] + async fn test_deploy_signed_run_chain_error() { + let event_source = + MockNodeEventSource::new().with_wait_result("test-deploy-id", Some(true)); + let service = make_service(event_source); + + let result = service + .deploy_signed_run_agents_team(test_signed_code()) + .await; + + assert!(result.is_err(), "expected Err when deploy errored on chain"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("errored on chain"), + "expected 'errored on chain' in error message, got: {err_msg}" + ); + } + + #[tokio::test] + async fn test_deploy_signed_run_not_finalized() { + let event_source = MockNodeEventSource::new().with_wait_result("test-deploy-id", None); + let service = make_service(event_source); + + let result = service + .deploy_signed_run_agents_team(test_signed_code()) + .await; + + assert!(result.is_err(), "expected Err when block is not finalized"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("not finalized"), + "expected 'not finalized' in error message, got: {err_msg}" + ); } } diff --git a/packages/embers/src/domain/agents_teams/run_on_firesky.rs b/packages/embers/src/domain/agents_teams/run_on_firesky.rs index 537d4618..ea35c6c2 100644 --- a/packages/embers/src/domain/agents_teams/run_on_firesky.rs +++ b/packages/embers/src/domain/agents_teams/run_on_firesky.rs @@ -8,13 +8,14 @@ use atrium_api::record::KnownRecord; use atrium_api::types::string::{AtIdentifier, Datetime}; use atrium_api::types::{Collection, TryIntoUnknown, Union}; use atrium_xrpc_client::reqwest::ReqwestClient; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use futures::{StreamExt, stream}; use crate::domain::agents_teams::AgentsTeamsService; use crate::domain::agents_teams::models::{DeploySignedRunOnFireskyReq, RunReq, RunResp}; use crate::domain::common::upload_blob_from_url; -impl AgentsTeamsService { +impl AgentsTeamsService { #[tracing::instrument( level = "info", skip_all, diff --git a/packages/embers/src/domain/agents_teams/save.rs b/packages/embers/src/domain/agents_teams/save.rs index 6ef176db..2aa284dd 100644 --- a/packages/embers/src/domain/agents_teams/save.rs +++ b/packages/embers/src/domain/agents_teams/save.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use firefly_client::models::{DeployId, SignedCode, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use uuid::Uuid; use crate::domain::agents_teams::AgentsTeamsService; @@ -21,7 +22,7 @@ struct Save { graph: Option, } -impl AgentsTeamsService { +impl AgentsTeamsService { #[tracing::instrument( level = "info", skip_all, @@ -74,7 +75,7 @@ impl AgentsTeamsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; + Ok(deploy_id) } } diff --git a/packages/embers/src/domain/common.rs b/packages/embers/src/domain/common.rs index 70d417bb..b4ccb10a 100644 --- a/packages/embers/src/domain/common.rs +++ b/packages/embers/src/domain/common.rs @@ -134,3 +134,156 @@ pub struct RegistryDeploy { pub uri_pub_key: PublicKey, pub signature: Vec, } + +#[cfg(test)] +mod tests { + use aes_gcm::Aes256Gcm; + use aes_gcm::aead::KeyInit; + + use super::*; + + // ------------------------------------------------------- + // prepare_for_signing tests + // ------------------------------------------------------- + + #[test] + fn test_prepare_for_signing_default_phlo_limit() { + let contract = prepare_for_signing() + .code("new Nil".into()) + .valid_after_block_number(0) + .call(); + + let proto = DeployDataProto::decode(contract.0.as_slice()) + .expect("should decode as DeployDataProto"); + assert_eq!(proto.phlo_limit, 5_000_000); + assert_eq!(proto.term, "new Nil"); + assert_eq!(proto.shard_id, "root"); + assert_eq!(proto.phlo_price, 1); + } + + #[test] + fn test_prepare_for_signing_custom_phlo_limit() { + let contract = prepare_for_signing() + .code("code".into()) + .valid_after_block_number(10) + .phlo_limit(PositiveNonZero(1_000_000)) + .call(); + + let proto = DeployDataProto::decode(contract.0.as_slice()) + .expect("should decode as DeployDataProto"); + assert_eq!(proto.phlo_limit, 1_000_000); + assert_eq!(proto.valid_after_block_number, 10); + } + + #[test] + fn test_prepare_for_signing_custom_timestamp() { + let ts = chrono::DateTime::parse_from_rfc3339("2025-06-15T12:00:00Z") + .unwrap() + .to_utc(); + let contract = prepare_for_signing() + .code("code".into()) + .valid_after_block_number(0) + .timestamp(ts) + .call(); + + let proto = DeployDataProto::decode(contract.0.as_slice()) + .expect("should decode as DeployDataProto"); + assert_eq!(proto.timestamp, ts.timestamp_millis()); + } + + #[test] + fn test_prepare_for_signing_shard_id_always_root() { + let contract = prepare_for_signing() + .code("test".into()) + .valid_after_block_number(99) + .call(); + + let proto = DeployDataProto::decode(contract.0.as_slice()) + .expect("should decode as DeployDataProto"); + assert_eq!(proto.shard_id, "root"); + } + + // ------------------------------------------------------- + // encrypt/decrypt round-trip tests + // ------------------------------------------------------- + + fn test_aes_key() -> Key { + *Key::::from_slice(&[42u8; 32]) + } + + #[test] + fn test_encrypt_decrypt_round_trip_string() { + let key = test_aes_key(); + let original = "hello, world!".to_string(); + let encrypted = serialize_encrypted(&original, &key).expect("should encrypt"); + let decrypted: String = deserialize_decrypted(encrypted, &key).expect("should decrypt"); + assert_eq!(decrypted, original); + } + + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + struct TestCredentials { + email: String, + token: String, + } + + #[test] + fn test_encrypt_decrypt_round_trip_struct() { + let key = test_aes_key(); + let original = TestCredentials { + email: "test@example.com".into(), + token: "secret-token".into(), + }; + let encrypted = serialize_encrypted(&original, &key).expect("should encrypt"); + let decrypted: TestCredentials = + deserialize_decrypted(encrypted, &key).expect("should decrypt"); + assert_eq!(decrypted, original); + } + + #[test] + fn test_decrypt_wrong_key_fails() { + let key1 = *Key::::from_slice(&[1u8; 32]); + let key2 = *Key::::from_slice(&[2u8; 32]); + let encrypted = serialize_encrypted(&"secret", &key1).expect("should encrypt"); + let result = deserialize_decrypted::(encrypted, &key2); + assert!(result.is_err(), "decryption with wrong key should fail"); + } + + #[test] + fn test_decrypt_invalid_nonce_fails() { + let key = test_aes_key(); + let msg = EncryptedMsg { + nonce: vec![0, 1, 2], // Too short -- AES-256-GCM needs 12 bytes + ciphertext: vec![0; 32], + }; + let result = deserialize_decrypted::(msg, &key); + assert!(result.is_err(), "invalid nonce length should fail"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("invalid nonce length"), + "expected 'invalid nonce length', got: {err_msg}" + ); + } + + // ------------------------------------------------------- + // PositiveNonZero tests + // ------------------------------------------------------- + + #[test] + fn test_positive_non_zero_valid() { + let result = PositiveNonZero::::try_from(42); + assert!(result.is_ok()); + assert_eq!(result.unwrap().0, 42); + } + + #[test] + fn test_positive_non_zero_zero() { + let result = PositiveNonZero::::try_from(0); + assert!(matches!(result, Err(PositiveNonZeroParsingError::Zero))); + } + + #[test] + fn test_positive_non_zero_negative() { + let result = PositiveNonZero::::try_from(-1); + assert!(matches!(result, Err(PositiveNonZeroParsingError::Negative))); + } +} diff --git a/packages/embers/src/domain/oslfs.rs b/packages/embers/src/domain/oslfs.rs index 65947735..c7ba4fd0 100644 --- a/packages/embers/src/domain/oslfs.rs +++ b/packages/embers/src/domain/oslfs.rs @@ -1,8 +1,15 @@ use anyhow::Context; use firefly_client::helpers::insert_signed_signature; -use firefly_client::models::{DeployData, Uri}; +use firefly_client::models::{DeployData, DeployId, Uri}; use firefly_client::rendering::Render; -use firefly_client::{ReadNodeClient, WriteNodeClient}; +use firefly_client::{ + NodeEventSource, + NodeEvents, + ReadNode, + ReadNodeClient, + WriteNode, + WriteNodeClient, +}; use secp256k1::{PublicKey, Secp256k1, SecretKey}; mod create; @@ -14,10 +21,15 @@ pub mod models; mod save; #[derive(Clone)] -pub struct OslfsService { +pub struct OslfsService< + R: ReadNode = ReadNodeClient, + W: WriteNode = WriteNodeClient, + N: NodeEventSource = NodeEvents, +> { pub uri: Uri, - pub write_client: WriteNodeClient, - pub read_client: ReadNodeClient, + pub write_client: W, + pub read_client: R, + pub observer_node_events: N, } #[allow(unused)] @@ -31,14 +43,15 @@ struct InitEnv { } #[allow(unused)] -impl OslfsService { +impl OslfsService { #[tracing::instrument(level = "info", skip_all, err(Debug))] pub async fn bootstrap( - mut write_client: WriteNodeClient, - read_client: ReadNodeClient, + mut write_client: W, + read_client: R, + observer_node_events: N, deployer_key: &SecretKey, env_key: &SecretKey, - ) -> anyhow::Result { + ) -> anyhow::Result<(Self, DeployId)> { let secp = Secp256k1::new(); let env_public_key = PublicKey::from_secret_key(&secp, env_key); let deployer_public_key = PublicKey::from_secret_key(&secp, deployer_key); @@ -60,15 +73,19 @@ impl OslfsService { let deploy_data = DeployData::builder(code).timestamp(timestamp).build(); - write_client + let deploy_id = write_client .deploy(deployer_key, deploy_data) .await .context("failed to deploy oslf env")?; - Ok(Self { - uri: env_uri, - write_client, - read_client, - }) + Ok(( + Self { + uri: env_uri, + write_client, + read_client, + observer_node_events, + }, + deploy_id, + )) } } diff --git a/packages/embers/src/domain/oslfs/create.rs b/packages/embers/src/domain/oslfs/create.rs index 001405c5..7dc69c94 100644 --- a/packages/embers/src/domain/oslfs/create.rs +++ b/packages/embers/src/domain/oslfs/create.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use firefly_client::models::{DeployId, SignedCode, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use uuid::Uuid; use crate::domain::common::{prepare_for_signing, record_trace}; @@ -19,7 +20,7 @@ struct Create { query: Option, } -impl OslfsService { +impl OslfsService { #[tracing::instrument( level = "info", skip_all, @@ -68,7 +69,7 @@ impl OslfsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; + Ok(deploy_id) } } diff --git a/packages/embers/src/domain/oslfs/delete.rs b/packages/embers/src/domain/oslfs/delete.rs index e6f5cbba..8962496d 100644 --- a/packages/embers/src/domain/oslfs/delete.rs +++ b/packages/embers/src/domain/oslfs/delete.rs @@ -1,5 +1,6 @@ use firefly_client::models::{DeployId, SignedCode, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::domain::common::{prepare_for_signing, record_trace}; use crate::domain::oslfs::OslfsService; @@ -12,7 +13,7 @@ struct Delete { id: String, } -impl OslfsService { +impl OslfsService { #[tracing::instrument(level = "info", skip(self), err(Debug), ret(Debug, level = "trace"))] pub async fn prepare_delete_contract(&self, id: String) -> anyhow::Result { let contract = Delete { @@ -43,7 +44,6 @@ impl OslfsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; Ok(deploy_id) } } diff --git a/packages/embers/src/domain/oslfs/get.rs b/packages/embers/src/domain/oslfs/get.rs index 8b45b3d4..c8cd9630 100644 --- a/packages/embers/src/domain/oslfs/get.rs +++ b/packages/embers/src/domain/oslfs/get.rs @@ -1,5 +1,6 @@ use firefly_client::models::{Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::blockchain::oslfs::models; use crate::domain::common::record_trace; @@ -15,7 +16,7 @@ struct Get { version: String, } -impl OslfsService { +impl OslfsService { #[tracing::instrument( level = "info", skip_all, @@ -39,7 +40,7 @@ impl OslfsService { } .render()?; - let oslf: Option = self.read_client.get_data(code).await?; + let oslf: Option = self.read_client.get_data_or_none(code).await?.flatten(); Ok(oslf.map(Into::into)) } } diff --git a/packages/embers/src/domain/oslfs/list.rs b/packages/embers/src/domain/oslfs/list.rs index 795957ed..bae0fffc 100644 --- a/packages/embers/src/domain/oslfs/list.rs +++ b/packages/embers/src/domain/oslfs/list.rs @@ -1,5 +1,6 @@ use firefly_client::models::{Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::blockchain::oslfs::models; use crate::domain::common::record_trace; @@ -13,7 +14,7 @@ struct List { address: WalletAddress, } -impl OslfsService { +impl OslfsService { #[tracing::instrument( level = "info", skip_all, @@ -29,12 +30,13 @@ impl OslfsService { address, } .render()?; - self.read_client - .get_data(code) - .await - .map(|oslfs: Vec| Oslfs { - oslfs: oslfs.into_iter().map(Into::into).collect(), - }) - .map_err(Into::into) + let oslfs: Vec = self + .read_client + .get_data_or_none(code) + .await? + .unwrap_or_default(); + Ok(Oslfs { + oslfs: oslfs.into_iter().map(Into::into).collect(), + }) } } diff --git a/packages/embers/src/domain/oslfs/list_versions.rs b/packages/embers/src/domain/oslfs/list_versions.rs index b2b9f84a..0113ac2e 100644 --- a/packages/embers/src/domain/oslfs/list_versions.rs +++ b/packages/embers/src/domain/oslfs/list_versions.rs @@ -1,5 +1,6 @@ use firefly_client::models::{Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::blockchain::oslfs::models; use crate::domain::common::record_trace; @@ -14,7 +15,7 @@ struct ListVersions { id: String, } -impl OslfsService { +impl OslfsService { #[tracing::instrument( level = "info", skip_all, @@ -36,7 +37,8 @@ impl OslfsService { } .render()?; - let oslfs: Option> = self.read_client.get_data(code).await?; + let oslfs: Option> = + self.read_client.get_data_or_none(code).await?.flatten(); Ok(oslfs.map(|mut oslfs| { oslfs.sort_by(|l, r| l.version.cmp(&r.version)); Oslfs { diff --git a/packages/embers/src/domain/oslfs/save.rs b/packages/embers/src/domain/oslfs/save.rs index e1a0cb02..1715b265 100644 --- a/packages/embers/src/domain/oslfs/save.rs +++ b/packages/embers/src/domain/oslfs/save.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use firefly_client::models::{DeployId, SignedCode, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use uuid::Uuid; use crate::domain::common::{prepare_for_signing, record_trace}; @@ -19,7 +20,7 @@ struct Save { query: Option, } -impl OslfsService { +impl OslfsService { #[tracing::instrument( level = "info", skip_all, @@ -70,7 +71,7 @@ impl OslfsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; + Ok(deploy_id) } } diff --git a/packages/embers/src/domain/test_helpers.rs b/packages/embers/src/domain/test_helpers.rs new file mode 100644 index 00000000..92e1b2bd --- /dev/null +++ b/packages/embers/src/domain/test_helpers.rs @@ -0,0 +1,338 @@ +use std::collections::VecDeque; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use firefly_client::errors::ReadNodeError; +use firefly_client::models::{DeployData, DeployId, SignedCode, Uri, WalletAddress}; +use firefly_client::node_events::DeployEvent; +use firefly_client::traits::{NodeEventSource, ReadNode, WriteNode}; +use futures::stream; +use secp256k1::{PublicKey, Secp256k1, SecretKey}; + +// ------------------------------------------------------- +// MockReadNode +// ------------------------------------------------------- + +/// A mock read node client that returns pre-configured responses. +/// +/// Responses are matched by checking whether the Rholang code +/// passed to each method contains a specified substring. +#[derive(Clone)] +pub struct MockReadNode { + responses: Arc>, + calls: Arc>>, +} + +struct MockReadResponse { + contains: String, + value: serde_json::Value, + error: Option, + /// Number of leading matching reads that should return `ReturnValueMissing` + /// before `value` is served — simulates the read-node visibility lag a + /// retrying caller must tolerate. + empty_before: usize, + /// How many times this response has matched so far (interior mutability so + /// `find_response` can sequence empty-then-data through `&self`). + hits: AtomicUsize, +} + +impl MockReadNode { + pub fn new() -> Self { + Self { + responses: Arc::new(Vec::new()), + calls: Arc::new(Mutex::new(Vec::new())), + } + } + + /// Add a response that triggers when the Rholang code contains `substr`. + pub fn on_code_containing(mut self, substr: &str, value: serde_json::Value) -> Self { + Arc::get_mut(&mut self.responses) + .expect("no other clones should exist during setup") + .push(MockReadResponse { + contains: substr.to_owned(), + value, + error: None, + empty_before: 0, + hits: AtomicUsize::new(0), + }); + self + } + + /// Add a response that returns `ReturnValueMissing` for the first + /// `empty_before` matching reads and then serves `value`. Models the + /// read-after-write visibility lag that a retrying caller must tolerate. + pub fn on_code_containing_empty_then( + mut self, + substr: &str, + empty_before: usize, + value: serde_json::Value, + ) -> Self { + Arc::get_mut(&mut self.responses) + .expect("no other clones should exist during setup") + .push(MockReadResponse { + contains: substr.to_owned(), + value, + error: None, + empty_before, + hits: AtomicUsize::new(0), + }); + self + } + + /// Add an error response that triggers when the Rholang code contains `substr`. + pub fn on_code_containing_error(mut self, substr: &str, error: ReadNodeError) -> Self { + Arc::get_mut(&mut self.responses) + .expect("no other clones should exist during setup") + .push(MockReadResponse { + contains: substr.to_owned(), + value: serde_json::Value::Null, + error: Some(error), + empty_before: 0, + hits: AtomicUsize::new(0), + }); + self + } + + /// Return the list of Rholang codes that were passed to get_data/get_data_or_none. + pub fn calls(&self) -> Vec { + self.calls.lock().unwrap().clone() + } + + fn find_response(&self, code: &str) -> Result { + self.calls.lock().unwrap().push(code.to_owned()); + for resp in self.responses.iter() { + if code.contains(&resp.contains) { + // Sequenced empty-then-data: the first `empty_before` matching + // reads return `ReturnValueMissing`, so a retrying caller only + // succeeds once the simulated visibility lag clears. + let hit = resp.hits.fetch_add(1, Ordering::SeqCst); + if hit < resp.empty_before { + return Err(ReadNodeError::ReturnValueMissing); + } + if let Some(ref err) = resp.error { + return Err(match err { + ReadNodeError::ReturnValueMissing => ReadNodeError::ReturnValueMissing, + ReadNodeError::Api(status, body) => { + ReadNodeError::Api(*status, body.clone()) + } + ReadNodeError::Timeout(d) => ReadNodeError::Timeout(*d), + _ => ReadNodeError::ReturnValueMissing, + }); + } + return Ok(resp.value.clone()); + } + } + // Default: return missing + Err(ReadNodeError::ReturnValueMissing) + } +} + +impl ReadNode for MockReadNode { + async fn get_data( + &self, + rholang_code: String, + ) -> Result { + let value = self.find_response(&rholang_code)?; + serde_json::from_value(value).map_err(|e| ReadNodeError::Deserialization(e.into())) + } + + async fn get_data_or_none( + &self, + rholang_code: String, + ) -> Result, ReadNodeError> { + match self.get_data(rholang_code).await { + Ok(data) => Ok(Some(data)), + Err(ReadNodeError::ReturnValueMissing) => Ok(None), + Err(err) => Err(err), + } + } + + async fn get_data_with_retry( + &self, + rholang_code: String, + max_retries: u32, + _delay: Duration, + ) -> Result { + // Faithfully model the real client: retry only on an empty result + // (`ReturnValueMissing`), up to `max_retries` times. `delay` is ignored + // so tests stay fast. + let mut attempts = 0u32; + loop { + match self.get_data(rholang_code.clone()).await { + Err(ReadNodeError::ReturnValueMissing) if attempts < max_retries => { + attempts += 1; + } + result => return result, + } + } + } + + async fn get_data_or_none_with_retry( + &self, + rholang_code: String, + max_retries: u32, + delay: Duration, + ) -> Result, ReadNodeError> { + match self.get_data_with_retry(rholang_code, max_retries, delay).await { + Ok(data) => Ok(Some(data)), + Err(ReadNodeError::ReturnValueMissing) => Ok(None), + Err(err) => Err(err), + } + } +} + +// ------------------------------------------------------- +// MockWriteNode +// ------------------------------------------------------- + +/// A mock write node client that returns pre-configured deploy IDs. +#[derive(Clone)] +pub struct MockWriteNode { + head_block_index: u64, + deploy_responses: Arc>>>, + deployed_contracts: Arc>>>, +} + +impl MockWriteNode { + pub fn new() -> Self { + Self { + head_block_index: 0, + deploy_responses: Arc::new(Mutex::new(VecDeque::new())), + deployed_contracts: Arc::new(Mutex::new(Vec::new())), + } + } + + pub fn with_head_block_index(mut self, index: u64) -> Self { + self.head_block_index = index; + self + } + + pub fn with_deploy_response(self, response: anyhow::Result) -> Self { + self.deploy_responses.lock().unwrap().push_back(response); + self + } + + pub fn deployed_contracts(&self) -> Vec> { + self.deployed_contracts.lock().unwrap().clone() + } + + fn next_deploy_response(&self) -> anyhow::Result { + self.deploy_responses + .lock() + .unwrap() + .pop_front() + .unwrap_or_else(|| Ok(test_deploy_id())) + } +} + +impl WriteNode for MockWriteNode { + async fn deploy( + &mut self, + _key: &SecretKey, + _deploy_data: DeployData, + ) -> anyhow::Result { + self.next_deploy_response() + } + + async fn deploy_signed_contract(&mut self, contract: SignedCode) -> anyhow::Result { + self.deployed_contracts + .lock() + .unwrap() + .push(contract.contract.clone()); + self.next_deploy_response() + } + + async fn full_deploy( + &mut self, + key: &SecretKey, + deploy_data: DeployData, + ) -> anyhow::Result { + self.deploy(key, deploy_data).await + } + + async fn get_head_block_index(&mut self) -> anyhow::Result { + Ok(self.head_block_index) + } +} + +// ------------------------------------------------------- +// MockNodeEventSource +// ------------------------------------------------------- + +/// A mock node event source that returns pre-configured results. +#[derive(Clone)] +pub struct MockNodeEventSource { + wait_results: Arc>>>, +} + +impl MockNodeEventSource { + pub fn new() -> Self { + Self { + wait_results: Arc::new(Mutex::new(std::collections::HashMap::new())), + } + } + + /// Configure wait_for_deploy to return the given result for the given deploy ID. + pub fn with_wait_result(self, deploy_id: &str, result: Option) -> Self { + self.wait_results + .lock() + .unwrap() + .insert(deploy_id.to_owned(), result); + self + } +} + +impl NodeEventSource for MockNodeEventSource { + fn wait_for_deploy( + &self, + deploy_id: &DeployId, + _max_wait: Duration, + ) -> impl std::future::Future> + Send { + let result = self + .wait_results + .lock() + .unwrap() + .get(&deploy_id.to_string()) + .copied() + .unwrap_or(Some(false)); + async move { result } + } + + fn subscribe_for_deploys( + &self, + _wallet_address: WalletAddress, + ) -> impl futures::Stream + Send { + stream::empty() + } +} + +// ------------------------------------------------------- +// Helper functions +// ------------------------------------------------------- + +pub fn test_secret_key() -> SecretKey { + SecretKey::from_byte_array([1u8; 32]).expect("valid secret key") +} + +pub fn test_public_key() -> PublicKey { + let secp = Secp256k1::new(); + PublicKey::from_secret_key(&secp, &test_secret_key()) +} + +pub fn test_uri() -> Uri { + test_public_key().into() +} + +pub fn test_deploy_id() -> DeployId { + DeployId::from("test-deploy-id".to_owned()) +} + +pub fn test_signed_code() -> SignedCode { + SignedCode { + contract: vec![0; 16], + sig: vec![0; 64], + sig_algorithm: "secp256k1".into(), + deployer: vec![0; 65], + } +} diff --git a/packages/embers/src/domain/testnet.rs b/packages/embers/src/domain/testnet.rs index 6a49a7d5..efc66de7 100644 --- a/packages/embers/src/domain/testnet.rs +++ b/packages/embers/src/domain/testnet.rs @@ -1,8 +1,15 @@ use anyhow::Context; use firefly_client::helpers::insert_signed_signature; -use firefly_client::models::{DeployData, Uri}; +use firefly_client::models::{DeployData, DeployId, Uri}; use firefly_client::rendering::Render; -use firefly_client::{NodeEvents, ReadNodeClient, WriteNodeClient}; +use firefly_client::{ + NodeEventSource, + NodeEvents, + ReadNode, + ReadNodeClient, + WriteNode, + WriteNodeClient, +}; use secp256k1::{PublicKey, Secp256k1, SecretKey}; mod create_test_wallet; @@ -10,12 +17,16 @@ mod deploy_test; pub mod models; #[derive(Clone)] -pub struct TestnetService { +pub struct TestnetService< + R: ReadNode = ReadNodeClient, + W: WriteNode = WriteNodeClient, + N: NodeEventSource = NodeEvents, +> { pub uri: Uri, pub service_key: SecretKey, - pub write_client: WriteNodeClient, - pub read_client: ReadNodeClient, - pub observer_node_events: NodeEvents, + pub write_client: W, + pub read_client: R, + pub observer_node_events: N, } #[allow(unused)] @@ -29,15 +40,15 @@ struct InitTestnetEnv { } #[allow(unused)] -impl TestnetService { +impl TestnetService { #[tracing::instrument(level = "info", skip_all, err(Debug))] pub async fn bootstrap( - mut write_client: WriteNodeClient, - read_client: ReadNodeClient, - observer_node_events: NodeEvents, + mut write_client: W, + read_client: R, + observer_node_events: N, deployer_key: SecretKey, env_key: &SecretKey, - ) -> anyhow::Result { + ) -> anyhow::Result<(Self, DeployId)> { let secp = Secp256k1::new(); let env_public_key = PublicKey::from_secret_key(&secp, env_key); let deployer_public_key = PublicKey::from_secret_key(&secp, &deployer_key); @@ -59,17 +70,20 @@ impl TestnetService { let deploy_data = DeployData::builder(code).timestamp(timestamp).build(); - write_client + let deploy_id = write_client .deploy(&deployer_key, deploy_data) .await .context("failed to deploy testnet env")?; - Ok(Self { - uri: env_uri, - service_key: deployer_key, - write_client, - read_client, - observer_node_events, - }) + Ok(( + Self { + uri: env_uri, + service_key: deployer_key, + write_client, + read_client, + observer_node_events, + }, + deploy_id, + )) } } diff --git a/packages/embers/src/domain/testnet/create_test_wallet.rs b/packages/embers/src/domain/testnet/create_test_wallet.rs index 2cd1cf9b..a806494d 100644 --- a/packages/embers/src/domain/testnet/create_test_wallet.rs +++ b/packages/embers/src/domain/testnet/create_test_wallet.rs @@ -1,5 +1,6 @@ use firefly_client::models::WalletAddress; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use secp256k1::{PublicKey, Secp256k1, rand}; use crate::domain::testnet::TestnetService; @@ -15,7 +16,7 @@ struct FundTestWallet { amount: i64, } -impl TestnetService { +impl TestnetService { #[tracing::instrument(level = "info", skip_all, err(Debug), ret(Debug, level = "trace"))] pub async fn create_wallet(&self) -> anyhow::Result { let sk = Secp256k1::new(); @@ -33,7 +34,6 @@ impl TestnetService { let mut write_client = self.write_client.clone(); write_client.deploy(&self.service_key, deploy_data).await?; - write_client.propose().await?; Ok(CreateTestwalletResp { key: test_account_secret_key, diff --git a/packages/embers/src/domain/testnet/deploy_test.rs b/packages/embers/src/domain/testnet/deploy_test.rs index b75d68d5..4d0a4224 100644 --- a/packages/embers/src/domain/testnet/deploy_test.rs +++ b/packages/embers/src/domain/testnet/deploy_test.rs @@ -3,6 +3,7 @@ use std::time::Duration; use anyhow::anyhow; use firefly_client::models::{DeployId, Uri}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::blockchain::testnet::models; use crate::domain::common::{prepare_for_signing, record_trace}; @@ -21,7 +22,7 @@ struct GetLogs { deploy_id: DeployId, } -impl TestnetService { +impl TestnetService { #[tracing::instrument( level = "info", skip_all, @@ -72,8 +73,6 @@ impl TestnetService { error: err.to_string(), }); } - - write_client.propose().await?; } let result = write_client.deploy_signed_contract(request.test).await; @@ -86,14 +85,15 @@ impl TestnetService { } }; - let deploy_waiter = self + let result = self .observer_node_events - .wait_for_deploy(&deploy_id, Duration::from_mins(1)); - let (_, finalized) = - tokio::try_join!(write_client.propose(), async { Ok(deploy_waiter.await) })?; + .wait_for_deploy(&deploy_id, Duration::from_mins(1)) + .await; - if !finalized { - return Err(anyhow!("block is not finalized")); + match result { + Some(true) => return Err(anyhow!("deploy {deploy_id} errored on chain")), + None => return Err(anyhow!("block is not finalized")), + Some(false) => {} } let code = GetLogs { @@ -102,7 +102,23 @@ impl TestnetService { } .render()?; - let logs: Option> = self.read_client.get_data(code).await?; + // A test contract that produces no log entries (e.g. `Nil`) yields no + // value from the log-lookup explore-deploy. Treat that as "no logs" + // rather than propagating `ReturnValueMissing` as a 500. + // + // Retry on empty: after the observer's WS reports the deploy finalized, + // there is a brief window before that block's RSpace state is queryable + // via explore-deploy on the observer node, so a single-shot read can + // intermittently miss logs the deploy did write (read-after-write lag, + // worse under load). Mirror the analogous deploy-then-read retry in + // `run_agents_team.rs`. A genuinely empty (no-log) deploy simply exhausts + // the retries and resolves to `Ok(None)` -> `logs: []`; the worst-case + // retry window stays well under the client's 45s cap, so this never + // surfaces as a Timeout/500. + let logs: Option> = self + .read_client + .get_data_or_none_with_retry(code, 5, Duration::from_millis(500)) + .await?; Ok(DeploySignedTestResp::Ok { logs: logs @@ -113,3 +129,355 @@ impl TestnetService { }) } } + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + use crate::domain::test_helpers::*; + use crate::domain::testnet::models::{DeployTestReq, LogLevel}; + + fn make_service_with( + write_client: MockWriteNode, + read_client: MockReadNode, + observer: MockNodeEventSource, + ) -> TestnetService { + TestnetService { + uri: test_uri(), + service_key: test_secret_key(), + write_client, + read_client, + observer_node_events: observer, + } + } + + fn make_service() -> TestnetService { + make_service_with( + MockWriteNode::new().with_head_block_index(10), + MockReadNode::new(), + MockNodeEventSource::new(), + ) + } + + // ------------------------------------------------------- + // prepare_test_contract tests + // ------------------------------------------------------- + + #[tokio::test] + async fn test_prepare_with_env_and_test() { + let service = make_service(); + let request = DeployTestReq { + env: Some("env-code".into()), + test: "test-code".into(), + }; + + let resp = service + .prepare_test_contract(request) + .await + .expect("prepare_test_contract should succeed"); + + assert!( + resp.env_contract.is_some(), + "env_contract should be present when env is Some" + ); + // test_contract is always present + assert!( + !resp.test_contract.0.is_empty(), + "test_contract should be non-empty" + ); + } + + #[tokio::test] + async fn test_prepare_test_only() { + let service = make_service(); + let request = DeployTestReq { + env: None, + test: "test-code".into(), + }; + + let resp = service + .prepare_test_contract(request) + .await + .expect("prepare_test_contract should succeed"); + + assert!( + resp.env_contract.is_none(), + "env_contract should be None when env is None" + ); + assert!( + !resp.test_contract.0.is_empty(), + "test_contract should be non-empty" + ); + } + + // ------------------------------------------------------- + // deploy_test_contract tests + // ------------------------------------------------------- + + #[tokio::test] + async fn test_deploy_env_failure_short_circuits() { + let write_client = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Err(anyhow!("env deploy exploded"))); + + let service = make_service_with( + write_client, + MockReadNode::new(), + MockNodeEventSource::new(), + ); + + let request = DeploySignedTestReq { + env: Some(test_signed_code()), + test: test_signed_code(), + }; + + let resp = service + .deploy_test_contract(request) + .await + .expect("deploy_test_contract should return Ok(EnvDeployFailed), not Err"); + + match resp { + DeploySignedTestResp::EnvDeployFailed { error } => { + assert!( + error.contains("env deploy exploded"), + "expected error to contain 'env deploy exploded', got: {error}" + ); + } + other => panic!("expected EnvDeployFailed, got: {other:?}"), + } + } + + #[tokio::test] + async fn test_deploy_test_failure() { + // First deploy (env) succeeds, second deploy (test) fails + let write_client = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Ok(test_deploy_id())) + .with_deploy_response(Err(anyhow!("test deploy failed"))); + + let service = make_service_with( + write_client, + MockReadNode::new(), + MockNodeEventSource::new(), + ); + + let request = DeploySignedTestReq { + env: Some(test_signed_code()), + test: test_signed_code(), + }; + + let resp = service + .deploy_test_contract(request) + .await + .expect("deploy_test_contract should return Ok(TestDeployFailed), not Err"); + + match resp { + DeploySignedTestResp::TestDeployFailed { error } => { + assert!( + error.contains("test deploy failed"), + "expected error to contain 'test deploy failed', got: {error}" + ); + } + other => panic!("expected TestDeployFailed, got: {other:?}"), + } + } + + #[tokio::test] + async fn test_deploy_chain_error() { + let deploy_id = test_deploy_id(); + let observer = + MockNodeEventSource::new().with_wait_result(&deploy_id.to_string(), Some(true)); + + let write_client = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Ok(deploy_id)); + + let service = make_service_with(write_client, MockReadNode::new(), observer); + + let request = DeploySignedTestReq { + env: None, + test: test_signed_code(), + }; + + let result = service.deploy_test_contract(request).await; + assert!(result.is_err(), "expected Err when deploy errored on chain"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("errored on chain"), + "expected 'errored on chain' in error, got: {err_msg}" + ); + } + + #[tokio::test] + async fn test_deploy_not_finalized() { + let deploy_id = test_deploy_id(); + let observer = MockNodeEventSource::new().with_wait_result(&deploy_id.to_string(), None); + + let write_client = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Ok(deploy_id)); + + let service = make_service_with(write_client, MockReadNode::new(), observer); + + let request = DeploySignedTestReq { + env: None, + test: test_signed_code(), + }; + + let result = service.deploy_test_contract(request).await; + assert!(result.is_err(), "expected Err when block is not finalized"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("block is not finalized"), + "expected 'block is not finalized' in error, got: {err_msg}" + ); + } + + #[tokio::test] + async fn test_deploy_success_fetches_logs() { + let deploy_id = test_deploy_id(); + + // wait_for_deploy returns Some(false) -> success (no error) + let observer = + MockNodeEventSource::new().with_wait_result(&deploy_id.to_string(), Some(false)); + + let write_client = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Ok(deploy_id)); + + // The rendered GetLogs template contains "get" -- match on that to return mock logs + let read_client = MockReadNode::new().on_code_containing( + "get", + json!([ + { "level": "info", "message": "test passed" }, + { "level": "error", "message": "assertion failed" } + ]), + ); + + let service = make_service_with(write_client, read_client, observer); + + let request = DeploySignedTestReq { + env: None, + test: test_signed_code(), + }; + + let resp = service + .deploy_test_contract(request) + .await + .expect("deploy_test_contract should succeed"); + + match resp { + DeploySignedTestResp::Ok { logs } => { + assert_eq!(logs.len(), 2, "expected 2 log entries, got {}", logs.len()); + assert!( + matches!(logs[0].level, LogLevel::Info), + "expected first log level Info, got {:?}", + logs[0].level + ); + assert_eq!(logs[0].message, "test passed"); + assert!( + matches!(logs[1].level, LogLevel::Error), + "expected second log level Error, got {:?}", + logs[1].level + ); + assert_eq!(logs[1].message, "assertion failed"); + } + other => panic!("expected Ok with logs, got: {other:?}"), + } + } + + #[tokio::test] + async fn test_deploy_success_retries_until_logs_visible() { + let deploy_id = test_deploy_id(); + + // wait_for_deploy returns Some(false) -> finalized successfully + let observer = + MockNodeEventSource::new().with_wait_result(&deploy_id.to_string(), Some(false)); + + let write_client = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Ok(deploy_id)); + + // The GetLogs read is empty for the first two attempts (simulating the + // read node's post-finalization visibility lag) then returns the log. + // A single-shot read would observe the first empty and return `[]`; the + // retry must poll until the logs materialize. This is the regression + // guard for the read-after-write flake. + let read_client = MockReadNode::new().on_code_containing_empty_then( + "get", + 2, + json!([{ "level": "info", "message": "test passed" }]), + ); + + let service = make_service_with(write_client, read_client, observer); + + let request = DeploySignedTestReq { + env: None, + test: test_signed_code(), + }; + + let resp = service + .deploy_test_contract(request) + .await + .expect("deploy_test_contract should succeed"); + + match resp { + DeploySignedTestResp::Ok { logs } => { + assert_eq!( + logs.len(), + 1, + "retry should surface the log once visible, got {}", + logs.len() + ); + assert!( + matches!(logs[0].level, LogLevel::Info), + "expected log level Info, got {:?}", + logs[0].level + ); + assert_eq!(logs[0].message, "test passed"); + } + other => panic!("expected Ok with logs after retry, got: {other:?}"), + } + } + + #[tokio::test] + async fn test_deploy_nil_no_logs_returns_empty() { + let deploy_id = test_deploy_id(); + + let observer = + MockNodeEventSource::new().with_wait_result(&deploy_id.to_string(), Some(false)); + + let write_client = MockWriteNode::new() + .with_head_block_index(10) + .with_deploy_response(Ok(deploy_id)); + + // No configured response -> the log-lookup read is always empty, as for + // a `Nil` test that logs nothing. The retry must exhaust and resolve to + // an empty log list (never a Timeout/500), preserving the no-logs + // semantic through the retry path. + let read_client = MockReadNode::new(); + + let service = make_service_with(write_client, read_client, observer); + + let request = DeploySignedTestReq { + env: None, + test: test_signed_code(), + }; + + let resp = service + .deploy_test_contract(request) + .await + .expect("deploy_test_contract should succeed with no logs"); + + match resp { + DeploySignedTestResp::Ok { logs } => { + assert!( + logs.is_empty(), + "no-log deploy should yield an empty list, got: {logs:?}" + ); + } + other => panic!("expected Ok with empty logs, got: {other:?}"), + } + } +} diff --git a/packages/embers/src/domain/wallets.rs b/packages/embers/src/domain/wallets.rs index 4850efe5..7f5450f4 100644 --- a/packages/embers/src/domain/wallets.rs +++ b/packages/embers/src/domain/wallets.rs @@ -1,8 +1,15 @@ use anyhow::Context; use firefly_client::helpers::insert_signed_signature; -use firefly_client::models::{DeployData, Uri}; +use firefly_client::models::{DeployData, DeployId, Uri}; use firefly_client::rendering::Render; -use firefly_client::{NodeEvents, ReadNodeClient, WriteNodeClient}; +use firefly_client::{ + NodeEventSource, + NodeEvents, + ReadNode, + ReadNodeClient, + WriteNode, + WriteNodeClient, +}; use secp256k1::{PublicKey, Secp256k1, SecretKey}; mod boost; @@ -12,12 +19,16 @@ mod subscribe_to_deploys; mod transfer; #[derive(Clone)] -pub struct WalletsService { +pub struct WalletsService< + R: ReadNode = ReadNodeClient, + W: WriteNode = WriteNodeClient, + N: NodeEventSource = NodeEvents, +> { pub uri: Uri, - pub write_client: WriteNodeClient, - pub read_client: ReadNodeClient, - pub validator_node_events: NodeEvents, - pub observer_node_events: NodeEvents, + pub write_client: W, + pub read_client: R, + pub validator_node_events: N, + pub observer_node_events: N, } #[allow(unused)] @@ -31,16 +42,16 @@ struct InitWalletsEnv { } #[allow(unused)] -impl WalletsService { +impl WalletsService { #[tracing::instrument(level = "info", skip_all, err(Debug))] pub async fn bootstrap( - mut write_client: WriteNodeClient, - read_client: ReadNodeClient, - validator_node_events: NodeEvents, - observer_node_events: NodeEvents, + mut write_client: W, + read_client: R, + validator_node_events: N, + observer_node_events: N, deployer_key: &SecretKey, env_key: &SecretKey, - ) -> anyhow::Result { + ) -> anyhow::Result<(Self, DeployId)> { let secp = Secp256k1::new(); let env_public_key = PublicKey::from_secret_key(&secp, env_key); let deployer_public_key = PublicKey::from_secret_key(&secp, deployer_key); @@ -62,17 +73,49 @@ impl WalletsService { let deploy_data = DeployData::builder(code).timestamp(timestamp).build(); - write_client + let deploy_id = write_client .deploy(deployer_key, deploy_data) .await .context("failed to deploy wallets env")?; - Ok(Self { - uri: env_uri, - write_client, - read_client, - validator_node_events, - observer_node_events, - }) + Ok(( + Self { + uri: env_uri, + write_client, + read_client, + validator_node_events, + observer_node_events, + }, + deploy_id, + )) + } +} + +#[cfg(test)] +mod template_tests { + use firefly_client::rendering::Render; + + use super::*; + + #[test] + fn test_init_template_renders_valid_rholang() { + let secp = secp256k1::Secp256k1::new(); + let sk = secp256k1::SecretKey::from_byte_array([4u8; 32]).expect("valid key"); + let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk); + let env_uri: Uri = pk.into(); + + let code = InitWalletsEnv { + env_uri: env_uri.clone(), + version: 0, + public_key: pk.serialize_uncompressed().into(), + sig: vec![1, 2, 3, 4], + } + .render() + .expect("template should render"); + + assert!(!code.is_empty()); + assert!(code.contains("rho:registry:insertSigned:secp256k1")); + let uri_str: &str = env_uri.as_ref(); + assert!(code.contains(uri_str)); } } diff --git a/packages/embers/src/domain/wallets/boost.rs b/packages/embers/src/domain/wallets/boost.rs index b98375c4..d5ca2d16 100644 --- a/packages/embers/src/domain/wallets/boost.rs +++ b/packages/embers/src/domain/wallets/boost.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use firefly_client::models::{DeployId, SignedCode, Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::domain::common::{prepare_for_signing, record_trace}; use crate::domain::wallets::WalletsService; @@ -19,7 +20,7 @@ struct BoostContract { post_id: Option, } -impl WalletsService { +impl WalletsService { #[tracing::instrument( level = "info", skip_all, @@ -64,7 +65,6 @@ impl WalletsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; Ok(deploy_id) } } diff --git a/packages/embers/src/domain/wallets/get_wallet_state_and_history.rs b/packages/embers/src/domain/wallets/get_wallet_state_and_history.rs index 3c3a25d5..3bfc7962 100644 --- a/packages/embers/src/domain/wallets/get_wallet_state_and_history.rs +++ b/packages/embers/src/domain/wallets/get_wallet_state_and_history.rs @@ -1,5 +1,6 @@ use firefly_client::models::{Either, Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::blockchain::wallets::models; use crate::domain::common::record_trace; @@ -13,7 +14,7 @@ struct GetBalanceAndHistory { wallet_address: WalletAddress, } -impl WalletsService { +impl WalletsService { #[tracing::instrument( level = "info", skip_all, diff --git a/packages/embers/src/domain/wallets/subscribe_to_deploys.rs b/packages/embers/src/domain/wallets/subscribe_to_deploys.rs index d27fcd60..3d5634c8 100644 --- a/packages/embers/src/domain/wallets/subscribe_to_deploys.rs +++ b/packages/embers/src/domain/wallets/subscribe_to_deploys.rs @@ -1,17 +1,16 @@ use std::io; use firefly_client::models::WalletAddress; -use firefly_client::node_events; +use firefly_client::{NodeEventSource, ReadNode, WriteNode, node_events}; use futures::{Sink, SinkExt, StreamExt, stream}; -use tracing::Instrument; use crate::domain::wallets::WalletsService; use crate::domain::wallets::models::{DeployDescription, DeployEvent, NodeType}; -impl WalletsService { +impl WalletsService { #[tracing::instrument(level = "info", skip_all)] - pub fn subscribe_to_deploys( - &self, + pub async fn subscribe_to_deploys( + self, wallet_address: WalletAddress, sink: impl Sink + Send + 'static, ) { @@ -45,19 +44,14 @@ impl WalletsService { }) .map(Ok); - tokio::spawn( - async move { - let sum_stream = stream::select(observer_deploys, validator_deploys); + let sum_stream = stream::select(observer_deploys, validator_deploys); - tokio::pin!(sum_stream); - tokio::pin!(sink); + tokio::pin!(sum_stream); + tokio::pin!(sink); - let _ = sink - .send_all(&mut sum_stream) - .await - .inspect_err(|err| tracing::debug!("error in sink: {err:?}")); - } - .in_current_span(), - ); + let _ = sink + .send_all(&mut sum_stream) + .await + .inspect_err(|err| tracing::debug!("error in wallet deploy sink: {err:?}")); } } diff --git a/packages/embers/src/domain/wallets/transfer.rs b/packages/embers/src/domain/wallets/transfer.rs index ed474bde..721b7558 100644 --- a/packages/embers/src/domain/wallets/transfer.rs +++ b/packages/embers/src/domain/wallets/transfer.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use firefly_client::models::{DeployId, SignedCode, Uri, WalletAddress}; use firefly_client::rendering::Render; +use firefly_client::{NodeEventSource, ReadNode, WriteNode}; use crate::domain::common::{prepare_for_signing, record_trace}; use crate::domain::wallets::WalletsService; @@ -17,7 +18,7 @@ struct TransferContract { description: Option, } -impl WalletsService { +impl WalletsService { #[tracing::instrument( level = "info", skip_all, @@ -62,7 +63,6 @@ impl WalletsService { let mut write_client = self.write_client.clone(); let deploy_id = write_client.deploy_signed_contract(contract).await?; - write_client.propose().await?; Ok(deploy_id) } } diff --git a/packages/embers/src/main.rs b/packages/embers/src/main.rs index 4830e001..6c0e88b9 100644 --- a/packages/embers/src/main.rs +++ b/packages/embers/src/main.rs @@ -1,4 +1,7 @@ +use std::time::Duration; + use anyhow::Context; +use firefly_client::models::Uri; use firefly_client::{NodeEvents, ReadNodeClient, WriteNodeClient}; use poem::listener::TcpListener; use poem::middleware::{Compression, Cors, NormalizePath, RequestId, Tracing, TrailingSlash}; @@ -53,49 +56,90 @@ async fn main() -> anyhow::Result<()> { let ((agents_service, agents_teams_service, oslfs_service, wallets_service), testnet_service) = try_join!( async { - let mut write_client = WriteNodeClient::new( - config.mainnet.deploy_service_url, - config.mainnet.propose_service_url, - ) - .await?; + let write_client = WriteNodeClient::new(config.mainnet.deploy_service_url).await?; - let agents_service = AgentsService::bootstrap( + let (agents_service, agents_deploy_id) = AgentsService::bootstrap( write_client.clone(), read_client.clone(), + validator_node_events.clone(), &config.mainnet.service_key, &config.mainnet.agents_env_key, ) .await?; - let agents_teams_service = AgentsTeamsService::bootstrap( + let (agents_teams_service, agents_teams_deploy_id) = AgentsTeamsService::bootstrap( write_client.clone(), read_client.clone(), - observer_node_events.clone(), + validator_node_events.clone(), &config.mainnet.service_key, &config.mainnet.agents_teams_env_key, config.aes_encryption_key.into(), ) .await?; - let oslfs_service = OslfsService::bootstrap( + let (oslfs_service, oslfs_deploy_id) = OslfsService::bootstrap( write_client.clone(), read_client.clone(), + validator_node_events.clone(), &config.mainnet.service_key, &config.mainnet.oslfs_env_key, ) .await?; - let wallets_service = WalletsService::bootstrap( + let (wallets_service, wallets_deploy_id) = WalletsService::bootstrap( write_client.clone(), - read_client, - validator_node_events, - observer_node_events, + read_client.clone(), + validator_node_events.clone(), + observer_node_events.clone(), &config.mainnet.service_key, &config.mainnet.wallets_env_key, ) .await?; - write_client.propose().await?; + tracing::info!( + agents = %agents_deploy_id, + agents_teams = %agents_teams_deploy_id, + oslfs = %oslfs_deploy_id, + wallets = %wallets_deploy_id, + "mainnet init deploys submitted, waiting for finalization" + ); + + let init_timeout = Duration::from_secs(60); + let agents_waiter = + validator_node_events.wait_for_deploy(&agents_deploy_id, init_timeout); + let agents_teams_waiter = + validator_node_events.wait_for_deploy(&agents_teams_deploy_id, init_timeout); + let oslfs_waiter = + validator_node_events.wait_for_deploy(&oslfs_deploy_id, init_timeout); + let wallets_waiter = + validator_node_events.wait_for_deploy(&wallets_deploy_id, init_timeout); + + // Wait for all init deploys to finalize + let (a, at, o, w) = tokio::join!( + agents_waiter, + agents_teams_waiter, + oslfs_waiter, + wallets_waiter + ); + for (name, result) in [ + ("agents", a), + ("agents_teams", at), + ("oslfs", o), + ("wallets", w), + ] { + match result { + Some(true) => anyhow::bail!("{name} init deploy errored on chain"), + None => anyhow::bail!("{name} init deploy not finalized within 60s"), + Some(false) => tracing::info!("{name} init deploy finalized successfully"), + } + } + + // Verify envs are readable from the observer node + verify_env_readable(&read_client, "agents", &agents_service.uri).await?; + verify_env_readable(&read_client, "agents_teams", &agents_teams_service.uri) + .await?; + verify_env_readable(&read_client, "oslfs", &oslfs_service.uri).await?; + verify_env_readable(&read_client, "wallets", &wallets_service.uri).await?; anyhow::Ok(( agents_service, @@ -105,27 +149,103 @@ async fn main() -> anyhow::Result<()> { )) }, async { - let mut testnet_write_client = WriteNodeClient::new( - config.testnet.deploy_service_url, - config.testnet.propose_service_url, - ) - .await?; + let testnet_write_client = + WriteNodeClient::new(config.testnet.deploy_service_url).await?; - let testnet_service = TestnetService::bootstrap( + let (testnet_service, testnet_deploy_id) = TestnetService::bootstrap( testnet_write_client.clone(), - testnet_read_client, - testnet_observer_node_events, + testnet_read_client.clone(), + testnet_observer_node_events.clone(), config.testnet.service_key, &config.testnet.env_key, ) .await?; - testnet_write_client.propose().await?; + tracing::info!(deploy_id = %testnet_deploy_id, "testnet init deploy submitted"); + + let testnet_waiter = testnet_observer_node_events + .wait_for_deploy(&testnet_deploy_id, Duration::from_secs(60)); + + match testnet_waiter.await { + Some(true) => anyhow::bail!("testnet init deploy errored on chain"), + None => anyhow::bail!("testnet init deploy not finalized within 60s"), + Some(false) => tracing::info!("testnet init deploy finalized successfully"), + } + + // Verify the testnet env actually registered (consistent with the + // mainnet envs above). Without this the testnet env could silently + // fail to register — e.g. if the testnet service wallet is unfunded + // the init deploy finalizes as a no-op — and the failure would only + // surface later as empty testnet deploy logs. + verify_env_readable(&testnet_read_client, "testnet", &testnet_service.uri).await?; anyhow::Ok(testnet_service) }, )?; + // Spawn periodic registry health check task + { + let health_read_client = read_client.clone(); + let env_uris: Vec<(String, Uri)> = vec![ + ("agents".into(), agents_service.uri.clone()), + ("agents_teams".into(), agents_teams_service.uri.clone()), + ("oslfs".into(), oslfs_service.uri.clone()), + ("wallets".into(), wallets_service.uri.clone()), + ]; + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(60)); + loop { + interval.tick().await; + + // Control probe: check a well-known system URI + let system_probe_code = + r#"new ret, rl(`rho:registry:lookup`) in { rl!(`rho:lang:treeHashMap`, *ret) }"# + .to_string(); + match health_read_client + .get_data::(system_probe_code) + .await + { + Ok(value) => tracing::info!( + value = %value, + "registry health: system URI (treeHashMap) OK" + ), + Err(err) => tracing::error!( + error = %err, + "registry health: system URI (treeHashMap) FAILED — entire registry may be broken" + ), + } + + // Probe each env URI + for (name, uri) in &env_uris { + let uri_str: &str = uri.as_ref(); + let code = format!( + r#"new ret, rl(`rho:registry:lookup`) in {{ rl!(`{uri_str}`, *ret) }}"#, + ); + match health_read_client.get_data::(code).await { + Ok(serde_json::Value::Null) => tracing::warn!( + service = name.as_str(), + uri = uri_str, + "registry health: {name} env registered as Nil" + ), + Ok(value) => tracing::info!( + service = name.as_str(), + uri = uri_str, + value = %value, + "registry health: {name} env OK" + ), + Err(err) => tracing::error!( + service = name.as_str(), + uri = uri_str, + error = %err, + "registry health: {name} env LOST" + ), + } + } + } + }); + tracing::info!("periodic registry health check started (every 60s)"); + } + let secret = Alphanumeric.sample_string(&mut rand::rng(), 20); let api = OpenApiService::new( @@ -178,3 +298,27 @@ async fn main() -> anyhow::Result<()> { Ok(()) } + +async fn verify_env_readable( + read_client: &ReadNodeClient, + name: &str, + env_uri: &Uri, +) -> anyhow::Result<()> { + let uri_str: &str = env_uri.as_ref(); + let code = format!(r#"new ret, rl(`rho:registry:lookup`) in {{ rl!(`{uri_str}`, *ret) }}"#,); + let result: Result = read_client + .get_data_with_retry(code, 10, Duration::from_millis(500)) + .await; + match &result { + Ok(serde_json::Value::Null) => { + anyhow::bail!( + "{name} env URI registered as Nil in registry — init contract did not register properly" + ); + } + Ok(value) => tracing::info!("{name} env registry lookup succeeded: {value}"), + Err(err) => { + anyhow::bail!("{name} env not readable from observer: {err}"); + } + } + Ok(()) +} diff --git a/packages/embers/templates/agents/init.rho b/packages/embers/templates/agents/init.rho index ce04c52e..b0455cde 100644 --- a/packages/embers/templates/agents/init.rho +++ b/packages/embers/templates/agents/init.rho @@ -6,7 +6,7 @@ {%- filter indent(8) -%} new rl(`rho:registry:lookup`), - revAddress(`rho:rev:address`), + revAddress(`rho:vault:address`), devNull(`rho:io:devNull`), abort(`rho:execution:abort`), deployData(`rho:deploy:data`), @@ -173,8 +173,8 @@ in { visit!("agentVersions", address, id, *valueCh, *nilCh) | for(@agentVersions <- valueCh; treeHashMap, _ <<- treeHashMapCh; listOps <<- listOpsCh) { - contract toAgentHeader(@(version, agent), ret) = { - ret!(agent.delete("code").union({"id": id, "version": version})) + contract toAgentHeader(@(version, agentData), ret) = { + ret!(agentData.delete("code").union({"id": id, "version": version})) } | treeHashMap!("toMap", agentVersions, *agentVersionsMapCh) | @@ -282,14 +282,14 @@ in { for(@agentVersions <- valueCh; treeHashMap, _ <<- treeHashMapCh) { treeHashMap!("getOrElse", agentVersions, version, *valueCh, *errCh) | - for(@agent <- valueCh) { - treeHashMap!("set", agentVersions, version, agent.union({"last_deploy": last_deploy}), *devNull) + for(@agentData <- valueCh) { + treeHashMap!("set", agentVersions, version, agentData.union({"last_deploy": last_deploy}), *devNull) } | treeHashMap!("getOrElse", agentVersions, "latest", *latestCh, *errCh) | - for(@agent <- latestCh) { - if(agent.get("version") == version) { - treeHashMap!("set", agentVersions, "latest", agent.union({"last_deploy": last_deploy}), *devNull) + for(@agentData <- latestCh) { + if(agentData.get("version") == version) { + treeHashMap!("set", agentVersions, "latest", agentData.union({"last_deploy": last_deploy}), *devNull) } } } | diff --git a/packages/embers/templates/agents_teams/init.rho b/packages/embers/templates/agents_teams/init.rho index dd9fd9f6..26354f31 100644 --- a/packages/embers/templates/agents_teams/init.rho +++ b/packages/embers/templates/agents_teams/init.rho @@ -6,7 +6,7 @@ {%- filter indent(8) -%} new rl(`rho:registry:lookup`), - revAddress(`rho:rev:address`), + revAddress(`rho:vault:address`), devNull(`rho:io:devNull`), abort(`rho:execution:abort`), deployData(`rho:deploy:data`), diff --git a/packages/embers/templates/oslfs/init.rho b/packages/embers/templates/oslfs/init.rho index e98177c7..387f7d74 100644 --- a/packages/embers/templates/oslfs/init.rho +++ b/packages/embers/templates/oslfs/init.rho @@ -6,7 +6,7 @@ {%- filter indent(8) -%} new rl(`rho:registry:lookup`), - revAddress(`rho:rev:address`), + revAddress(`rho:vault:address`), devNull(`rho:io:devNull`), abort(`rho:execution:abort`), deployData(`rho:deploy:data`), diff --git a/packages/embers/templates/testnet/fund_test_wallet.rho b/packages/embers/templates/testnet/fund_test_wallet.rho index 1605874a..3e9df00c 100644 --- a/packages/embers/templates/testnet/fund_test_wallet.rho +++ b/packages/embers/templates/testnet/fund_test_wallet.rho @@ -10,7 +10,7 @@ new rl(`rho:registry:lookup`), transferOp, transferResultCh in { - rl!(`rho:rchain:revVault`, *revVaultCh) | + rl!(`rho:vault:system`, *revVaultCh) | rl!(`rho:lang:either`, *eitherCh) | contract okOrAbort(eitherCh, @log) = { diff --git a/packages/embers/templates/testnet/init.rho b/packages/embers/templates/testnet/init.rho index 5b802118..0c87743d 100644 --- a/packages/embers/templates/testnet/init.rho +++ b/packages/embers/templates/testnet/init.rho @@ -8,20 +8,28 @@ new rl(`rho:registry:lookup`), devNull(`rho:io:devNull`), treeHashMapCh, + treeHashMapLookupCh, + treeHashMapInitCh, stackCh, + stackLookupCh, private in { - rl!(`rho:lang:treeHashMap`, *treeHashMapCh) | - for(treeHashMap <- treeHashMapCh) { - treeHashMap!("init", 3, *treeHashMapCh) | - - for(@map <- treeHashMapCh) { + // Look each shared resource up into its OWN channel and only forward the + // unwrapped resource onto the channel the log contracts peek (`<<-`). If the + // raw `(nonce, resource)` registry-lookup result were placed on the peeked + // channel, a bare `<<-` peek can bind that tuple instead of the resource and + // the join never fires — see the same fix in wallets/init.rho. + rl!(`rho:lang:treeHashMap`, *treeHashMapLookupCh) | + for(treeHashMap <- treeHashMapLookupCh) { + treeHashMap!("init", 3, *treeHashMapInitCh) | + + for(@map <- treeHashMapInitCh) { treeHashMapCh!(*treeHashMap, map) } } | - rl!(`rho:lang:stack`, *stackCh) | - for(@(_, stack) <- stackCh) { + rl!(`rho:lang:stack`, *stackLookupCh) | + for(@(_, stack) <- stackLookupCh) { stackCh!(stack) } | diff --git a/packages/embers/templates/wallets/init.rho b/packages/embers/templates/wallets/init.rho index 675ca901..64ae9f74 100644 --- a/packages/embers/templates/wallets/init.rho +++ b/packages/embers/templates/wallets/init.rho @@ -7,9 +7,14 @@ new rl(`rho:registry:lookup`), treeHashMapCh, + treeHashMapLookupCh, + treeHashMapInitCh, revVaultCh, + revVaultLookupCh, eitherCh, + eitherLookupCh, stackCh, + stackLookupCh, abort(`rho:execution:abort`), devNull(`rho:io:devNull`), deployData(`rho:deploy:data`), @@ -21,27 +26,40 @@ new rl(`rho:registry:lookup`), getTransactionsHistory, getBalance in { - rl!(`rho:lang:treeHashMap`, *treeHashMapCh) | - for(treeHashMap <- treeHashMapCh) { - treeHashMap!("init", 3, *treeHashMapCh) | - - for(@map <- treeHashMapCh) { + // NOTE: each shared resource is looked up into its OWN channel and only the + // unwrapped resource is (re)sent to the channel that contracts peek (`<<-`). + // Sending the raw `rho:registry:lookup` result `(nonce, resource)` to the + // same channel that is later peeked races the peek — on f1r3node the bare + // peek pattern can match the `(nonce, resource)` tuple instead of the + // unwrapped resource, so the join never completes (transfers silently do + // not credit; balance queries return nothing). Keeping the lookup off the + // peeked channel avoids that ambiguity. + rl!(`rho:lang:treeHashMap`, *treeHashMapLookupCh) | + for(treeHashMap <- treeHashMapLookupCh) { + treeHashMap!("init", 3, *treeHashMapInitCh) | + + for(@map <- treeHashMapInitCh) { treeHashMapCh!(*treeHashMap, map) } } | - rl!(`rho:rchain:revVault`, *revVaultCh) | - for(@(_, revVault) <- revVaultCh) { + // f1r3node migrated the REV vault registry from the legacy + // `rho:rchain:revVault` to `rho:vault:system` (SystemVault). The legacy URN + // still resolves but is a SEPARATE, empty vault: transfers to it never touch + // the SystemVault balances that deploy pre-charge reads, so funded wallets + // stay broke. Use the SystemVault the node actually charges against. + rl!(`rho:vault:system`, *revVaultLookupCh) | + for(@(_, revVault) <- revVaultLookupCh) { revVaultCh!(revVault) } | - rl!(`rho:lang:either`, *eitherCh) | - for(@(_, either) <- eitherCh) { + rl!(`rho:lang:either`, *eitherLookupCh) | + for(@(_, either) <- eitherLookupCh) { eitherCh!(either) } | - rl!(`rho:lang:stack`, *stackCh) | - for(@(_, stack) <- stackCh) { + rl!(`rho:lang:stack`, *stackLookupCh) | + for(@(_, stack) <- stackLookupCh) { stackCh!(stack) } | diff --git a/packages/embers/tests/compile_proto.py b/packages/embers/tests/compile_proto.py index 9ad0856d..7b203490 100644 --- a/packages/embers/tests/compile_proto.py +++ b/packages/embers/tests/compile_proto.py @@ -1,34 +1,89 @@ +import os import pathlib +import shutil import subprocess import sys -ROOT = pathlib.Path(__file__).resolve().parent.parent.parent +ROOT = pathlib.Path(__file__).resolve().parent.parent.parent # embers/packages -PROTO_DIR = ROOT / "firefly-client" / "protobuf" -PROTO_EXTERNAL_DIR = PROTO_DIR / "protobuf_external" +# Proto definitions are sourced directly from the f1r3node-rust `models` subcrate +# (the single source of truth) rather than being vendored into this repository, +# which is error prone to keep in sync. Point F1R3NODE_MODELS_DIR at the `models` +# subcrate of a f1r3node-rust checkout/worktree; the default assumes the +# cost-accounting-transpiler worktree sits alongside this repository. +DEFAULT_MODELS_DIR = ROOT.parent.parent / "f1r3node-rust-cost-accounting-transpiler" / "models" +MODELS_DIR = pathlib.Path(os.environ.get("F1R3NODE_MODELS_DIR", DEFAULT_MODELS_DIR)).resolve() + +PROTO_DIR = MODELS_DIR / "src" / "main" / "protobuf" +# `scalapb/scalapb.proto` is imported as "scalapb/scalapb.proto" and lives under +# models/src, so that directory is an additional include root. The google/* +# well-known types (e.g. google/protobuf/empty.proto) are bundled with +# grpcio-tools and resolved automatically. +PROTO_INCLUDE_DIR = MODELS_DIR / "src" OUT_DIR = ROOT / "embers" / "tests" / "protobuf" +# Client-facing protos only, mirroring firefly-client's +# `pub use f1r3node_models::{casper, rhoapi, servicemodelapi}` (plus routing). +# The internal node protos (RholangScalaRustTypes, RSpacePlusPlusTypes) are not +# part of the client API surface and are intentionally excluded. +PROTO_FILES = [ + "CasperMessage.proto", + "DeployServiceCommon.proto", + "DeployServiceV1.proto", + "ExternalCommunicationServiceCommon.proto", + "ExternalCommunicationServiceV1.proto", + "ProposeServiceCommon.proto", + "ProposeServiceV1.proto", + "RhoTypes.proto", + "ServiceError.proto", + "routing.proto", +] + +# The betterproto2 compiler plugin pins an older `ruff` than this project uses, +# so it is NOT a project dependency. It is a build-time-only tool (the tests +# import only the betterproto2 runtime), supplied here from an isolated, +# ephemeral environment via `uv`. Keep the compiler pinned to the betterproto2 +# runtime's minor line (see pyproject: betterproto2 >=0.9,<0.10). +COMPILER_PYTHON = "3.13" +COMPILER_PLUGIN = "betterproto2-compiler==0.9.0" + def compile_all(): + if shutil.which("uv") is None: + sys.exit( + "This script needs `uv` to run the betterproto2 compiler plugin in an " + "isolated environment (the plugin conflicts with the project's ruff pin).\n" + "Install uv: https://docs.astral.sh/uv/", + ) + if not PROTO_DIR.is_dir(): + sys.exit( + f"Proto source directory not found: {PROTO_DIR}\n" + "Set F1R3NODE_MODELS_DIR to the `models` subcrate of a f1r3node-rust checkout.", + ) + + proto_paths = [PROTO_DIR / name for name in PROTO_FILES] + missing = [str(p) for p in proto_paths if not p.is_file()] + if missing: + sys.exit("Missing proto files:\n" + "\n".join(missing)) + OUT_DIR.mkdir(parents=True, exist_ok=True) - proto_files = list(PROTO_DIR.rglob("*.proto")) - if not proto_files: - print("No .proto files found.") # noqa: T201 - return - - for proto in proto_files: - cmd = [ - sys.executable, - "-m", - "grpc_tools.protoc", - f"-I{PROTO_DIR}", - f"-I{PROTO_EXTERNAL_DIR}", - f"--python_betterproto2_out={OUT_DIR}", - str(proto), - ] - print("Compiling:", proto) # noqa: T201 - subprocess.run(cmd, check=True) # noqa: S603 + # A single protoc invocation so betterproto2 emits coherent package modules + # (several files share the `casper` / `casper.v1` packages). `--no-project` + # stops `uv` from trying to build this (non-package) project. + cmd = [ + "uv", "run", "--no-project", "--python", COMPILER_PYTHON, + "--with", "grpcio-tools", "--with", COMPILER_PLUGIN, + "--", + "python", "-m", "grpc_tools.protoc", + f"-I{PROTO_DIR}", + f"-I{PROTO_INCLUDE_DIR}", + f"--python_betterproto2_out={OUT_DIR}", + *[str(p) for p in proto_paths], + ] + print("Compiling from:", PROTO_DIR) # noqa: T201 + print("Protos:", ", ".join(PROTO_FILES)) # noqa: T201 + subprocess.run(cmd, check=True) # noqa: S603 if __name__ == "__main__": diff --git a/packages/embers/tests/protobuf/casper/__init__.py b/packages/embers/tests/protobuf/casper/__init__.py index e760b9d6..c502a5d5 100644 --- a/packages/embers/tests/protobuf/casper/__init__.py +++ b/packages/embers/tests/protobuf/casper/__init__.py @@ -1,5 +1,5 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! -# sources: CasperMessage.proto, DeployServiceCommon.proto +# sources: CasperMessage.proto, DeployServiceCommon.proto, ExternalCommunicationServiceCommon.proto, ProposeServiceCommon.proto # plugin: python-betterproto2 # This file has been @generated @@ -30,6 +30,9 @@ "DataAtNameQuery", "DataWithBlockInfo", "DeployDataProto", + "DeployFinalizationStateProto", + "DeployFinalizationStatusInfo", + "DeployFinalizationStatusQuery", "DeployInfo", "DeployInfoWithEventData", "EventProto", @@ -45,12 +48,18 @@ "LastFinalizedBlockQuery", "LightBlockInfo", "MachineVerifyQuery", + "MergeableEntryRequestProto", + "MergeableEntryResponseProto", "NoApprovedBlockAvailableProto", "PeekProto", + "PeerInfo", + "PrintUnmatchedSendsQuery", "PrivateNamePreviewQuery", "ProcessedDeployProto", "ProcessedSystemDeployProto", "ProduceEventProto", + "ProposeQuery", + "ProposeResultQuery", "RChainStateProto", "RejectedDeployInfo", "RejectedDeployProto", @@ -69,7 +78,10 @@ "StoreNodeKeyProto", "SystemDeployDataProto", "SystemDeployInfoWithEventData", + "TransferInfo", "UnapprovedBlockProto", + "UpdateNotification", + "UpdateNotificationResponse", "VersionInfo", "VisualizeDagQuery", "WaitingContinuationInfo", @@ -85,6 +97,18 @@ betterproto2.check_compiler_version(_COMPILER_VERSION) +class DeployFinalizationStateProto(betterproto2.Enum): + DEPLOY_STATE_UNSPECIFIED = 0 + + DEPLOY_STATE_FINALIZED = 1 + + DEPLOY_STATE_FAILED = 2 + + DEPLOY_STATE_PENDING = 3 + + DEPLOY_STATE_EXPIRED = 4 + + @dataclass(eq=False, repr=False) class ApprovedBlockCandidateProto(betterproto2.Message): """ @@ -253,6 +277,11 @@ class BlockMetadataInternal(betterproto2.Message): whether the block is finalized """ + fault_tolerance_value: "float" = betterproto2.field(11, betterproto2.TYPE_FLOAT) + """ + cached normalized FT at finalization time + """ + default_message_pool.register_message("casper", "BlockMetadataInternal", BlockMetadataInternal) @@ -472,10 +501,46 @@ class DeployDataProto(betterproto2.Message): shard ID to prevent replay of deploys between shards """ + language: "str" = betterproto2.field(12, betterproto2.TYPE_STRING) + """ + language (rholang or metta) of the source code + """ + + expiration_timestamp: "int" = betterproto2.field(13, betterproto2.TYPE_INT64) + """ + optional millisecond timestamp after which deploy is invalid (0 = no expiration) + """ + default_message_pool.register_message("casper", "DeployDataProto", DeployDataProto) +@dataclass(eq=False, repr=False) +class DeployFinalizationStatusInfo(betterproto2.Message): + state: "DeployFinalizationStateProto" = betterproto2.field( + 1, betterproto2.TYPE_ENUM, default_factory=lambda: DeployFinalizationStateProto(0) + ) + + rejection_count: "int" = betterproto2.field(2, betterproto2.TYPE_UINT32) + + latest_block_hash: "bytes | None" = betterproto2.field(3, betterproto2.TYPE_BYTES, optional=True) + """ + Present when the sig has been included in at least one block. + Absent (no field set) when the deploy is unknown to this node. + """ + + +default_message_pool.register_message("casper", "DeployFinalizationStatusInfo", DeployFinalizationStatusInfo) + + +@dataclass(eq=False, repr=False) +class DeployFinalizationStatusQuery(betterproto2.Message): + deploy_sig: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES) + + +default_message_pool.register_message("casper", "DeployFinalizationStatusQuery", DeployFinalizationStatusQuery) + + @dataclass(eq=False, repr=False) class DeployInfo(betterproto2.Message): deployer: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) @@ -500,6 +565,13 @@ class DeployInfo(betterproto2.Message): system_deploy_error: "str" = betterproto2.field(12, betterproto2.TYPE_STRING) + transfers: "list[TransferInfo]" = betterproto2.field(13, betterproto2.TYPE_MESSAGE, repeated=True) + + transfers_available: "bool" = betterproto2.field(14, betterproto2.TYPE_BOOL) + """ + true when transfers were extracted (readonly), false on validators + """ + default_message_pool.register_message("casper", "DeployInfo", DeployInfo) @@ -692,6 +764,8 @@ class LightBlockInfo(betterproto2.Message): rejected_deploys: "list[RejectedDeployInfo]" = betterproto2.field(21, betterproto2.TYPE_MESSAGE, repeated=True) + is_finalized: "bool" = betterproto2.field(22, betterproto2.TYPE_BOOL) + default_message_pool.register_message("casper", "LightBlockInfo", LightBlockInfo) @@ -704,11 +778,39 @@ class MachineVerifyQuery(betterproto2.Message): default_message_pool.register_message("casper", "MachineVerifyQuery", MachineVerifyQuery) +@dataclass(eq=False, repr=False) +class MergeableEntryRequestProto(betterproto2.Message): + """ + --------- End Last finalized state -------- + + --------- Mergeable channels store sync -------- + """ + + block_hash: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES) + + +default_message_pool.register_message("casper", "MergeableEntryRequestProto", MergeableEntryRequestProto) + + +@dataclass(eq=False, repr=False) +class MergeableEntryResponseProto(betterproto2.Message): + block_hash: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES) + + serialized_entry: "bytes" = betterproto2.field(2, betterproto2.TYPE_BYTES) + """ + bincode of Vec. Empty bytes = peer has the block but + no entry for it. + """ + + +default_message_pool.register_message("casper", "MergeableEntryResponseProto", MergeableEntryResponseProto) + + @dataclass(eq=False, repr=False) class NoApprovedBlockAvailableProto(betterproto2.Message): identifier: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) - node_identifer: "str" = betterproto2.field(2, betterproto2.TYPE_STRING) + node_identifier: "str" = betterproto2.field(2, betterproto2.TYPE_STRING) default_message_pool.register_message("casper", "NoApprovedBlockAvailableProto", NoApprovedBlockAvailableProto) @@ -722,6 +824,54 @@ class PeekProto(betterproto2.Message): default_message_pool.register_message("casper", "PeekProto", PeekProto) +@dataclass(eq=False, repr=False) +class PeerInfo(betterproto2.Message): + address: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) + """ + Full rnode:// URL + """ + + node_id: "str" = betterproto2.field(2, betterproto2.TYPE_STRING) + """ + Public key hash (hex) + """ + + host: "str" = betterproto2.field(3, betterproto2.TYPE_STRING) + """ + Hostname or IP + """ + + protocol_port: "int" = betterproto2.field(4, betterproto2.TYPE_INT32) + """ + TCP port + """ + + discovery_port: "int" = betterproto2.field(5, betterproto2.TYPE_INT32) + """ + UDP port + """ + + is_connected: "bool" = betterproto2.field(6, betterproto2.TYPE_BOOL) + """ + True if active connection + """ + + +default_message_pool.register_message("casper", "PeerInfo", PeerInfo) + + +@dataclass(eq=False, repr=False) +class PrintUnmatchedSendsQuery(betterproto2.Message): + """ + TODO remove it + """ + + print_unmatched_sends: "bool" = betterproto2.field(1, betterproto2.TYPE_BOOL) + + +default_message_pool.register_message("casper", "PrintUnmatchedSendsQuery", PrintUnmatchedSendsQuery) + + @dataclass(eq=False, repr=False) class PrivateNamePreviewQuery(betterproto2.Message): user: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES) @@ -787,10 +937,32 @@ class ProduceEventProto(betterproto2.Message): times_repeated: "int" = betterproto2.field(4, betterproto2.TYPE_INT32) + is_deterministic: "bool" = betterproto2.field(5, betterproto2.TYPE_BOOL) + + output_value: "list[bytes]" = betterproto2.field(6, betterproto2.TYPE_BYTES, repeated=True) + + failed: "bool" = betterproto2.field(7, betterproto2.TYPE_BOOL) + default_message_pool.register_message("casper", "ProduceEventProto", ProduceEventProto) +@dataclass(eq=False, repr=False) +class ProposeQuery(betterproto2.Message): + is_async: "bool" = betterproto2.field(1, betterproto2.TYPE_BOOL) + + +default_message_pool.register_message("casper", "ProposeQuery", ProposeQuery) + + +@dataclass(eq=False, repr=False) +class ProposeResultQuery(betterproto2.Message): + pass + + +default_message_pool.register_message("casper", "ProposeResultQuery", ProposeResultQuery) + + @dataclass(eq=False, repr=False) class RChainStateProto(betterproto2.Message): pre_state_hash: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES) @@ -946,6 +1118,33 @@ class Status(betterproto2.Message): min_phlo_price: "int" = betterproto2.field(7, betterproto2.TYPE_INT64) + peer_list: "list[PeerInfo]" = betterproto2.field(8, betterproto2.TYPE_MESSAGE, repeated=True) + """ + Detailed peer list + """ + + native_token_name: "str" = betterproto2.field(9, betterproto2.TYPE_STRING) + """ + Native token metadata baked into genesis state. Queryable on-chain via + the TokenMetadata contract at `rho:system:tokenMetadata`. + """ + + native_token_symbol: "str" = betterproto2.field(10, betterproto2.TYPE_STRING) + + native_token_decimals: "int" = betterproto2.field(11, betterproto2.TYPE_UINT32) + + last_finalized_block_number: "int" = betterproto2.field(12, betterproto2.TYPE_INT64) + + is_validator: "bool" = betterproto2.field(13, betterproto2.TYPE_BOOL) + + is_read_only: "bool" = betterproto2.field(14, betterproto2.TYPE_BOOL) + + is_ready: "bool" = betterproto2.field(15, betterproto2.TYPE_BOOL) + + current_epoch: "int" = betterproto2.field(16, betterproto2.TYPE_INT64) + + epoch_length: "int" = betterproto2.field(17, betterproto2.TYPE_INT32) + default_message_pool.register_message("casper", "Status", Status) @@ -1031,6 +1230,37 @@ class SystemDeployInfoWithEventData(betterproto2.Message): default_message_pool.register_message("casper", "SystemDeployInfoWithEventData", SystemDeployInfoWithEventData) +@dataclass(eq=False, repr=False) +class TransferInfo(betterproto2.Message): + from_addr: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) + """ + Sender address + """ + + to_addr: "str" = betterproto2.field(2, betterproto2.TYPE_STRING) + """ + Recipient address + """ + + amount: "int" = betterproto2.field(3, betterproto2.TYPE_INT64) + """ + Amount in dust (smallest unit) + """ + + success: "bool" = betterproto2.field(4, betterproto2.TYPE_BOOL) + """ + Whether the transfer succeeded + """ + + fail_reason: "str" = betterproto2.field(5, betterproto2.TYPE_STRING) + """ + Error message if success is false, empty otherwise + """ + + +default_message_pool.register_message("casper", "TransferInfo", TransferInfo) + + @dataclass(eq=False, repr=False) class UnapprovedBlockProto(betterproto2.Message): candidate: "ApprovedBlockCandidateProto | None" = betterproto2.field(1, betterproto2.TYPE_MESSAGE, optional=True) @@ -1043,6 +1273,26 @@ class UnapprovedBlockProto(betterproto2.Message): default_message_pool.register_message("casper", "UnapprovedBlockProto", UnapprovedBlockProto) +@dataclass(eq=False, repr=False) +class UpdateNotification(betterproto2.Message): + client_host: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) + + client_port: "int" = betterproto2.field(2, betterproto2.TYPE_INT32) + + payload: "str" = betterproto2.field(3, betterproto2.TYPE_STRING) + + +default_message_pool.register_message("casper", "UpdateNotification", UpdateNotification) + + +@dataclass(eq=False, repr=False) +class UpdateNotificationResponse(betterproto2.Message): + pass + + +default_message_pool.register_message("casper", "UpdateNotificationResponse", UpdateNotificationResponse) + + @dataclass(eq=False, repr=False) class VersionInfo(betterproto2.Message): api: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) diff --git a/packages/embers/tests/protobuf/casper/v1/__init__.py b/packages/embers/tests/protobuf/casper/v1/__init__.py index 97a14ee7..ba6f6066 100644 --- a/packages/embers/tests/protobuf/casper/v1/__init__.py +++ b/packages/embers/tests/protobuf/casper/v1/__init__.py @@ -1,5 +1,5 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! -# sources: DeployServiceV1.proto +# sources: DeployServiceV1.proto, ExternalCommunicationServiceV1.proto, ProposeServiceV1.proto # plugin: python-betterproto2 # This file has been @generated @@ -9,21 +9,25 @@ "BondStatusResponse", "ContinuationAtNamePayload", "ContinuationAtNameResponse", + "DeployFinalizationStatusResponse", "DeployResponse", "DeployServiceStub", "EventInfoResponse", "ExploratoryDeployResponse", + "ExternalCommunicationServiceStub", "FindDeployResponse", "IsFinalizedResponse", "LastFinalizedBlockResponse", - "ListeningNameDataPayload", - "ListeningNameDataResponse", "MachineVerifyResponse", "PrivateNamePreviewPayload", "PrivateNamePreviewResponse", + "ProposeResponse", + "ProposeResultResponse", + "ProposeServiceStub", "RhoDataPayload", "RhoDataResponse", "StatusResponse", + "UpdateNotificationResponse", "VisualizeBlocksResponse", ) @@ -133,6 +137,27 @@ class ContinuationAtNameResponse(betterproto2.Message): default_message_pool.register_message("casper.v1", "ContinuationAtNameResponse", ContinuationAtNameResponse) +@dataclass(eq=False, repr=False) +class DeployFinalizationStatusResponse(betterproto2.Message): + """ + deployFinalizationStatus + + Oneofs: + - message: + """ + + error: "__servicemodelapi__.ServiceError | None" = betterproto2.field( + 1, betterproto2.TYPE_MESSAGE, optional=True, group="message" + ) + + status: "__casper__.DeployFinalizationStatusInfo | None" = betterproto2.field( + 2, betterproto2.TYPE_MESSAGE, optional=True, group="message" + ) + + +default_message_pool.register_message("casper.v1", "DeployFinalizationStatusResponse", DeployFinalizationStatusResponse) + + @dataclass(eq=False, repr=False) class DeployResponse(betterproto2.Message): """ @@ -190,6 +215,8 @@ class ExploratoryDeployResponse(betterproto2.Message): 2, betterproto2.TYPE_MESSAGE, optional=True, group="message" ) + cost: "int" = betterproto2.field(3, betterproto2.TYPE_UINT64) + default_message_pool.register_message("casper.v1", "ExploratoryDeployResponse", ExploratoryDeployResponse) @@ -256,19 +283,39 @@ class LastFinalizedBlockResponse(betterproto2.Message): @dataclass(eq=False, repr=False) -class ListeningNameDataPayload(betterproto2.Message): - block_info: "list[__casper__.DataWithBlockInfo]" = betterproto2.field(1, betterproto2.TYPE_MESSAGE, repeated=True) +class MachineVerifyResponse(betterproto2.Message): + """ + machineVerifiableDag - length: "int" = betterproto2.field(2, betterproto2.TYPE_INT32) + Oneofs: + - message: + """ + + error: "__servicemodelapi__.ServiceError | None" = betterproto2.field( + 1, betterproto2.TYPE_MESSAGE, optional=True, group="message" + ) + + content: "str | None" = betterproto2.field(2, betterproto2.TYPE_STRING, optional=True, group="message") -default_message_pool.register_message("casper.v1", "ListeningNameDataPayload", ListeningNameDataPayload) +default_message_pool.register_message("casper.v1", "MachineVerifyResponse", MachineVerifyResponse) @dataclass(eq=False, repr=False) -class ListeningNameDataResponse(betterproto2.Message): +class PrivateNamePreviewPayload(betterproto2.Message): + ids: "list[bytes]" = betterproto2.field(1, betterproto2.TYPE_BYTES, repeated=True) """ - listenForDataAtName + a la GPrivate + """ + + +default_message_pool.register_message("casper.v1", "PrivateNamePreviewPayload", PrivateNamePreviewPayload) + + +@dataclass(eq=False, repr=False) +class PrivateNamePreviewResponse(betterproto2.Message): + """ + previewPrivateNames Oneofs: - message: @@ -278,18 +325,18 @@ class ListeningNameDataResponse(betterproto2.Message): 1, betterproto2.TYPE_MESSAGE, optional=True, group="message" ) - payload: "ListeningNameDataPayload | None" = betterproto2.field( + payload: "PrivateNamePreviewPayload | None" = betterproto2.field( 2, betterproto2.TYPE_MESSAGE, optional=True, group="message" ) -default_message_pool.register_message("casper.v1", "ListeningNameDataResponse", ListeningNameDataResponse) +default_message_pool.register_message("casper.v1", "PrivateNamePreviewResponse", PrivateNamePreviewResponse) @dataclass(eq=False, repr=False) -class MachineVerifyResponse(betterproto2.Message): +class ProposeResponse(betterproto2.Message): """ - machineVerifiableDag + Oneofs: - message: @@ -299,28 +346,17 @@ class MachineVerifyResponse(betterproto2.Message): 1, betterproto2.TYPE_MESSAGE, optional=True, group="message" ) - content: "str | None" = betterproto2.field(2, betterproto2.TYPE_STRING, optional=True, group="message") + result: "str | None" = betterproto2.field(2, betterproto2.TYPE_STRING, optional=True, group="message") -default_message_pool.register_message("casper.v1", "MachineVerifyResponse", MachineVerifyResponse) +default_message_pool.register_message("casper.v1", "ProposeResponse", ProposeResponse) @dataclass(eq=False, repr=False) -class PrivateNamePreviewPayload(betterproto2.Message): - ids: "list[bytes]" = betterproto2.field(1, betterproto2.TYPE_BYTES, repeated=True) - """ - a la GPrivate +class ProposeResultResponse(betterproto2.Message): """ -default_message_pool.register_message("casper.v1", "PrivateNamePreviewPayload", PrivateNamePreviewPayload) - - -@dataclass(eq=False, repr=False) -class PrivateNamePreviewResponse(betterproto2.Message): - """ - previewPrivateNames - Oneofs: - message: """ @@ -329,12 +365,10 @@ class PrivateNamePreviewResponse(betterproto2.Message): 1, betterproto2.TYPE_MESSAGE, optional=True, group="message" ) - payload: "PrivateNamePreviewPayload | None" = betterproto2.field( - 2, betterproto2.TYPE_MESSAGE, optional=True, group="message" - ) + result: "str | None" = betterproto2.field(2, betterproto2.TYPE_STRING, optional=True, group="message") -default_message_pool.register_message("casper.v1", "PrivateNamePreviewResponse", PrivateNamePreviewResponse) +default_message_pool.register_message("casper.v1", "ProposeResultResponse", ProposeResultResponse) @dataclass(eq=False, repr=False) @@ -387,6 +421,25 @@ class StatusResponse(betterproto2.Message): default_message_pool.register_message("casper.v1", "StatusResponse", StatusResponse) +@dataclass(eq=False, repr=False) +class UpdateNotificationResponse(betterproto2.Message): + """ + + + Oneofs: + - message: + """ + + error: "__servicemodelapi__.ServiceError | None" = betterproto2.field( + 1, betterproto2.TYPE_MESSAGE, optional=True, group="message" + ) + + result: "str | None" = betterproto2.field(2, betterproto2.TYPE_STRING, optional=True, group="message") + + +default_message_pool.register_message("casper.v1", "UpdateNotificationResponse", UpdateNotificationResponse) + + @dataclass(eq=False, repr=False) class VisualizeBlocksResponse(betterproto2.Message): """ @@ -412,7 +465,7 @@ class DeployServiceStub: `ProposeServiceV2.propose` to make a new block with the results of running them all. - To get results back, use `listenForDataAtName`. + To get results back, use `getDataAtName`. """ def __init__(self, channel: grpc.Channel): @@ -483,18 +536,6 @@ def get_blocks(self, message: "__casper__.BlocksQuery") -> "Iterator[BlockInfoRe BlockInfoResponse.FromString, )(message) - def listen_for_data_at_name(self, message: "__casper__.DataAtNameQuery") -> "ListeningNameDataResponse": - """ - Find data sent to a name. - OBSOLETE: Use getDataAtName instead. This method will be removed in the future version of RNode. - """ - - return self._channel.unary_unary( - "/casper.v1.DeployService/listenForDataAtName", - __casper__.DataAtNameQuery.SerializeToString, - ListeningNameDataResponse.FromString, - )(message) - def get_data_at_name(self, message: "__casper__.DataAtNameByBlockQuery") -> "RhoDataResponse": """ Find data sent to a name. @@ -568,6 +609,21 @@ def is_finalized(self, message: "__casper__.IsFinalizedQuery") -> "IsFinalizedRe IsFinalizedResponse.FromString, )(message) + def deploy_finalization_status( + self, message: "__casper__.DeployFinalizationStatusQuery" + ) -> "DeployFinalizationStatusResponse": + """ + Query the canonical-state finalization status of a deploy by its signature. + Prefer this over isFinalized for deploy tracking — isFinalized can return + true for a block whose deploy effects were dropped by merge rejection. + """ + + return self._channel.unary_unary( + "/casper.v1.DeployService/deployFinalizationStatus", + __casper__.DeployFinalizationStatusQuery.SerializeToString, + DeployFinalizationStatusResponse.FromString, + )(message) + def bond_status(self, message: "__casper__.BondStatusQuery") -> "BondStatusResponse": """ Check if a given validator is bonded. @@ -628,6 +684,40 @@ def status(self, message: "__google__protobuf__.Empty | None" = None) -> "Status )(message) +class ExternalCommunicationServiceStub: + def __init__(self, channel: grpc.Channel): + self._channel = channel + + def send_notification(self, message: "__casper__.UpdateNotification") -> "UpdateNotificationResponse": + return self._channel.unary_unary( + "/casper.v1.ExternalCommunicationService/sendNotification", + __casper__.UpdateNotification.SerializeToString, + UpdateNotificationResponse.FromString, + )(message) + + +class ProposeServiceStub: + def __init__(self, channel: grpc.Channel): + self._channel = channel + + def propose(self, message: "__casper__.ProposeQuery") -> "ProposeResponse": + return self._channel.unary_unary( + "/casper.v1.ProposeService/propose", + __casper__.ProposeQuery.SerializeToString, + ProposeResponse.FromString, + )(message) + + def propose_result(self, message: "__casper__.ProposeResultQuery | None" = None) -> "ProposeResultResponse": + if message is None: + message = __casper__.ProposeResultQuery() + + return self._channel.unary_unary( + "/casper.v1.ProposeService/proposeResult", + __casper__.ProposeResultQuery.SerializeToString, + ProposeResultResponse.FromString, + )(message) + + from ... import casper as __casper__ from ... import rhoapi as __rhoapi__ from ... import servicemodelapi as __servicemodelapi__ diff --git a/packages/embers/tests/protobuf/google/protobuf/__init__.py b/packages/embers/tests/protobuf/google/protobuf/__init__.py index cb748095..115fcb3f 100644 --- a/packages/embers/tests/protobuf/google/protobuf/__init__.py +++ b/packages/embers/tests/protobuf/google/protobuf/__init__.py @@ -1,5 +1,5 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! -# sources: google/protobuf/descriptor.proto +# sources: google/protobuf/descriptor.proto, google/protobuf/empty.proto # plugin: python-betterproto2 # This file has been @generated @@ -7,24 +7,45 @@ "DescriptorProto", "DescriptorProtoExtensionRange", "DescriptorProtoReservedRange", + "Edition", + "Empty", "EnumDescriptorProto", "EnumDescriptorProtoEnumReservedRange", "EnumOptions", "EnumValueDescriptorProto", "EnumValueOptions", "ExtensionRangeOptions", + "ExtensionRangeOptionsDeclaration", + "ExtensionRangeOptionsVerificationState", + "FeatureSet", + "FeatureSetDefaults", + "FeatureSetDefaultsFeatureSetEditionDefault", + "FeatureSetEnforceNamingStyle", + "FeatureSetEnumType", + "FeatureSetFieldPresence", + "FeatureSetJsonFormat", + "FeatureSetMessageEncoding", + "FeatureSetRepeatedFieldEncoding", + "FeatureSetUtf8Validation", + "FeatureSetVisibilityFeature", + "FeatureSetVisibilityFeatureDefaultSymbolVisibility", "FieldDescriptorProto", "FieldDescriptorProtoLabel", "FieldDescriptorProtoType", "FieldOptions", "FieldOptionsCType", + "FieldOptionsEditionDefault", + "FieldOptionsFeatureSupport", "FieldOptionsJsType", + "FieldOptionsOptionRetention", + "FieldOptionsOptionTargetType", "FileDescriptorProto", "FileDescriptorSet", "FileOptions", "FileOptionsOptimizeMode", "GeneratedCodeInfo", "GeneratedCodeInfoAnnotation", + "GeneratedCodeInfoAnnotationSemantic", "MessageOptions", "MethodDescriptorProto", "MethodOptions", @@ -35,6 +56,7 @@ "ServiceOptions", "SourceCodeInfo", "SourceCodeInfoLocation", + "SymbolVisibility", "UninterpretedOption", "UninterpretedOptionNamePart", ) @@ -50,30 +72,236 @@ betterproto2.check_compiler_version(_COMPILER_VERSION) +class Edition(betterproto2.Enum): + """ + The full set of known editions. + """ + + UNKNOWN = 0 + """ + A placeholder for an unknown edition value. + """ + + LEGACY = 900 + """ + A placeholder edition for specifying default behaviors *before* a feature + was first introduced. This is effectively an "infinite past". + """ + + PROTO2 = 998 + """ + Legacy syntax "editions". These pre-date editions, but behave much like + distinct editions. These can't be used to specify the edition of proto + files, but feature definitions must supply proto2/proto3 defaults for + backwards compatibility. + """ + + PROTO3 = 999 + + _2023 = 1000 + """ + Editions that have been released. The specific values are arbitrary and + should not be depended on, but they will always be time-ordered for easy + comparison. + """ + + _2024 = 1001 + + _2026 = 1002 + + UNSTABLE = 9999 + """ + A placeholder edition for developing and testing unscheduled features. + """ + + _1_TEST_ONLY = 1 + """ + Placeholder editions for testing feature resolution. These should not be + used or relied on outside of tests. + """ + + _2_TEST_ONLY = 2 + + _99997_TEST_ONLY = 99997 + + _99998_TEST_ONLY = 99998 + + _99999_TEST_ONLY = 99999 + + MAX = 2147483647 + """ + Placeholder for specifying unbounded edition support. This should only + ever be used by plugins that can expect to never require any changes to + support a new edition. + """ + + @classmethod + def betterproto_value_to_renamed_proto_names(cls) -> dict[int, str]: + return { + 0: "EDITION_UNKNOWN", + 900: "EDITION_LEGACY", + 998: "EDITION_PROTO2", + 999: "EDITION_PROTO3", + 1000: "EDITION_2023", + 1001: "EDITION_2024", + 1002: "EDITION_2026", + 9999: "EDITION_UNSTABLE", + 1: "EDITION_1_TEST_ONLY", + 2: "EDITION_2_TEST_ONLY", + 99997: "EDITION_99997_TEST_ONLY", + 99998: "EDITION_99998_TEST_ONLY", + 99999: "EDITION_99999_TEST_ONLY", + 2147483647: "EDITION_MAX", + } + + @classmethod + def betterproto_renamed_proto_names_to_value(cls) -> dict[str, int]: + return { + "EDITION_UNKNOWN": 0, + "EDITION_LEGACY": 900, + "EDITION_PROTO2": 998, + "EDITION_PROTO3": 999, + "EDITION_2023": 1000, + "EDITION_2024": 1001, + "EDITION_2026": 1002, + "EDITION_UNSTABLE": 9999, + "EDITION_1_TEST_ONLY": 1, + "EDITION_2_TEST_ONLY": 2, + "EDITION_99997_TEST_ONLY": 99997, + "EDITION_99998_TEST_ONLY": 99998, + "EDITION_99999_TEST_ONLY": 99999, + "EDITION_MAX": 2147483647, + } + + +class ExtensionRangeOptionsVerificationState(betterproto2.Enum): + """ + The verification state of the extension range. + """ + + DECLARATION = 0 + """ + All the extensions of the range must be declared. + """ + + UNVERIFIED = 1 + + +class FeatureSetEnforceNamingStyle(betterproto2.Enum): + ENFORCE_NAMING_STYLE_UNKNOWN = 0 + + STYLE2024 = 1 + + STYLE_LEGACY = 2 + + STYLE2026 = 3 + + +class FeatureSetEnumType(betterproto2.Enum): + ENUM_TYPE_UNKNOWN = 0 + + OPEN = 1 + + CLOSED = 2 + + +class FeatureSetFieldPresence(betterproto2.Enum): + FIELD_PRESENCE_UNKNOWN = 0 + + EXPLICIT = 1 + + IMPLICIT = 2 + + LEGACY_REQUIRED = 3 + + +class FeatureSetJsonFormat(betterproto2.Enum): + JSON_FORMAT_UNKNOWN = 0 + + ALLOW = 1 + + LEGACY_BEST_EFFORT = 2 + + +class FeatureSetMessageEncoding(betterproto2.Enum): + MESSAGE_ENCODING_UNKNOWN = 0 + + LENGTH_PREFIXED = 1 + + DELIMITED = 2 + + +class FeatureSetRepeatedFieldEncoding(betterproto2.Enum): + REPEATED_FIELD_ENCODING_UNKNOWN = 0 + + PACKED = 1 + + EXPANDED = 2 + + +class FeatureSetUtf8Validation(betterproto2.Enum): + UTF8_VALIDATION_UNKNOWN = 0 + + VERIFY = 2 + + NONE = 3 + + +class FeatureSetVisibilityFeatureDefaultSymbolVisibility(betterproto2.Enum): + DEFAULT_SYMBOL_VISIBILITY_UNKNOWN = 0 + + EXPORT_ALL = 1 + """ + Default pre-EDITION_2024, all UNSET visibility are export. + """ + + EXPORT_TOP_LEVEL = 2 + """ + All top-level symbols default to export, nested default to local. + """ + + LOCAL_ALL = 3 + """ + All symbols default to local. + """ + + STRICT = 4 + """ + All symbols local by default. Nested types cannot be exported. + With special case caveat for message { enum {} reserved 1 to max; } + This is the recommended setting for new protos. + """ + + class FieldDescriptorProtoLabel(betterproto2.Enum): OPTIONAL = 1 """ 0 is reserved for errors """ - REQUIRED = 2 - REPEATED = 3 + REQUIRED = 2 + """ + The required label is only allowed in google.protobuf. In proto3 and Editions + it's explicitly prohibited. In Editions, the `field_presence` feature + can be used to get this behavior. + """ + @classmethod def betterproto_value_to_renamed_proto_names(cls) -> dict[int, str]: return { 1: "LABEL_OPTIONAL", - 2: "LABEL_REQUIRED", 3: "LABEL_REPEATED", + 2: "LABEL_REQUIRED", } @classmethod def betterproto_renamed_proto_names_to_value(cls) -> dict[str, int]: return { "LABEL_OPTIONAL": 1, - "LABEL_REQUIRED": 2, "LABEL_REPEATED": 3, + "LABEL_REQUIRED": 2, } @@ -111,9 +339,10 @@ class FieldDescriptorProtoType(betterproto2.Enum): GROUP = 10 """ Tag-delimited aggregate. - Group type is deprecated and not supported in proto3. However, Proto3 + Group type is deprecated and not supported after google.protobuf. However, Proto3 implementations should still be able to parse the group wire format and - treat group fields as unknown fields. + treat group fields as unknown fields. In Editions, the group wire format + can be enabled via the `message_encoding` feature. """ MESSAGE = 11 @@ -198,6 +427,14 @@ class FieldOptionsCType(betterproto2.Enum): """ CORD = 1 + """ + The option [ctype=CORD] may be applied to a non-repeated field of type + "bytes". It indicates that in C++, the data should be stored in a Cord + instead of a string. For very large strings, this may reduce memory + fragmentation. It may also allow better performance when parsing from a + Cord, or when parsing with aliasing enabled, as the parsed Cord may then + alias the original buffer. + """ STRING_PIECE = 2 @@ -219,6 +456,46 @@ class FieldOptionsJsType(betterproto2.Enum): """ +class FieldOptionsOptionRetention(betterproto2.Enum): + """ + If set to RETENTION_SOURCE, the option will be omitted from the binary. + """ + + RETENTION_UNKNOWN = 0 + + RETENTION_RUNTIME = 1 + + RETENTION_SOURCE = 2 + + +class FieldOptionsOptionTargetType(betterproto2.Enum): + """ + This indicates the types of entities that the field may apply to when used + as an option. If it is unset, then the field may be freely used as an + option on any kind of entity. + """ + + TARGET_TYPE_UNKNOWN = 0 + + TARGET_TYPE_FILE = 1 + + TARGET_TYPE_EXTENSION_RANGE = 2 + + TARGET_TYPE_MESSAGE = 3 + + TARGET_TYPE_FIELD = 4 + + TARGET_TYPE_ONEOF = 5 + + TARGET_TYPE_ENUM = 6 + + TARGET_TYPE_ENUM_ENTRY = 7 + + TARGET_TYPE_SERVICE = 8 + + TARGET_TYPE_METHOD = 9 + + class FileOptionsOptimizeMode(betterproto2.Enum): """ Generated classes can be optimized for speed or code size. @@ -242,6 +519,28 @@ class FileOptionsOptimizeMode(betterproto2.Enum): """ +class GeneratedCodeInfoAnnotationSemantic(betterproto2.Enum): + """ + Represents the identified object's effect on the element in the original + .proto file. + """ + + NONE = 0 + """ + There is no effect or the effect is indescribable. + """ + + SET = 1 + """ + The element is set or otherwise mutated. + """ + + ALIAS = 2 + """ + An alias to the element is returned. + """ + + class MethodOptionsIdempotencyLevel(betterproto2.Enum): """ Is this method side-effect-free (or safe in HTTP parlance), or idempotent, @@ -262,6 +561,22 @@ class MethodOptionsIdempotencyLevel(betterproto2.Enum): """ +class SymbolVisibility(betterproto2.Enum): + """ + Describes the 'visibility' of a symbol with respect to the proto import + system. Symbols can only be imported when the visibility rules do not prevent + it (ex: local symbols cannot be imported). Visibility modifiers can only set + on `message` and `enum` as they are the only types available to be referenced + from other files. + """ + + VISIBILITY_UNSET = 0 + + VISIBILITY_LOCAL = 1 + + VISIBILITY_EXPORT = 2 + + @dataclass(eq=False, repr=False) class DescriptorProto(betterproto2.Message): """ @@ -296,6 +611,13 @@ class DescriptorProto(betterproto2.Message): A given name may only be reserved once. """ + visibility: "SymbolVisibility" = betterproto2.field( + 11, betterproto2.TYPE_ENUM, default_factory=lambda: SymbolVisibility(0) + ) + """ + Support for `export` and `local` keywords on enums. + """ + default_message_pool.register_message("google.protobuf", "DescriptorProto", DescriptorProto) @@ -342,6 +664,24 @@ class DescriptorProtoReservedRange(betterproto2.Message): default_message_pool.register_message("google.protobuf", "DescriptorProto.ReservedRange", DescriptorProtoReservedRange) +@dataclass(eq=False, repr=False) +class Empty(betterproto2.Message): + """ + A generic empty message that you can re-use to avoid defining duplicated + empty messages in your APIs. A typical example is to use it as the request + or the response type of an API method. For instance: + + service Foo { + rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); + } + """ + + pass + + +default_message_pool.register_message("google.protobuf", "Empty", Empty) + + @dataclass(eq=False, repr=False) class EnumDescriptorProto(betterproto2.Message): """ @@ -369,6 +709,13 @@ class EnumDescriptorProto(betterproto2.Message): be reserved once. """ + visibility: "SymbolVisibility" = betterproto2.field( + 6, betterproto2.TYPE_ENUM, default_factory=lambda: SymbolVisibility(0) + ) + """ + Support for `export` and `local` keywords on enums. + """ + default_message_pool.register_message("google.protobuf", "EnumDescriptorProto", EnumDescriptorProto) @@ -416,6 +763,24 @@ class EnumOptions(betterproto2.Message): is a formalization for deprecating enums. """ + deprecated_legacy_json_field_conflicts: "bool" = betterproto2.field(6, betterproto2.TYPE_BOOL) + """ + Enable the legacy handling of JSON field name conflicts. This lowercases + and strips underscored from the fields before comparison in proto3 only. + The new behavior takes `json_name` into account and applies to proto2 as + well. + TODO Remove this legacy behavior once downstream teams have + had time to migrate. + """ + + features: "FeatureSet | None" = betterproto2.field(7, betterproto2.TYPE_MESSAGE, optional=True) + """ + Any features defined in the specific edition. + WARNING: This field should only be used by protobuf plugins or special + cases like the proto compiler. Other uses are discouraged and + developers should rely on the protoreflect APIs for their client language. + """ + uninterpreted_option: "list[UninterpretedOption]" = betterproto2.field( 999, betterproto2.TYPE_MESSAGE, repeated=True ) @@ -423,6 +788,11 @@ class EnumOptions(betterproto2.Message): The parser stores options it doesn't recognize here. See above. """ + def __post_init__(self) -> None: + super().__post_init__() + if self.is_set("deprecated_legacy_json_field_conflicts"): + warnings.warn("EnumOptions.deprecated_legacy_json_field_conflicts is deprecated", DeprecationWarning) + default_message_pool.register_message("google.protobuf", "EnumOptions", EnumOptions) @@ -453,6 +823,28 @@ class EnumValueOptions(betterproto2.Message): this is a formalization for deprecating enum values. """ + features: "FeatureSet | None" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, optional=True) + """ + Any features defined in the specific edition. + WARNING: This field should only be used by protobuf plugins or special + cases like the proto compiler. Other uses are discouraged and + developers should rely on the protoreflect APIs for their client language. + """ + + debug_redact: "bool" = betterproto2.field(3, betterproto2.TYPE_BOOL) + """ + Indicate that fields annotated with this enum value should not be printed + out when using debug formats, e.g. when the field contains sensitive + credentials. + """ + + feature_support: "FieldOptionsFeatureSupport | None" = betterproto2.field( + 4, betterproto2.TYPE_MESSAGE, optional=True + ) + """ + Information about the support window of a feature value. + """ + uninterpreted_option: "list[UninterpretedOption]" = betterproto2.field( 999, betterproto2.TYPE_MESSAGE, repeated=True ) @@ -473,10 +865,186 @@ class ExtensionRangeOptions(betterproto2.Message): The parser stores options it doesn't recognize here. See above. """ + declaration: "list[ExtensionRangeOptionsDeclaration]" = betterproto2.field( + 2, betterproto2.TYPE_MESSAGE, repeated=True + ) + """ + For external users: DO NOT USE. We are in the process of open sourcing + extension declaration and executing internal cleanups before it can be + used externally. + """ + + features: "FeatureSet | None" = betterproto2.field(50, betterproto2.TYPE_MESSAGE, optional=True) + """ + Any features defined in the specific edition. + """ + + verification: "ExtensionRangeOptionsVerificationState" = betterproto2.field( + 3, betterproto2.TYPE_ENUM, default_factory=lambda: ExtensionRangeOptionsVerificationState(0) + ) + """ + The verification state of the range. + TODO: flip the default to DECLARATION once all empty ranges + are marked as UNVERIFIED. + """ + default_message_pool.register_message("google.protobuf", "ExtensionRangeOptions", ExtensionRangeOptions) +@dataclass(eq=False, repr=False) +class ExtensionRangeOptionsDeclaration(betterproto2.Message): + number: "int" = betterproto2.field(1, betterproto2.TYPE_INT32) + """ + The extension number declared within the extension range. + """ + + full_name: "str" = betterproto2.field(2, betterproto2.TYPE_STRING) + """ + The fully-qualified name of the extension field. There must be a leading + dot in front of the full name. + """ + + type: "str" = betterproto2.field(3, betterproto2.TYPE_STRING) + """ + The fully-qualified type name of the extension field. Unlike + Metadata.type, Declaration.type must have a leading dot for messages + and enums. + """ + + reserved: "bool" = betterproto2.field(5, betterproto2.TYPE_BOOL) + """ + If true, indicates that the number is reserved in the extension range, + and any extension field with the number will fail to compile. Set this + when a declared extension field is deleted. + """ + + repeated: "bool" = betterproto2.field(6, betterproto2.TYPE_BOOL) + """ + If true, indicates that the extension must be defined as repeated. + Otherwise the extension must be defined as optional. + """ + + +default_message_pool.register_message( + "google.protobuf", "ExtensionRangeOptions.Declaration", ExtensionRangeOptionsDeclaration +) + + +@dataclass(eq=False, repr=False) +class FeatureSet(betterproto2.Message): + """ + =================================================================== + Features + + TODO Enums in C++ gencode (and potentially other languages) are + not well scoped. This means that each of the feature enums below can clash + with each other. The short names we've chosen maximize call-site + readability, but leave us very open to this scenario. A future feature will + be designed and implemented to handle this, hopefully before we ever hit a + conflict here. + """ + + field_presence: "FeatureSetFieldPresence" = betterproto2.field( + 1, betterproto2.TYPE_ENUM, default_factory=lambda: FeatureSetFieldPresence(0) + ) + + enum_type: "FeatureSetEnumType" = betterproto2.field( + 2, betterproto2.TYPE_ENUM, default_factory=lambda: FeatureSetEnumType(0) + ) + + repeated_field_encoding: "FeatureSetRepeatedFieldEncoding" = betterproto2.field( + 3, betterproto2.TYPE_ENUM, default_factory=lambda: FeatureSetRepeatedFieldEncoding(0) + ) + + utf8_validation: "FeatureSetUtf8Validation" = betterproto2.field( + 4, betterproto2.TYPE_ENUM, default_factory=lambda: FeatureSetUtf8Validation(0) + ) + + message_encoding: "FeatureSetMessageEncoding" = betterproto2.field( + 5, betterproto2.TYPE_ENUM, default_factory=lambda: FeatureSetMessageEncoding(0) + ) + + json_format: "FeatureSetJsonFormat" = betterproto2.field( + 6, betterproto2.TYPE_ENUM, default_factory=lambda: FeatureSetJsonFormat(0) + ) + + enforce_naming_style: "FeatureSetEnforceNamingStyle" = betterproto2.field( + 7, betterproto2.TYPE_ENUM, default_factory=lambda: FeatureSetEnforceNamingStyle(0) + ) + + default_symbol_visibility: "FeatureSetVisibilityFeatureDefaultSymbolVisibility" = betterproto2.field( + 8, betterproto2.TYPE_ENUM, default_factory=lambda: FeatureSetVisibilityFeatureDefaultSymbolVisibility(0) + ) + + +default_message_pool.register_message("google.protobuf", "FeatureSet", FeatureSet) + + +@dataclass(eq=False, repr=False) +class FeatureSetVisibilityFeature(betterproto2.Message): + pass + + +default_message_pool.register_message("google.protobuf", "FeatureSet.VisibilityFeature", FeatureSetVisibilityFeature) + + +@dataclass(eq=False, repr=False) +class FeatureSetDefaults(betterproto2.Message): + """ + A compiled specification for the defaults of a set of features. These + messages are generated from FeatureSet extensions and can be used to seed + feature resolution. The resolution with this object becomes a simple search + for the closest matching edition, followed by proto merges. + """ + + defaults: "list[FeatureSetDefaultsFeatureSetEditionDefault]" = betterproto2.field( + 1, betterproto2.TYPE_MESSAGE, repeated=True + ) + + minimum_edition: "Edition" = betterproto2.field(4, betterproto2.TYPE_ENUM, default_factory=lambda: Edition(0)) + """ + The minimum supported edition (inclusive) when this was constructed. + Editions before this will not have defaults. + """ + + maximum_edition: "Edition" = betterproto2.field(5, betterproto2.TYPE_ENUM, default_factory=lambda: Edition(0)) + """ + The maximum known edition (inclusive) when this was constructed. Editions + after this will not have reliable defaults. + """ + + +default_message_pool.register_message("google.protobuf", "FeatureSetDefaults", FeatureSetDefaults) + + +@dataclass(eq=False, repr=False) +class FeatureSetDefaultsFeatureSetEditionDefault(betterproto2.Message): + """ + A map from every known edition with a unique set of defaults to its + defaults. Not all editions may be contained here. For a given edition, + the defaults at the closest matching edition ordered at or before it should + be used. This field must be in strict ascending order by edition. + """ + + edition: "Edition" = betterproto2.field(3, betterproto2.TYPE_ENUM, default_factory=lambda: Edition(0)) + + overridable_features: "FeatureSet | None" = betterproto2.field(4, betterproto2.TYPE_MESSAGE, optional=True) + """ + Defaults of features that can be overridden in this edition. + """ + + fixed_features: "FeatureSet | None" = betterproto2.field(5, betterproto2.TYPE_MESSAGE, optional=True) + """ + Defaults of features that can't be overridden in this edition. + """ + + +default_message_pool.register_message( + "google.protobuf", "FeatureSetDefaults.FeatureSetEditionDefault", FeatureSetDefaultsFeatureSetEditionDefault +) + + @dataclass(eq=False, repr=False) class FieldDescriptorProto(betterproto2.Message): """ @@ -520,7 +1088,6 @@ class FieldDescriptorProto(betterproto2.Message): For booleans, "true" or "false". For strings, contains the default text contents (not escaped in any way). For bytes, contains the C escaped value. All bytes >= 128 are escaped. - TODO(kenton): Base-64 encode? """ oneof_index: "int" = betterproto2.field(9, betterproto2.TYPE_INT32) @@ -544,12 +1111,12 @@ class FieldDescriptorProto(betterproto2.Message): If true, this is a proto3 "optional". When a proto3 field is optional, it tracks presence regardless of field type. - When proto3_optional is true, this field must be belong to a oneof to - signal to old proto3 clients that presence is tracked for this field. This - oneof is known as a "synthetic" oneof, and this field must be its sole - member (each proto3 optional field gets its own synthetic oneof). Synthetic - oneofs exist in the descriptor only, and do not generate any API. Synthetic - oneofs must be ordered after all "real" oneofs. + When proto3_optional is true, this field must belong to a oneof to signal + to old proto3 clients that presence is tracked for this field. This oneof + is known as a "synthetic" oneof, and this field must be its sole member + (each proto3 optional field gets its own synthetic oneof). Synthetic oneofs + exist in the descriptor only, and do not generate any API. Synthetic oneofs + must be ordered after all "real" oneofs. For message fields, proto3_optional doesn't create any semantic change, since non-repeated message fields always track presence. However it still @@ -574,10 +1141,13 @@ class FieldOptions(betterproto2.Message): 1, betterproto2.TYPE_ENUM, default_factory=lambda: FieldOptionsCType(0) ) """ + NOTE: ctype is deprecated. Use `features.(pb.cpp).string_type` instead. The ctype option instructs the C++ code generator to use a different representation of the field than it normally would. See the specific - options below. This option is not yet implemented in the open source - release -- sorry, we'll try to include it in a future version! + options below. This option is only implemented to support use of + [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of + type "bytes" in the open source release. + TODO: make ctype actually deprecated. """ packed: "bool" = betterproto2.field(2, betterproto2.TYPE_BOOL) @@ -586,7 +1156,9 @@ class FieldOptions(betterproto2.Message): a more efficient representation on the wire. Rather than repeatedly writing the tag and type for each element, the entire array is encoded as a single length-delimited blob. In proto3, only explicit setting it to - false will avoid using packed encoding. + false will avoid using packed encoding. This option is prohibited in + Editions, but the `repeated_field_encoding` feature can be used to control + the behavior. """ jstype: "FieldOptionsJsType" = betterproto2.field( @@ -625,16 +1197,18 @@ class FieldOptions(betterproto2.Message): call from multiple threads concurrently, while non-const methods continue to require exclusive access. - Note that implementations may choose not to check required fields within - a lazy sub-message. That is, calling IsInitialized() on the outer message - may return true even if the inner message has missing required fields. - This is necessary because otherwise the inner message would have to be - parsed in order to perform the check, defeating the purpose of lazy - parsing. An implementation which chooses not to check required fields - must be consistent about it. That is, for any particular sub-message, the - implementation must either *always* check its required fields, or *never* - check its required fields, regardless of whether or not the message has - been parsed. + Note that lazy message fields are still eagerly verified to check + ill-formed wireformat or missing required fields. Calling IsInitialized() + on the outer message would fail if the inner message has missing required + fields. Failed verification would result in parsing failure (except when + uninitialized messages are acceptable). + """ + + unverified_lazy: "bool" = betterproto2.field(15, betterproto2.TYPE_BOOL) + """ + unverified_lazy does no correctness checks on the byte stream. This should + only be used where lazy with verification is prohibitive for performance + reasons. """ deprecated: "bool" = betterproto2.field(3, betterproto2.TYPE_BOOL) @@ -647,9 +1221,38 @@ class FieldOptions(betterproto2.Message): weak: "bool" = betterproto2.field(10, betterproto2.TYPE_BOOL) """ + DEPRECATED. DO NOT USE! For Google-internal migration only. Do not use. """ + debug_redact: "bool" = betterproto2.field(16, betterproto2.TYPE_BOOL) + """ + Indicate that the field value should not be printed out when using debug + formats, e.g. when the field contains sensitive credentials. + """ + + retention: "FieldOptionsOptionRetention" = betterproto2.field( + 17, betterproto2.TYPE_ENUM, default_factory=lambda: FieldOptionsOptionRetention(0) + ) + + targets: "list[FieldOptionsOptionTargetType]" = betterproto2.field(19, betterproto2.TYPE_ENUM, repeated=True) + + edition_defaults: "list[FieldOptionsEditionDefault]" = betterproto2.field( + 20, betterproto2.TYPE_MESSAGE, repeated=True + ) + + features: "FeatureSet | None" = betterproto2.field(21, betterproto2.TYPE_MESSAGE, optional=True) + """ + Any features defined in the specific edition. + WARNING: This field should only be used by protobuf plugins or special + cases like the proto compiler. Other uses are discouraged and + developers should rely on the protoreflect APIs for their client language. + """ + + feature_support: "FieldOptionsFeatureSupport | None" = betterproto2.field( + 22, betterproto2.TYPE_MESSAGE, optional=True + ) + uninterpreted_option: "list[UninterpretedOption]" = betterproto2.field( 999, betterproto2.TYPE_MESSAGE, repeated=True ) @@ -657,10 +1260,70 @@ class FieldOptions(betterproto2.Message): The parser stores options it doesn't recognize here. See above. """ + def __post_init__(self) -> None: + super().__post_init__() + if self.is_set("weak"): + warnings.warn("FieldOptions.weak is deprecated", DeprecationWarning) + default_message_pool.register_message("google.protobuf", "FieldOptions", FieldOptions) +@dataclass(eq=False, repr=False) +class FieldOptionsEditionDefault(betterproto2.Message): + edition: "Edition" = betterproto2.field(3, betterproto2.TYPE_ENUM, default_factory=lambda: Edition(0)) + + value: "str" = betterproto2.field(2, betterproto2.TYPE_STRING) + """ + Textproto value. + """ + + +default_message_pool.register_message("google.protobuf", "FieldOptions.EditionDefault", FieldOptionsEditionDefault) + + +@dataclass(eq=False, repr=False) +class FieldOptionsFeatureSupport(betterproto2.Message): + """ + Information about the support window of a feature. + """ + + edition_introduced: "Edition" = betterproto2.field(1, betterproto2.TYPE_ENUM, default_factory=lambda: Edition(0)) + """ + The edition that this feature was first available in. In editions + earlier than this one, the default assigned to EDITION_LEGACY will be + used, and proto files will not be able to override it. + """ + + edition_deprecated: "Edition" = betterproto2.field(2, betterproto2.TYPE_ENUM, default_factory=lambda: Edition(0)) + """ + The edition this feature becomes deprecated in. Using this after this + edition may trigger warnings. + """ + + deprecation_warning: "str" = betterproto2.field(3, betterproto2.TYPE_STRING) + """ + The deprecation warning text if this feature is used after the edition it + was marked deprecated in. + """ + + edition_removed: "Edition" = betterproto2.field(4, betterproto2.TYPE_ENUM, default_factory=lambda: Edition(0)) + """ + The edition this feature is no longer available in. In editions after + this one, the last default assigned will be used, and proto files will + not be able to override it. + """ + + removal_error: "str" = betterproto2.field(5, betterproto2.TYPE_STRING) + """ + The removal error text if this feature is used after the edition it was + removed in. + """ + + +default_message_pool.register_message("google.protobuf", "FieldOptions.FeatureSupport", FieldOptionsFeatureSupport) + + @dataclass(eq=False, repr=False) class FileDescriptorProto(betterproto2.Message): """ @@ -693,6 +1356,12 @@ class FileDescriptorProto(betterproto2.Message): For Google-internal migration only. Do not use. """ + option_dependency: "list[str]" = betterproto2.field(15, betterproto2.TYPE_STRING, repeated=True) + """ + Names of files imported by this file purely for the purpose of providing + option extensions. These are excluded from the dependency list above. + """ + message_type: "list[DescriptorProto]" = betterproto2.field(4, betterproto2.TYPE_MESSAGE, repeated=True) """ All top-level definitions in this file. @@ -717,7 +1386,20 @@ class FileDescriptorProto(betterproto2.Message): syntax: "str" = betterproto2.field(12, betterproto2.TYPE_STRING) """ The syntax of the proto file. - The supported values are "proto2" and "proto3". + The supported values are "proto2", "proto3", and "editions". + + If `edition` is present, this value must be "editions". + WARNING: This field should only be used by protobuf plugins or special + cases like the proto compiler. Other uses are discouraged and + developers should rely on the protoreflect APIs for their client language. + """ + + edition: "Edition" = betterproto2.field(14, betterproto2.TYPE_ENUM, default_factory=lambda: Edition(0)) + """ + The edition of the proto file. + WARNING: This field should only be used by protobuf plugins or special + cases like the proto compiler. Other uses are discouraged and + developers should rely on the protoreflect APIs for their client language. """ @@ -807,12 +1489,16 @@ class FileOptions(betterproto2.Message): java_string_check_utf8: "bool" = betterproto2.field(27, betterproto2.TYPE_BOOL) """ - If set true, then the Java2 code generator will generate code that - throws an exception whenever an attempt is made to assign a non-UTF-8 - byte sequence to a string field. - Message reflection will do the same. - However, an extension field still accepts non-UTF-8 byte sequences. - This option has no effect on when used with the lite runtime. + A proto2 file can set this to true to opt in to UTF-8 checking for Java, + which will throw an exception if invalid UTF-8 is parsed from the wire or + assigned to a string field. + + TODO: clarify exactly what kinds of field types this option + applies to, and update these docs accordingly. + + Proto3 files already perform these checks. Setting the option explicitly to + false has no effect: it cannot be used to opt proto3 files out of UTF-8 + checks. """ optimize_for: "FileOptionsOptimizeMode" = betterproto2.field( @@ -846,8 +1532,6 @@ class FileOptions(betterproto2.Message): py_generic_services: "bool" = betterproto2.field(18, betterproto2.TYPE_BOOL) - php_generic_services: "bool" = betterproto2.field(42, betterproto2.TYPE_BOOL) - deprecated: "bool" = betterproto2.field(23, betterproto2.TYPE_BOOL) """ Is this file deprecated? @@ -908,6 +1592,14 @@ class FileOptions(betterproto2.Message): determining the ruby package. """ + features: "FeatureSet | None" = betterproto2.field(50, betterproto2.TYPE_MESSAGE, optional=True) + """ + Any features defined in the specific edition. + WARNING: This field should only be used by protobuf plugins or special + cases like the proto compiler. Other uses are discouraged and + developers should rely on the protoreflect APIs for their client language. + """ + uninterpreted_option: "list[UninterpretedOption]" = betterproto2.field( 999, betterproto2.TYPE_MESSAGE, repeated=True ) @@ -965,10 +1657,14 @@ class GeneratedCodeInfoAnnotation(betterproto2.Message): end: "int" = betterproto2.field(4, betterproto2.TYPE_INT32) """ Identifies the ending offset in bytes in the generated code that - relates to the identified offset. The end offset should be one past + relates to the identified object. The end offset should be one past the last relevant byte (so the length of the text = end - begin). """ + semantic: "GeneratedCodeInfoAnnotationSemantic" = betterproto2.field( + 5, betterproto2.TYPE_ENUM, default_factory=lambda: GeneratedCodeInfoAnnotationSemantic(0) + ) + default_message_pool.register_message("google.protobuf", "GeneratedCodeInfo.Annotation", GeneratedCodeInfoAnnotation) @@ -1037,6 +1733,28 @@ class MessageOptions(betterproto2.Message): parser. """ + deprecated_legacy_json_field_conflicts: "bool" = betterproto2.field(11, betterproto2.TYPE_BOOL) + """ + Enable the legacy handling of JSON field name conflicts. This lowercases + and strips underscored from the fields before comparison in proto3 only. + The new behavior takes `json_name` into account and applies to proto2 as + well. + + This should only be used as a temporary measure against broken builds due + to the change in behavior for JSON field name conflicts. + + TODO This is legacy behavior we plan to remove once downstream + teams have had time to migrate. + """ + + features: "FeatureSet | None" = betterproto2.field(12, betterproto2.TYPE_MESSAGE, optional=True) + """ + Any features defined in the specific edition. + WARNING: This field should only be used by protobuf plugins or special + cases like the proto compiler. Other uses are discouraged and + developers should rely on the protoreflect APIs for their client language. + """ + uninterpreted_option: "list[UninterpretedOption]" = betterproto2.field( 999, betterproto2.TYPE_MESSAGE, repeated=True ) @@ -1044,6 +1762,11 @@ class MessageOptions(betterproto2.Message): The parser stores options it doesn't recognize here. See above. """ + def __post_init__(self) -> None: + super().__post_init__() + if self.is_set("deprecated_legacy_json_field_conflicts"): + warnings.warn("MessageOptions.deprecated_legacy_json_field_conflicts is deprecated", DeprecationWarning) + default_message_pool.register_message("google.protobuf", "MessageOptions", MessageOptions) @@ -1099,6 +1822,14 @@ class MethodOptions(betterproto2.Message): 34, betterproto2.TYPE_ENUM, default_factory=lambda: MethodOptionsIdempotencyLevel(0) ) + features: "FeatureSet | None" = betterproto2.field(35, betterproto2.TYPE_MESSAGE, optional=True) + """ + Any features defined in the specific edition. + WARNING: This field should only be used by protobuf plugins or special + cases like the proto compiler. Other uses are discouraged and + developers should rely on the protoreflect APIs for their client language. + """ + uninterpreted_option: "list[UninterpretedOption]" = betterproto2.field( 999, betterproto2.TYPE_MESSAGE, repeated=True ) @@ -1126,6 +1857,14 @@ class OneofDescriptorProto(betterproto2.Message): @dataclass(eq=False, repr=False) class OneofOptions(betterproto2.Message): + features: "FeatureSet | None" = betterproto2.field(1, betterproto2.TYPE_MESSAGE, optional=True) + """ + Any features defined in the specific edition. + WARNING: This field should only be used by protobuf plugins or special + cases like the proto compiler. Other uses are discouraged and + developers should rely on the protoreflect APIs for their client language. + """ + uninterpreted_option: "list[UninterpretedOption]" = betterproto2.field( 999, betterproto2.TYPE_MESSAGE, repeated=True ) @@ -1155,6 +1894,14 @@ class ServiceDescriptorProto(betterproto2.Message): @dataclass(eq=False, repr=False) class ServiceOptions(betterproto2.Message): + features: "FeatureSet | None" = betterproto2.field(34, betterproto2.TYPE_MESSAGE, optional=True) + """ + Any features defined in the specific edition. + WARNING: This field should only be used by protobuf plugins or special + cases like the proto compiler. Other uses are discouraged and + developers should rely on the protoreflect APIs for their client language. + """ + deprecated: "bool" = betterproto2.field(33, betterproto2.TYPE_BOOL) """ Note: Field numbers 1 through 32 are reserved for Google's internal RPC @@ -1248,8 +1995,8 @@ class SourceCodeInfoLocation(betterproto2.Message): location. Each element is a field number or an index. They form a path from - the root FileDescriptorProto to the place where the definition. For - example, this path: + the root FileDescriptorProto to the place where the definition appears. + For example, this path: [ 4, 3, 2, 7, 1 ] refers to: file.message_type(3) // 4, 3 @@ -1307,13 +2054,13 @@ class SourceCodeInfoLocation(betterproto2.Message): // Comment attached to baz. // Another line attached to baz. - // Comment attached to qux. + // Comment attached to moo. // - // Another line attached to qux. - optional double qux = 4; + // Another line attached to moo. + optional double moo = 4; // Detached comment for corge. This is not leading or trailing comments - // to qux or corge because there are blank lines separating it from + // to moo or corge because there are blank lines separating it from // both. // Detached comment for corge paragraph 2. @@ -1376,8 +2123,8 @@ class UninterpretedOptionNamePart(betterproto2.Message): The name of the uninterpreted option. Each string represents a segment in a dot-separated name. is_extension is true iff a segment represents an extension (denoted with parentheses in options specs in .proto files). - E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents - "foo.(bar.baz).qux". + E.g.,{ ["foo", false], ["bar.baz", true], ["moo", false] } represents + "foo.(bar.baz).moo". """ name_part: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) diff --git a/packages/embers/tests/protobuf/google/protobuf/compiler/__init__.py b/packages/embers/tests/protobuf/google/protobuf/compiler/__init__.py deleted file mode 100644 index 0cff8936..00000000 --- a/packages/embers/tests/protobuf/google/protobuf/compiler/__init__.py +++ /dev/null @@ -1,229 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# sources: protobuf_external/google/protobuf/compiler/plugin.proto -# plugin: python-betterproto2 -# This file has been @generated - -__all__ = ( - "CodeGeneratorRequest", - "CodeGeneratorResponse", - "CodeGeneratorResponseFeature", - "CodeGeneratorResponseFile", - "Version", -) - -from dataclasses import dataclass - -import betterproto2 - -from ....message_pool import default_message_pool - -_COMPILER_VERSION = "0.9.0" -betterproto2.check_compiler_version(_COMPILER_VERSION) - - -class CodeGeneratorResponseFeature(betterproto2.Enum): - """ - Sync with code_generator.h. - """ - - NONE = 0 - - PROTO3_OPTIONAL = 1 - - @classmethod - def betterproto_value_to_renamed_proto_names(cls) -> dict[int, str]: - return { - 0: "FEATURE_NONE", - 1: "FEATURE_PROTO3_OPTIONAL", - } - - @classmethod - def betterproto_renamed_proto_names_to_value(cls) -> dict[str, int]: - return { - "FEATURE_NONE": 0, - "FEATURE_PROTO3_OPTIONAL": 1, - } - - -@dataclass(eq=False, repr=False) -class CodeGeneratorRequest(betterproto2.Message): - """ - An encoded CodeGeneratorRequest is written to the plugin's stdin. - """ - - file_to_generate: "list[str]" = betterproto2.field(1, betterproto2.TYPE_STRING, repeated=True) - """ - The .proto files that were explicitly listed on the command-line. The - code generator should generate code only for these files. Each file's - descriptor will be included in proto_file, below. - """ - - parameter: "str" = betterproto2.field(2, betterproto2.TYPE_STRING) - """ - The generator parameter passed on the command-line. - """ - - proto_file: "list[__protobuf__.FileDescriptorProto]" = betterproto2.field( - 15, betterproto2.TYPE_MESSAGE, repeated=True - ) - """ - FileDescriptorProtos for all files in files_to_generate and everything - they import. The files will appear in topological order, so each file - appears before any file that imports it. - - protoc guarantees that all proto_files will be written after - the fields above, even though this is not technically guaranteed by the - protobuf wire format. This theoretically could allow a plugin to stream - in the FileDescriptorProtos and handle them one by one rather than read - the entire set into memory at once. However, as of this writing, this - is not similarly optimized on protoc's end -- it will store all fields in - memory at once before sending them to the plugin. - - Type names of fields and extensions in the FileDescriptorProto are always - fully qualified. - """ - - compiler_version: "Version | None" = betterproto2.field(3, betterproto2.TYPE_MESSAGE, optional=True) - """ - The version number of protocol compiler. - """ - - -default_message_pool.register_message("google.protobuf.compiler", "CodeGeneratorRequest", CodeGeneratorRequest) - - -@dataclass(eq=False, repr=False) -class CodeGeneratorResponse(betterproto2.Message): - """ - The plugin writes an encoded CodeGeneratorResponse to stdout. - """ - - error: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) - """ - Error message. If non-empty, code generation failed. The plugin process - should exit with status code zero even if it reports an error in this way. - - This should be used to indicate errors in .proto files which prevent the - code generator from generating correct code. Errors which indicate a - problem in protoc itself -- such as the input CodeGeneratorRequest being - unparseable -- should be reported by writing a message to stderr and - exiting with a non-zero status code. - """ - - supported_features: "int" = betterproto2.field(2, betterproto2.TYPE_UINT64) - """ - A bitmask of supported features that the code generator supports. - This is a bitwise "or" of values from the Feature enum. - """ - - file: "list[CodeGeneratorResponseFile]" = betterproto2.field(15, betterproto2.TYPE_MESSAGE, repeated=True) - - -default_message_pool.register_message("google.protobuf.compiler", "CodeGeneratorResponse", CodeGeneratorResponse) - - -@dataclass(eq=False, repr=False) -class CodeGeneratorResponseFile(betterproto2.Message): - """ - Represents a single generated file. - """ - - name: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) - """ - The file name, relative to the output directory. The name must not - contain "." or ".." components and must be relative, not be absolute (so, - the file cannot lie outside the output directory). "/" must be used as - the path separator, not "\\". - - If the name is omitted, the content will be appended to the previous - file. This allows the generator to break large files into small chunks, - and allows the generated text to be streamed back to protoc so that large - files need not reside completely in memory at one time. Note that as of - this writing protoc does not optimize for this -- it will read the entire - CodeGeneratorResponse before writing files to disk. - """ - - insertion_point: "str" = betterproto2.field(2, betterproto2.TYPE_STRING) - """ - If non-empty, indicates that the named file should already exist, and the - content here is to be inserted into that file at a defined insertion - point. This feature allows a code generator to extend the output - produced by another code generator. The original generator may provide - insertion points by placing special annotations in the file that look - like: - @@protoc_insertion_point(NAME) - The annotation can have arbitrary text before and after it on the line, - which allows it to be placed in a comment. NAME should be replaced with - an identifier naming the point -- this is what other generators will use - as the insertion_point. Code inserted at this point will be placed - immediately above the line containing the insertion point (thus multiple - insertions to the same point will come out in the order they were added). - The double-@ is intended to make it unlikely that the generated code - could contain things that look like insertion points by accident. - - For example, the C++ code generator places the following line in the - .pb.h files that it generates: - // @@protoc_insertion_point(namespace_scope) - This line appears within the scope of the file's package namespace, but - outside of any particular class. Another plugin can then specify the - insertion_point "namespace_scope" to generate additional classes or - other declarations that should be placed in this scope. - - Note that if the line containing the insertion point begins with - whitespace, the same whitespace will be added to every line of the - inserted text. This is useful for languages like Python, where - indentation matters. In these languages, the insertion point comment - should be indented the same amount as any inserted code will need to be - in order to work correctly in that context. - - The code generator that generates the initial file and the one which - inserts into it must both run as part of a single invocation of protoc. - Code generators are executed in the order in which they appear on the - command line. - - If |insertion_point| is present, |name| must also be present. - """ - - content: "str" = betterproto2.field(15, betterproto2.TYPE_STRING) - """ - The file contents. - """ - - generated_code_info: "__protobuf__.GeneratedCodeInfo | None" = betterproto2.field( - 16, betterproto2.TYPE_MESSAGE, optional=True - ) - """ - Information describing the file content being inserted. If an insertion - point is used, this information will be appropriately offset and inserted - into the code generation metadata for the generated files. - """ - - -default_message_pool.register_message( - "google.protobuf.compiler", "CodeGeneratorResponse.File", CodeGeneratorResponseFile -) - - -@dataclass(eq=False, repr=False) -class Version(betterproto2.Message): - """ - The version number of protocol compiler. - """ - - major: "int" = betterproto2.field(1, betterproto2.TYPE_INT32) - - minor: "int" = betterproto2.field(2, betterproto2.TYPE_INT32) - - patch: "int" = betterproto2.field(3, betterproto2.TYPE_INT32) - - suffix: "str" = betterproto2.field(4, betterproto2.TYPE_STRING) - """ - A suffix for alpha, beta or rc release, e.g., "alpha-1", "rc2". It should - be empty for mainline stable releases. - """ - - -default_message_pool.register_message("google.protobuf.compiler", "Version", Version) - - -from ... import protobuf as __protobuf__ diff --git a/packages/embers/tests/protobuf/rhoapi/__init__.py b/packages/embers/tests/protobuf/rhoapi/__init__.py index d1f07b11..129a5c03 100644 --- a/packages/embers/tests/protobuf/rhoapi/__init__.py +++ b/packages/embers/tests/protobuf/rhoapi/__init__.py @@ -29,18 +29,23 @@ "ENeq", "ENot", "EOr", + "EPathMap", "EPercentPercent", "EPlus", "EPlusPlus", "ESet", "ETuple", "EVar", + "EZipper", "Expr", + "GBigRational", "GDeployId", "GDeployerId", + "GFixedPoint", "GPrivate", "GSysAuthToken", "GUnforgeable", + "If", "KeyValuePair", "ListBindPatterns", "ListParWithRandom", @@ -383,6 +388,20 @@ class EOr(betterproto2.Message): default_message_pool.register_message("rhoapi", "EOr", EOr) +@dataclass(eq=False, repr=False) +class EPathMap(betterproto2.Message): + ps: "list[Par]" = betterproto2.field(1, betterproto2.TYPE_MESSAGE, repeated=True) + + locally_free: "bytes" = betterproto2.field(3, betterproto2.TYPE_BYTES) + + connective_used: "bool" = betterproto2.field(4, betterproto2.TYPE_BOOL) + + remainder: "Var | None" = betterproto2.field(5, betterproto2.TYPE_MESSAGE, optional=True) + + +default_message_pool.register_message("rhoapi", "EPathMap", EPathMap) + + @dataclass(eq=False, repr=False) class EPercentPercent(betterproto2.Message): """ @@ -533,6 +552,14 @@ class Expr(betterproto2.Message): 24, betterproto2.TYPE_MESSAGE, optional=True, group="expr_instance" ) + e_pathmap_body: "EPathMap | None" = betterproto2.field( + 32, betterproto2.TYPE_MESSAGE, optional=True, group="expr_instance" + ) + + e_zipper_body: "EZipper | None" = betterproto2.field( + 33, betterproto2.TYPE_MESSAGE, optional=True, group="expr_instance" + ) + e_matches_body: "EMatches | None" = betterproto2.field( 27, betterproto2.TYPE_MESSAGE, optional=True, group="expr_instance" ) @@ -560,10 +587,91 @@ class Expr(betterproto2.Message): e_mod_body: "EMod | None" = betterproto2.field(31, betterproto2.TYPE_MESSAGE, optional=True, group="expr_instance") + g_double: "int | None" = betterproto2.field(34, betterproto2.TYPE_FIXED64, optional=True, group="expr_instance") + """ + Fields 32-33 reserved for future use + + Extended numeric types (Rholang numeric types spec, 2025-11-13) + fixed64 (not protobuf double) to preserve bit-exact IEEE 754 semantics (-0.0 vs 0.0, NaN payloads) + + IEEE 754 f64 stored as raw bits (covers f32 losslessly) + """ + + g_big_int: "bytes | None" = betterproto2.field(35, betterproto2.TYPE_BYTES, optional=True, group="expr_instance") + """ + Arbitrary-precision signed integer (big-endian two's complement) + """ + + g_big_rat: "GBigRational | None" = betterproto2.field( + 36, betterproto2.TYPE_MESSAGE, optional=True, group="expr_instance" + ) + """ + Exact rational number + """ + + g_fixed_point: "GFixedPoint | None" = betterproto2.field( + 37, betterproto2.TYPE_MESSAGE, optional=True, group="expr_instance" + ) + """ + Fixed-point decimal + """ + default_message_pool.register_message("rhoapi", "Expr", Expr) +@dataclass(eq=False, repr=False) +class EZipper(betterproto2.Message): + """ + * + Zipper for navigating and modifying PathMaps. + Zippers maintain a focus position within a PathMap structure, + allowing efficient navigation and modification operations. + """ + + pathmap: "EPathMap | None" = betterproto2.field(1, betterproto2.TYPE_MESSAGE, optional=True) + """ + The underlying PathMap being navigated + """ + + current_path: "list[bytes]" = betterproto2.field(2, betterproto2.TYPE_BYTES, repeated=True) + """ + Current path position in the zipper (list of path segments as bytes) + Each segment is encoded as bytes representing a Par value + """ + + is_write_zipper: "bool" = betterproto2.field(3, betterproto2.TYPE_BOOL) + """ + Whether this is a write zipper (true) or read zipper (false) + """ + + locally_free: "bytes" = betterproto2.field(4, betterproto2.TYPE_BYTES) + """ + Metadata from the PathMap + """ + + connective_used: "bool" = betterproto2.field(5, betterproto2.TYPE_BOOL) + + +default_message_pool.register_message("rhoapi", "EZipper", EZipper) + + +@dataclass(eq=False, repr=False) +class GBigRational(betterproto2.Message): + numerator: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES) + """ + BigInt as big-endian two's complement + """ + + denominator: "bytes" = betterproto2.field(2, betterproto2.TYPE_BYTES) + """ + BigInt as big-endian two's complement, always > 0 + """ + + +default_message_pool.register_message("rhoapi", "GBigRational", GBigRational) + + @dataclass(eq=False, repr=False) class GDeployerId(betterproto2.Message): public_key: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES) @@ -580,6 +688,22 @@ class GDeployId(betterproto2.Message): default_message_pool.register_message("rhoapi", "GDeployId", GDeployId) +@dataclass(eq=False, repr=False) +class GFixedPoint(betterproto2.Message): + unscaled: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES) + """ + BigInt as big-endian two's complement + """ + + scale: "int" = betterproto2.field(2, betterproto2.TYPE_UINT32) + """ + Number of decimal digits: actual_value = unscaled / 10^scale + """ + + +default_message_pool.register_message("rhoapi", "GFixedPoint", GFixedPoint) + + @dataclass(eq=False, repr=False) class GPrivate(betterproto2.Message): id: "bytes" = betterproto2.field(1, betterproto2.TYPE_BYTES) @@ -627,6 +751,30 @@ class GUnforgeable(betterproto2.Message): default_message_pool.register_message("rhoapi", "GUnforgeable", GUnforgeable) +@dataclass(eq=False, repr=False) +class If(betterproto2.Message): + """ + First-class conditional. Reduction evaluates `condition`'s expression slot; + fires `if_true` when it reduces to GBool(true), `if_false` when it reduces + to GBool(false), and raises InterpreterError::IfConditionTypeError + otherwise. Side effects in `condition` (sends/news/receives) are inert + because the evaluator only walks par.exprs. + """ + + condition: "Par | None" = betterproto2.field(1, betterproto2.TYPE_MESSAGE, optional=True) + + if_true: "Par | None" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, optional=True) + + if_false: "Par | None" = betterproto2.field(3, betterproto2.TYPE_MESSAGE, optional=True) + + locally_free: "bytes" = betterproto2.field(4, betterproto2.TYPE_BYTES) + + connective_used: "bool" = betterproto2.field(5, betterproto2.TYPE_BOOL) + + +default_message_pool.register_message("rhoapi", "If", If) + + @dataclass(eq=False, repr=False) class KeyValuePair(betterproto2.Message): key: "Par | None" = betterproto2.field(1, betterproto2.TYPE_MESSAGE, optional=True) @@ -677,6 +825,15 @@ class MatchCase(betterproto2.Message): free_count: "int" = betterproto2.field(3, betterproto2.TYPE_INT32) + guard: "Par | None" = betterproto2.field(4, betterproto2.TYPE_MESSAGE, optional=True) + """ + Optional per-case `where`-clause guard. When non-empty, the case only + fires if the pattern matches AND the guard evaluates to GBool(true) + under the pattern's bindings. Anything else (false, non-bool, error) + falls through to the next case. An empty Par means no guard. See plan + §3.4 / §3.8. + """ + default_message_pool.register_message("rhoapi", "MatchCase", MatchCase) @@ -692,14 +849,16 @@ class New(betterproto2.Message): bind_count: "int" = betterproto2.field(1, betterproto2.TYPE_SINT32) """ - Includes any uris listed below. This makes it easier to substitute or walk a term. + Includes any uris listed below. This makes it easier to substitute or walk + a term. """ p: "Par | None" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, optional=True) uri: "list[str]" = betterproto2.field(3, betterproto2.TYPE_STRING, repeated=True) """ - For normalization, uri-referenced variables come at the end, and in lexicographical order. + For normalization, uri-referenced variables come at the end, and in + lexicographical order. """ injections: "dict[str, Par]" = betterproto2.field( @@ -743,6 +902,8 @@ class Par(betterproto2.Message): connectives: "list[Connective]" = betterproto2.field(8, betterproto2.TYPE_MESSAGE, repeated=True) + conditionals: "list[If]" = betterproto2.field(12, betterproto2.TYPE_MESSAGE, repeated=True) + locally_free: "bytes" = betterproto2.field(9, betterproto2.TYPE_BYTES) connective_used: "bool" = betterproto2.field(10, betterproto2.TYPE_BOOL) @@ -805,6 +966,14 @@ class Receive(betterproto2.Message): connective_used: "bool" = betterproto2.field(7, betterproto2.TYPE_BOOL) + condition: "Par | None" = betterproto2.field(8, betterproto2.TYPE_MESSAGE, optional=True) + """ + Optional `where`-clause guard. When non-empty, the receive only commits + (consumes the matched messages) if every spatial pattern matches AND + the condition evaluates to GBool(true) under the bound variables. An + empty Par means no guard. See plan §3.3 / §3.5. + """ + default_message_pool.register_message("rhoapi", "Receive", Receive) @@ -862,6 +1031,15 @@ class TaggedContinuation(betterproto2.Message): scala_body_ref: "int | None" = betterproto2.field(2, betterproto2.TYPE_INT64, optional=True, group="tagged_cont") + guard: "Par | None" = betterproto2.field(3, betterproto2.TYPE_MESSAGE, optional=True) + """ + Optional `where`-clause guard, lifted from `Receive.condition` when the + continuation is registered with rspace. The matcher coordinator evaluates + it (via rho-pure-eval) against the *combined* bindings of all binds after + every spatial pattern has matched, so cross-channel guards see every + bound variable. Empty Par = no guard. See plan §7.12. + """ + default_message_pool.register_message("rhoapi", "TaggedContinuation", TaggedContinuation) diff --git a/packages/embers/tests/protobuf/scalapb/__init__.py b/packages/embers/tests/protobuf/scalapb/__init__.py index 0e5d9931..3c2ebdb7 100644 --- a/packages/embers/tests/protobuf/scalapb/__init__.py +++ b/packages/embers/tests/protobuf/scalapb/__init__.py @@ -1,5 +1,5 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! -# sources: protobuf_external/scalapb/scalapb.proto +# sources: scalapb/scalapb.proto # plugin: python-betterproto2 # This file has been @generated @@ -286,6 +286,21 @@ class MessageOptions(betterproto2.Message): Additional classes and traits to mix in to generated sealed oneof base trait's companion object. """ + derives: "list[str]" = betterproto2.field(11, betterproto2.TYPE_STRING, repeated=True) + """ + Adds a derives clause to the message case class + """ + + sealed_oneof_derives: "list[str]" = betterproto2.field(12, betterproto2.TYPE_STRING, repeated=True) + """ + Additional classes and traits to add to the derives clause of a sealed oneof. + """ + + sealed_oneof_empty_extends: "list[str]" = betterproto2.field(13, betterproto2.TYPE_STRING, repeated=True) + """ + Additional traits to mixin for the empty case object of sealed oneofs. + """ + default_message_pool.register_message("scalapb", "MessageOptions", MessageOptions) @@ -485,6 +500,16 @@ class ScalaPbOptions(betterproto2.Message): If true, getters will be generated. """ + scala3_sources: "bool" = betterproto2.field(28, betterproto2.TYPE_BOOL) + """ + Generate sources that are compatible with Scala 3 + """ + + public_constructor_parameters: "bool" = betterproto2.field(29, betterproto2.TYPE_BOOL) + """ + Makes constructor parameters public, including defaults and TypeMappers. + """ + test_only_no_java_conversions: "bool" = betterproto2.field(999, betterproto2.TYPE_BOOL) """ For use in tests only. Inhibit Java conversions even when when generator parameters @@ -505,7 +530,8 @@ class ScalaPbOptionsAuxEnumOptions(betterproto2.Message): target: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) """ - The fully-qualified name of the enum in the proto name space. + The fully-qualified name of the enum in the proto name space. Set to `*` to apply to + all enums in scope. """ options: "EnumOptions | None" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, optional=True) @@ -528,7 +554,8 @@ class ScalaPbOptionsAuxEnumValueOptions(betterproto2.Message): target: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) """ - The fully-qualified name of the enum value in the proto name space. + The fully-qualified name of the enum value in the proto name space. Set to `*` to apply + to all enum values in scope. """ options: "EnumValueOptions | None" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, optional=True) @@ -553,7 +580,8 @@ class ScalaPbOptionsAuxFieldOptions(betterproto2.Message): target: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) """ - The fully-qualified name of the field in the proto name space. + The fully-qualified name of the field in the proto name space. Set to `*` to apply to all + fields in scope. """ options: "FieldOptions | None" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, optional=True) @@ -576,7 +604,8 @@ class ScalaPbOptionsAuxMessageOptions(betterproto2.Message): target: "str" = betterproto2.field(1, betterproto2.TYPE_STRING) """ - The fully-qualified name of the message in the proto name space. + The fully-qualified name of the message in the proto name space. Set to `*` to apply to all + messages in scope. """ options: "MessageOptions | None" = betterproto2.field(2, betterproto2.TYPE_MESSAGE, optional=True) diff --git a/packages/events-sync/src/main.rs b/packages/events-sync/src/main.rs index cf8212f7..fb09c33c 100644 --- a/packages/events-sync/src/main.rs +++ b/packages/events-sync/src/main.rs @@ -11,7 +11,7 @@ use base64::prelude::BASE64_STANDARD; use clap::{Parser, Subcommand}; use contracts::{rho_init_events_channels, rho_subscribe_to_service, rho_unsubscribe_from_service}; use firefly_client::CommunicationService; -use firefly_client::models::{BlockId, DeployData}; +use firefly_client::models::{BlockId, DeployData, DeployId}; use futures::stream::select_all; use futures::{FutureExt, SinkExt, Stream, StreamExt, TryStreamExt, future}; use secp256k1::SecretKey; @@ -34,10 +34,6 @@ struct Args { #[arg(long)] deploy_service_url: String, - /// Firefly propose service url - #[arg(long)] - propose_service_url: String, - /// Globally unique service identifier #[arg(long)] service_id: String, @@ -90,10 +86,9 @@ enum Commands { async fn main() -> anyhow::Result<()> { let args = Args::parse(); - let mut client = - firefly_client::WriteNodeClient::new(args.deploy_service_url, args.propose_service_url) - .await - .context("failed to create firefly client")?; + let mut client = firefly_client::WriteNodeClient::new(args.deploy_service_url) + .await + .context("failed to create firefly client")?; match args.command { Commands::Listen { @@ -179,16 +174,16 @@ async fn main() -> anyhow::Result<()> { println!("events: {}", events.len()); let rho_code = rho_save_events(channel_name, &events)?; let deploy_data = DeployData::builder(rho_code).build(); - let hash = client + let deploy_id = client .full_deploy(&args.wallet_key, deploy_data) .await .context("failed save events")?; - println!("events deployed"); + println!("events deployed: {deploy_id}"); let rho_code = rho_notify_listeners( &args.service_id, &NotifyMsg { - block_hash: hash, + deploy_id, channel_name: channel_name.to_string(), }, ); @@ -211,11 +206,11 @@ async fn main() -> anyhow::Result<()> { Commands::Init => { let rho_code = rho_init_events_channels(&args.service_id); let deploy_data = DeployData::builder(rho_code).build(); - let hash = client + let deploy_id = client .full_deploy(&args.wallet_key, deploy_data) .await .context("failed to init channels")?; - println!("{hash}"); + println!("{deploy_id}"); } } @@ -249,7 +244,7 @@ struct Entry { #[derive(Debug, Clone, Serialize, Deserialize)] struct NotifyMsg { - block_hash: BlockId, + deploy_id: DeployId, channel_name: String, } @@ -373,8 +368,11 @@ async fn subscribe_to_firefly( Ok(async_stream::stream! { while let Some(event) = rx_updates.recv().await { + // NOTE: With auto-propose, deploy_id is no longer a valid block hash. + // events-sync needs redesign to obtain the block hash from NodeEvents. + let block_hash: BlockId = Into::::into(event.deploy_id).into(); let bytes: Vec = client - .get_channel_value(event.block_hash, event.channel_name) + .get_channel_value(block_hash, event.channel_name) .await .context("failed to get events")?; diff --git a/packages/firefly-client/Cargo.toml b/packages/firefly-client/Cargo.toml index 0ba06321..b727450b 100644 --- a/packages/firefly-client/Cargo.toml +++ b/packages/firefly-client/Cargo.toml @@ -19,6 +19,7 @@ crc = { version = "3.4" } dashmap = { version = "6.1" } derive_more = { version = "2.1", features = ["full"] } digest = { version = "0.10" } +f1r3node-models = { package = "models", git = "https://github.com/F1R3FLY-io/f1r3node-rust.git", rev = "91b5c70a0740f91c3ba2a414af3e29fe830263c3" } firefly-client-macros = { path = "../firefly-client-macros" } futures = { version = "0.3" } hex = { version = "0.4" } @@ -30,7 +31,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } sha3 = { version = "0.10" } thiserror = { version = "2.0" } -tokio = { version = "1.49" } +tokio = { version = "1.49", features = ["time"] } tokio-stream = { version = "0.1", features = ["sync"] } tokio-tungstenite = { version = "0.28" } tonic = { version = "0.14" } @@ -39,8 +40,9 @@ tracing = { version = "0.1" } uuid = { version = "1.20", features = ["v7"] } zbase32 = { version = "0.1" } -[build-dependencies] -tonic-prost-build = { version = "0.14" } +[dev-dependencies] +tokio = { version = "1.49", features = ["rt-multi-thread", "macros", "time", "test-util", "net"] } +wiremock = { version = "0.6" } [lints.clippy] cast_possible_wrap = "allow" diff --git a/packages/firefly-client/build.rs b/packages/firefly-client/build.rs deleted file mode 100644 index 34e1a7f8..00000000 --- a/packages/firefly-client/build.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::io::Result; - -fn main() -> Result<()> { - tonic_prost_build::configure() - .build_client(true) - .build_server(true) - .compile_protos( - &[ - "protobuf/DeployServiceV1.proto", - "protobuf/ProposeServiceV1.proto", - "protobuf/ExternalCommunicationServiceV1.proto", - ], - &["protobuf/", "protobuf/protobuf_external"], - ) -} diff --git a/packages/firefly-client/protobuf/CasperMessage.proto b/packages/firefly-client/protobuf/CasperMessage.proto deleted file mode 100644 index c8cd2f98..00000000 --- a/packages/firefly-client/protobuf/CasperMessage.proto +++ /dev/null @@ -1,271 +0,0 @@ -/** - * The main API is `DeployService`. - */ -syntax = "proto3"; - -package casper; - -// If you are building for other languages "scalapb.proto" -// can be manually obtained here: -// https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto -// make a scalapb directory in this file's location and place it inside - -import "scalapb/scalapb.proto"; -import "RhoTypes.proto"; - -option (scalapb.options) = { - package_name: "coop.rchain.casper.protocol" - flat_package: true - single_file: true - preamble: "sealed trait CasperMessageProto" - preserve_unknown_fields: false -}; - -message HasBlockRequestProto { - option (scalapb.message).extends = "CasperMessageProto"; - bytes hash = 1; -} - -message HasBlockProto { - option (scalapb.message).extends = "CasperMessageProto"; - bytes hash = 1; -} - -message BlockRequestProto { - option (scalapb.message).extends = "CasperMessageProto"; - bytes hash = 1; -} - -message ForkChoiceTipRequestProto { - option (scalapb.message).extends = "CasperMessageProto"; -} - -// ---------- Signing Protocol --------- -message ApprovedBlockCandidateProto { - option (scalapb.message).extends = "CasperMessageProto"; - BlockMessageProto block = 1 [(scalapb.field).no_box = true]; - int32 requiredSigs = 2; -} - -message UnapprovedBlockProto { - option (scalapb.message).extends = "CasperMessageProto"; - ApprovedBlockCandidateProto candidate = 1 [(scalapb.field).no_box = true]; - int64 timestamp = 2; - int64 duration = 3; -} - -message Signature { - bytes publicKey = 1; - string algorithm = 2; - bytes sig = 3; -} - -message BlockApprovalProto { - option (scalapb.message).extends = "CasperMessageProto"; - ApprovedBlockCandidateProto candidate = 1 [(scalapb.field).no_box = true]; - Signature sig = 2 [(scalapb.field).no_box = true]; -} - -message ApprovedBlockProto { - option (scalapb.message).extends = "CasperMessageProto"; - ApprovedBlockCandidateProto candidate = 1 [(scalapb.field).no_box = true]; - repeated Signature sigs = 2; -} - -message ApprovedBlockRequestProto { - option (scalapb.message).extends = "CasperMessageProto"; - string identifier = 1; - bool trimState = 2; -} - -message NoApprovedBlockAvailableProto { - option (scalapb.message).extends = "CasperMessageProto"; - string identifier = 1; - string nodeIdentifer = 2; -} - -// ------- End Signing Protocol -------- - -// --------- Core Protocol -------- -message BlockMessageProto { - option (scalapb.message).extends = "CasperMessageProto"; - bytes blockHash = 1; // obtained by hashing the information in the header - HeaderProto header = 2 [(scalapb.field).no_box = true]; - BodyProto body = 3 [(scalapb.field).no_box = true]; - repeated JustificationProto justifications = 4; // map of all validators to latest blocks based on current view - bytes sender = 5; // public key of the validator that created the block - int32 seqNum = 6; // number of blocks created by the validator - bytes sig = 7; // signature generated by signing `hash(hash(justification) concat blockHash)`. - string sigAlgorithm = 8; // name of the algorithm used to sign - string shardId = 9; // identifier of the shard where the block was created - bytes extraBytes = 10; -} - -message BlockHashMessageProto { - option (scalapb.message).extends = "CasperMessageProto"; - bytes hash = 1; - bytes blockCreator = 2; -} - -message BlockMetadataInternal { - // This message in mapped to a different Scala class because of protobuf's inability to create map for - // bonds. - option (scalapb.message).type = "coop.rchain.models.BlockMetadata"; - - bytes blockHash = 1; - repeated bytes parents = 2 [(scalapb.field).collection_type="collection.immutable.List"]; - bytes sender = 3; - repeated JustificationProto justifications = 4 [(scalapb.field).collection_type="collection.immutable.List"]; - repeated BondProto bonds = 5 [(scalapb.field).collection_type="collection.immutable.List"]; - int64 blockNum = 6; - int32 seqNum = 7; - bool invalid = 8; // whether the block was marked as invalid - bool directlyFinalized = 9; // whether the block has been last finalized block (LFB) - bool finalized = 10;// whether the block is finalized -} - -message HeaderProto { - repeated bytes parentsHashList = 1; //list of parent block hashes - int64 timestamp = 5; - int64 version = 6; - bytes extraBytes = 7; -} - -/** - * Note: deploys are uniquely keyed by `user`, `timestamp`. - * - * **TODO**: details of signatures and payment. See RHOL-781 - */ -message DeployDataProto { - bytes deployer = 1; //public key - string term = 2; //rholang source code to deploy (will be parsed into `Par`) - int64 timestamp = 3; //millisecond timestamp - bytes sig = 4; //signature of (hash(term) + timestamp) using private key - string sigAlgorithm = 5; //name of the algorithm used to sign - int64 phloPrice = 7; //phlo price - int64 phloLimit = 8; //phlo limit for the deployment - int64 validAfterBlockNumber = 10; - string shardId = 11;//shard ID to prevent replay of deploys between shards -} - -message ProcessedDeployProto { - DeployDataProto deploy = 1 [(scalapb.field).no_box = true]; - rhoapi.PCost cost = 2 [(scalapb.field).no_box = true]; - repeated EventProto deployLog = 3; //the new terms and comm. rule reductions from this deploy - bool errored = 5; //true if deploy encountered a user error - string systemDeployError = 6; -} - -message SlashSystemDeployDataProto { - bytes invalidBlockHash = 1; - bytes issuerPublicKey = 2; -} - -message CloseBlockSystemDeployDataProto{ -} - -message SystemDeployDataProto{ - oneof systemDeploy{ - SlashSystemDeployDataProto slashSystemDeploy = 1; - CloseBlockSystemDeployDataProto closeBlockSystemDeploy = 2; - } -} - -message ProcessedSystemDeployProto { - SystemDeployDataProto systemDeploy = 1 [(scalapb.field).no_box = true]; - repeated EventProto deployLog = 2; - string errorMsg = 3; -} - -message BodyProto { - RChainStateProto state = 1 [(scalapb.field).no_box = true]; - repeated ProcessedDeployProto deploys = 2; - repeated ProcessedSystemDeployProto systemDeploys = 3; - bytes extraBytes = 4; - repeated RejectedDeployProto rejectedDeploys = 5; -} - -message RejectedDeployProto{ - bytes sig = 1; -} - -message JustificationProto { - bytes validator = 1; - bytes latestBlockHash = 2; -} - -message RChainStateProto { - bytes preStateHash = 1; //hash of the tuplespace contents before new deploys - bytes postStateHash = 2; //hash of the tuplespace contents after new deploys - - //Internals of what will be the "blessed" PoS contract - //(which will be part of the tuplespace in the real implementation). - repeated BondProto bonds = 3; - int64 blockNumber = 4; -} - -message EventProto { - oneof event_instance { - ProduceEventProto produce = 1; - ConsumeEventProto consume = 2; - CommEventProto comm = 3; - } -} - -message ProduceEventProto { - bytes channelsHash = 1; - bytes hash = 2; - bool persistent = 3; - int32 timesRepeated = 4; -} - -message ConsumeEventProto { - repeated bytes channelsHashes = 1; - bytes hash = 2; - bool persistent = 3; -} - -message CommEventProto { - ConsumeEventProto consume = 1 [(scalapb.field).no_box = true]; - repeated ProduceEventProto produces = 2; - repeated PeekProto peeks = 3; -} - -message PeekProto { - int32 channelIndex = 1; -} - -message BondProto { - bytes validator = 1; - int64 stake = 2; -} -// --------- End Core Protocol -------- - -// --------- Last finalized state -------- - -message StoreNodeKeyProto { - bytes hash = 1; - int32 index = 2; -} - -message StoreItemsMessageRequestProto { - option (scalapb.message).extends = "CasperMessageProto"; - repeated StoreNodeKeyProto startPath = 1 [(scalapb.field).collection_type="collection.immutable.List"]; - int32 skip = 2; - int32 take = 3; -} - -message StoreItemProto { - bytes key = 1; - bytes value = 2; -} - -message StoreItemsMessageProto { - option (scalapb.message).extends = "CasperMessageProto"; - repeated StoreNodeKeyProto startPath = 1 [(scalapb.field).collection_type="collection.immutable.List"]; - repeated StoreNodeKeyProto lastPath = 2 [(scalapb.field).collection_type="collection.immutable.List"]; - repeated StoreItemProto historyItems = 3 [(scalapb.field).collection_type="collection.immutable.List"]; - repeated StoreItemProto dataItems = 4 [(scalapb.field).collection_type="collection.immutable.List"]; -} - -// --------- End Last finalized state -------- diff --git a/packages/firefly-client/protobuf/DeployServiceCommon.proto b/packages/firefly-client/protobuf/DeployServiceCommon.proto deleted file mode 100644 index cf322659..00000000 --- a/packages/firefly-client/protobuf/DeployServiceCommon.proto +++ /dev/null @@ -1,244 +0,0 @@ -/** - * The main API is `DeployService`. - */ -syntax = "proto3"; - -package casper; - -import "CasperMessage.proto"; - -// If you are building for other languages "scalapb.proto" -// can be manually obtained here: -// https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto -// make a scalapb directory in this file's location and place it inside - -import "scalapb/scalapb.proto"; -import "RhoTypes.proto"; - -option (scalapb.options) = { - package_name: "coop.rchain.casper.protocol" - flat_package: true - single_file: true - preamble: "sealed trait ReportEventProto" - preserve_unknown_fields: false -}; - -message FindDeployQuery { - bytes deployId = 1; -} - -message BlockQuery { - string hash = 1; -} - -message ReportQuery{ - string hash = 1; - bool forceReplay = 2; -} - -message BlocksQuery { - int32 depth = 1; -} - -message BlocksQueryByHeight{ - int64 startBlockNumber = 1; - int64 endBlockNumber = 2; -} - -message DataAtNameQuery { - int32 depth = 1; - rhoapi.Par name = 2 [(scalapb.field).no_box = true]; -} - -message DataAtNameByBlockQuery { - rhoapi.Par par = 1 [(scalapb.field).no_box = true]; - string blockHash = 2; - bool usePreStateHash = 3; -} - -message ContinuationAtNameQuery { - int32 depth = 1; - repeated rhoapi.Par names = 2; -} - -message VisualizeDagQuery { - int32 depth = 1; - bool showJustificationLines = 2; - int32 startBlockNumber = 3; -} - -message MachineVerifyQuery { -} - -message PrivateNamePreviewQuery { - bytes user = 1; // public key a la DeployData - int64 timestamp = 2; // millisecond timestamp - int32 nameQty = 3; // how many names to preview? (max: 1024) -} - -message LastFinalizedBlockQuery { -} - -message IsFinalizedQuery { - string hash = 1; -} - -message BondStatusQuery { - bytes publicKey = 1; -} - -message ExploratoryDeployQuery{ - string term = 1; - string blockHash = 2; - bool usePreStateHash = 3; -} - -message BondInfo{ - string validator = 1; - int64 stake = 2; -} - -message JustificationInfo{ - string validator = 1; - string latestBlockHash = 2; -} - -message DeployInfo{ - string deployer = 1; - string term = 2; - int64 timestamp = 3; - string sig = 4; - string sigAlgorithm = 5; - int64 phloPrice = 7; - int64 phloLimit = 8; - int64 validAfterBlockNumber = 9; - uint64 cost = 10; - bool errored = 11; - string systemDeployError = 12; -} - -message LightBlockInfo { - // BlockMessageProto message - string blockHash = 1; - string sender = 2; - int64 seqNum = 3; - string sig = 4; - string sigAlgorithm = 5; - string shardId = 6; - bytes extraBytes = 7; - - // HeaderProto message - int64 version = 8; - int64 timestamp = 9; - bytes headerExtraBytes = 10; - repeated string parentsHashList = 11; - - // BodyProto message - int64 blockNumber = 12; - string preStateHash = 13; - string postStateHash = 14; - bytes bodyExtraBytes = 15; - repeated BondInfo bonds = 16; - - // extra - string blockSize = 17; - int32 deployCount = 18; - float faultTolerance = 19; - - repeated JustificationInfo justifications = 20; - repeated RejectedDeployInfo rejectedDeploys = 21; -} - -message RejectedDeployInfo{ - string sig = 1; -} - -// For node clients, see BlockMessage for actual Casper protocol Block representation -message BlockInfo { - LightBlockInfo blockInfo = 1 [(scalapb.field).no_box = true]; - repeated DeployInfo deploys = 2; -} - -message DataWithBlockInfo { - repeated rhoapi.Par postBlockData = 1; - LightBlockInfo block = 2 [(scalapb.field).no_box = true]; -} - -message ContinuationsWithBlockInfo { - repeated WaitingContinuationInfo postBlockContinuations = 1; - LightBlockInfo block = 2 [(scalapb.field).no_box = true]; -} - -message WaitingContinuationInfo { - repeated rhoapi.BindPattern postBlockPatterns = 1; - rhoapi.Par postBlockContinuation = 2 [(scalapb.field).no_box = true]; -} - - -message ReportProduceProto{ - option (scalapb.message).extends = "ReportEventProto"; - - rhoapi.Par channel = 1 [(scalapb.field).no_box = true]; - rhoapi.ListParWithRandom data = 2 [(scalapb.field).no_box = true]; -} - -message ReportConsumeProto{ - option (scalapb.message).extends = "ReportEventProto"; - - repeated rhoapi.Par channels = 1; - repeated rhoapi.BindPattern patterns = 2; - // disable because can not work and it is not important actually -// TaggedContinuation continuation=3; - repeated PeekProto peeks = 4; -} - -message ReportCommProto{ - option (scalapb.message).extends = "ReportEventProto"; - - ReportConsumeProto consume = 1 [(scalapb.field).no_box = true]; - repeated ReportProduceProto produces = 2; -} - -message ReportProto{ - oneof report { - ReportProduceProto produce = 1; - ReportConsumeProto consume = 2; - ReportCommProto comm = 3; - } -} - -message SingleReport{ - repeated ReportProto events = 1; -} - -message DeployInfoWithEventData{ - DeployInfo deployInfo = 1 [(scalapb.field).no_box = true]; - repeated SingleReport report = 2; -} - -message SystemDeployInfoWithEventData{ - SystemDeployDataProto systemDeploy = 1 [(scalapb.field).no_box = true]; - repeated SingleReport report = 2; -} - -message BlockEventInfo{ - LightBlockInfo blockInfo = 1 [(scalapb.field).no_box = true]; - repeated DeployInfoWithEventData deploys = 2; - repeated SystemDeployInfoWithEventData systemDeploys = 3; - bytes postStateHash = 4; -} - -message Status { - VersionInfo version = 1 [(scalapb.field).no_box = true]; - string address = 2; - string networkId = 3; - string shardId = 4; - int32 peers = 5; - int32 nodes = 6; - int64 minPhloPrice = 7; -} - -message VersionInfo { - string api = 1; - string node = 2; -} diff --git a/packages/firefly-client/protobuf/DeployServiceV1.proto b/packages/firefly-client/protobuf/DeployServiceV1.proto deleted file mode 100644 index f46300c3..00000000 --- a/packages/firefly-client/protobuf/DeployServiceV1.proto +++ /dev/null @@ -1,215 +0,0 @@ -/** - * The main API is `DeployService`. - */ -syntax = "proto3"; - -package casper.v1; - -import "CasperMessage.proto"; -import "ServiceError.proto"; -import "DeployServiceCommon.proto"; - -// If you are building for other languages "scalapb.proto" -// can be manually obtained here: -// https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto -// make a scalapb directory in this file's location and place it inside - -import "scalapb/scalapb.proto"; -import "RhoTypes.proto"; - -import "google/protobuf/empty.proto"; - -option (scalapb.options) = { - package_name: "coop.rchain.casper.protocol.deploy.v1" - flat_package: true - single_file: true - preserve_unknown_fields: false -}; - -// Use `doDeploy` to queue deployments of Rholang code and then -// `ProposeServiceV2.propose` to make a new block with the results of running them -// all. -// -// To get results back, use `listenForDataAtName`. -service DeployService { - // Queue deployment of Rholang code (or fail to parse). - rpc doDeploy(DeployDataProto) returns (DeployResponse) {} - // Get details about a particular block. - rpc getBlock(BlockQuery) returns (BlockResponse) {} - // Get dag - rpc visualizeDag(VisualizeDagQuery) returns (stream VisualizeBlocksResponse) {} - rpc machineVerifiableDag(MachineVerifyQuery) returns (MachineVerifyResponse) {} - // Returns on success LightBlockInfo - rpc showMainChain(BlocksQuery) returns (stream BlockInfoResponse) {} - // Get a summary of blocks on the blockchain. - rpc getBlocks(BlocksQuery) returns (stream BlockInfoResponse) {} - // Find data sent to a name. - // OBSOLETE: Use getDataAtName instead. This method will be removed in the future version of RNode. - rpc listenForDataAtName(DataAtNameQuery) returns (ListeningNameDataResponse) {} - // Find data sent to a name. - rpc getDataAtName(DataAtNameByBlockQuery) returns (RhoDataResponse) {} - // Find processes receiving on a name. - rpc listenForContinuationAtName(ContinuationAtNameQuery) returns (ContinuationAtNameResponse) {} - // Find block containing a deploy. - rpc findDeploy(FindDeployQuery) returns (FindDeployResponse) {} - // Preview new top-level unforgeable names (for example, to compute signatures over them). - rpc previewPrivateNames(PrivateNamePreviewQuery) returns (PrivateNamePreviewResponse) {} - // Get details about a particular block. - rpc lastFinalizedBlock(LastFinalizedBlockQuery) returns (LastFinalizedBlockResponse) {} - // Check if a given block is finalized. - rpc isFinalized(IsFinalizedQuery) returns (IsFinalizedResponse) {} - // Check if a given validator is bonded. - // Returns on success BondStatusResponse - rpc bondStatus(BondStatusQuery) returns (BondStatusResponse) {} - // Executes deploy as user deploy with immediate rollback and return result - rpc exploratoryDeploy(ExploratoryDeployQuery) returns (ExploratoryDeployResponse) {} - // get blocks by block height - rpc getBlocksByHeights(BlocksQueryByHeight) returns (stream BlockInfoResponse){} - // temporary api for testing - rpc getEventByHash(ReportQuery) returns (EventInfoResponse){} - // Get node status - rpc status(google.protobuf.Empty) returns (StatusResponse){} -} - -message EventInfoResponse{ - oneof message{ - servicemodelapi.ServiceError error = 1; - BlockEventInfo result = 2; - } -} - -message ExploratoryDeployResponse{ - oneof message{ - servicemodelapi.ServiceError error = 1; - DataWithBlockInfo result = 2; - } -} - -// doDeploy -message DeployResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - string result = 2; - } -} - -// getBlock -message BlockResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - BlockInfo blockInfo = 2; - } -} - -// visualizeDag -message VisualizeBlocksResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - string content = 2; - } -} - -// machineVerifiableDag -message MachineVerifyResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - string content = 2; - } -} - -// showMainChain & getBlocks -message BlockInfoResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - LightBlockInfo blockInfo = 2; - } -} - -// listenForDataAtName -message ListeningNameDataResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - ListeningNameDataPayload payload = 2; - } -} - -message ListeningNameDataPayload { - repeated DataWithBlockInfo blockInfo = 1; - int32 length = 2; -} - -// listenForDataAtPar -message RhoDataResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - RhoDataPayload payload = 2; - } -} - -message RhoDataPayload { - repeated rhoapi.Par par = 1; - LightBlockInfo block = 2 [(scalapb.field).no_box = true]; -} - -// listenForContinuationAtName -message ContinuationAtNameResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - ContinuationAtNamePayload payload = 2; - } -} - -message ContinuationAtNamePayload { - repeated ContinuationsWithBlockInfo blockResults = 1; - int32 length = 2; -} - -// findDeploy -message FindDeployResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - LightBlockInfo blockInfo = 2; - } -} - -// previewPrivateNames -message PrivateNamePreviewResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - PrivateNamePreviewPayload payload = 2; - } -} - -message PrivateNamePreviewPayload { - repeated bytes ids = 1; // a la GPrivate -} - -// lastFinalizedBlock -message LastFinalizedBlockResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - BlockInfo blockInfo = 2; - } -} - -// isFinalized -message IsFinalizedResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - bool isFinalized = 2; - } -} - -message BondStatusResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - bool isBonded = 2; - } -} - -message StatusResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - Status status = 2; - } -} diff --git a/packages/firefly-client/protobuf/ExternalCommunicationServiceCommon.proto b/packages/firefly-client/protobuf/ExternalCommunicationServiceCommon.proto deleted file mode 100644 index 0eb56cea..00000000 --- a/packages/firefly-client/protobuf/ExternalCommunicationServiceCommon.proto +++ /dev/null @@ -1,24 +0,0 @@ -syntax = "proto3"; -package casper; - -// If you are building for other languages "scalapb.proto" -// can be manually obtained here: -// https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto -// make a scalapb directory in this file's location and place it inside - -import "scalapb/scalapb.proto"; - -option (scalapb.options) = { - package_name: "coop.rchain.casper.clients" - flat_package: true - single_file: true - preserve_unknown_fields: false -}; - -message UpdateNotification { - string clientHost = 1; - int32 clientPort = 2; - string payload = 3; -} - -message UpdateNotificationResponse {} diff --git a/packages/firefly-client/protobuf/ExternalCommunicationServiceV1.proto b/packages/firefly-client/protobuf/ExternalCommunicationServiceV1.proto deleted file mode 100644 index ba56116e..00000000 --- a/packages/firefly-client/protobuf/ExternalCommunicationServiceV1.proto +++ /dev/null @@ -1,30 +0,0 @@ -syntax = "proto3"; -package casper.v1; - -import "ServiceError.proto"; -import "ExternalCommunicationServiceCommon.proto"; - -// If you are building for other languages "scalapb.proto" -// can be manually obtained here: -// https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto -// make a scalapb directory in this file's location and place it inside - -import "scalapb/scalapb.proto"; - -option (scalapb.options) = { - package_name: "coop.rchain.casper.client.external.v1" - flat_package: true - single_file: true - preserve_unknown_fields: false -}; - -service ExternalCommunicationService { - rpc sendNotification(UpdateNotification) returns (UpdateNotificationResponse) {} -} - -message UpdateNotificationResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - string result = 2; - } -} diff --git a/packages/firefly-client/protobuf/ProposeServiceCommon.proto b/packages/firefly-client/protobuf/ProposeServiceCommon.proto deleted file mode 100644 index 6633f50b..00000000 --- a/packages/firefly-client/protobuf/ProposeServiceCommon.proto +++ /dev/null @@ -1,28 +0,0 @@ -syntax = "proto3"; -package casper; - -// If you are building for other languages "scalapb.proto" -// can be manually obtained here: -// https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto -// make a scalapb directory in this file's location and place it inside - -import "scalapb/scalapb.proto"; - -option (scalapb.options) = { - package_name: "coop.rchain.casper.protocol" - flat_package: true - single_file: true - preserve_unknown_fields: false -}; - -// TODO remove it -message PrintUnmatchedSendsQuery { - bool printUnmatchedSends = 1; -} - -message ProposeResultQuery{ -} - -message ProposeQuery{ - bool isAsync = 1; -} diff --git a/packages/firefly-client/protobuf/ProposeServiceV1.proto b/packages/firefly-client/protobuf/ProposeServiceV1.proto deleted file mode 100644 index 4e9d194c..00000000 --- a/packages/firefly-client/protobuf/ProposeServiceV1.proto +++ /dev/null @@ -1,38 +0,0 @@ -syntax = "proto3"; -package casper.v1; - -import "ServiceError.proto"; -import "ProposeServiceCommon.proto"; - -// If you are building for other languages "scalapb.proto" -// can be manually obtained here: -// https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto -// make a scalapb directory in this file's location and place it inside - -import "scalapb/scalapb.proto"; - -option (scalapb.options) = { - package_name: "coop.rchain.casper.protocol.propose.v1" - flat_package: true - single_file: true - preserve_unknown_fields: false -}; - -service ProposeService { - rpc propose(ProposeQuery) returns (ProposeResponse) {} - rpc proposeResult(ProposeResultQuery) returns (ProposeResultResponse) {} -} - -message ProposeResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - string result = 2; - } -} - -message ProposeResultResponse { - oneof message { - servicemodelapi.ServiceError error = 1; - string result = 2; - } -} diff --git a/packages/firefly-client/protobuf/RhoTypes.proto b/packages/firefly-client/protobuf/RhoTypes.proto deleted file mode 100644 index d36db87e..00000000 --- a/packages/firefly-client/protobuf/RhoTypes.proto +++ /dev/null @@ -1,423 +0,0 @@ -/** - * Rholang Term Structure - * - * The top level is `Par`. - */ -syntax = "proto3"; - -package rhoapi; - -// If you are building for other languages "scalapb.proto" -// can be manually obtained here: -// https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto -// make a scalapb directory in this file's location and place it inside - -import "scalapb/scalapb.proto"; - -option (scalapb.options) = { - package_name: "coop.rchain.models" - import: "coop.rchain.models.BitSetBytesMapper.bitSetBytesMapper" - import: "coop.rchain.models.ParSetTypeMapper.parSetESetTypeMapper" - import: "coop.rchain.models.ParMapTypeMapper.parMapEMapTypeMapper" - preserve_unknown_fields: false -}; - -/** - * Rholang process - * - * For example, `@0!(1) | @2!(3) | for(x <- @0) { Nil }` has two sends - * and one receive. - * - * The Nil process is a `Par` with no sends, receives, etc. - */ -message Par { - repeated Send sends = 1; - repeated Receive receives = 2; - repeated New news = 4; - repeated Expr exprs = 5; - repeated Match matches = 6; - repeated GUnforgeable unforgeables = 7; // unforgeable names - repeated Bundle bundles = 11; - repeated Connective connectives = 8; - bytes locallyFree = 9 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; - bool connective_used = 10; -} - -/** - * Either rholang code or code built in to the interpreter. - */ -message TaggedContinuation { - oneof tagged_cont { - ParWithRandom par_body = 1; - int64 scala_body_ref = 2; - } -} - -/** - * Rholang code along with the state of a split random number - * generator for generating new unforgeable names. - */ -message ParWithRandom { - Par body = 1 [(scalapb.field).no_box = true]; - bytes randomState = 2 [(scalapb.field).type = "coop.rchain.crypto.hash.Blake2b512Random"]; -} - -/** - * Cost of the performed operations. - */ -message PCost { - uint64 cost = 1; -} - -message ListParWithRandom { - repeated Par pars = 1; - bytes randomState = 2 [(scalapb.field).type = "coop.rchain.crypto.hash.Blake2b512Random"]; -} - -// While we use vars in both positions, when producing the normalized -// representation we need a discipline to track whether a var is a name or a -// process. -// These are DeBruijn levels -message Var { - message WildcardMsg {} - oneof var_instance { - sint32 bound_var = 1; - sint32 free_var = 2; - WildcardMsg wildcard = 3; - } -} - -/** - * Nothing can be received from a (quoted) bundle with `readFlag = false`. - * Likeise nothing can be sent to a (quoted) bundle with `writeFlag = false`. - * - * If both flags are set to false, bundle allows only for equivalance check. - */ -message Bundle { - Par body = 1 [(scalapb.field).no_box = true]; - bool writeFlag = 2; // flag indicating whether bundle is writeable - bool readFlag = 3; // flag indicating whether bundle is readable -} - -/** - * A send is written `chan!(data)` or `chan!!(data)` for a persistent send. - * - * Upon send, all free variables in data are substituted with their values. - */ -message Send { - Par chan = 1 [(scalapb.field).no_box = true]; - repeated Par data = 2; - bool persistent = 3; - bytes locallyFree = 5 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; - bool connective_used = 6; -} - -message ReceiveBind { - repeated Par patterns = 1; - Par source = 2 [(scalapb.field).no_box = true]; - Var remainder = 3; - int32 freeCount = 4; -} - -message BindPattern { - repeated Par patterns = 1; - Var remainder = 2; - int32 freeCount = 3; -} - -message ListBindPatterns { - repeated BindPattern patterns = 1; -} - -/** - * A receive is written `for(binds) { body }` - * i.e. `for(patterns <- source) { body }` - * or for a persistent recieve: `for(patterns <= source) { body }`. - * - * It's an error for free Variable to occur more than once in a pattern. - */ -message Receive { - repeated ReceiveBind binds = 1; - Par body = 2 [(scalapb.field).no_box = true]; - bool persistent = 3; - bool peek = 4; - int32 bindCount = 5; - bytes locallyFree = 6 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; - bool connective_used = 7; -} - -// Number of variables bound in the new statement. -// For normalized form, p should not contain solely another new. -// Also for normalized form, the first use should be level+0, next use level+1 -// up to level+count for the last used variable. -message New { - // Includes any uris listed below. This makes it easier to substitute or walk a term. - sint32 bindCount = 1; - Par p = 2 [(scalapb.field).no_box = true]; - // For normalization, uri-referenced variables come at the end, and in lexicographical order. - repeated string uri = 3; - map injections = 4; - bytes locallyFree = 5 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; -} - -message MatchCase { - Par pattern = 1 [(scalapb.field).no_box = true]; - Par source = 2 [(scalapb.field).no_box = true]; - int32 freeCount = 3; -} - -message Match { - Par target = 1 [(scalapb.field).no_box = true]; - repeated MatchCase cases = 2; - bytes locallyFree = 4 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; - bool connective_used = 5; -} - -// Any process may be an operand to an expression. -// Only processes equivalent to a ground process of compatible type will reduce. -message Expr { - oneof expr_instance { - bool g_bool = 1; - sint64 g_int = 2; - string g_string = 3; - string g_uri = 4; - bytes g_byte_array = 25; - - ENot e_not_body = 5; - ENeg e_neg_body = 6; - EMult e_mult_body = 7; - EDiv e_div_body = 8; - EPlus e_plus_body = 9; - EMinus e_minus_body = 10; - ELt e_lt_body = 11; - ELte e_lte_body = 12; - EGt e_gt_body = 13; - EGte e_gte_body = 14; - EEq e_eq_body = 15; - ENeq e_neq_body = 16; - EAnd e_and_body = 17; - EOr e_or_body = 18; - EVar e_var_body = 19; - - EList e_list_body = 20; - ETuple e_tuple_body = 21; - ESet e_set_body = 22 [(scalapb.field).type = "coop.rchain.models.ParSet"]; - EMap e_map_body = 23 [(scalapb.field).type = "coop.rchain.models.ParMap"]; - EMethod e_method_body = 24; - - EMatches e_matches_body = 27; - EPercentPercent e_percent_percent_body = 28; // string interpolation - EPlusPlus e_plus_plus_body = 29; // concatenation - EMinusMinus e_minus_minus_body = 30; // set difference - - EMod e_mod_body = 31; - } -} - -message EList { - repeated Par ps = 1; - bytes locallyFree = 3 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; - bool connective_used = 4; - Var remainder = 5; -} - -message ETuple { - repeated Par ps = 1; - bytes locallyFree = 3 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; - bool connective_used = 4; -} - -message ESet { - repeated Par ps = 1; - bytes locallyFree = 3 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; - bool connective_used = 4; - Var remainder = 5; -} - -message EMap { - repeated KeyValuePair kvs = 1; - bytes locallyFree = 3 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; - bool connective_used = 4; - Var remainder = 5; -} - -/** - * `target.method(arguments)` - */ -message EMethod { - string methodName = 1; - Par target = 2 [(scalapb.field).no_box = true]; - repeated Par arguments = 3; - bytes locallyFree = 5 [(scalapb.field).type = "coop.rchain.models.AlwaysEqual[scala.collection.immutable.BitSet]"]; - bool connective_used = 6; -} - -message KeyValuePair { - Par key = 1 [(scalapb.field).no_box = true]; - Par value = 2 [(scalapb.field).no_box = true]; -} - -// A variable used as a var should be bound in a process context, not a name -// context. For example: -// `for (@x <- c1; @y <- c2) { z!(x + y) }` is fine, but -// `for (x <- c1; y <- c2) { z!(x + y) }` should raise an error. -message EVar { - Var v = 1 [(scalapb.field).no_box = true]; -} - -message ENot { - Par p = 1 [(scalapb.field).no_box = true]; -} - -message ENeg { - Par p = 1 [(scalapb.field).no_box = true]; -} - -message EMult { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message EDiv { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message EMod { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message EPlus { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message EMinus { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message ELt { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message ELte { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message EGt { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message EGte { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message EEq { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message ENeq { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message EAnd { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message EOr { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message EMatches { - Par target = 1 [(scalapb.field).no_box = true]; - Par pattern = 2 [(scalapb.field).no_box = true]; -} - -/** - * String interpolation - * - * `"Hello, {name}" %% {"name": "Bob"}` denotes `"Hello, Bob"` - */ -message EPercentPercent { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -// Concatenation -message EPlusPlus { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -// Set difference -message EMinusMinus { - Par p1 = 1 [(scalapb.field).no_box = true]; - Par p2 = 2 [(scalapb.field).no_box = true]; -} - -message Connective { - oneof connective_instance { - ConnectiveBody conn_and_body = 1; - ConnectiveBody conn_or_body = 2; - Par conn_not_body = 3; - VarRef var_ref_body = 4; - bool conn_bool = 5; - bool conn_int = 6; - bool conn_string = 7; - bool conn_uri = 8; - bool conn_byte_array = 9; - } -} - -message VarRef { - sint32 index = 1; - sint32 depth = 2; -} - -message ConnectiveBody { - repeated Par ps = 1; -} - -message DeployId { - bytes sig = 1; -} - -message DeployerId { - bytes publicKey = 1; -} - -// Unforgeable names resulting from `new x { ... }` -// These should only occur as the program is being evaluated. There is no way in -// the grammar to construct them. -message GUnforgeable { - oneof unf_instance { - GPrivate g_private_body = 1; - GDeployId g_deploy_id_body = 2; - GDeployerId g_deployer_id_body = 3; - GSysAuthToken g_sys_auth_token_body = 4; - } -} - -message GPrivate { - bytes id = 1; -} - -message GDeployId { - bytes sig = 1; -} - -message GDeployerId { - bytes publicKey = 1; -} - -message GSysAuthToken {} diff --git a/packages/firefly-client/protobuf/ServiceError.proto b/packages/firefly-client/protobuf/ServiceError.proto deleted file mode 100644 index 561a6333..00000000 --- a/packages/firefly-client/protobuf/ServiceError.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax = "proto3"; - -package servicemodelapi; - -// If you are building for other languages "scalapb.proto" -// can be manually obtained here: -// https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto -// make a scalapb directory in this file's location and place it inside - -import "scalapb/scalapb.proto"; - -option (scalapb.options) = { - package_name: "coop.rchain.casper.protocol" - preserve_unknown_fields: false -}; - -message ServiceError { - repeated string messages = 1; -} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/any.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/any.proto deleted file mode 100644 index 6ed8a23c..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/any.proto +++ /dev/null @@ -1,158 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option go_package = "google.golang.org/protobuf/types/known/anypb"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "AnyProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; - -// `Any` contains an arbitrary serialized protocol buffer message along with a -// URL that describes the type of the serialized message. -// -// Protobuf library provides support to pack/unpack Any values in the form -// of utility functions or additional generated methods of the Any type. -// -// Example 1: Pack and unpack a message in C++. -// -// Foo foo = ...; -// Any any; -// any.PackFrom(foo); -// ... -// if (any.UnpackTo(&foo)) { -// ... -// } -// -// Example 2: Pack and unpack a message in Java. -// -// Foo foo = ...; -// Any any = Any.pack(foo); -// ... -// if (any.is(Foo.class)) { -// foo = any.unpack(Foo.class); -// } -// -// Example 3: Pack and unpack a message in Python. -// -// foo = Foo(...) -// any = Any() -// any.Pack(foo) -// ... -// if any.Is(Foo.DESCRIPTOR): -// any.Unpack(foo) -// ... -// -// Example 4: Pack and unpack a message in Go -// -// foo := &pb.Foo{...} -// any, err := anypb.New(foo) -// if err != nil { -// ... -// } -// ... -// foo := &pb.Foo{} -// if err := any.UnmarshalTo(foo); err != nil { -// ... -// } -// -// The pack methods provided by protobuf library will by default use -// 'type.googleapis.com/full.type.name' as the type URL and the unpack -// methods only use the fully qualified type name after the last '/' -// in the type URL, for example "foo.bar.com/x/y.z" will yield type -// name "y.z". -// -// -// JSON -// ==== -// The JSON representation of an `Any` value uses the regular -// representation of the deserialized, embedded message, with an -// additional field `@type` which contains the type URL. Example: -// -// package google.profile; -// message Person { -// string first_name = 1; -// string last_name = 2; -// } -// -// { -// "@type": "type.googleapis.com/google.profile.Person", -// "firstName": , -// "lastName": -// } -// -// If the embedded message type is well-known and has a custom JSON -// representation, that representation will be embedded adding a field -// `value` which holds the custom JSON in addition to the `@type` -// field. Example (for message [google.protobuf.Duration][]): -// -// { -// "@type": "type.googleapis.com/google.protobuf.Duration", -// "value": "1.212s" -// } -// -message Any { - // A URL/resource name that uniquely identifies the type of the serialized - // protocol buffer message. This string must contain at least - // one "/" character. The last segment of the URL's path must represent - // the fully qualified name of the type (as in - // `path/google.protobuf.Duration`). The name should be in a canonical form - // (e.g., leading "." is not accepted). - // - // In practice, teams usually precompile into the binary all types that they - // expect it to use in the context of Any. However, for URLs which use the - // scheme `http`, `https`, or no scheme, one can optionally set up a type - // server that maps type URLs to message definitions as follows: - // - // * If no scheme is provided, `https` is assumed. - // * An HTTP GET on the URL must yield a [google.protobuf.Type][] - // value in binary format, or produce an error. - // * Applications are allowed to cache lookup results based on the - // URL, or have them precompiled into a binary to avoid any - // lookup. Therefore, binary compatibility needs to be preserved - // on changes to types. (Use versioned type names to manage - // breaking changes.) - // - // Note: this functionality is not currently available in the official - // protobuf release, and it is not used for type URLs beginning with - // type.googleapis.com. - // - // Schemes other than `http`, `https` (or the empty scheme) might be - // used with implementation specific semantics. - // - string type_url = 1; - - // Must be a valid serialized protocol buffer of the above specified type. - bytes value = 2; -} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/api.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/api.proto deleted file mode 100644 index 3d598fc8..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/api.proto +++ /dev/null @@ -1,208 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -import "google/protobuf/source_context.proto"; -import "google/protobuf/type.proto"; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "ApiProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; -option go_package = "google.golang.org/protobuf/types/known/apipb"; - -// Api is a light-weight descriptor for an API Interface. -// -// Interfaces are also described as "protocol buffer services" in some contexts, -// such as by the "service" keyword in a .proto file, but they are different -// from API Services, which represent a concrete implementation of an interface -// as opposed to simply a description of methods and bindings. They are also -// sometimes simply referred to as "APIs" in other contexts, such as the name of -// this message itself. See https://cloud.google.com/apis/design/glossary for -// detailed terminology. -message Api { - // The fully qualified name of this interface, including package name - // followed by the interface's simple name. - string name = 1; - - // The methods of this interface, in unspecified order. - repeated Method methods = 2; - - // Any metadata attached to the interface. - repeated Option options = 3; - - // A version string for this interface. If specified, must have the form - // `major-version.minor-version`, as in `1.10`. If the minor version is - // omitted, it defaults to zero. If the entire version field is empty, the - // major version is derived from the package name, as outlined below. If the - // field is not empty, the version in the package name will be verified to be - // consistent with what is provided here. - // - // The versioning schema uses [semantic - // versioning](http://semver.org) where the major version number - // indicates a breaking change and the minor version an additive, - // non-breaking change. Both version numbers are signals to users - // what to expect from different versions, and should be carefully - // chosen based on the product plan. - // - // The major version is also reflected in the package name of the - // interface, which must end in `v`, as in - // `google.feature.v1`. For major versions 0 and 1, the suffix can - // be omitted. Zero major versions must only be used for - // experimental, non-GA interfaces. - // - // - string version = 4; - - // Source context for the protocol buffer service represented by this - // message. - SourceContext source_context = 5; - - // Included interfaces. See [Mixin][]. - repeated Mixin mixins = 6; - - // The source syntax of the service. - Syntax syntax = 7; -} - -// Method represents a method of an API interface. -message Method { - // The simple name of this method. - string name = 1; - - // A URL of the input message type. - string request_type_url = 2; - - // If true, the request is streamed. - bool request_streaming = 3; - - // The URL of the output message type. - string response_type_url = 4; - - // If true, the response is streamed. - bool response_streaming = 5; - - // Any metadata attached to the method. - repeated Option options = 6; - - // The source syntax of this method. - Syntax syntax = 7; -} - -// Declares an API Interface to be included in this interface. The including -// interface must redeclare all the methods from the included interface, but -// documentation and options are inherited as follows: -// -// - If after comment and whitespace stripping, the documentation -// string of the redeclared method is empty, it will be inherited -// from the original method. -// -// - Each annotation belonging to the service config (http, -// visibility) which is not set in the redeclared method will be -// inherited. -// -// - If an http annotation is inherited, the path pattern will be -// modified as follows. Any version prefix will be replaced by the -// version of the including interface plus the [root][] path if -// specified. -// -// Example of a simple mixin: -// -// package google.acl.v1; -// service AccessControl { -// // Get the underlying ACL object. -// rpc GetAcl(GetAclRequest) returns (Acl) { -// option (google.api.http).get = "/v1/{resource=**}:getAcl"; -// } -// } -// -// package google.storage.v2; -// service Storage { -// rpc GetAcl(GetAclRequest) returns (Acl); -// -// // Get a data record. -// rpc GetData(GetDataRequest) returns (Data) { -// option (google.api.http).get = "/v2/{resource=**}"; -// } -// } -// -// Example of a mixin configuration: -// -// apis: -// - name: google.storage.v2.Storage -// mixins: -// - name: google.acl.v1.AccessControl -// -// The mixin construct implies that all methods in `AccessControl` are -// also declared with same name and request/response types in -// `Storage`. A documentation generator or annotation processor will -// see the effective `Storage.GetAcl` method after inheriting -// documentation and annotations as follows: -// -// service Storage { -// // Get the underlying ACL object. -// rpc GetAcl(GetAclRequest) returns (Acl) { -// option (google.api.http).get = "/v2/{resource=**}:getAcl"; -// } -// ... -// } -// -// Note how the version in the path pattern changed from `v1` to `v2`. -// -// If the `root` field in the mixin is specified, it should be a -// relative path under which inherited HTTP paths are placed. Example: -// -// apis: -// - name: google.storage.v2.Storage -// mixins: -// - name: google.acl.v1.AccessControl -// root: acls -// -// This implies the following inherited HTTP annotation: -// -// service Storage { -// // Get the underlying ACL object. -// rpc GetAcl(GetAclRequest) returns (Acl) { -// option (google.api.http).get = "/v2/acls/{resource=**}:getAcl"; -// } -// ... -// } -message Mixin { - // The fully qualified name of the interface which is included. - string name = 1; - - // If non-empty specifies a path under which inherited HTTP paths - // are rooted. - string root = 2; -} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/compiler/plugin.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/compiler/plugin.proto deleted file mode 100644 index 9242aacc..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/compiler/plugin.proto +++ /dev/null @@ -1,183 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -// Author: kenton@google.com (Kenton Varda) -// -// WARNING: The plugin interface is currently EXPERIMENTAL and is subject to -// change. -// -// protoc (aka the Protocol Compiler) can be extended via plugins. A plugin is -// just a program that reads a CodeGeneratorRequest from stdin and writes a -// CodeGeneratorResponse to stdout. -// -// Plugins written using C++ can use google/protobuf/compiler/plugin.h instead -// of dealing with the raw protocol defined here. -// -// A plugin executable needs only to be placed somewhere in the path. The -// plugin should be named "protoc-gen-$NAME", and will then be used when the -// flag "--${NAME}_out" is passed to protoc. - -syntax = "proto2"; - -package google.protobuf.compiler; -option java_package = "com.google.protobuf.compiler"; -option java_outer_classname = "PluginProtos"; - -option go_package = "google.golang.org/protobuf/types/pluginpb"; - -import "google/protobuf/descriptor.proto"; - -// The version number of protocol compiler. -message Version { - optional int32 major = 1; - optional int32 minor = 2; - optional int32 patch = 3; - // A suffix for alpha, beta or rc release, e.g., "alpha-1", "rc2". It should - // be empty for mainline stable releases. - optional string suffix = 4; -} - -// An encoded CodeGeneratorRequest is written to the plugin's stdin. -message CodeGeneratorRequest { - // The .proto files that were explicitly listed on the command-line. The - // code generator should generate code only for these files. Each file's - // descriptor will be included in proto_file, below. - repeated string file_to_generate = 1; - - // The generator parameter passed on the command-line. - optional string parameter = 2; - - // FileDescriptorProtos for all files in files_to_generate and everything - // they import. The files will appear in topological order, so each file - // appears before any file that imports it. - // - // protoc guarantees that all proto_files will be written after - // the fields above, even though this is not technically guaranteed by the - // protobuf wire format. This theoretically could allow a plugin to stream - // in the FileDescriptorProtos and handle them one by one rather than read - // the entire set into memory at once. However, as of this writing, this - // is not similarly optimized on protoc's end -- it will store all fields in - // memory at once before sending them to the plugin. - // - // Type names of fields and extensions in the FileDescriptorProto are always - // fully qualified. - repeated FileDescriptorProto proto_file = 15; - - // The version number of protocol compiler. - optional Version compiler_version = 3; - -} - -// The plugin writes an encoded CodeGeneratorResponse to stdout. -message CodeGeneratorResponse { - // Error message. If non-empty, code generation failed. The plugin process - // should exit with status code zero even if it reports an error in this way. - // - // This should be used to indicate errors in .proto files which prevent the - // code generator from generating correct code. Errors which indicate a - // problem in protoc itself -- such as the input CodeGeneratorRequest being - // unparseable -- should be reported by writing a message to stderr and - // exiting with a non-zero status code. - optional string error = 1; - - // A bitmask of supported features that the code generator supports. - // This is a bitwise "or" of values from the Feature enum. - optional uint64 supported_features = 2; - - // Sync with code_generator.h. - enum Feature { - FEATURE_NONE = 0; - FEATURE_PROTO3_OPTIONAL = 1; - } - - // Represents a single generated file. - message File { - // The file name, relative to the output directory. The name must not - // contain "." or ".." components and must be relative, not be absolute (so, - // the file cannot lie outside the output directory). "/" must be used as - // the path separator, not "\". - // - // If the name is omitted, the content will be appended to the previous - // file. This allows the generator to break large files into small chunks, - // and allows the generated text to be streamed back to protoc so that large - // files need not reside completely in memory at one time. Note that as of - // this writing protoc does not optimize for this -- it will read the entire - // CodeGeneratorResponse before writing files to disk. - optional string name = 1; - - // If non-empty, indicates that the named file should already exist, and the - // content here is to be inserted into that file at a defined insertion - // point. This feature allows a code generator to extend the output - // produced by another code generator. The original generator may provide - // insertion points by placing special annotations in the file that look - // like: - // @@protoc_insertion_point(NAME) - // The annotation can have arbitrary text before and after it on the line, - // which allows it to be placed in a comment. NAME should be replaced with - // an identifier naming the point -- this is what other generators will use - // as the insertion_point. Code inserted at this point will be placed - // immediately above the line containing the insertion point (thus multiple - // insertions to the same point will come out in the order they were added). - // The double-@ is intended to make it unlikely that the generated code - // could contain things that look like insertion points by accident. - // - // For example, the C++ code generator places the following line in the - // .pb.h files that it generates: - // // @@protoc_insertion_point(namespace_scope) - // This line appears within the scope of the file's package namespace, but - // outside of any particular class. Another plugin can then specify the - // insertion_point "namespace_scope" to generate additional classes or - // other declarations that should be placed in this scope. - // - // Note that if the line containing the insertion point begins with - // whitespace, the same whitespace will be added to every line of the - // inserted text. This is useful for languages like Python, where - // indentation matters. In these languages, the insertion point comment - // should be indented the same amount as any inserted code will need to be - // in order to work correctly in that context. - // - // The code generator that generates the initial file and the one which - // inserts into it must both run as part of a single invocation of protoc. - // Code generators are executed in the order in which they appear on the - // command line. - // - // If |insertion_point| is present, |name| must also be present. - optional string insertion_point = 2; - - // The file contents. - optional string content = 15; - - // Information describing the file content being inserted. If an insertion - // point is used, this information will be appropriately offset and inserted - // into the code generation metadata for the generated files. - optional GeneratedCodeInfo generated_code_info = 16; - } - repeated File file = 15; -} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/descriptor.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/descriptor.proto deleted file mode 100644 index 156e410a..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/descriptor.proto +++ /dev/null @@ -1,911 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -// Author: kenton@google.com (Kenton Varda) -// Based on original Protocol Buffers design by -// Sanjay Ghemawat, Jeff Dean, and others. -// -// The messages in this file describe the definitions found in .proto files. -// A valid .proto file can be translated directly to a FileDescriptorProto -// without any other information (e.g. without reading its imports). - - -syntax = "proto2"; - -package google.protobuf; - -option go_package = "google.golang.org/protobuf/types/descriptorpb"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "DescriptorProtos"; -option csharp_namespace = "Google.Protobuf.Reflection"; -option objc_class_prefix = "GPB"; -option cc_enable_arenas = true; - -// descriptor.proto must be optimized for speed because reflection-based -// algorithms don't work during bootstrapping. -option optimize_for = SPEED; - -// The protocol compiler can output a FileDescriptorSet containing the .proto -// files it parses. -message FileDescriptorSet { - repeated FileDescriptorProto file = 1; -} - -// Describes a complete .proto file. -message FileDescriptorProto { - optional string name = 1; // file name, relative to root of source tree - optional string package = 2; // e.g. "foo", "foo.bar", etc. - - // Names of files imported by this file. - repeated string dependency = 3; - // Indexes of the public imported files in the dependency list above. - repeated int32 public_dependency = 10; - // Indexes of the weak imported files in the dependency list. - // For Google-internal migration only. Do not use. - repeated int32 weak_dependency = 11; - - // All top-level definitions in this file. - repeated DescriptorProto message_type = 4; - repeated EnumDescriptorProto enum_type = 5; - repeated ServiceDescriptorProto service = 6; - repeated FieldDescriptorProto extension = 7; - - optional FileOptions options = 8; - - // This field contains optional information about the original source code. - // You may safely remove this entire field without harming runtime - // functionality of the descriptors -- the information is needed only by - // development tools. - optional SourceCodeInfo source_code_info = 9; - - // The syntax of the proto file. - // The supported values are "proto2" and "proto3". - optional string syntax = 12; -} - -// Describes a message type. -message DescriptorProto { - optional string name = 1; - - repeated FieldDescriptorProto field = 2; - repeated FieldDescriptorProto extension = 6; - - repeated DescriptorProto nested_type = 3; - repeated EnumDescriptorProto enum_type = 4; - - message ExtensionRange { - optional int32 start = 1; // Inclusive. - optional int32 end = 2; // Exclusive. - - optional ExtensionRangeOptions options = 3; - } - repeated ExtensionRange extension_range = 5; - - repeated OneofDescriptorProto oneof_decl = 8; - - optional MessageOptions options = 7; - - // Range of reserved tag numbers. Reserved tag numbers may not be used by - // fields or extension ranges in the same message. Reserved ranges may - // not overlap. - message ReservedRange { - optional int32 start = 1; // Inclusive. - optional int32 end = 2; // Exclusive. - } - repeated ReservedRange reserved_range = 9; - // Reserved field names, which may not be used by fields in the same message. - // A given name may only be reserved once. - repeated string reserved_name = 10; -} - -message ExtensionRangeOptions { - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -// Describes a field within a message. -message FieldDescriptorProto { - enum Type { - // 0 is reserved for errors. - // Order is weird for historical reasons. - TYPE_DOUBLE = 1; - TYPE_FLOAT = 2; - // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if - // negative values are likely. - TYPE_INT64 = 3; - TYPE_UINT64 = 4; - // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if - // negative values are likely. - TYPE_INT32 = 5; - TYPE_FIXED64 = 6; - TYPE_FIXED32 = 7; - TYPE_BOOL = 8; - TYPE_STRING = 9; - // Tag-delimited aggregate. - // Group type is deprecated and not supported in proto3. However, Proto3 - // implementations should still be able to parse the group wire format and - // treat group fields as unknown fields. - TYPE_GROUP = 10; - TYPE_MESSAGE = 11; // Length-delimited aggregate. - - // New in version 2. - TYPE_BYTES = 12; - TYPE_UINT32 = 13; - TYPE_ENUM = 14; - TYPE_SFIXED32 = 15; - TYPE_SFIXED64 = 16; - TYPE_SINT32 = 17; // Uses ZigZag encoding. - TYPE_SINT64 = 18; // Uses ZigZag encoding. - } - - enum Label { - // 0 is reserved for errors - LABEL_OPTIONAL = 1; - LABEL_REQUIRED = 2; - LABEL_REPEATED = 3; - } - - optional string name = 1; - optional int32 number = 3; - optional Label label = 4; - - // If type_name is set, this need not be set. If both this and type_name - // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. - optional Type type = 5; - - // For message and enum types, this is the name of the type. If the name - // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping - // rules are used to find the type (i.e. first the nested types within this - // message are searched, then within the parent, on up to the root - // namespace). - optional string type_name = 6; - - // For extensions, this is the name of the type being extended. It is - // resolved in the same manner as type_name. - optional string extendee = 2; - - // For numeric types, contains the original text representation of the value. - // For booleans, "true" or "false". - // For strings, contains the default text contents (not escaped in any way). - // For bytes, contains the C escaped value. All bytes >= 128 are escaped. - // TODO(kenton): Base-64 encode? - optional string default_value = 7; - - // If set, gives the index of a oneof in the containing type's oneof_decl - // list. This field is a member of that oneof. - optional int32 oneof_index = 9; - - // JSON name of this field. The value is set by protocol compiler. If the - // user has set a "json_name" option on this field, that option's value - // will be used. Otherwise, it's deduced from the field's name by converting - // it to camelCase. - optional string json_name = 10; - - optional FieldOptions options = 8; - - // If true, this is a proto3 "optional". When a proto3 field is optional, it - // tracks presence regardless of field type. - // - // When proto3_optional is true, this field must be belong to a oneof to - // signal to old proto3 clients that presence is tracked for this field. This - // oneof is known as a "synthetic" oneof, and this field must be its sole - // member (each proto3 optional field gets its own synthetic oneof). Synthetic - // oneofs exist in the descriptor only, and do not generate any API. Synthetic - // oneofs must be ordered after all "real" oneofs. - // - // For message fields, proto3_optional doesn't create any semantic change, - // since non-repeated message fields always track presence. However it still - // indicates the semantic detail of whether the user wrote "optional" or not. - // This can be useful for round-tripping the .proto file. For consistency we - // give message fields a synthetic oneof also, even though it is not required - // to track presence. This is especially important because the parser can't - // tell if a field is a message or an enum, so it must always create a - // synthetic oneof. - // - // Proto2 optional fields do not set this flag, because they already indicate - // optional with `LABEL_OPTIONAL`. - optional bool proto3_optional = 17; -} - -// Describes a oneof. -message OneofDescriptorProto { - optional string name = 1; - optional OneofOptions options = 2; -} - -// Describes an enum type. -message EnumDescriptorProto { - optional string name = 1; - - repeated EnumValueDescriptorProto value = 2; - - optional EnumOptions options = 3; - - // Range of reserved numeric values. Reserved values may not be used by - // entries in the same enum. Reserved ranges may not overlap. - // - // Note that this is distinct from DescriptorProto.ReservedRange in that it - // is inclusive such that it can appropriately represent the entire int32 - // domain. - message EnumReservedRange { - optional int32 start = 1; // Inclusive. - optional int32 end = 2; // Inclusive. - } - - // Range of reserved numeric values. Reserved numeric values may not be used - // by enum values in the same enum declaration. Reserved ranges may not - // overlap. - repeated EnumReservedRange reserved_range = 4; - - // Reserved enum value names, which may not be reused. A given name may only - // be reserved once. - repeated string reserved_name = 5; -} - -// Describes a value within an enum. -message EnumValueDescriptorProto { - optional string name = 1; - optional int32 number = 2; - - optional EnumValueOptions options = 3; -} - -// Describes a service. -message ServiceDescriptorProto { - optional string name = 1; - repeated MethodDescriptorProto method = 2; - - optional ServiceOptions options = 3; -} - -// Describes a method of a service. -message MethodDescriptorProto { - optional string name = 1; - - // Input and output type names. These are resolved in the same way as - // FieldDescriptorProto.type_name, but must refer to a message type. - optional string input_type = 2; - optional string output_type = 3; - - optional MethodOptions options = 4; - - // Identifies if client streams multiple client messages - optional bool client_streaming = 5 [default = false]; - // Identifies if server streams multiple server messages - optional bool server_streaming = 6 [default = false]; -} - - -// =================================================================== -// Options - -// Each of the definitions above may have "options" attached. These are -// just annotations which may cause code to be generated slightly differently -// or may contain hints for code that manipulates protocol messages. -// -// Clients may define custom options as extensions of the *Options messages. -// These extensions may not yet be known at parsing time, so the parser cannot -// store the values in them. Instead it stores them in a field in the *Options -// message called uninterpreted_option. This field must have the same name -// across all *Options messages. We then use this field to populate the -// extensions when we build a descriptor, at which point all protos have been -// parsed and so all extensions are known. -// -// Extension numbers for custom options may be chosen as follows: -// * For options which will only be used within a single application or -// organization, or for experimental options, use field numbers 50000 -// through 99999. It is up to you to ensure that you do not use the -// same number for multiple options. -// * For options which will be published and used publicly by multiple -// independent entities, e-mail protobuf-global-extension-registry@google.com -// to reserve extension numbers. Simply provide your project name (e.g. -// Objective-C plugin) and your project website (if available) -- there's no -// need to explain how you intend to use them. Usually you only need one -// extension number. You can declare multiple options with only one extension -// number by putting them in a sub-message. See the Custom Options section of -// the docs for examples: -// https://developers.google.com/protocol-buffers/docs/proto#options -// If this turns out to be popular, a web service will be set up -// to automatically assign option numbers. - -message FileOptions { - - // Sets the Java package where classes generated from this .proto will be - // placed. By default, the proto package is used, but this is often - // inappropriate because proto packages do not normally start with backwards - // domain names. - optional string java_package = 1; - - - // Controls the name of the wrapper Java class generated for the .proto file. - // That class will always contain the .proto file's getDescriptor() method as - // well as any top-level extensions defined in the .proto file. - // If java_multiple_files is disabled, then all the other classes from the - // .proto file will be nested inside the single wrapper outer class. - optional string java_outer_classname = 8; - - // If enabled, then the Java code generator will generate a separate .java - // file for each top-level message, enum, and service defined in the .proto - // file. Thus, these types will *not* be nested inside the wrapper class - // named by java_outer_classname. However, the wrapper class will still be - // generated to contain the file's getDescriptor() method as well as any - // top-level extensions defined in the file. - optional bool java_multiple_files = 10 [default = false]; - - // This option does nothing. - optional bool java_generate_equals_and_hash = 20 [deprecated=true]; - - // If set true, then the Java2 code generator will generate code that - // throws an exception whenever an attempt is made to assign a non-UTF-8 - // byte sequence to a string field. - // Message reflection will do the same. - // However, an extension field still accepts non-UTF-8 byte sequences. - // This option has no effect on when used with the lite runtime. - optional bool java_string_check_utf8 = 27 [default = false]; - - - // Generated classes can be optimized for speed or code size. - enum OptimizeMode { - SPEED = 1; // Generate complete code for parsing, serialization, - // etc. - CODE_SIZE = 2; // Use ReflectionOps to implement these methods. - LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. - } - optional OptimizeMode optimize_for = 9 [default = SPEED]; - - // Sets the Go package where structs generated from this .proto will be - // placed. If omitted, the Go package will be derived from the following: - // - The basename of the package import path, if provided. - // - Otherwise, the package statement in the .proto file, if present. - // - Otherwise, the basename of the .proto file, without extension. - optional string go_package = 11; - - - - - // Should generic services be generated in each language? "Generic" services - // are not specific to any particular RPC system. They are generated by the - // main code generators in each language (without additional plugins). - // Generic services were the only kind of service generation supported by - // early versions of google.protobuf. - // - // Generic services are now considered deprecated in favor of using plugins - // that generate code specific to your particular RPC system. Therefore, - // these default to false. Old code which depends on generic services should - // explicitly set them to true. - optional bool cc_generic_services = 16 [default = false]; - optional bool java_generic_services = 17 [default = false]; - optional bool py_generic_services = 18 [default = false]; - optional bool php_generic_services = 42 [default = false]; - - // Is this file deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for everything in the file, or it will be completely ignored; in the very - // least, this is a formalization for deprecating files. - optional bool deprecated = 23 [default = false]; - - // Enables the use of arenas for the proto messages in this file. This applies - // only to generated classes for C++. - optional bool cc_enable_arenas = 31 [default = true]; - - - // Sets the objective c class prefix which is prepended to all objective c - // generated classes from this .proto. There is no default. - optional string objc_class_prefix = 36; - - // Namespace for generated classes; defaults to the package. - optional string csharp_namespace = 37; - - // By default Swift generators will take the proto package and CamelCase it - // replacing '.' with underscore and use that to prefix the types/symbols - // defined. When this options is provided, they will use this value instead - // to prefix the types/symbols defined. - optional string swift_prefix = 39; - - // Sets the php class prefix which is prepended to all php generated classes - // from this .proto. Default is empty. - optional string php_class_prefix = 40; - - // Use this option to change the namespace of php generated classes. Default - // is empty. When this option is empty, the package name will be used for - // determining the namespace. - optional string php_namespace = 41; - - // Use this option to change the namespace of php generated metadata classes. - // Default is empty. When this option is empty, the proto file name will be - // used for determining the namespace. - optional string php_metadata_namespace = 44; - - // Use this option to change the package of ruby generated classes. Default - // is empty. When this option is not set, the package name will be used for - // determining the ruby package. - optional string ruby_package = 45; - - - // The parser stores options it doesn't recognize here. - // See the documentation for the "Options" section above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. - // See the documentation for the "Options" section above. - extensions 1000 to max; - - reserved 38; -} - -message MessageOptions { - // Set true to use the old proto1 MessageSet wire format for extensions. - // This is provided for backwards-compatibility with the MessageSet wire - // format. You should not use this for any other reason: It's less - // efficient, has fewer features, and is more complicated. - // - // The message must be defined exactly as follows: - // message Foo { - // option message_set_wire_format = true; - // extensions 4 to max; - // } - // Note that the message cannot have any defined fields; MessageSets only - // have extensions. - // - // All extensions of your type must be singular messages; e.g. they cannot - // be int32s, enums, or repeated messages. - // - // Because this is an option, the above two restrictions are not enforced by - // the protocol compiler. - optional bool message_set_wire_format = 1 [default = false]; - - // Disables the generation of the standard "descriptor()" accessor, which can - // conflict with a field of the same name. This is meant to make migration - // from proto1 easier; new code should avoid fields named "descriptor". - optional bool no_standard_descriptor_accessor = 2 [default = false]; - - // Is this message deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for the message, or it will be completely ignored; in the very least, - // this is a formalization for deprecating messages. - optional bool deprecated = 3 [default = false]; - - reserved 4, 5, 6; - - // Whether the message is an automatically generated map entry type for the - // maps field. - // - // For maps fields: - // map map_field = 1; - // The parsed descriptor looks like: - // message MapFieldEntry { - // option map_entry = true; - // optional KeyType key = 1; - // optional ValueType value = 2; - // } - // repeated MapFieldEntry map_field = 1; - // - // Implementations may choose not to generate the map_entry=true message, but - // use a native map in the target language to hold the keys and values. - // The reflection APIs in such implementations still need to work as - // if the field is a repeated message field. - // - // NOTE: Do not set the option in .proto files. Always use the maps syntax - // instead. The option should only be implicitly set by the proto compiler - // parser. - optional bool map_entry = 7; - - reserved 8; // javalite_serializable - reserved 9; // javanano_as_lite - - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -message FieldOptions { - // The ctype option instructs the C++ code generator to use a different - // representation of the field than it normally would. See the specific - // options below. This option is not yet implemented in the open source - // release -- sorry, we'll try to include it in a future version! - optional CType ctype = 1 [default = STRING]; - enum CType { - // Default mode. - STRING = 0; - - CORD = 1; - - STRING_PIECE = 2; - } - // The packed option can be enabled for repeated primitive fields to enable - // a more efficient representation on the wire. Rather than repeatedly - // writing the tag and type for each element, the entire array is encoded as - // a single length-delimited blob. In proto3, only explicit setting it to - // false will avoid using packed encoding. - optional bool packed = 2; - - // The jstype option determines the JavaScript type used for values of the - // field. The option is permitted only for 64 bit integral and fixed types - // (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING - // is represented as JavaScript string, which avoids loss of precision that - // can happen when a large value is converted to a floating point JavaScript. - // Specifying JS_NUMBER for the jstype causes the generated JavaScript code to - // use the JavaScript "number" type. The behavior of the default option - // JS_NORMAL is implementation dependent. - // - // This option is an enum to permit additional types to be added, e.g. - // goog.math.Integer. - optional JSType jstype = 6 [default = JS_NORMAL]; - enum JSType { - // Use the default type. - JS_NORMAL = 0; - - // Use JavaScript strings. - JS_STRING = 1; - - // Use JavaScript numbers. - JS_NUMBER = 2; - } - - // Should this field be parsed lazily? Lazy applies only to message-type - // fields. It means that when the outer message is initially parsed, the - // inner message's contents will not be parsed but instead stored in encoded - // form. The inner message will actually be parsed when it is first accessed. - // - // This is only a hint. Implementations are free to choose whether to use - // eager or lazy parsing regardless of the value of this option. However, - // setting this option true suggests that the protocol author believes that - // using lazy parsing on this field is worth the additional bookkeeping - // overhead typically needed to implement it. - // - // This option does not affect the public interface of any generated code; - // all method signatures remain the same. Furthermore, thread-safety of the - // interface is not affected by this option; const methods remain safe to - // call from multiple threads concurrently, while non-const methods continue - // to require exclusive access. - // - // - // Note that implementations may choose not to check required fields within - // a lazy sub-message. That is, calling IsInitialized() on the outer message - // may return true even if the inner message has missing required fields. - // This is necessary because otherwise the inner message would have to be - // parsed in order to perform the check, defeating the purpose of lazy - // parsing. An implementation which chooses not to check required fields - // must be consistent about it. That is, for any particular sub-message, the - // implementation must either *always* check its required fields, or *never* - // check its required fields, regardless of whether or not the message has - // been parsed. - optional bool lazy = 5 [default = false]; - - // Is this field deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for accessors, or it will be completely ignored; in the very least, this - // is a formalization for deprecating fields. - optional bool deprecated = 3 [default = false]; - - // For Google-internal migration only. Do not use. - optional bool weak = 10 [default = false]; - - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; - - reserved 4; // removed jtype -} - -message OneofOptions { - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -message EnumOptions { - - // Set this option to true to allow mapping different tag names to the same - // value. - optional bool allow_alias = 2; - - // Is this enum deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for the enum, or it will be completely ignored; in the very least, this - // is a formalization for deprecating enums. - optional bool deprecated = 3 [default = false]; - - reserved 5; // javanano_as_lite - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -message EnumValueOptions { - // Is this enum value deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for the enum value, or it will be completely ignored; in the very least, - // this is a formalization for deprecating enum values. - optional bool deprecated = 1 [default = false]; - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -message ServiceOptions { - - // Note: Field numbers 1 through 32 are reserved for Google's internal RPC - // framework. We apologize for hoarding these numbers to ourselves, but - // we were already using them long before we decided to release Protocol - // Buffers. - - // Is this service deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for the service, or it will be completely ignored; in the very least, - // this is a formalization for deprecating services. - optional bool deprecated = 33 [default = false]; - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -message MethodOptions { - - // Note: Field numbers 1 through 32 are reserved for Google's internal RPC - // framework. We apologize for hoarding these numbers to ourselves, but - // we were already using them long before we decided to release Protocol - // Buffers. - - // Is this method deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for the method, or it will be completely ignored; in the very least, - // this is a formalization for deprecating methods. - optional bool deprecated = 33 [default = false]; - - // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, - // or neither? HTTP based RPC implementation may choose GET verb for safe - // methods, and PUT verb for idempotent methods instead of the default POST. - enum IdempotencyLevel { - IDEMPOTENCY_UNKNOWN = 0; - NO_SIDE_EFFECTS = 1; // implies idempotent - IDEMPOTENT = 2; // idempotent, but may have side effects - } - optional IdempotencyLevel idempotency_level = 34 - [default = IDEMPOTENCY_UNKNOWN]; - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - - -// A message representing a option the parser does not recognize. This only -// appears in options protos created by the compiler::Parser class. -// DescriptorPool resolves these when building Descriptor objects. Therefore, -// options protos in descriptor objects (e.g. returned by Descriptor::options(), -// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions -// in them. -message UninterpretedOption { - // The name of the uninterpreted option. Each string represents a segment in - // a dot-separated name. is_extension is true iff a segment represents an - // extension (denoted with parentheses in options specs in .proto files). - // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents - // "foo.(bar.baz).qux". - message NamePart { - required string name_part = 1; - required bool is_extension = 2; - } - repeated NamePart name = 2; - - // The value of the uninterpreted option, in whatever type the tokenizer - // identified it as during parsing. Exactly one of these should be set. - optional string identifier_value = 3; - optional uint64 positive_int_value = 4; - optional int64 negative_int_value = 5; - optional double double_value = 6; - optional bytes string_value = 7; - optional string aggregate_value = 8; -} - -// =================================================================== -// Optional source code info - -// Encapsulates information about the original source file from which a -// FileDescriptorProto was generated. -message SourceCodeInfo { - // A Location identifies a piece of source code in a .proto file which - // corresponds to a particular definition. This information is intended - // to be useful to IDEs, code indexers, documentation generators, and similar - // tools. - // - // For example, say we have a file like: - // message Foo { - // optional string foo = 1; - // } - // Let's look at just the field definition: - // optional string foo = 1; - // ^ ^^ ^^ ^ ^^^ - // a bc de f ghi - // We have the following locations: - // span path represents - // [a,i) [ 4, 0, 2, 0 ] The whole field definition. - // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). - // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). - // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). - // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). - // - // Notes: - // - A location may refer to a repeated field itself (i.e. not to any - // particular index within it). This is used whenever a set of elements are - // logically enclosed in a single code segment. For example, an entire - // extend block (possibly containing multiple extension definitions) will - // have an outer location whose path refers to the "extensions" repeated - // field without an index. - // - Multiple locations may have the same path. This happens when a single - // logical declaration is spread out across multiple places. The most - // obvious example is the "extend" block again -- there may be multiple - // extend blocks in the same scope, each of which will have the same path. - // - A location's span is not always a subset of its parent's span. For - // example, the "extendee" of an extension declaration appears at the - // beginning of the "extend" block and is shared by all extensions within - // the block. - // - Just because a location's span is a subset of some other location's span - // does not mean that it is a descendant. For example, a "group" defines - // both a type and a field in a single declaration. Thus, the locations - // corresponding to the type and field and their components will overlap. - // - Code which tries to interpret locations should probably be designed to - // ignore those that it doesn't understand, as more types of locations could - // be recorded in the future. - repeated Location location = 1; - message Location { - // Identifies which part of the FileDescriptorProto was defined at this - // location. - // - // Each element is a field number or an index. They form a path from - // the root FileDescriptorProto to the place where the definition. For - // example, this path: - // [ 4, 3, 2, 7, 1 ] - // refers to: - // file.message_type(3) // 4, 3 - // .field(7) // 2, 7 - // .name() // 1 - // This is because FileDescriptorProto.message_type has field number 4: - // repeated DescriptorProto message_type = 4; - // and DescriptorProto.field has field number 2: - // repeated FieldDescriptorProto field = 2; - // and FieldDescriptorProto.name has field number 1: - // optional string name = 1; - // - // Thus, the above path gives the location of a field name. If we removed - // the last element: - // [ 4, 3, 2, 7 ] - // this path refers to the whole field declaration (from the beginning - // of the label to the terminating semicolon). - repeated int32 path = 1 [packed = true]; - - // Always has exactly three or four elements: start line, start column, - // end line (optional, otherwise assumed same as start line), end column. - // These are packed into a single field for efficiency. Note that line - // and column numbers are zero-based -- typically you will want to add - // 1 to each before displaying to a user. - repeated int32 span = 2 [packed = true]; - - // If this SourceCodeInfo represents a complete declaration, these are any - // comments appearing before and after the declaration which appear to be - // attached to the declaration. - // - // A series of line comments appearing on consecutive lines, with no other - // tokens appearing on those lines, will be treated as a single comment. - // - // leading_detached_comments will keep paragraphs of comments that appear - // before (but not connected to) the current element. Each paragraph, - // separated by empty lines, will be one comment element in the repeated - // field. - // - // Only the comment content is provided; comment markers (e.g. //) are - // stripped out. For block comments, leading whitespace and an asterisk - // will be stripped from the beginning of each line other than the first. - // Newlines are included in the output. - // - // Examples: - // - // optional int32 foo = 1; // Comment attached to foo. - // // Comment attached to bar. - // optional int32 bar = 2; - // - // optional string baz = 3; - // // Comment attached to baz. - // // Another line attached to baz. - // - // // Comment attached to qux. - // // - // // Another line attached to qux. - // optional double qux = 4; - // - // // Detached comment for corge. This is not leading or trailing comments - // // to qux or corge because there are blank lines separating it from - // // both. - // - // // Detached comment for corge paragraph 2. - // - // optional string corge = 5; - // /* Block comment attached - // * to corge. Leading asterisks - // * will be removed. */ - // /* Block comment attached to - // * grault. */ - // optional int32 grault = 6; - // - // // ignored detached comments. - optional string leading_comments = 3; - optional string trailing_comments = 4; - repeated string leading_detached_comments = 6; - } -} - -// Describes the relationship between generated code and its original source -// file. A GeneratedCodeInfo message is associated with only one generated -// source file, but may contain references to different source .proto files. -message GeneratedCodeInfo { - // An Annotation connects some span of text in generated code to an element - // of its generating .proto file. - repeated Annotation annotation = 1; - message Annotation { - // Identifies the element in the original source .proto file. This field - // is formatted the same as SourceCodeInfo.Location.path. - repeated int32 path = 1 [packed = true]; - - // Identifies the filesystem path to the original source .proto. - optional string source_file = 2; - - // Identifies the starting offset in bytes in the generated code - // that relates to the identified object. - optional int32 begin = 3; - - // Identifies the ending offset in bytes in the generated code that - // relates to the identified offset. The end offset should be one past - // the last relevant byte (so the length of the text = end - begin). - optional int32 end = 4; - } -} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/duration.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/duration.proto deleted file mode 100644 index 81c3e369..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/duration.proto +++ /dev/null @@ -1,116 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option cc_enable_arenas = true; -option go_package = "google.golang.org/protobuf/types/known/durationpb"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "DurationProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; - -// A Duration represents a signed, fixed-length span of time represented -// as a count of seconds and fractions of seconds at nanosecond -// resolution. It is independent of any calendar and concepts like "day" -// or "month". It is related to Timestamp in that the difference between -// two Timestamp values is a Duration and it can be added or subtracted -// from a Timestamp. Range is approximately +-10,000 years. -// -// # Examples -// -// Example 1: Compute Duration from two Timestamps in pseudo code. -// -// Timestamp start = ...; -// Timestamp end = ...; -// Duration duration = ...; -// -// duration.seconds = end.seconds - start.seconds; -// duration.nanos = end.nanos - start.nanos; -// -// if (duration.seconds < 0 && duration.nanos > 0) { -// duration.seconds += 1; -// duration.nanos -= 1000000000; -// } else if (duration.seconds > 0 && duration.nanos < 0) { -// duration.seconds -= 1; -// duration.nanos += 1000000000; -// } -// -// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. -// -// Timestamp start = ...; -// Duration duration = ...; -// Timestamp end = ...; -// -// end.seconds = start.seconds + duration.seconds; -// end.nanos = start.nanos + duration.nanos; -// -// if (end.nanos < 0) { -// end.seconds -= 1; -// end.nanos += 1000000000; -// } else if (end.nanos >= 1000000000) { -// end.seconds += 1; -// end.nanos -= 1000000000; -// } -// -// Example 3: Compute Duration from datetime.timedelta in Python. -// -// td = datetime.timedelta(days=3, minutes=10) -// duration = Duration() -// duration.FromTimedelta(td) -// -// # JSON Mapping -// -// In JSON format, the Duration type is encoded as a string rather than an -// object, where the string ends in the suffix "s" (indicating seconds) and -// is preceded by the number of seconds, with nanoseconds expressed as -// fractional seconds. For example, 3 seconds with 0 nanoseconds should be -// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should -// be expressed in JSON format as "3.000000001s", and 3 seconds and 1 -// microsecond should be expressed in JSON format as "3.000001s". -// -// -message Duration { - // Signed seconds of the span of time. Must be from -315,576,000,000 - // to +315,576,000,000 inclusive. Note: these bounds are computed from: - // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years - int64 seconds = 1; - - // Signed fractions of a second at nanosecond resolution of the span - // of time. Durations less than one second are represented with a 0 - // `seconds` field and a positive or negative `nanos` field. For durations - // of one second or more, a non-zero value for the `nanos` field must be - // of the same sign as the `seconds` field. Must be from -999,999,999 - // to +999,999,999 inclusive. - int32 nanos = 2; -} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/empty.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/empty.proto deleted file mode 100644 index 5f992de9..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/empty.proto +++ /dev/null @@ -1,52 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option go_package = "google.golang.org/protobuf/types/known/emptypb"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "EmptyProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; -option cc_enable_arenas = true; - -// A generic empty message that you can re-use to avoid defining duplicated -// empty messages in your APIs. A typical example is to use it as the request -// or the response type of an API method. For instance: -// -// service Foo { -// rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); -// } -// -// The JSON representation for `Empty` is empty JSON object `{}`. -message Empty {} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/field_mask.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/field_mask.proto deleted file mode 100644 index 6b5104f1..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/field_mask.proto +++ /dev/null @@ -1,245 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "FieldMaskProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; -option go_package = "google.golang.org/protobuf/types/known/fieldmaskpb"; -option cc_enable_arenas = true; - -// `FieldMask` represents a set of symbolic field paths, for example: -// -// paths: "f.a" -// paths: "f.b.d" -// -// Here `f` represents a field in some root message, `a` and `b` -// fields in the message found in `f`, and `d` a field found in the -// message in `f.b`. -// -// Field masks are used to specify a subset of fields that should be -// returned by a get operation or modified by an update operation. -// Field masks also have a custom JSON encoding (see below). -// -// # Field Masks in Projections -// -// When used in the context of a projection, a response message or -// sub-message is filtered by the API to only contain those fields as -// specified in the mask. For example, if the mask in the previous -// example is applied to a response message as follows: -// -// f { -// a : 22 -// b { -// d : 1 -// x : 2 -// } -// y : 13 -// } -// z: 8 -// -// The result will not contain specific values for fields x,y and z -// (their value will be set to the default, and omitted in proto text -// output): -// -// -// f { -// a : 22 -// b { -// d : 1 -// } -// } -// -// A repeated field is not allowed except at the last position of a -// paths string. -// -// If a FieldMask object is not present in a get operation, the -// operation applies to all fields (as if a FieldMask of all fields -// had been specified). -// -// Note that a field mask does not necessarily apply to the -// top-level response message. In case of a REST get operation, the -// field mask applies directly to the response, but in case of a REST -// list operation, the mask instead applies to each individual message -// in the returned resource list. In case of a REST custom method, -// other definitions may be used. Where the mask applies will be -// clearly documented together with its declaration in the API. In -// any case, the effect on the returned resource/resources is required -// behavior for APIs. -// -// # Field Masks in Update Operations -// -// A field mask in update operations specifies which fields of the -// targeted resource are going to be updated. The API is required -// to only change the values of the fields as specified in the mask -// and leave the others untouched. If a resource is passed in to -// describe the updated values, the API ignores the values of all -// fields not covered by the mask. -// -// If a repeated field is specified for an update operation, new values will -// be appended to the existing repeated field in the target resource. Note that -// a repeated field is only allowed in the last position of a `paths` string. -// -// If a sub-message is specified in the last position of the field mask for an -// update operation, then new value will be merged into the existing sub-message -// in the target resource. -// -// For example, given the target message: -// -// f { -// b { -// d: 1 -// x: 2 -// } -// c: [1] -// } -// -// And an update message: -// -// f { -// b { -// d: 10 -// } -// c: [2] -// } -// -// then if the field mask is: -// -// paths: ["f.b", "f.c"] -// -// then the result will be: -// -// f { -// b { -// d: 10 -// x: 2 -// } -// c: [1, 2] -// } -// -// An implementation may provide options to override this default behavior for -// repeated and message fields. -// -// In order to reset a field's value to the default, the field must -// be in the mask and set to the default value in the provided resource. -// Hence, in order to reset all fields of a resource, provide a default -// instance of the resource and set all fields in the mask, or do -// not provide a mask as described below. -// -// If a field mask is not present on update, the operation applies to -// all fields (as if a field mask of all fields has been specified). -// Note that in the presence of schema evolution, this may mean that -// fields the client does not know and has therefore not filled into -// the request will be reset to their default. If this is unwanted -// behavior, a specific service may require a client to always specify -// a field mask, producing an error if not. -// -// As with get operations, the location of the resource which -// describes the updated values in the request message depends on the -// operation kind. In any case, the effect of the field mask is -// required to be honored by the API. -// -// ## Considerations for HTTP REST -// -// The HTTP kind of an update operation which uses a field mask must -// be set to PATCH instead of PUT in order to satisfy HTTP semantics -// (PUT must only be used for full updates). -// -// # JSON Encoding of Field Masks -// -// In JSON, a field mask is encoded as a single string where paths are -// separated by a comma. Fields name in each path are converted -// to/from lower-camel naming conventions. -// -// As an example, consider the following message declarations: -// -// message Profile { -// User user = 1; -// Photo photo = 2; -// } -// message User { -// string display_name = 1; -// string address = 2; -// } -// -// In proto a field mask for `Profile` may look as such: -// -// mask { -// paths: "user.display_name" -// paths: "photo" -// } -// -// In JSON, the same mask is represented as below: -// -// { -// mask: "user.displayName,photo" -// } -// -// # Field Masks and Oneof Fields -// -// Field masks treat fields in oneofs just as regular fields. Consider the -// following message: -// -// message SampleMessage { -// oneof test_oneof { -// string name = 4; -// SubMessage sub_message = 9; -// } -// } -// -// The field mask can be: -// -// mask { -// paths: "name" -// } -// -// Or: -// -// mask { -// paths: "sub_message" -// } -// -// Note that oneof type names ("test_oneof" in this case) cannot be used in -// paths. -// -// ## Field Mask Verification -// -// The implementation of any API method which has a FieldMask type field in the -// request should verify the included field paths, and return an -// `INVALID_ARGUMENT` error if any path is unmappable. -message FieldMask { - // The set of field mask paths. - repeated string paths = 1; -} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/source_context.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/source_context.proto deleted file mode 100644 index 06bfc43a..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/source_context.proto +++ /dev/null @@ -1,48 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "SourceContextProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; -option go_package = "google.golang.org/protobuf/types/known/sourcecontextpb"; - -// `SourceContext` represents information about the source of a -// protobuf element, like the file in which it is defined. -message SourceContext { - // The path-qualified name of the .proto file that contained the associated - // protobuf element. For example: `"google/protobuf/source_context.proto"`. - string file_name = 1; -} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/struct.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/struct.proto deleted file mode 100644 index 0ac843ca..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/struct.proto +++ /dev/null @@ -1,95 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option cc_enable_arenas = true; -option go_package = "google.golang.org/protobuf/types/known/structpb"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "StructProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; - -// `Struct` represents a structured data value, consisting of fields -// which map to dynamically typed values. In some languages, `Struct` -// might be supported by a native representation. For example, in -// scripting languages like JS a struct is represented as an -// object. The details of that representation are described together -// with the proto support for the language. -// -// The JSON representation for `Struct` is JSON object. -message Struct { - // Unordered map of dynamically typed values. - map fields = 1; -} - -// `Value` represents a dynamically typed value which can be either -// null, a number, a string, a boolean, a recursive struct value, or a -// list of values. A producer of value is expected to set one of these -// variants. Absence of any variant indicates an error. -// -// The JSON representation for `Value` is JSON value. -message Value { - // The kind of value. - oneof kind { - // Represents a null value. - NullValue null_value = 1; - // Represents a double value. - double number_value = 2; - // Represents a string value. - string string_value = 3; - // Represents a boolean value. - bool bool_value = 4; - // Represents a structured value. - Struct struct_value = 5; - // Represents a repeated `Value`. - ListValue list_value = 6; - } -} - -// `NullValue` is a singleton enumeration to represent the null value for the -// `Value` type union. -// -// The JSON representation for `NullValue` is JSON `null`. -enum NullValue { - // Null value. - NULL_VALUE = 0; -} - -// `ListValue` is a wrapper around a repeated field of values. -// -// The JSON representation for `ListValue` is JSON array. -message ListValue { - // Repeated field of dynamically typed values. - repeated Value values = 1; -} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/timestamp.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/timestamp.proto deleted file mode 100644 index 3b2df6d9..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/timestamp.proto +++ /dev/null @@ -1,147 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option cc_enable_arenas = true; -option go_package = "google.golang.org/protobuf/types/known/timestamppb"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "TimestampProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; - -// A Timestamp represents a point in time independent of any time zone or local -// calendar, encoded as a count of seconds and fractions of seconds at -// nanosecond resolution. The count is relative to an epoch at UTC midnight on -// January 1, 1970, in the proleptic Gregorian calendar which extends the -// Gregorian calendar backwards to year one. -// -// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap -// second table is needed for interpretation, using a [24-hour linear -// smear](https://developers.google.com/time/smear). -// -// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By -// restricting to that range, we ensure that we can convert to and from [RFC -// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. -// -// # Examples -// -// Example 1: Compute Timestamp from POSIX `time()`. -// -// Timestamp timestamp; -// timestamp.set_seconds(time(NULL)); -// timestamp.set_nanos(0); -// -// Example 2: Compute Timestamp from POSIX `gettimeofday()`. -// -// struct timeval tv; -// gettimeofday(&tv, NULL); -// -// Timestamp timestamp; -// timestamp.set_seconds(tv.tv_sec); -// timestamp.set_nanos(tv.tv_usec * 1000); -// -// Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. -// -// FILETIME ft; -// GetSystemTimeAsFileTime(&ft); -// UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; -// -// // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z -// // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. -// Timestamp timestamp; -// timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); -// timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); -// -// Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. -// -// long millis = System.currentTimeMillis(); -// -// Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) -// .setNanos((int) ((millis % 1000) * 1000000)).build(); -// -// -// Example 5: Compute Timestamp from Java `Instant.now()`. -// -// Instant now = Instant.now(); -// -// Timestamp timestamp = -// Timestamp.newBuilder().setSeconds(now.getEpochSecond()) -// .setNanos(now.getNano()).build(); -// -// -// Example 6: Compute Timestamp from current time in Python. -// -// timestamp = Timestamp() -// timestamp.GetCurrentTime() -// -// # JSON Mapping -// -// In JSON format, the Timestamp type is encoded as a string in the -// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the -// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" -// where {year} is always expressed using four digits while {month}, {day}, -// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional -// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), -// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone -// is required. A proto3 JSON serializer should always use UTC (as indicated by -// "Z") when printing the Timestamp type and a proto3 JSON parser should be -// able to accept both UTC and other timezones (as indicated by an offset). -// -// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past -// 01:30 UTC on January 15, 2017. -// -// In JavaScript, one can convert a Date object to this format using the -// standard -// [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) -// method. In Python, a standard `datetime.datetime` object can be converted -// to this format using -// [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with -// the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use -// the Joda Time's [`ISODateTimeFormat.dateTime()`]( -// http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D -// ) to obtain a formatter capable of generating timestamps in this format. -// -// -message Timestamp { - // Represents seconds of UTC time since Unix epoch - // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to - // 9999-12-31T23:59:59Z inclusive. - int64 seconds = 1; - - // Non-negative fractions of a second at nanosecond resolution. Negative - // second values with fractions must still have non-negative nanos values - // that count forward in time. Must be from 0 to 999,999,999 - // inclusive. - int32 nanos = 2; -} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/type.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/type.proto deleted file mode 100644 index d3f6a68b..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/type.proto +++ /dev/null @@ -1,187 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -import "google/protobuf/any.proto"; -import "google/protobuf/source_context.proto"; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option cc_enable_arenas = true; -option java_package = "com.google.protobuf"; -option java_outer_classname = "TypeProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; -option go_package = "google.golang.org/protobuf/types/known/typepb"; - -// A protocol buffer message type. -message Type { - // The fully qualified message name. - string name = 1; - // The list of fields. - repeated Field fields = 2; - // The list of types appearing in `oneof` definitions in this type. - repeated string oneofs = 3; - // The protocol buffer options. - repeated Option options = 4; - // The source context. - SourceContext source_context = 5; - // The source syntax. - Syntax syntax = 6; -} - -// A single field of a message type. -message Field { - // Basic field types. - enum Kind { - // Field type unknown. - TYPE_UNKNOWN = 0; - // Field type double. - TYPE_DOUBLE = 1; - // Field type float. - TYPE_FLOAT = 2; - // Field type int64. - TYPE_INT64 = 3; - // Field type uint64. - TYPE_UINT64 = 4; - // Field type int32. - TYPE_INT32 = 5; - // Field type fixed64. - TYPE_FIXED64 = 6; - // Field type fixed32. - TYPE_FIXED32 = 7; - // Field type bool. - TYPE_BOOL = 8; - // Field type string. - TYPE_STRING = 9; - // Field type group. Proto2 syntax only, and deprecated. - TYPE_GROUP = 10; - // Field type message. - TYPE_MESSAGE = 11; - // Field type bytes. - TYPE_BYTES = 12; - // Field type uint32. - TYPE_UINT32 = 13; - // Field type enum. - TYPE_ENUM = 14; - // Field type sfixed32. - TYPE_SFIXED32 = 15; - // Field type sfixed64. - TYPE_SFIXED64 = 16; - // Field type sint32. - TYPE_SINT32 = 17; - // Field type sint64. - TYPE_SINT64 = 18; - } - - // Whether a field is optional, required, or repeated. - enum Cardinality { - // For fields with unknown cardinality. - CARDINALITY_UNKNOWN = 0; - // For optional fields. - CARDINALITY_OPTIONAL = 1; - // For required fields. Proto2 syntax only. - CARDINALITY_REQUIRED = 2; - // For repeated fields. - CARDINALITY_REPEATED = 3; - } - - // The field type. - Kind kind = 1; - // The field cardinality. - Cardinality cardinality = 2; - // The field number. - int32 number = 3; - // The field name. - string name = 4; - // The field type URL, without the scheme, for message or enumeration - // types. Example: `"type.googleapis.com/google.protobuf.Timestamp"`. - string type_url = 6; - // The index of the field type in `Type.oneofs`, for message or enumeration - // types. The first type has index 1; zero means the type is not in the list. - int32 oneof_index = 7; - // Whether to use alternative packed wire representation. - bool packed = 8; - // The protocol buffer options. - repeated Option options = 9; - // The field JSON name. - string json_name = 10; - // The string value of the default value of this field. Proto2 syntax only. - string default_value = 11; -} - -// Enum type definition. -message Enum { - // Enum type name. - string name = 1; - // Enum value definitions. - repeated EnumValue enumvalue = 2; - // Protocol buffer options. - repeated Option options = 3; - // The source context. - SourceContext source_context = 4; - // The source syntax. - Syntax syntax = 5; -} - -// Enum value definition. -message EnumValue { - // Enum value name. - string name = 1; - // Enum value number. - int32 number = 2; - // Protocol buffer options. - repeated Option options = 3; -} - -// A protocol buffer option, which can be attached to a message, field, -// enumeration, etc. -message Option { - // The option's name. For protobuf built-in options (options defined in - // descriptor.proto), this is the short name. For example, `"map_entry"`. - // For custom options, it should be the fully-qualified name. For example, - // `"google.api.http"`. - string name = 1; - // The option's value packed in an Any message. If the value is a primitive, - // the corresponding wrapper type defined in google/protobuf/wrappers.proto - // should be used. If the value is an enum, it should be stored as an int32 - // value using the google.protobuf.Int32Value type. - Any value = 2; -} - -// The syntax in which a protocol buffer element is defined. -enum Syntax { - // Syntax `proto2`. - SYNTAX_PROTO2 = 0; - // Syntax `proto3`. - SYNTAX_PROTO3 = 1; -} diff --git a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/wrappers.proto b/packages/firefly-client/protobuf/protobuf_external/google/protobuf/wrappers.proto deleted file mode 100644 index d49dd53c..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/google/protobuf/wrappers.proto +++ /dev/null @@ -1,123 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -// Wrappers for primitive (non-message) types. These types are useful -// for embedding primitives in the `google.protobuf.Any` type and for places -// where we need to distinguish between the absence of a primitive -// typed field and its default value. -// -// These wrappers have no meaningful use within repeated fields as they lack -// the ability to detect presence on individual elements. -// These wrappers have no meaningful use within a map or a oneof since -// individual entries of a map or fields of a oneof can already detect presence. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option cc_enable_arenas = true; -option go_package = "google.golang.org/protobuf/types/known/wrapperspb"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "WrappersProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; - -// Wrapper message for `double`. -// -// The JSON representation for `DoubleValue` is JSON number. -message DoubleValue { - // The double value. - double value = 1; -} - -// Wrapper message for `float`. -// -// The JSON representation for `FloatValue` is JSON number. -message FloatValue { - // The float value. - float value = 1; -} - -// Wrapper message for `int64`. -// -// The JSON representation for `Int64Value` is JSON string. -message Int64Value { - // The int64 value. - int64 value = 1; -} - -// Wrapper message for `uint64`. -// -// The JSON representation for `UInt64Value` is JSON string. -message UInt64Value { - // The uint64 value. - uint64 value = 1; -} - -// Wrapper message for `int32`. -// -// The JSON representation for `Int32Value` is JSON number. -message Int32Value { - // The int32 value. - int32 value = 1; -} - -// Wrapper message for `uint32`. -// -// The JSON representation for `UInt32Value` is JSON number. -message UInt32Value { - // The uint32 value. - uint32 value = 1; -} - -// Wrapper message for `bool`. -// -// The JSON representation for `BoolValue` is JSON `true` and `false`. -message BoolValue { - // The bool value. - bool value = 1; -} - -// Wrapper message for `string`. -// -// The JSON representation for `StringValue` is JSON string. -message StringValue { - // The string value. - string value = 1; -} - -// Wrapper message for `bytes`. -// -// The JSON representation for `BytesValue` is JSON string. -message BytesValue { - // The bytes value. - bytes value = 1; -} diff --git a/packages/firefly-client/protobuf/protobuf_external/scalapb/scalapb.proto b/packages/firefly-client/protobuf/protobuf_external/scalapb/scalapb.proto deleted file mode 100644 index 2e9449f2..00000000 --- a/packages/firefly-client/protobuf/protobuf_external/scalapb/scalapb.proto +++ /dev/null @@ -1,379 +0,0 @@ -syntax = "proto2"; - -package scalapb; - -option go_package = "scalapb.github.io/protobuf/scalapb"; -option java_package = "scalapb.options"; - -option (options) = { - package_name: "scalapb.options" - flat_package: true -}; - -import "google/protobuf/descriptor.proto"; - -message ScalaPbOptions { - // If set then it overrides the java_package and package. - optional string package_name = 1; - - // If true, the compiler does not append the proto base file name - // into the generated package name. If false (the default), the - // generated scala package name is the package_name.basename where - // basename is the proto file name without the .proto extension. - optional bool flat_package = 2; - - // Adds the following imports at the top of the file (this is meant - // to provide implicit TypeMappers) - repeated string import = 3; - - // Text to add to the generated scala file. This can be used only - // when single_file is true. - repeated string preamble = 4; - - // If true, all messages and enums (but not services) will be written - // to a single Scala file. - optional bool single_file = 5; - - // By default, wrappers defined at - // https://github.com/google/protobuf/blob/master/src/google/protobuf/wrappers.proto, - // are mapped to an Option[T] where T is a primitive type. When this field - // is set to true, we do not perform this transformation. - optional bool no_primitive_wrappers = 7; - - // DEPRECATED. In ScalaPB <= 0.5.47, it was necessary to explicitly enable - // primitive_wrappers. This field remains here for backwards compatibility, - // but it has no effect on generated code. It is an error to set both - // `primitive_wrappers` and `no_primitive_wrappers`. - optional bool primitive_wrappers = 6; - - // Scala type to be used for repeated fields. If unspecified, - // `scala.collection.Seq` will be used. - optional string collection_type = 8; - - // If set to true, all generated messages in this file will preserve unknown - // fields. - optional bool preserve_unknown_fields = 9 [default=true]; - - // If defined, sets the name of the file-level object that would be generated. This - // object extends `GeneratedFileObject` and contains descriptors, and list of message - // and enum companions. - optional string object_name = 10; - - // Whether to apply the options only to this file, or for the entire package (and its subpackages) - enum OptionsScope { - // Apply the options for this file only (default) - FILE = 0; - - // Apply the options for the entire package and its subpackages. - PACKAGE = 1; - } - // Experimental: scope to apply the given options. - optional OptionsScope scope = 11; - - // If true, lenses will be generated. - optional bool lenses = 12 [default=true]; - - // If true, then source-code info information will be included in the - // generated code - normally the source code info is cleared out to reduce - // code size. The source code info is useful for extracting source code - // location from the descriptors as well as comments. - optional bool retain_source_code_info = 13; - - // Scala type to be used for maps. If unspecified, - // `scala.collection.immutable.Map` will be used. - optional string map_type = 14; - - // If true, no default values will be generated in message constructors. - // This setting can be overridden at the message-level and for individual - // fields. - optional bool no_default_values_in_constructor = 15; - - /* Naming convention for generated enum values */ - enum EnumValueNaming { - AS_IN_PROTO = 0; // Enum value names in Scala use the same name as in the proto - CAMEL_CASE = 1; // Convert enum values to CamelCase in Scala. - } - optional EnumValueNaming enum_value_naming = 16; - - // Indicate if prefix (enum name + optional underscore) should be removed in scala code - // Strip is applied before enum value naming changes. - optional bool enum_strip_prefix = 17 [default=false]; - - // Scala type to use for bytes fields. - optional string bytes_type = 21; - - // Enable java conversions for this file. - optional bool java_conversions = 23; - - // AuxMessageOptions enables you to set message-level options through package-scoped options. - // This is useful when you can't add a dependency on scalapb.proto from the proto file that - // defines the message. - message AuxMessageOptions { - // The fully-qualified name of the message in the proto name space. - optional string target = 1; - - // Options to apply to the message. If there are any options defined on the target message - // they take precedence over the options. - optional MessageOptions options = 2; - } - - // AuxFieldOptions enables you to set field-level options through package-scoped options. - // This is useful when you can't add a dependency on scalapb.proto from the proto file that - // defines the field. - message AuxFieldOptions { - // The fully-qualified name of the field in the proto name space. - optional string target = 1; - - // Options to apply to the field. If there are any options defined on the target message - // they take precedence over the options. - optional FieldOptions options = 2; - } - - // AuxEnumOptions enables you to set enum-level options through package-scoped options. - // This is useful when you can't add a dependency on scalapb.proto from the proto file that - // defines the enum. - message AuxEnumOptions { - // The fully-qualified name of the enum in the proto name space. - optional string target = 1; - - // Options to apply to the enum. If there are any options defined on the target enum - // they take precedence over the options. - optional EnumOptions options = 2; - } - - // AuxEnumValueOptions enables you to set enum value level options through package-scoped - // options. This is useful when you can't add a dependency on scalapb.proto from the proto - // file that defines the enum. - message AuxEnumValueOptions { - // The fully-qualified name of the enum value in the proto name space. - optional string target = 1; - - // Options to apply to the enum value. If there are any options defined on - // the target enum value they take precedence over the options. - optional EnumValueOptions options = 2; - } - - // List of message options to apply to some messages. - repeated AuxMessageOptions aux_message_options = 18; - - // List of message options to apply to some fields. - repeated AuxFieldOptions aux_field_options = 19; - - // List of message options to apply to some enums. - repeated AuxEnumOptions aux_enum_options = 20; - - // List of enum value options to apply to some enum values. - repeated AuxEnumValueOptions aux_enum_value_options = 22; - - // List of preprocessors to apply. - repeated string preprocessors = 24; - - repeated FieldTransformation field_transformations = 25; - - // Ignores all transformations for this file. This is meant to allow specific files to - // opt out from transformations inherited through package-scoped options. - optional bool ignore_all_transformations = 26; - - // If true, getters will be generated. - optional bool getters = 27 [default=true]; - - // For use in tests only. Inhibit Java conversions even when when generator parameters - // request for it. - optional bool test_only_no_java_conversions = 999; - - extensions 1000 to max; -} - -extend google.protobuf.FileOptions { - // File-level optionals for ScalaPB. - // Extension number officially assigned by protobuf-global-extension-registry@google.com - optional ScalaPbOptions options = 1020; -} - -message MessageOptions { - // Additional classes and traits to mix in to the case class. - repeated string extends = 1; - - // Additional classes and traits to mix in to the companion object. - repeated string companion_extends = 2; - - // Custom annotations to add to the generated case class. - repeated string annotations = 3; - - // All instances of this message will be converted to this type. An implicit TypeMapper - // must be present. - optional string type = 4; - - // Custom annotations to add to the companion object of the generated class. - repeated string companion_annotations = 5; - - // Additional classes and traits to mix in to generated sealed_oneof base trait. - repeated string sealed_oneof_extends = 6; - - // If true, when this message is used as an optional field, do not wrap it in an `Option`. - // This is equivalent of setting `(field).no_box` to true on each field with the message type. - optional bool no_box = 7; - - // Custom annotations to add to the generated `unknownFields` case class field. - repeated string unknown_fields_annotations = 8; - - // If true, no default values will be generated in message constructors. - // If set (to true or false), the message-level setting overrides the - // file-level value, and can be overridden by the field-level setting. - optional bool no_default_values_in_constructor = 9; - - // Additional classes and traits to mix in to generated sealed oneof base trait's companion object. - repeated string sealed_oneof_companion_extends = 10; - - extensions 1000 to max; -} - -extend google.protobuf.MessageOptions { - // Message-level optionals for ScalaPB. - // Extension number officially assigned by protobuf-global-extension-registry@google.com - optional MessageOptions message = 1020; -} - -// Represents a custom Collection type in Scala. This allows ScalaPB to integrate with -// collection types that are different enough from the ones in the standard library. -message Collection { - // Type of the collection - optional string type = 1; - - // Set to true if this collection type is not allowed to be empty, for example - // cats.data.NonEmptyList. When true, ScalaPB will not generate `clearX` for the repeated - // field and not provide a default argument in the constructor. - optional bool non_empty = 2; - - // An Adapter is a Scala object available at runtime that provides certain static methods - // that can operate on this collection type. - optional string adapter = 3; -} - -message FieldOptions { - optional string type = 1; - - optional string scala_name = 2; - - // Can be specified only if this field is repeated. If unspecified, - // it falls back to the file option named `collection_type`, which defaults - // to `scala.collection.Seq`. - optional string collection_type = 3; - - optional Collection collection = 8; - - // If the field is a map, you can specify custom Scala types for the key - // or value. - optional string key_type = 4; - optional string value_type = 5; - - // Custom annotations to add to the field. - repeated string annotations = 6; - - // Can be specified only if this field is a map. If unspecified, - // it falls back to the file option named `map_type` which defaults to - // `scala.collection.immutable.Map` - optional string map_type = 7; - - // If true, no default value will be generated for this field in the message - // constructor. If this field is set, it has the highest precedence and overrides the - // values at the message-level and file-level. - optional bool no_default_value_in_constructor = 9; - - // Do not box this value in Option[T]. If set, this overrides MessageOptions.no_box - optional bool no_box = 30; - - // Like no_box it does not box a value in Option[T], but also fails parsing when a value - // is not provided. This enables to emulate required fields in proto3. - optional bool required = 31; - - extensions 1000 to max; -} - -extend google.protobuf.FieldOptions { - // Field-level optionals for ScalaPB. - // Extension number officially assigned by protobuf-global-extension-registry@google.com - optional FieldOptions field = 1020; -} - -message EnumOptions { - // Additional classes and traits to mix in to the base trait - repeated string extends = 1; - - // Additional classes and traits to mix in to the companion object. - repeated string companion_extends = 2; - - // All instances of this enum will be converted to this type. An implicit TypeMapper - // must be present. - optional string type = 3; - - // Custom annotations to add to the generated enum's base class. - repeated string base_annotations = 4; - - // Custom annotations to add to the generated trait. - repeated string recognized_annotations = 5; - - // Custom annotations to add to the generated Unrecognized case class. - repeated string unrecognized_annotations = 6; - - extensions 1000 to max; -} - -extend google.protobuf.EnumOptions { - // Enum-level optionals for ScalaPB. - // Extension number officially assigned by protobuf-global-extension-registry@google.com - // - // The field is called enum_options and not enum since enum is not allowed in Java. - optional EnumOptions enum_options = 1020; -} - -message EnumValueOptions { - // Additional classes and traits to mix in to an individual enum value. - repeated string extends = 1; - - // Name in Scala to use for this enum value. - optional string scala_name = 2; - - // Custom annotations to add to the generated case object for this enum value. - repeated string annotations = 3; - - extensions 1000 to max; -} - -extend google.protobuf.EnumValueOptions { - // Enum-level optionals for ScalaPB. - // Extension number officially assigned by protobuf-global-extension-registry@google.com - optional EnumValueOptions enum_value = 1020; -} - -message OneofOptions { - // Additional traits to mix in to a oneof. - repeated string extends = 1; - - // Name in Scala to use for this oneof field. - optional string scala_name = 2; - - extensions 1000 to max; -} - -extend google.protobuf.OneofOptions { - // Enum-level optionals for ScalaPB. - // Extension number officially assigned by protobuf-global-extension-registry@google.com - optional OneofOptions oneof = 1020; -} - -enum MatchType { - CONTAINS = 0; - EXACT = 1; - PRESENCE = 2; -} - -message FieldTransformation { - optional google.protobuf.FieldDescriptorProto when = 1; - optional MatchType match_type = 2 [default=CONTAINS]; - optional google.protobuf.FieldOptions set = 3; -} - -message PreprocessorOutput { - map options_by_file = 1; -} diff --git a/packages/firefly-client/protobuf/routing.proto b/packages/firefly-client/protobuf/routing.proto deleted file mode 100644 index 1a57c679..00000000 --- a/packages/firefly-client/protobuf/routing.proto +++ /dev/null @@ -1,103 +0,0 @@ -syntax = "proto3"; -package routing; - -// If you are building for other languages "scalapb.proto" -// can be manually obtained here: -// https://raw.githubusercontent.com/scalapb/ScalaPB/master/protobuf/scalapb/scalapb.proto -// make a scalapb directory in this file's location and place it inside - -import "scalapb/scalapb.proto"; - -option (scalapb.options) = { - package_name: "coop.rchain.comm.protocol.routing" - flat_package: true - preserve_unknown_fields: false -}; - -message Node { - bytes id = 1; - bytes host = 2; - uint32 tcp_port = 3; - uint32 udp_port = 4; -} - -message Header { - Node sender = 1 [(scalapb.field).no_box = true]; - string networkId = 2; -} - -message Heartbeat { -} - -message HeartbeatResponse { -} - -message ProtocolHandshake { - bytes nonce = 1; -} - -message ProtocolHandshakeResponse { - bytes nonce = 1; -} - -message Packet { - string typeId = 1; - bytes content = 2; -} - -message Disconnect { -} - -message Protocol { - Header header = 1 [(scalapb.field).no_box = true]; - oneof message { - Heartbeat heartbeat = 2; - ProtocolHandshake protocol_handshake = 3; - ProtocolHandshakeResponse protocol_handshake_response = 4; - Packet packet = 5; - Disconnect disconnect = 6; - } -} - -service TransportLayer { - rpc Send (TLRequest) returns (TLResponse) {} - rpc Stream (stream Chunk) returns (TLResponse) {} -} - -message TLRequest { - Protocol protocol = 1 [(scalapb.field).no_box = true]; -} - -message InternalServerError { - bytes error = 1; -} - -message Ack { - Header header = 1 [(scalapb.field).no_box = true]; -} - -message TLResponse { - oneof payload { - Ack ack = 1; - InternalServerError internalServerError = 2; - } -} - -message ChunkHeader { - Node sender = 1 [(scalapb.field).no_box = true]; - string typeId = 2; - bool compressed = 3; - int32 contentLength = 4; - string networkId = 5; -} - -message ChunkData { - bytes contentData = 1; -} - -message Chunk { - oneof content { - ChunkHeader header = 1; - ChunkData data = 2; - } -} diff --git a/packages/firefly-client/src/errors.rs b/packages/firefly-client/src/errors.rs index 398a5fb1..511f81da 100644 --- a/packages/firefly-client/src/errors.rs +++ b/packages/firefly-client/src/errors.rs @@ -8,4 +8,50 @@ pub enum ReadNodeError { Deserialization(anyhow::Error), #[error("http transport error: {0}")] Transport(#[from] reqwest::Error), + #[error("explore-deploy timed out after {0:?}")] + Timeout(std::time::Duration), +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::*; + + #[test] + fn test_return_value_missing_display() { + let err = ReadNodeError::ReturnValueMissing; + assert_eq!(err.to_string(), "contract did not return any value"); + } + + #[test] + fn test_api_error_display() { + let err = ReadNodeError::Api( + reqwest::StatusCode::INTERNAL_SERVER_ERROR, + "body text".to_string(), + ); + let display = err.to_string(); + assert!(display.contains("500"), "expected '500' in: {display}"); + assert!( + display.contains("body text"), + "expected 'body text' in: {display}" + ); + } + + #[test] + fn test_deserialization_error_display() { + let err = ReadNodeError::Deserialization(anyhow::anyhow!("parse failed")); + let display = err.to_string(); + assert!( + display.contains("parse failed"), + "expected 'parse failed' in: {display}" + ); + } + + #[test] + fn test_timeout_error_display() { + let err = ReadNodeError::Timeout(Duration::from_secs(45)); + let display = err.to_string(); + assert!(display.contains("45"), "expected '45' in: {display}"); + } } diff --git a/packages/firefly-client/src/lib.rs b/packages/firefly-client/src/lib.rs index e83abe6c..c4622743 100644 --- a/packages/firefly-client/src/lib.rs +++ b/packages/firefly-client/src/lib.rs @@ -5,9 +5,11 @@ pub mod models; pub mod node_events; mod read_node_client; pub mod rendering; +pub mod traits; mod write_node_client; pub use communication_service::CommunicationService; pub use node_events::NodeEvents; pub use read_node_client::ReadNodeClient; +pub use traits::{NodeEventSource, ReadNode, WriteNode}; pub use write_node_client::WriteNodeClient; diff --git a/packages/firefly-client/src/models.rs b/packages/firefly-client/src/models.rs index b1504e96..f8e83ec1 100644 --- a/packages/firefly-client/src/models.rs +++ b/packages/firefly-client/src/models.rs @@ -8,6 +8,7 @@ use crc::Crc; use derive_more::{AsRef, Display, From, Into}; use digest::OutputSizeUser; use digest::typenum::Unsigned; +pub use f1r3node_models::{casper, rhoapi, servicemodelapi}; use secp256k1::PublicKey; use serde::{Deserialize, Deserializer, Serialize, de}; use thiserror::Error; @@ -15,34 +16,6 @@ use thiserror::Error; use crate::helpers::ShortHex; use crate::rendering::{IntoValue, Value}; -pub mod servicemodelapi { - #![allow(warnings)] - #![allow(clippy::all)] - #![allow(clippy::pedantic)] - #![allow(clippy::nursery)] - tonic::include_proto!("servicemodelapi"); -} - -pub mod rhoapi { - #![allow(warnings)] - #![allow(clippy::all)] - #![allow(clippy::pedantic)] - #![allow(clippy::nursery)] - tonic::include_proto!("rhoapi"); -} - -pub mod casper { - #![allow(warnings)] - #![allow(clippy::all)] - #![allow(clippy::pedantic)] - #![allow(clippy::nursery)] - tonic::include_proto!("casper"); - - pub mod v1 { - tonic::include_proto!("casper.v1"); - } -} - #[derive( Debug, Clone, @@ -119,18 +92,49 @@ impl From for serde_json::Value { #[derive(Debug, Clone, Deserialize)] pub enum ReadNodeExpr { - ExprTuple { data: Vec }, - ExprList { data: Vec }, - ExprSet { data: Vec }, - ExprMap { data: HashMap }, + ExprTuple { + data: Vec, + }, + ExprList { + data: Vec, + }, + ExprSet { + data: Vec, + }, + ExprMap { + data: HashMap, + }, ExprNil {}, - ExprBool { data: bool }, - ExprInt { data: serde_json::Number }, - ExprString { data: String }, - ExprBytes { data: String }, - ExprUri { data: String }, - ExprUnforg { data: ReadNodeExprUnforg }, + ExprBool { + data: bool, + }, + ExprInt { + data: serde_json::Number, + }, + ExprString { + data: String, + }, + ExprBytes { + data: String, + }, + ExprUri { + data: String, + }, + ExprUnforg { + data: ReadNodeExprUnforg, + }, + + // A bundle wraps an inner value with read/write permission flags. The read + // node returns bundled names from registry lookups (e.g. `rho:registry:lookup` + // yields a `bundle+{...}` around an unforgeable name). For JSON purposes the + // wrapper is transparent — we resolve to the inner value, mirroring how + // `ExprUnforg` unwraps to its data. + ExprBundle { + data: Box, + read: bool, + write: bool, + }, } impl From for serde_json::Value { @@ -155,6 +159,7 @@ impl From for serde_json::Value { ReadNodeExpr::ExprBytes { data } => Self::String(data), ReadNodeExpr::ExprUri { data } => Self::String(data), ReadNodeExpr::ExprUnforg { data } => data.into(), + ReadNodeExpr::ExprBundle { data, .. } => (*data).into(), } } } @@ -254,9 +259,23 @@ pub struct DeployData { #[serde(tag = "event", rename_all = "kebab-case")] pub enum NodeEvent { Started, - BlockAdded { payload: BlockEventPayload }, - BlockCreated { payload: BlockEventPayload }, - BlockFinalised { payload: BlockEventPayload }, + BlockAdded { + payload: BlockEventPayload, + }, + BlockCreated { + payload: BlockEventPayload, + }, + BlockFinalised { + payload: BlockEventPayload, + }, + + // Catch-all for lifecycle/status events embers does not act on. f1r3node + // >= 0.4.15 emits additional variants (`node-started`, `sent-approved-block`, + // `transfers-available`, ...); without this they would fail to deserialize + // and log spurious warnings on every tick. We only care about the block + // events above, so anything else is absorbed and ignored. + #[serde(other)] + Other, } #[derive(Debug, Clone, Deserialize)] @@ -427,3 +446,829 @@ impl IntoValue for Uri { Value::Uri(self.0) } } + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + /// Helper: deterministic key pair for tests. + fn test_keypair() -> (secp256k1::SecretKey, secp256k1::PublicKey) { + let secp = secp256k1::Secp256k1::new(); + let secret_key = + secp256k1::SecretKey::from_byte_array([1u8; 32]).expect("valid 32-byte secret key"); + let public_key = secp256k1::PublicKey::from_secret_key(&secp, &secret_key); + (secret_key, public_key) + } + + // ----------------------------------------------------------------------- + // WalletAddress + // ----------------------------------------------------------------------- + + #[test] + fn wallet_address_from_public_key_roundtrips() { + let (_sk, pk) = test_keypair(); + let addr = WalletAddress::from(pk); + let addr_string: String = addr.clone().into(); + + // Re-parsing the produced string must succeed and yield the same value. + let parsed = WalletAddress::try_from(addr_string.clone()) + .expect("WalletAddress produced by From must parse successfully"); + assert_eq!( + addr, parsed, + "roundtripped WalletAddress should equal the original" + ); + } + + #[test] + fn wallet_address_try_from_invalid_base58() { + // '0', 'O', 'I', 'l' are not in the Base58 alphabet. + let result = WalletAddress::try_from("0OIl!!!".to_string()); + assert!( + result.is_err(), + "invalid base58 characters should produce an error" + ); + assert!( + matches!( + result.unwrap_err(), + ParseWalletAddressError::EncoderError(_) + ), + "expected EncoderError variant for bad base58" + ); + } + + #[test] + fn wallet_address_try_from_too_short() { + // A valid base58 string that decodes to fewer than 5 bytes (payload needs + // at least 1 byte + 4 checksum bytes). + let result = WalletAddress::try_from("1".to_string()); + assert!( + result.is_err(), + "a too-short address should produce an error" + ); + assert!( + matches!( + result.unwrap_err(), + ParseWalletAddressError::InvalidRevAddressSize(_) + ), + "expected InvalidRevAddressSize for too-short input" + ); + } + + #[test] + fn wallet_address_try_from_invalid_checksum() { + let (_sk, pk) = test_keypair(); + let addr = WalletAddress::from(pk); + let addr_string: String = addr.into(); + + // Corrupt the last character of the base58 string to invalidate the checksum. + let mut corrupted = addr_string.clone(); + let last = corrupted.pop().expect("non-empty address"); + // Pick a different valid base58 character. + let replacement = if last == '1' { '2' } else { '1' }; + corrupted.push(replacement); + + let result = WalletAddress::try_from(corrupted); + assert!( + result.is_err(), + "a corrupted checksum should produce an error" + ); + assert!( + matches!( + result.unwrap_err(), + ParseWalletAddressError::InvalidAddress(_) + ), + "expected InvalidAddress for bad checksum" + ); + } + + // ----------------------------------------------------------------------- + // Uri + // ----------------------------------------------------------------------- + + #[test] + fn uri_from_public_key_roundtrips() { + let (_sk, pk) = test_keypair(); + let uri = Uri::from(pk); + let uri_string: String = uri.clone().into(); + + assert!( + uri_string.starts_with("rho:id:"), + "URI must start with rho:id: prefix, got: {uri_string}" + ); + + let parsed = Uri::try_from(uri_string.clone()) + .expect("Uri produced by From must parse successfully"); + assert_eq!(uri, parsed, "roundtripped Uri should equal the original"); + } + + #[test] + fn uri_try_from_invalid_prefix() { + let result = Uri::try_from("invalid:prefix:abc".to_string()); + assert!(result.is_err(), "wrong prefix should produce an error"); + assert!( + matches!(result.unwrap_err(), ParseUriError::IvalidPrefix), + "expected IvalidPrefix variant" + ); + } + + #[test] + fn uri_try_from_invalid_zbase32() { + // zbase32 alphabet is ybndrfg8ejkmcpqxot1uwisza345h769. + // 'v' and '2' are NOT in the alphabet. We need a 54-char string + // (270 bits / 5 bits per char) so the library does not panic on + // length before checking character validity. + let bad_encoded = "v".repeat(54); + let result = Uri::try_from(format!("rho:id:{bad_encoded}")); + assert!( + result.is_err(), + "invalid zbase32 characters should produce an error" + ); + assert!( + matches!(result.unwrap_err(), ParseUriError::InvalidZBase32(_)), + "expected InvalidZBase32 variant" + ); + } + + #[test] + fn uri_try_from_invalid_checksum() { + let (_sk, pk) = test_keypair(); + let uri = Uri::from(pk); + let uri_string: String = uri.into(); + + // Flip a character in the encoded portion to corrupt the checksum. + let encoded_part = uri_string + .strip_prefix("rho:id:") + .expect("has rho:id: prefix"); + let mut chars: Vec = encoded_part.chars().collect(); + // Change the first character to something different but still valid zbase32. + chars[0] = if chars[0] == 'y' { 'b' } else { 'y' }; + let corrupted = format!("rho:id:{}", chars.into_iter().collect::()); + + let result = Uri::try_from(corrupted); + assert!( + result.is_err(), + "corrupted checksum should produce an error" + ); + // May be ChecksumMistmatch or InvalidDecodedLength depending on what + // the corruption does to the decoded bytes. + let err = result.unwrap_err(); + assert!( + matches!( + err, + ParseUriError::ChecksumMistmatch | ParseUriError::InvalidDecodedLength + ), + "expected ChecksumMistmatch or InvalidDecodedLength, got: {err:?}" + ); + } + + // ----------------------------------------------------------------------- + // ReadNodeExpr -> serde_json::Value conversion + // ----------------------------------------------------------------------- + + #[test] + fn read_node_expr_nil_converts_to_null() { + let expr = ReadNodeExpr::ExprNil {}; + let val: serde_json::Value = expr.into(); + assert_eq!(val, serde_json::Value::Null, "ExprNil should become null"); + } + + #[test] + fn read_node_expr_bool_converts() { + let expr_true = ReadNodeExpr::ExprBool { data: true }; + let expr_false = ReadNodeExpr::ExprBool { data: false }; + assert_eq!( + serde_json::Value::from(expr_true), + json!(true), + "ExprBool(true)" + ); + assert_eq!( + serde_json::Value::from(expr_false), + json!(false), + "ExprBool(false)" + ); + } + + #[test] + fn read_node_expr_int_converts() { + let expr = ReadNodeExpr::ExprInt { + data: serde_json::Number::from(42), + }; + assert_eq!(serde_json::Value::from(expr), json!(42), "ExprInt(42)"); + } + + #[test] + fn read_node_expr_string_converts() { + let expr = ReadNodeExpr::ExprString { + data: "hello".to_string(), + }; + assert_eq!(serde_json::Value::from(expr), json!("hello"), "ExprString"); + } + + #[test] + fn read_node_expr_bytes_converts() { + let expr = ReadNodeExpr::ExprBytes { + data: "deadbeef".to_string(), + }; + assert_eq!( + serde_json::Value::from(expr), + json!("deadbeef"), + "ExprBytes" + ); + } + + #[test] + fn read_node_expr_uri_converts() { + let expr = ReadNodeExpr::ExprUri { + data: "rho:id:abc".to_string(), + }; + assert_eq!( + serde_json::Value::from(expr), + json!("rho:id:abc"), + "ExprUri" + ); + } + + #[test] + fn read_node_expr_tuple_converts() { + let expr = ReadNodeExpr::ExprTuple { + data: vec![ + ReadNodeExpr::ExprInt { + data: serde_json::Number::from(1), + }, + ReadNodeExpr::ExprBool { data: true }, + ], + }; + assert_eq!(serde_json::Value::from(expr), json!([1, true]), "ExprTuple"); + } + + #[test] + fn read_node_expr_list_converts() { + let expr = ReadNodeExpr::ExprList { + data: vec![ + ReadNodeExpr::ExprString { + data: "a".to_string(), + }, + ReadNodeExpr::ExprString { + data: "b".to_string(), + }, + ], + }; + assert_eq!(serde_json::Value::from(expr), json!(["a", "b"]), "ExprList"); + } + + #[test] + fn read_node_expr_set_converts() { + let expr = ReadNodeExpr::ExprSet { + data: vec![ReadNodeExpr::ExprInt { + data: serde_json::Number::from(99), + }], + }; + assert_eq!( + serde_json::Value::from(expr), + json!([99]), + "ExprSet should become a JSON array" + ); + } + + #[test] + fn read_node_expr_map_converts() { + let mut map = HashMap::new(); + map.insert( + "key".to_string(), + ReadNodeExpr::ExprString { + data: "value".to_string(), + }, + ); + let expr = ReadNodeExpr::ExprMap { data: map }; + assert_eq!( + serde_json::Value::from(expr), + json!({"key": "value"}), + "ExprMap" + ); + } + + #[test] + fn read_node_expr_unforg_private_converts() { + let expr = ReadNodeExpr::ExprUnforg { + data: ReadNodeExprUnforg::UnforgPrivate { + data: "priv123".to_string(), + }, + }; + assert_eq!( + serde_json::Value::from(expr), + json!("priv123"), + "ExprUnforg(Private)" + ); + } + + #[test] + fn read_node_expr_unforg_deploy_converts() { + let expr = ReadNodeExpr::ExprUnforg { + data: ReadNodeExprUnforg::UnforgDeploy { + data: "deploy456".to_string(), + }, + }; + assert_eq!( + serde_json::Value::from(expr), + json!("deploy456"), + "ExprUnforg(Deploy)" + ); + } + + #[test] + fn read_node_expr_unforg_deployer_converts() { + let expr = ReadNodeExpr::ExprUnforg { + data: ReadNodeExprUnforg::UnforgDeployer { + data: "deployer789".to_string(), + }, + }; + assert_eq!( + serde_json::Value::from(expr), + json!("deployer789"), + "ExprUnforg(Deployer)" + ); + } + + #[test] + fn read_node_expr_bundle_converts_transparently() { + // A bundle is a permission wrapper; converting it must resolve to the + // inner value (here an unforgeable private name -> its hex string). + let expr = ReadNodeExpr::ExprBundle { + data: Box::new(ReadNodeExpr::ExprUnforg { + data: ReadNodeExprUnforg::UnforgPrivate { + data: "abc123".to_string(), + }, + }), + read: false, + write: true, + }; + assert_eq!( + serde_json::Value::from(expr), + json!("abc123"), + "ExprBundle should resolve to its inner value" + ); + } + + #[test] + fn deserialize_expr_bundle_from_read_node_json() { + // Exact shape returned by the f1r3node >= 0.4.15 read node when a + // registry lookup (`rho:registry:lookup`) yields a bundled unforgeable + // name. Prior to the ExprBundle variant this failed to deserialize and + // blocked embers startup ("failed to deserialize intermediate model"). + let v = json!({ + "ExprBundle": { + "data": {"ExprUnforg": {"data": {"UnforgPrivate": {"data": "deadbeef"}}}}, + "read": false, + "write": true + } + }); + let expr: ReadNodeExpr = serde_json::from_value(v).expect("ExprBundle should deserialize"); + match &expr { + ReadNodeExpr::ExprBundle { read, write, .. } => { + assert!(!*read, "read flag"); + assert!(*write, "write flag"); + } + other => panic!("expected ExprBundle, got {other:?}"), + } + assert_eq!( + serde_json::Value::from(expr), + json!("deadbeef"), + "bundled unforgeable name resolves to its hex string" + ); + } + + #[test] + fn deserialize_bundled_registry_tuple_end_to_end() { + // The precise `/expr/0` payload observed from the read node for the + // agents-env registry lookup: a tuple of (nonce, bundle+{name}). + let v = json!({ + "ExprTuple": { + "data": [ + {"ExprInt": {"data": 0}}, + {"ExprBundle": { + "data": {"ExprUnforg": {"data": {"UnforgPrivate": {"data": "aadeb1f8"}}}}, + "read": false, + "write": true + }} + ] + } + }); + let expr: ReadNodeExpr = + serde_json::from_value(v).expect("bundled registry tuple should deserialize"); + assert_eq!( + serde_json::Value::from(expr), + json!([0, "aadeb1f8"]), + "tuple resolves with the bundle unwrapped to its inner name" + ); + } + + // ----------------------------------------------------------------------- + // ReadNodeExpr deserialization from JSON + // ----------------------------------------------------------------------- + + #[test] + fn deserialize_expr_nil() { + let v = json!({"ExprNil": {}}); + let expr: ReadNodeExpr = serde_json::from_value(v).expect("ExprNil should deserialize"); + assert!( + matches!(expr, ReadNodeExpr::ExprNil {}), + "expected ExprNil variant" + ); + } + + #[test] + fn deserialize_expr_bool() { + let v = json!({"ExprBool": {"data": true}}); + let expr: ReadNodeExpr = serde_json::from_value(v).expect("ExprBool should deserialize"); + assert!( + matches!(expr, ReadNodeExpr::ExprBool { data: true }), + "expected ExprBool {{ data: true }}" + ); + } + + #[test] + fn deserialize_expr_int() { + let v = json!({"ExprInt": {"data": -7}}); + let expr: ReadNodeExpr = serde_json::from_value(v).expect("ExprInt should deserialize"); + match expr { + ReadNodeExpr::ExprInt { data } => { + assert_eq!(data, serde_json::Number::from(-7), "expected -7"); + } + other => panic!("expected ExprInt, got {other:?}"), + } + } + + #[test] + fn deserialize_expr_string() { + let v = json!({"ExprString": {"data": "hello world"}}); + let expr: ReadNodeExpr = serde_json::from_value(v).expect("ExprString should deserialize"); + match expr { + ReadNodeExpr::ExprString { data } => { + assert_eq!(data, "hello world"); + } + other => panic!("expected ExprString, got {other:?}"), + } + } + + #[test] + fn deserialize_expr_bytes() { + let v = json!({"ExprBytes": {"data": "cafebabe"}}); + let expr: ReadNodeExpr = serde_json::from_value(v).expect("ExprBytes should deserialize"); + match expr { + ReadNodeExpr::ExprBytes { data } => { + assert_eq!(data, "cafebabe"); + } + other => panic!("expected ExprBytes, got {other:?}"), + } + } + + #[test] + fn deserialize_expr_uri() { + let v = json!({"ExprUri": {"data": "rho:id:xyz"}}); + let expr: ReadNodeExpr = serde_json::from_value(v).expect("ExprUri should deserialize"); + match expr { + ReadNodeExpr::ExprUri { data } => { + assert_eq!(data, "rho:id:xyz"); + } + other => panic!("expected ExprUri, got {other:?}"), + } + } + + #[test] + fn deserialize_expr_tuple() { + let v = json!({"ExprTuple": {"data": [{"ExprInt": {"data": 1}}, {"ExprBool": {"data": false}}]}}); + let expr: ReadNodeExpr = serde_json::from_value(v).expect("ExprTuple should deserialize"); + match expr { + ReadNodeExpr::ExprTuple { data } => { + assert_eq!(data.len(), 2, "tuple should have 2 elements"); + } + other => panic!("expected ExprTuple, got {other:?}"), + } + } + + #[test] + fn deserialize_expr_list() { + let v = json!({"ExprList": {"data": [{"ExprString": {"data": "a"}}]}}); + let expr: ReadNodeExpr = serde_json::from_value(v).expect("ExprList should deserialize"); + match expr { + ReadNodeExpr::ExprList { data } => { + assert_eq!(data.len(), 1, "list should have 1 element"); + } + other => panic!("expected ExprList, got {other:?}"), + } + } + + #[test] + fn deserialize_expr_set() { + let v = json!({"ExprSet": {"data": []}}); + let expr: ReadNodeExpr = serde_json::from_value(v).expect("ExprSet should deserialize"); + match expr { + ReadNodeExpr::ExprSet { data } => { + assert!(data.is_empty(), "set should be empty"); + } + other => panic!("expected ExprSet, got {other:?}"), + } + } + + #[test] + fn deserialize_expr_map() { + let v = json!({"ExprMap": {"data": {"k": {"ExprNil": {}}}}}); + let expr: ReadNodeExpr = serde_json::from_value(v).expect("ExprMap should deserialize"); + match expr { + ReadNodeExpr::ExprMap { data } => { + assert!(data.contains_key("k"), "map should contain key 'k'"); + } + other => panic!("expected ExprMap, got {other:?}"), + } + } + + #[test] + fn deserialize_expr_unforg_private() { + let v = json!({"ExprUnforg": {"data": {"UnforgPrivate": {"data": "abc"}}}}); + let expr: ReadNodeExpr = + serde_json::from_value(v).expect("ExprUnforg(Private) should deserialize"); + match expr { + ReadNodeExpr::ExprUnforg { + data: ReadNodeExprUnforg::UnforgPrivate { data }, + } => { + assert_eq!(data, "abc"); + } + other => panic!("expected ExprUnforg(UnforgPrivate), got {other:?}"), + } + } + + #[test] + fn deserialize_expr_unforg_deploy() { + let v = json!({"ExprUnforg": {"data": {"UnforgDeploy": {"data": "dep"}}}}); + let expr: ReadNodeExpr = + serde_json::from_value(v).expect("ExprUnforg(Deploy) should deserialize"); + match expr { + ReadNodeExpr::ExprUnforg { + data: ReadNodeExprUnforg::UnforgDeploy { data }, + } => { + assert_eq!(data, "dep"); + } + other => panic!("expected ExprUnforg(UnforgDeploy), got {other:?}"), + } + } + + #[test] + fn deserialize_expr_unforg_deployer() { + let v = json!({"ExprUnforg": {"data": {"UnforgDeployer": {"data": "dplyr"}}}}); + let expr: ReadNodeExpr = + serde_json::from_value(v).expect("ExprUnforg(Deployer) should deserialize"); + match expr { + ReadNodeExpr::ExprUnforg { + data: ReadNodeExprUnforg::UnforgDeployer { data }, + } => { + assert_eq!(data, "dplyr"); + } + other => panic!("expected ExprUnforg(UnforgDeployer), got {other:?}"), + } + } + + // ----------------------------------------------------------------------- + // Either + // ----------------------------------------------------------------------- + + #[test] + fn either_deserialize_right() { + let v = json!([true, 42]); + let either: Either = + serde_json::from_value(v).expect("Either Right should deserialize"); + assert!(matches!(either, Either::Right(42)), "expected Right(42)"); + } + + #[test] + fn either_deserialize_left() { + let v = json!([false, "error message"]); + let either: Either = + serde_json::from_value(v).expect("Either Left should deserialize"); + match either { + Either::Left(msg) => assert_eq!(msg, "error message"), + Either::Right(n) => panic!("expected Left, got Right({n})"), + } + } + + #[test] + fn either_to_result_ok() { + let either: Either = Either::Right(100); + let result = either.to_result(); + assert_eq!(result, Ok(100), "Right should become Ok"); + } + + #[test] + fn either_to_result_err() { + let either: Either = Either::Left("fail".to_string()); + let result = either.to_result(); + assert_eq!(result, Err("fail".to_string()), "Left should become Err"); + } + + #[test] + fn either_from_into_result() { + let right: Either<&str, u64> = Either::Right(7); + let result: Result = right.into(); + assert_eq!(result, Ok(7)); + + let left: Either<&str, u64> = Either::Left("nope"); + let result: Result = left.into(); + assert_eq!(result, Err("nope")); + } + + // ----------------------------------------------------------------------- + // NodeEvent deserialization + // ----------------------------------------------------------------------- + + #[test] + fn node_event_started() { + let v = json!({"event": "started"}); + let event: NodeEvent = serde_json::from_value(v).expect("Started event should deserialize"); + assert!( + matches!(event, NodeEvent::Started), + "expected Started variant" + ); + } + + #[test] + fn node_event_unknown_variants_map_to_other() { + // f1r3node >= 0.4.15 lifecycle/status events embers does not model. They + // must deserialize as `Other` (with or without an extra payload) instead + // of erroring, so the deploy-tracking stream is not disrupted. + for v in [ + json!({"event": "node-started"}), + json!({"event": "sent-approved-block"}), + json!({"event": "transfers-available", "payload": {"block-hash": "abc"}}), + ] { + let event: NodeEvent = + serde_json::from_value(v.clone()).unwrap_or_else(|e| panic!("{v} -> {e}")); + assert!( + matches!(event, NodeEvent::Other), + "unknown event {v} should map to Other, got {event:?}" + ); + } + } + + #[test] + fn node_event_block_added() { + let (_sk, pk) = test_keypair(); + let deployer_hex = hex::encode(pk.serialize()); + + let v = json!({ + "event": "block-added", + "payload": { + "block-hash": "abc123", + "deploys": [ + { + "id": "deploy-001", + "cost": 1000, + "deployer": deployer_hex, + "errored": false + } + ] + } + }); + + let event: NodeEvent = + serde_json::from_value(v).expect("BlockAdded event should deserialize"); + match event { + NodeEvent::BlockAdded { payload } => { + assert_eq!(payload.block_hash.as_ref(), "abc123"); + assert_eq!(payload.deploys.len(), 1); + let deploy = &payload.deploys[0]; + assert_eq!(deploy.id.as_ref(), "deploy-001"); + assert_eq!(deploy.cost, 1000); + assert_eq!(deploy.deployer, pk); + assert!(!deploy.errored); + } + other => panic!("expected BlockAdded, got {other:?}"), + } + } + + #[test] + fn node_event_block_finalised() { + let (_sk, pk) = test_keypair(); + let deployer_hex = hex::encode(pk.serialize()); + + let v = json!({ + "event": "block-finalised", + "payload": { + "block-hash": "finalhash", + "deploys": [ + { + "id": "deploy-fin-1", + "cost": 500, + "deployer": deployer_hex, + "errored": true + } + ] + } + }); + + let event: NodeEvent = + serde_json::from_value(v).expect("BlockFinalised event should deserialize"); + match event { + NodeEvent::BlockFinalised { payload } => { + assert_eq!(payload.block_hash.as_ref(), "finalhash"); + assert_eq!(payload.deploys.len(), 1); + let deploy = &payload.deploys[0]; + assert_eq!(deploy.id.as_ref(), "deploy-fin-1"); + assert_eq!(deploy.cost, 500); + assert_eq!(deploy.deployer, pk); + assert!(deploy.errored); + } + other => panic!("expected BlockFinalised, got {other:?}"), + } + } + + #[test] + fn node_event_block_finalised_empty_deploys() { + let v = json!({ + "event": "block-finalised", + "payload": { + "block-hash": "emptyhash", + "deploys": [] + } + }); + + let event: NodeEvent = + serde_json::from_value(v).expect("BlockFinalised with empty deploys should parse"); + match event { + NodeEvent::BlockFinalised { payload } => { + assert!(payload.deploys.is_empty(), "deploys should be empty"); + } + other => panic!("expected BlockFinalised, got {other:?}"), + } + } + + #[test] + fn node_event_block_created() { + let v = json!({ + "event": "block-created", + "payload": { + "block-hash": "created-hash", + "deploys": [] + } + }); + + let event: NodeEvent = + serde_json::from_value(v).expect("BlockCreated event should deserialize"); + assert!( + matches!(event, NodeEvent::BlockCreated { .. }), + "expected BlockCreated variant" + ); + } + + // ----------------------------------------------------------------------- + // DeployData builder + // ----------------------------------------------------------------------- + + #[test] + fn deploy_data_builder_defaults() { + let before = chrono::Utc::now(); + let deploy = DeployData::builder("new Nil".to_string()).build(); + let after = chrono::Utc::now(); + + assert_eq!(deploy.term, "new Nil", "term should match"); + assert_eq!( + deploy.phlo_limit, 5_000_000, + "default phlo_limit should be 5 million" + ); + assert!( + deploy.timestamp >= before && deploy.timestamp <= after, + "timestamp should be approximately now" + ); + assert!( + matches!(deploy.valid_after_block_number, ValidAfter::Head), + "default valid_after should be Head" + ); + } + + #[test] + fn deploy_data_builder_custom_values() { + let custom_time = DateTime::parse_from_rfc3339("2025-06-15T12:00:00Z") + .expect("valid rfc3339") + .with_timezone(&Utc); + + let deploy = DeployData::builder("@0!(true)".to_string()) + .phlo_limit(1_000) + .timestamp(custom_time) + .valid_after_block_number(ValidAfter::Index(42)) + .build(); + + assert_eq!(deploy.term, "@0!(true)"); + assert_eq!(deploy.phlo_limit, 1_000); + assert_eq!(deploy.timestamp, custom_time); + assert!( + matches!(deploy.valid_after_block_number, ValidAfter::Index(42)), + "valid_after should be Index(42)" + ); + } + + #[test] + fn deploy_data_builder_zero_phlo_limit() { + let deploy = DeployData::builder("Nil".to_string()).phlo_limit(0).build(); + assert_eq!(deploy.phlo_limit, 0, "phlo_limit of 0 should be allowed"); + } +} diff --git a/packages/firefly-client/src/node_events.rs b/packages/firefly-client/src/node_events.rs index a21fa463..dc1f9d66 100644 --- a/packages/firefly-client/src/node_events.rs +++ b/packages/firefly-client/src/node_events.rs @@ -1,3 +1,4 @@ +use std::collections::{HashSet, VecDeque}; use std::sync::Arc; use std::task::ready; use std::time::Duration; @@ -5,13 +6,15 @@ use std::time::Duration; use backon::{ExponentialBuilder, Retryable}; use dashmap::DashMap; use futures::{Stream, StreamExt}; -use tokio::sync::{Notify, broadcast}; +use tokio::sync::{broadcast, oneshot}; use tokio_stream::wrappers::BroadcastStream; +use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use tokio_tungstenite::tungstenite::Message; use tracing::Instrument; use uuid::Uuid; use crate::models::{BlockEventDeploy, DeployId, NodeEvent, WalletAddress}; +use crate::traits::NodeEventSource; #[derive(Debug, Clone)] pub enum DeployEvent { @@ -22,13 +25,65 @@ pub enum DeployEvent { }, } -type DeploySubscriptions = Arc>>>; +impl DeployEvent { + const fn deploy_id(&self) -> &DeployId { + match self { + Self::Finalized { id, .. } => id, + } + } +} + +type DeploySubscriptions = Arc>>>; type WalletSubscriptions = Arc>>; +/// A deploy that finalized recently, retained so that late or reconnecting +/// subscribers can still observe it. +/// +/// Carries everything needed to reconstruct a wallet-scoped `DeployEvent`. The +/// previous `(errored, Instant)` tuple could not — it lacked `cost` and the +/// deployer — which is why a client whose WebSocket was down at finalization +/// never learned that its deploy had landed, and reported a successful deploy +/// as a timeout. +#[derive(Debug, Clone)] +struct FinalizedDeploy { + wallet_address: WalletAddress, + cost: u64, + errored: bool, + finalized_at: tokio::time::Instant, +} + +impl FinalizedDeploy { + const fn to_event(&self, id: DeployId) -> DeployEvent { + DeployEvent::Finalized { + id, + cost: self.cost, + errored: self.errored, + } + } +} + +/// Cache of recently finalized deploys so late subscribers can still observe them. +type FinalizedDeploys = Arc>; + +/// How long finalized deploy results are retained in the cache. +const FINALIZED_CACHE_TTL: Duration = Duration::from_secs(5 * 60); + +/// How often the cache cleanup task runs. +const FINALIZED_CACHE_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); + +/// Ring capacity for each wallet's live-event broadcast. Sized so that a burst +/// of blocks cannot lag a subscriber that is briefly slow to poll. +const WALLET_BROADCAST_CAPACITY: usize = 256; + +/// Upper bound on how many cached events one `subscribe` may replay, so a client +/// cannot be flooded on connect. +const MAX_REPLAY_EVENTS: usize = 256; + #[derive(Clone)] pub struct NodeEvents { deploy_subscriptions: DeploySubscriptions, wallet_subscriptions: WalletSubscriptions, + finalized_deploys: FinalizedDeploys, } impl NodeEvents { @@ -37,7 +92,9 @@ impl NodeEvents { let tx = broadcast::Sender::::new(32); let deploy_subscriptions = DeploySubscriptions::default(); let wallet_subscriptions = WalletSubscriptions::default(); + let finalized_deploys = FinalizedDeploys::default(); + // WebSocket connection task tokio::spawn({ let tx = tx.clone(); async move { @@ -66,7 +123,7 @@ impl NodeEvents { let event = match serde_json::from_str(&buff) { Ok(event) => event, Err(err) => { - tracing::debug!("serde ws error: {err:?}"); + tracing::warn!("failed to deserialize ws event: {err:?}"); continue; } }; @@ -78,92 +135,254 @@ impl NodeEvents { .in_current_span() }); + // Event dispatch task + tokio::spawn( + dispatch_events( + tx.subscribe(), + deploy_subscriptions.clone(), + wallet_subscriptions.clone(), + finalized_deploys.clone(), + ) + .in_current_span(), + ); + + // Cache cleanup task — evict entries older than FINALIZED_CACHE_TTL. tokio::spawn({ - let mut rx = tx.subscribe(); - let deploy_subscriptions = deploy_subscriptions.clone(); - let wallet_subscriptions = wallet_subscriptions.clone(); + let finalized_deploys = finalized_deploys.clone(); async move { loop { - let deploys = match rx.recv().await { - Ok(NodeEvent::Started) => continue, - Ok(NodeEvent::BlockAdded { .. }) => continue, - Ok(NodeEvent::BlockCreated { .. }) => continue, - Ok(NodeEvent::BlockFinalised { payload }) => payload.deploys, - Err(broadcast::error::RecvError::Closed) => return, - Err(broadcast::error::RecvError::Lagged(_)) => continue, - }; - - for deploy in deploys { - deploy_subscriptions - .remove(&deploy.id) - .map(|(_, waiters)| waiters) - .into_iter() - .flatten() - .for_each(|(_, w)| w.notify_waiters()); - - if let Some(subscription) = - wallet_subscriptions.get(&deploy.deployer.into()) - { - let _ = subscription.send(deploy.into()); - } - } + tokio::time::sleep(FINALIZED_CACHE_CLEANUP_INTERVAL).await; + let cutoff = tokio::time::Instant::now() - FINALIZED_CACHE_TTL; + finalized_deploys.retain(|_, entry| entry.finalized_at > cutoff); } } - .in_current_span() }); Self { deploy_subscriptions, wallet_subscriptions, + finalized_deploys, } } + /// Wait for a deploy to be finalized, returning `Some(errored)` on success + /// or `None` on timeout. + /// + /// Uses a register-then-check pattern to avoid the race where finalization + /// occurs between `deploy_signed_contract()` returning and this subscription + /// being established: + /// + /// 1. Register oneshot in `deploy_subscriptions` + /// 2. Check `finalized_deploys` cache — if hit, return immediately + /// 3. Otherwise await the oneshot with timeout pub fn wait_for_deploy( &self, deploy_id: &DeployId, max_wait: Duration, - ) -> impl Future { + ) -> impl Future> { let id = Uuid::now_v7(); + let (tx, rx) = oneshot::channel::(); - let notify = Arc::::default(); - let notified = notify.clone().notified_owned(); - + // Step 1: Register subscription FIRST (before checking cache). self.deploy_subscriptions .entry(deploy_id.clone()) .or_default() - .insert(id, notify); - - let guard = scopeguard::guard( - self.deploy_subscriptions.clone(), - move |deploy_subscriptions| { - deploy_subscriptions.remove_if(deploy_id, |_, submap| { - submap.remove(&id); - submap.is_empty() - }); - }, - ); + .insert(id, tx); + + // Step 2: Check if the deploy was already finalized (cache hit). + // This closes the race: if finalization happened before step 1, we catch + // it here. If it happened between steps 1 and 2, both the cache and the + // oneshot will have the result — we just use the cache. + let cached = self + .finalized_deploys + .get(deploy_id) + .map(|entry| entry.errored); + + if cached.is_some() { + // Remove our subscription since we don't need it. + self.deploy_subscriptions.remove_if(deploy_id, |_, submap| { + submap.remove(&id); + submap.is_empty() + }); + } + + let deploy_subscriptions = self.deploy_subscriptions.clone(); + let deploy_id = deploy_id.clone(); + + let guard = scopeguard::guard((), move |()| { + deploy_subscriptions.remove_if(&deploy_id, |_, submap| { + submap.remove(&id); + submap.is_empty() + }); + }); async move { + // If we got a cache hit, return immediately. + if let Some(errored) = cached { + scopeguard::ScopeGuard::into_inner(guard); // defuse + return Some(errored); + } + + // Step 3: Await the oneshot with timeout. tokio::select! { - _ = notified => { + result = rx => { scopeguard::ScopeGuard::into_inner(guard); // defuse - true + result.ok() // Some(errored) or None if sender dropped }, - _ = tokio::time::sleep(max_wait) => false, + _ = tokio::time::sleep(max_wait) => None, } } } + /// Subscribe to finalization events for `wallet_address`. + /// + /// Delivers, in order: any deploys for this wallet that finalized within + /// `FINALIZED_CACHE_TTL` (oldest first), followed by live events. + /// + /// The replay is what makes a transient WebSocket drop survivable. Before + /// it, a deploy that finalized while the client was disconnected — or in the + /// window between the deploy being submitted and the client subscribing — + /// was lost forever, and the client reported a *successful* deploy as a + /// timeout. + /// + /// Ordering is load-bearing. We join the live broadcast (S1) *before* + /// scanning the cache (S2). Dispatch inserts into the cache (D1) before it + /// sends to the broadcast (D2). So for any deploy D: + /// * D2 after S1 — our receiver's cursor was set at S1, so we get it live. + /// * D2 before S1 — then D1 < D2 < S1 < S2, so the S2 scan must observe it. + /// + /// The cases are exhaustive, so no deploy can slip between the two paths. The + /// naive order (scan, then subscribe) leaves exactly such a gap — and that gap + /// is the bug being fixed here. Both paths can fire for the same deploy, so + /// replayed ids are deduped against the live stream in + /// `WalletSubscription::poll_next`. pub fn subscribe_for_deploys(&self, wallet_address: WalletAddress) -> WalletSubscription { - let tx = self - .wallet_subscriptions - .entry(wallet_address.clone()) - .or_insert_with(|| broadcast::Sender::new(32)); + // S1: join the live broadcast first. + // + // `subscribe()` must happen *inside* the entry guard: it serializes us + // against a concurrent last-subscriber `Drop`, which could otherwise + // remove the entry between a clone and the subscribe, leaving us holding + // a receiver on an orphaned sender that dispatch would never find. The + // guard must be released before S2 — holding a `wallet_subscriptions` + // shard while taking `finalized_deploys` shards would nest the two maps + // in the opposite order from dispatch. + let rx = { + let tx = self + .wallet_subscriptions + .entry(wallet_address.clone()) + .or_insert_with(|| broadcast::Sender::new(WALLET_BROADCAST_CAPACITY)); + tx.subscribe() + }; + + // S2: snapshot the cache. TTL-filtered here rather than trusting the + // cleanup task, which only runs every FINALIZED_CACHE_CLEANUP_INTERVAL. + // + // `checked_sub` because this runs on the request path: a bare + // `Instant::now() - TTL` panics if the process has been up for less than + // the TTL. `None` means "nothing can be older than the TTL yet", so every + // cached entry is still eligible. + let cutoff = tokio::time::Instant::now().checked_sub(FINALIZED_CACHE_TTL); + let mut replay: Vec<(tokio::time::Instant, DeployEvent)> = self + .finalized_deploys + .iter() + .filter(|entry| { + entry.wallet_address == wallet_address + && cutoff.is_none_or(|cutoff| entry.finalized_at > cutoff) + }) + .map(|entry| (entry.finalized_at, entry.to_event(entry.key().clone()))) + .collect(); + + // DashMap iteration order is shard-arbitrary; sort so replay is chronological. + replay.sort_by_key(|(at, _)| *at); + if replay.len() > MAX_REPLAY_EVENTS { + replay.drain(..replay.len() - MAX_REPLAY_EVENTS); // keep the newest + } + + let replay: VecDeque = replay.into_iter().map(|(_, event)| event).collect(); + let replayed: HashSet = replay + .iter() + .map(|event| event.deploy_id().clone()) + .collect(); WalletSubscription { wallet_address, wallet_subscriptions: self.wallet_subscriptions.clone(), - rx: BroadcastStream::new(tx.subscribe()), + replay, + replayed, + rx: BroadcastStream::new(rx), + } + } +} + +/// Consume node events and fan finalized deploys out to waiters, wallet +/// subscribers, and the replay cache. +/// +/// Shared by `NodeEvents::new` and `NodeEvents::new_for_test` so the two cannot +/// drift — they previously held copy-pasted, already-divergent copies, which +/// meant tests could exercise different logic than production. +async fn dispatch_events( + mut rx: broadcast::Receiver, + deploy_subscriptions: DeploySubscriptions, + wallet_subscriptions: WalletSubscriptions, + finalized_deploys: FinalizedDeploys, +) { + loop { + let deploys = match rx.recv().await { + Ok(NodeEvent::Started | NodeEvent::Other) => continue, + Ok(NodeEvent::BlockAdded { .. }) => continue, + Ok(NodeEvent::BlockCreated { .. }) => continue, + Ok(NodeEvent::BlockFinalised { payload }) => { + tracing::info!(deploy_count = payload.deploys.len(), "block finalised"); + for deploy in &payload.deploys { + tracing::info!( + deploy_id = ?deploy.id, + errored = deploy.errored, + cost = deploy.cost, + " deploy in finalized block" + ); + } + payload.deploys + } + Err(broadcast::error::RecvError::Closed) => return, + Err(broadcast::error::RecvError::Lagged(_)) => continue, + }; + + for deploy in deploys { + let errored = deploy.errored; + let wallet_address: WalletAddress = deploy.deployer.into(); + + // D1: cache BEFORE the live send, so a subscriber that misses the + // send still finds the deploy when it scans (see subscribe_for_deploys). + finalized_deploys.insert( + deploy.id.clone(), + FinalizedDeploy { + wallet_address: wallet_address.clone(), + cost: deploy.cost, + errored, + finalized_at: tokio::time::Instant::now(), + }, + ); + + // Notify any existing waiters via oneshot channels. + deploy_subscriptions + .remove(&deploy.id) + .map(|(_, waiters)| waiters) + .into_iter() + .flatten() + .for_each(|(_, sender)| { + let _ = sender.send(errored); + }); + + // D2: then send live. + if let Some(subscription) = wallet_subscriptions.get(&wallet_address) { + let _ = subscription.send(deploy.into()); + } else { + tracing::debug!( + deploy_id = ?deploy.id, + wallet_address = ?wallet_address, + "no live wallet subscription for deploy; cached for replay" + ); + } } } } @@ -181,19 +400,70 @@ impl From for DeployEvent { pub struct WalletSubscription { wallet_address: WalletAddress, wallet_subscriptions: WalletSubscriptions, + /// Cached events (oldest first) captured at subscribe time, drained before + /// any live event is yielded. + replay: VecDeque, + /// Ids present in `replay`, so that the same deploy arriving live is not + /// emitted twice. Seeded once at construction and only ever drained, so it + /// is bounded by the replay snapshot and never grows. + replayed: HashSet, rx: BroadcastStream, } impl Stream for WalletSubscription { type Item = DeployEvent; + // The `continue` in the `Lagged` arm is technically redundant (the loop would + // iterate anyway), but it is kept deliberately and explicitly: it is the + // entire point of the fix. Without re-polling, this task is left with no + // waker registered and the stream stalls forever. Leaving the arm to fall out + // silently invites a future reader to "tidy" it into a `Poll::Pending`, which + // would reintroduce the bug. + #[allow(clippy::needless_continue)] fn poll_next( - mut self: std::pin::Pin<&mut Self>, + self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { - ready!(self.rx.poll_next_unpin(cx)) - .transpose() - .map_or(std::task::Poll::Pending, std::task::Poll::Ready) + let this = self.get_mut(); + + // History first, oldest first. Draining the replay fully before polling + // the live stream makes it impossible to emit a deploy live and then + // replay it out of order, and gives a simple contract: history, then live. + if let Some(event) = this.replay.pop_front() { + return std::task::Poll::Ready(Some(event)); + } + + loop { + match ready!(this.rx.poll_next_unpin(cx)) { + Some(Ok(event)) => { + // Skip a live event we already emitted from the replay snapshot + // (both paths can legitimately fire for the same deploy). + if !this.replayed.is_empty() && this.replayed.remove(event.deploy_id()) { + continue; + } + return std::task::Poll::Ready(Some(event)); + } + Some(Err(BroadcastStreamRecvError::Lagged(skipped))) => { + tracing::warn!( + wallet_address = ?this.wallet_address, + skipped, + "wallet deploy subscription lagged; dropped events (recoverable on resubscribe)" + ); + // MUST re-poll rather than return `Pending`. On `Lagged`, + // `BroadcastStream` has already replaced its inner `Recv` + // future — dropping the waiter node registered with the + // channel — without polling the fresh one. Returning + // `Pending` here would leave this task with NO waker + // registered, so no later `send` could ever wake it: the + // stream would stall permanently and the client would once + // again see a successful deploy as a timeout. `ready!` only + // yields `Pending` when the inner stream is `Pending`, which + // is exactly when the waker *is* armed. + continue; + } + None => return std::task::Poll::Ready(None), // all senders dropped + } + } } } @@ -201,7 +471,552 @@ impl Drop for WalletSubscription { fn drop(&mut self) { self.wallet_subscriptions .remove_if(&self.wallet_address, |_, sender| { - sender.receiver_count() == 0 + // `<= 1`, not `== 0`: a type's fields are dropped *after* its + // `Drop::drop` body runs, so `self.rx` still owns our own + // receiver here and the count always includes us. The previous + // `== 0` could therefore never be true — the entry and its ring + // buffer leaked for every wallet that ever connected. + sender.receiver_count() <= 1 }); } } + +#[cfg(test)] +impl NodeEvents { + /// Creates a `NodeEvents` without spawning the WebSocket connection or + /// cache-cleanup tasks. Only the event dispatch task is started so that + /// tests can inject events through the returned `broadcast::Sender`. + pub(crate) fn new_for_test() -> (Self, broadcast::Sender) { + let tx = broadcast::Sender::::new(32); + let deploy_subscriptions = DeploySubscriptions::default(); + let wallet_subscriptions = WalletSubscriptions::default(); + let finalized_deploys = FinalizedDeploys::default(); + + // Spawn only the event dispatch task — the SAME function production + // uses, so tests cannot exercise different logic than production (the + // two previously held copy-pasted, already-divergent loops). + tokio::spawn(dispatch_events( + tx.subscribe(), + deploy_subscriptions.clone(), + wallet_subscriptions.clone(), + finalized_deploys.clone(), + )); + + ( + Self { + deploy_subscriptions, + wallet_subscriptions, + finalized_deploys, + }, + tx, + ) + } +} + +impl NodeEventSource for NodeEvents { + fn wait_for_deploy( + &self, + deploy_id: &DeployId, + max_wait: Duration, + ) -> impl Future> + Send { + self.wait_for_deploy(deploy_id, max_wait) + } + + fn subscribe_for_deploys( + &self, + wallet_address: WalletAddress, + ) -> impl Stream + Send { + self.subscribe_for_deploys(wallet_address) + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use futures::StreamExt; + use secp256k1::{PublicKey, Secp256k1, SecretKey}; + + use super::*; + use crate::models::{BlockEventPayload, BlockId, NodeEvent}; + + /// Helper to create a test public key deterministically from an index. + fn test_public_key(index: u8) -> PublicKey { + let secp = Secp256k1::new(); + let mut bytes = [0u8; 32]; + bytes[31] = index.max(1); // ensure non-zero + let sk = SecretKey::from_byte_array(bytes).expect("valid secret key"); + PublicKey::from_secret_key(&secp, &sk) + } + + /// Helper to create a `BlockFinalised` event with a single deploy. + fn finalize_event(deploy_id: &str, errored: bool, cost: u64) -> NodeEvent { + finalize_event_for(deploy_id, errored, cost, 1) + } + + /// Like `finalize_event`, but for a chosen deployer key. + fn finalize_event_for(deploy_id: &str, errored: bool, cost: u64, key_index: u8) -> NodeEvent { + NodeEvent::BlockFinalised { + payload: BlockEventPayload { + block_hash: BlockId::from("block-1".to_owned()), + deploys: vec![BlockEventDeploy { + id: DeployId::from(deploy_id.to_owned()), + cost, + deployer: test_public_key(key_index), + errored, + }], + }, + } + } + + /// A single `BlockFinalised` carrying `count` deploys, used to overrun a + /// subscriber's broadcast ring within one dispatch pass. Keeping it to ONE + /// `NodeEvent` means the dispatch channel itself cannot lag, so the test is + /// deterministic. + fn finalize_event_burst(count: usize, key_index: u8) -> NodeEvent { + NodeEvent::BlockFinalised { + payload: BlockEventPayload { + block_hash: BlockId::from("block-burst".to_owned()), + deploys: (0..count) + .map(|i| BlockEventDeploy { + id: DeployId::from(format!("deploy-{i}")), + cost: i as u64, + deployer: test_public_key(key_index), + errored: false, + }) + .collect(), + }, + } + } + + // ------------------------------------------------------- + // wait_for_deploy tests + // ------------------------------------------------------- + + #[tokio::test] + async fn test_wait_for_deploy_success() { + let (events, tx) = NodeEvents::new_for_test(); + let deploy_id = DeployId::from("deploy-1".to_owned()); + + let events_clone = events.clone(); + let deploy_id_clone = deploy_id.clone(); + let handle = tokio::spawn(async move { + events_clone + .wait_for_deploy(&deploy_id_clone, Duration::from_secs(5)) + .await + }); + + // Give the wait_for_deploy time to register + tokio::task::yield_now().await; + tokio::time::sleep(Duration::from_millis(10)).await; + + let _ = tx.send(finalize_event("deploy-1", false, 100)); + + let result = handle.await.expect("task should not panic"); + assert_eq!(result, Some(false)); + } + + #[tokio::test] + async fn test_wait_for_deploy_errored() { + let (events, tx) = NodeEvents::new_for_test(); + let deploy_id = DeployId::from("deploy-err".to_owned()); + + let events_clone = events.clone(); + let deploy_id_clone = deploy_id.clone(); + let handle = tokio::spawn(async move { + events_clone + .wait_for_deploy(&deploy_id_clone, Duration::from_secs(5)) + .await + }); + + tokio::task::yield_now().await; + tokio::time::sleep(Duration::from_millis(10)).await; + + let _ = tx.send(finalize_event("deploy-err", true, 50)); + + let result = handle.await.expect("task should not panic"); + assert_eq!(result, Some(true)); + } + + #[tokio::test] + async fn test_wait_for_deploy_timeout() { + let (events, _tx) = NodeEvents::new_for_test(); + let deploy_id = DeployId::from("deploy-never".to_owned()); + + let result = events + .wait_for_deploy(&deploy_id, Duration::from_millis(50)) + .await; + assert_eq!(result, None); + + // Verify subscription was cleaned up via scopeguard + assert!( + !events.deploy_subscriptions.contains_key(&deploy_id), + "subscription should be cleaned up after timeout" + ); + } + + #[tokio::test] + async fn test_wait_for_deploy_cached_result_immediate() { + let (events, _tx) = NodeEvents::new_for_test(); + let deploy_id = DeployId::from("deploy-cached".to_owned()); + + // Pre-populate the cache + events.finalized_deploys.insert( + deploy_id.clone(), + FinalizedDeploy { + wallet_address: test_public_key(1).into(), + cost: 0, + errored: false, + finalized_at: tokio::time::Instant::now(), + }, + ); + + let result = events + .wait_for_deploy(&deploy_id, Duration::from_secs(5)) + .await; + assert_eq!(result, Some(false)); + } + + #[tokio::test] + async fn test_wait_for_deploy_multiple_waiters() { + let (events, tx) = NodeEvents::new_for_test(); + let deploy_id = DeployId::from("deploy-multi".to_owned()); + + let e1 = events.clone(); + let d1 = deploy_id.clone(); + let w1 = tokio::spawn(async move { e1.wait_for_deploy(&d1, Duration::from_secs(5)).await }); + + let e2 = events.clone(); + let d2 = deploy_id.clone(); + let w2 = tokio::spawn(async move { e2.wait_for_deploy(&d2, Duration::from_secs(5)).await }); + + tokio::task::yield_now().await; + tokio::time::sleep(Duration::from_millis(10)).await; + + let _ = tx.send(finalize_event("deploy-multi", true, 200)); + + assert_eq!(w1.await.unwrap(), Some(true)); + assert_eq!(w2.await.unwrap(), Some(true)); + } + + // ------------------------------------------------------- + // subscribe_for_deploys tests + // ------------------------------------------------------- + + #[tokio::test] + async fn test_subscribe_receives_events() { + let (events, tx) = NodeEvents::new_for_test(); + let wallet_address: WalletAddress = test_public_key(1).into(); + + let mut sub = events.subscribe_for_deploys(wallet_address); + + // Send event + let _ = tx.send(finalize_event("deploy-sub", false, 300)); + + // Give dispatch task time to process + tokio::time::sleep(Duration::from_millis(50)).await; + + // Try to receive + match tokio::time::timeout(Duration::from_millis(100), sub.next()).await { + Ok(Some(DeployEvent::Finalized { id, cost, errored })) => { + assert_eq!(id, DeployId::from("deploy-sub".to_owned())); + assert_eq!(cost, 300); + assert!(!errored); + } + other => panic!("expected Finalized event, got {other:?}"), + } + } + + #[tokio::test] + async fn test_subscribe_drop_with_multiple_subscribers() { + let (events, _tx) = NodeEvents::new_for_test(); + let wallet_address: WalletAddress = test_public_key(1).into(); + + let sub1 = events.subscribe_for_deploys(wallet_address.clone()); + let sub2 = events.subscribe_for_deploys(wallet_address.clone()); + assert!(events.wallet_subscriptions.contains_key(&wallet_address)); + + // Drop one -- entry should still exist since the other subscriber is alive + drop(sub1); + assert!( + events.wallet_subscriptions.contains_key(&wallet_address), + "entry should remain with active subscriber" + ); + + drop(sub2); + } + + // ------------------------------------------------------- + // replay tests (the "successful deploy reported as timeout" bug) + // ------------------------------------------------------- + + /// The reported incident, distilled: the deploy finalized while nobody was + /// subscribed (the browser's WebSocket was down), so the live event was + /// dropped on the floor and a SUCCESSFUL deploy surfaced to the user as a + /// timeout. A late subscriber must still observe it. + #[tokio::test] + async fn test_subscribe_replays_deploy_finalized_before_subscribe() { + let (events, tx) = NodeEvents::new_for_test(); + let wallet_address: WalletAddress = test_public_key(1).into(); + + // Finalize with NO subscriber listening. + let _ = tx.send(finalize_event("deploy-early", false, 777)); + tokio::time::sleep(Duration::from_millis(50)).await; + + // Only now subscribe — the event must still be delivered. + let mut sub = events.subscribe_for_deploys(wallet_address); + + match tokio::time::timeout(Duration::from_millis(200), sub.next()).await { + Ok(Some(DeployEvent::Finalized { id, cost, errored })) => { + assert_eq!(id, DeployId::from("deploy-early".to_owned())); + assert_eq!(cost, 777, "cost must survive the cache round-trip"); + assert!(!errored); + } + other => panic!("expected replayed Finalized event, got {other:?}"), + } + } + + /// The exact shape of the incident: subscribed, the WebSocket drops, the + /// deploy finalizes during the gap, then the client reconnects. + #[tokio::test] + async fn test_subscribe_replay_after_reconnect_gap() { + let (events, tx) = NodeEvents::new_for_test(); + let wallet_address: WalletAddress = test_public_key(1).into(); + + let sub = events.subscribe_for_deploys(wallet_address.clone()); + drop(sub); // the WebSocket drops + + // Finalization happens while nobody is listening. + let _ = tx.send(finalize_event("deploy-gap", false, 5)); + tokio::time::sleep(Duration::from_millis(50)).await; + + // Client reconnects — must learn the deploy landed. + let mut sub = events.subscribe_for_deploys(wallet_address); + + match tokio::time::timeout(Duration::from_millis(200), sub.next()).await { + Ok(Some(DeployEvent::Finalized { id, .. })) => { + assert_eq!(id, DeployId::from("deploy-gap".to_owned())); + } + other => panic!("expected replay after reconnect, got {other:?}"), + } + } + + #[tokio::test] + async fn test_replay_is_scoped_to_wallet() { + let (events, tx) = NodeEvents::new_for_test(); + + let _ = tx.send(finalize_event_for("deploy-w1", false, 1, 1)); + let _ = tx.send(finalize_event_for("deploy-w2", false, 2, 2)); + tokio::time::sleep(Duration::from_millis(50)).await; + + let wallet_1: WalletAddress = test_public_key(1).into(); + let mut sub = events.subscribe_for_deploys(wallet_1); + + match tokio::time::timeout(Duration::from_millis(200), sub.next()).await { + Ok(Some(DeployEvent::Finalized { id, .. })) => { + assert_eq!(id, DeployId::from("deploy-w1".to_owned())); + } + other => panic!("expected only wallet 1's deploy, got {other:?}"), + } + + assert!( + tokio::time::timeout(Duration::from_millis(100), sub.next()) + .await + .is_err(), + "another wallet's deploy must not be replayed into this stream" + ); + } + + /// The subscribe-then-scan ordering deliberately makes the + /// "replayed AND delivered live" window reachable, so the same deploy must + /// still be emitted exactly once. + #[tokio::test] + async fn test_no_duplicate_when_replayed_deploy_also_arrives_live() { + let (events, tx) = NodeEvents::new_for_test(); + let wallet_address: WalletAddress = test_public_key(1).into(); + + // Cache deploy-x with nobody listening. + let _ = tx.send(finalize_event("deploy-x", false, 1)); + tokio::time::sleep(Duration::from_millis(50)).await; + + // Subscribe: deploy-x is now in the replay snapshot. + let mut sub = events.subscribe_for_deploys(wallet_address); + + // deploy-x ALSO arrives live, followed by a genuinely new deploy-y. + let _ = tx.send(finalize_event("deploy-x", false, 1)); + let _ = tx.send(finalize_event("deploy-y", false, 2)); + tokio::time::sleep(Duration::from_millis(50)).await; + + let mut seen = Vec::new(); + while let Ok(Some(DeployEvent::Finalized { id, .. })) = + tokio::time::timeout(Duration::from_millis(150), sub.next()).await + { + seen.push(id); + } + + assert_eq!( + seen, + vec![ + DeployId::from("deploy-x".to_owned()), + DeployId::from("deploy-y".to_owned()) + ], + "deploy-x must be emitted exactly once (from replay), not twice" + ); + } + + #[tokio::test(start_paused = true)] + async fn test_replay_skips_entries_older_than_ttl() { + let (events, _tx) = NodeEvents::new_for_test(); + let wallet_address: WalletAddress = test_public_key(1).into(); + + events.finalized_deploys.insert( + DeployId::from("deploy-stale".to_owned()), + FinalizedDeploy { + wallet_address: wallet_address.clone(), + cost: 1, + errored: false, + finalized_at: tokio::time::Instant::now(), + }, + ); + + tokio::time::advance(FINALIZED_CACHE_TTL + Duration::from_secs(1)).await; + + events.finalized_deploys.insert( + DeployId::from("deploy-fresh".to_owned()), + FinalizedDeploy { + wallet_address: wallet_address.clone(), + cost: 2, + errored: false, + finalized_at: tokio::time::Instant::now(), + }, + ); + + let sub = events.subscribe_for_deploys(wallet_address); + + let replayed: Vec = sub + .replay + .iter() + .map(|event| event.deploy_id().clone()) + .collect(); + assert_eq!( + replayed, + vec![DeployId::from("deploy-fresh".to_owned())], + "entries older than the TTL must not be replayed" + ); + } + + /// Regression: a `Lagged` must not stall the stream forever. + /// + /// The old `poll_next` mapped `Lagged` to `Poll::Pending` *without a + /// registered waker* (`BroadcastStream` drops its waiter node on lag), so the + /// subscription went permanently silent — producing the very same + /// "successful deploy reported as a timeout" symptom. This test hangs on the + /// old code and passes with the fix. + #[tokio::test] + async fn test_subscribe_lagged_does_not_stall_stream() { + let (events, tx) = NodeEvents::new_for_test(); + let wallet_address: WalletAddress = test_public_key(1).into(); + + let mut sub = events.subscribe_for_deploys(wallet_address); + + // Overrun the wallet ring within a single dispatch pass while nobody polls. + let burst = WALLET_BROADCAST_CAPACITY + 44; + let _ = tx.send(finalize_event_burst(burst, 1)); + tokio::time::sleep(Duration::from_millis(200)).await; + + // We must still make progress and reach the newest deploy. + let newest = DeployId::from(format!("deploy-{}", burst - 1)); + let found = tokio::time::timeout(Duration::from_secs(2), async { + while let Some(DeployEvent::Finalized { id, .. }) = sub.next().await { + if id == newest { + return true; + } + } + false + }) + .await + .expect("stream stalled permanently after Lagged"); + + assert!(found, "must still reach the newest deploy after lagging"); + } + + /// Regression: the wallet entry (and its ring) must be reclaimed when the + /// last subscriber drops. The old `receiver_count() == 0` could never be true + /// — fields drop *after* `Drop::drop` — so every wallet that ever connected + /// leaked an entry forever. + #[tokio::test] + async fn test_wallet_subscription_entry_removed_on_last_drop() { + let (events, _tx) = NodeEvents::new_for_test(); + let wallet_address: WalletAddress = test_public_key(1).into(); + + let sub = events.subscribe_for_deploys(wallet_address.clone()); + assert!(events.wallet_subscriptions.contains_key(&wallet_address)); + + drop(sub); + assert!( + !events.wallet_subscriptions.contains_key(&wallet_address), + "entry must be reclaimed when the last subscriber drops" + ); + } + + // ------------------------------------------------------- + // dispatch tests + // ------------------------------------------------------- + + #[tokio::test] + async fn test_dispatch_ignores_non_finalised_events() { + let (events, tx) = NodeEvents::new_for_test(); + let deploy_id = DeployId::from("deploy-ignored".to_owned()); + + // Start waiting + let events_clone = events.clone(); + let deploy_id_clone = deploy_id.clone(); + let handle = tokio::spawn(async move { + events_clone + .wait_for_deploy(&deploy_id_clone, Duration::from_millis(100)) + .await + }); + + // Send non-finalised events + let _ = tx.send(NodeEvent::Started); + let _ = tx.send(NodeEvent::BlockAdded { + payload: BlockEventPayload { + block_hash: BlockId::from("b1".to_owned()), + deploys: vec![], + }, + }); + let _ = tx.send(NodeEvent::BlockCreated { + payload: BlockEventPayload { + block_hash: BlockId::from("b2".to_owned()), + deploys: vec![], + }, + }); + + // Should timeout since no BlockFinalised was sent + let result = handle.await.unwrap(); + assert_eq!(result, None, "non-finalised events should not trigger wait"); + } + + // ------------------------------------------------------- + // DeployEvent conversion + // ------------------------------------------------------- + + #[test] + fn test_block_event_deploy_into_deploy_event() { + let deploy = BlockEventDeploy { + id: DeployId::from("d1".to_owned()), + cost: 42, + deployer: test_public_key(1), + errored: true, + }; + + let event: DeployEvent = deploy.into(); + match event { + DeployEvent::Finalized { id, cost, errored } => { + assert_eq!(id, DeployId::from("d1".to_owned())); + assert_eq!(cost, 42); + assert!(errored); + } + } + } +} diff --git a/packages/firefly-client/src/read_node_client.rs b/packages/firefly-client/src/read_node_client.rs index 27a937d9..d99e74c5 100644 --- a/packages/firefly-client/src/read_node_client.rs +++ b/packages/firefly-client/src/read_node_client.rs @@ -1,8 +1,12 @@ +use std::time::Duration; + use anyhow::Context; +use backon::{ExponentialBuilder, Retryable}; use serde_json::Value; use crate::errors::ReadNodeError; use crate::models::ReadNodeExpr; +use crate::traits::ReadNode; #[derive(Clone)] pub struct ReadNodeClient { @@ -10,49 +14,707 @@ pub struct ReadNodeClient { client: reqwest::Client, } -impl ReadNodeClient { - pub fn new(url: String) -> Self { +/// Transport bounds for the read (explore-deploy) HTTP client. +/// +/// reqwest's default client sets **no request timeout at all**, so `get_data` +/// and `get_data_or_none` were entirely unbounded — and `get_data` is called +/// directly by the periodic registry health check. reqwest's default +/// `tcp_user_timeout` (30s) only fires when data goes *unacked*; a read node +/// that accepts the connection and ACKs but then stalls at the HTTP layer would +/// hang the call forever. +#[derive(Clone, Copy, Debug)] +pub struct ReadNodeConfig { + /// ~80x the observed healthy latency (0.24s) — generous for a heavy + /// exploratory term, and small enough that two full attempts still fit + /// inside the 45s cap used by `get_data_with_retry`. + pub request_timeout: Duration, + pub connect_timeout: Duration, + pub tcp_keepalive: Option, +} + +impl Default for ReadNodeConfig { + fn default() -> Self { Self { - url, - client: Default::default(), + request_timeout: Duration::from_secs(20), + connect_timeout: Duration::from_secs(5), + tcp_keepalive: Some(Duration::from_secs(30)), } } +} + +impl ReadNodeClient { + pub fn new(url: String) -> Self { + Self::with_config(url, ReadNodeConfig::default()) + } + + /// # Panics + /// + /// Panics if the underlying HTTP client cannot be built, which can only + /// happen on a malformed TLS/proxy environment — an unrecoverable + /// misconfiguration at startup. + pub fn with_config(url: String, config: ReadNodeConfig) -> Self { + tracing::info!("ReadNodeClient targeting: {url}"); + let client = reqwest::Client::builder() + .connect_timeout(config.connect_timeout) + .timeout(config.request_timeout) + .tcp_keepalive(config.tcp_keepalive) + .build() + .expect("failed to build read-node HTTP client"); + + Self { url, client } + } pub async fn get_data(&self, rholang_code: String) -> Result where T: serde::de::DeserializeOwned, { + let code_len = rholang_code.len(); + let code_prefix: String = rholang_code.chars().take(200).collect(); let mut response_json = self.explore_deploy(rholang_code).await?; - let data_value = response_json - .pointer_mut("/expr/0") - .map(Value::take) - .ok_or(ReadNodeError::ReturnValueMissing)?; + let data_value = match response_json.pointer_mut("/expr/0").map(Value::take) { + Some(v) => v, + None => { + tracing::warn!( + response = %response_json, + code_len, + code_prefix = %code_prefix, + "explore-deploy returned no data at /expr/0" + ); + return Err(ReadNodeError::ReturnValueMissing); + } + }; - let intermediate: ReadNodeExpr = serde_json::from_value(data_value) + let intermediate: ReadNodeExpr = serde_json::from_value(data_value.clone()) .context("failed to deserialize intermediate model") - .map_err(ReadNodeError::Deserialization)?; + .map_err(|err| { + tracing::error!( + raw_value = %data_value, + code_prefix = %code_prefix, + error = %err, + "DIAG: intermediate deserialization failed — raw /expr/0 value logged above" + ); + ReadNodeError::Deserialization(err) + })?; - serde_json::from_value(intermediate.into()) - .context("failed to deserialize filed model") - .map_err(ReadNodeError::Deserialization) + let final_value: Value = intermediate.into(); + serde_json::from_value(final_value.clone()) + .context("failed to deserialize final model") + .map_err(|err| { + tracing::error!( + final_json = %final_value, + code_prefix = %code_prefix, + error = ?err, + type_name = std::any::type_name::(), + "DIAG: final model deserialization failed — converted JSON logged above" + ); + // Log each field's JSON type to help identify which field fails + if let Some(obj) = final_value.as_object() { + for (key, value) in obj { + let value_type = match value { + Value::Null => "null", + Value::Bool(_) => "bool", + Value::Number(_) => "number", + Value::String(s) => { + tracing::debug!( + field = %key, + value_prefix = %&s[..s.len().min(100)], + value_len = s.len(), + "DIAG: field detail (string)" + ); + "string" + } + Value::Array(_) => "array", + Value::Object(_) => "object", + }; + tracing::debug!( + field = %key, + value_type, + "DIAG: field type" + ); + } + } + ReadNodeError::Deserialization(err) + }) + } + + pub async fn get_data_or_none( + &self, + rholang_code: String, + ) -> Result, ReadNodeError> + where + T: serde::de::DeserializeOwned, + { + match self.get_data(rholang_code).await { + Ok(data) => Ok(Some(data)), + Err(ReadNodeError::ReturnValueMissing) => Ok(None), + Err(err) => Err(err), + } + } + + /// Retry `get_data` up to `max_retries` times with exponential backoff and jitter. + /// Only retries on `ReturnValueMissing` (empty explore-deploy result). + /// Enforces a 45-second total timeout to prevent unbounded hangs. + pub async fn get_data_with_retry( + &self, + rholang_code: String, + max_retries: u32, + delay: Duration, + ) -> Result + where + T: serde::de::DeserializeOwned, + { + let total_timeout = Duration::from_secs(45); + + let backoff = ExponentialBuilder::default() + .with_min_delay(delay) + .with_max_delay(delay.saturating_mul(5)) + .with_max_times(max_retries as usize) + .with_jitter(); + + let retry_fut = (|| self.get_data::(rholang_code.clone())) + .retry(backoff) + .when(|err| matches!(err, ReadNodeError::ReturnValueMissing)) + .notify(|err, dur| { + tracing::debug!(?err, ?dur, "explore-deploy returned empty, retrying"); + }); + + match tokio::time::timeout(total_timeout, retry_fut).await { + Ok(result) => result, + Err(_elapsed) => { + tracing::error!( + timeout_secs = total_timeout.as_secs(), + code_prefix = %&rholang_code[..rholang_code.len().min(200)], + "get_data_with_retry timed out" + ); + Err(ReadNodeError::Timeout(total_timeout)) + } + } + } + + /// Retry `get_data_or_none` up to `max_retries` times with `delay` between attempts. + /// Only retries when the result is `Ok(None)` (empty explore-deploy result). + pub async fn get_data_or_none_with_retry( + &self, + rholang_code: String, + max_retries: u32, + delay: Duration, + ) -> Result, ReadNodeError> + where + T: serde::de::DeserializeOwned, + { + match self + .get_data_with_retry(rholang_code, max_retries, delay) + .await + { + Ok(data) => Ok(Some(data)), + Err(ReadNodeError::ReturnValueMissing) => Ok(None), + Err(err) => Err(err), + } } async fn explore_deploy(&self, rholang_code: String) -> Result { let request = self .client .post(format!("{}/api/explore-deploy", self.url)) - .body(rholang_code) - .header("Content-Type", "text/plain") + .json(&serde_json::json!({ "term": rholang_code })) .send() .await?; if !request.status().is_success() { let status = request.status(); let body = request.text().await?; + tracing::warn!(%status, %body, "explore-deploy HTTP error"); return Err(ReadNodeError::Api(status, body)); } - request.json().await.map_err(Into::into) + let response: Value = request.json().await?; + + // Log block info from every explore-deploy response for diagnostics + if let Some(block) = response.get("block") { + tracing::info!( + target: "f1r3fly.rholang.diag", + block_number = block.get("blockNumber").and_then(|v| v.as_u64()), + block_hash = block.get("blockHash").and_then(|v| v.as_str()), + deploy_count = block.get("deployCount").and_then(|v| v.as_u64()), + post_state = block.get("postStateHash").and_then(|v| v.as_str()), + "explore_deploy: response block info" + ); + } + + // Warn on empty expr, debug-log otherwise + match response.pointer("/expr/0") { + Some(_) => tracing::trace!(response = %response, "explore_deploy response (has data)"), + None => tracing::warn!(response = %response, "explore_deploy response (empty expr)"), + } + + Ok(response) + } +} + +impl ReadNode for ReadNodeClient { + async fn get_data( + &self, + rholang_code: String, + ) -> Result { + self.get_data(rholang_code).await + } + + async fn get_data_or_none( + &self, + rholang_code: String, + ) -> Result, ReadNodeError> { + self.get_data_or_none(rholang_code).await + } + + async fn get_data_with_retry( + &self, + rholang_code: String, + max_retries: u32, + delay: Duration, + ) -> Result { + self.get_data_with_retry(rholang_code, max_retries, delay) + .await + } + + async fn get_data_or_none_with_retry( + &self, + rholang_code: String, + max_retries: u32, + delay: Duration, + ) -> Result, ReadNodeError> { + self.get_data_or_none_with_retry(rholang_code, max_retries, delay) + .await + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use serde::Deserialize; + use serde_json::json; + use wiremock::matchers::{method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + use super::*; + + /// Helper: returns a valid explore-deploy JSON response wrapping a string expr. + fn string_expr_response(data: &str) -> serde_json::Value { + json!({ "expr": [{ "ExprString": { "data": data } }] }) + } + + /// Helper: returns a valid explore-deploy JSON response wrapping a bool expr. + fn bool_expr_response(data: bool) -> serde_json::Value { + json!({ "expr": [{ "ExprBool": { "data": data } }] }) + } + + /// Helper: returns a valid explore-deploy JSON response wrapping an int expr. + fn int_expr_response(data: i64) -> serde_json::Value { + json!({ "expr": [{ "ExprInt": { "data": data } }] }) + } + + /// Helper: returns an explore-deploy JSON response with empty expr. + fn empty_expr_response() -> serde_json::Value { + json!({ "expr": [] }) + } + + /// Mount a mock on the server that returns the given body for POST /api/explore-deploy. + async fn mount_explore_deploy(server: &MockServer, body: serde_json::Value) { + Mock::given(method("POST")) + .and(path("/api/explore-deploy")) + .respond_with(ResponseTemplate::new(200).set_body_json(body)) + .mount(server) + .await; + } + + // ------------------------------------------------------- + // get_data tests + // ------------------------------------------------------- + + #[tokio::test] + async fn test_get_data_success_deserializes_string() { + let server = MockServer::start().await; + mount_explore_deploy(&server, string_expr_response("hello")).await; + + let client = ReadNodeClient::new(server.uri()); + let result: String = client + .get_data("code".into()) + .await + .expect("should succeed"); + assert_eq!(result, "hello"); + } + + #[tokio::test] + async fn test_get_data_success_deserializes_bool() { + let server = MockServer::start().await; + mount_explore_deploy(&server, bool_expr_response(true)).await; + + let client = ReadNodeClient::new(server.uri()); + let result: bool = client + .get_data("code".into()) + .await + .expect("should succeed"); + assert!(result); + } + + #[tokio::test] + async fn test_get_data_success_deserializes_int() { + let server = MockServer::start().await; + mount_explore_deploy(&server, int_expr_response(42)).await; + + let client = ReadNodeClient::new(server.uri()); + let result: i64 = client + .get_data("code".into()) + .await + .expect("should succeed"); + assert_eq!(result, 42); + } + + #[derive(Debug, Deserialize, PartialEq)] + struct TestStruct { + name: String, + value: i64, + } + + #[tokio::test] + async fn test_get_data_success_deserializes_struct() { + let server = MockServer::start().await; + let body = json!({ + "expr": [{ + "ExprMap": { + "data": { + "name": { "ExprString": { "data": "alice" } }, + "value": { "ExprInt": { "data": 99 } } + } + } + }] + }); + mount_explore_deploy(&server, body).await; + + let client = ReadNodeClient::new(server.uri()); + let result: TestStruct = client + .get_data("code".into()) + .await + .expect("should succeed"); + assert_eq!( + result, + TestStruct { + name: "alice".into(), + value: 99 + } + ); + } + + #[tokio::test] + async fn test_get_data_return_value_missing_on_empty_expr() { + let server = MockServer::start().await; + mount_explore_deploy(&server, empty_expr_response()).await; + + let client = ReadNodeClient::new(server.uri()); + let result = client.get_data::("code".into()).await; + assert!( + matches!(result, Err(ReadNodeError::ReturnValueMissing)), + "expected ReturnValueMissing, got {result:?}" + ); + } + + #[tokio::test] + async fn test_get_data_return_value_missing_on_no_expr_key() { + let server = MockServer::start().await; + mount_explore_deploy(&server, json!({})).await; + + let client = ReadNodeClient::new(server.uri()); + let result = client.get_data::("code".into()).await; + assert!(matches!(result, Err(ReadNodeError::ReturnValueMissing))); + } + + #[tokio::test] + async fn test_get_data_api_error_on_http_500() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/api/explore-deploy")) + .respond_with(ResponseTemplate::new(500).set_body_string("internal error")) + .mount(&server) + .await; + + let client = ReadNodeClient::new(server.uri()); + let result = client.get_data::("code".into()).await; + match result { + Err(ReadNodeError::Api(status, body)) => { + assert_eq!(status, reqwest::StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(body, "internal error"); + } + other => panic!("expected Api error, got {other:?}"), + } + } + + #[tokio::test] + async fn test_get_data_api_error_on_http_400() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/api/explore-deploy")) + .respond_with(ResponseTemplate::new(400).set_body_string("bad request")) + .mount(&server) + .await; + + let client = ReadNodeClient::new(server.uri()); + let result = client.get_data::("code".into()).await; + match result { + Err(ReadNodeError::Api(status, body)) => { + assert_eq!(status, reqwest::StatusCode::BAD_REQUEST); + assert_eq!(body, "bad request"); + } + other => panic!("expected Api error, got {other:?}"), + } + } + + #[tokio::test] + async fn test_get_data_deserialization_error_on_type_mismatch() { + let server = MockServer::start().await; + // Return a string expr, but try to deserialize as struct + mount_explore_deploy(&server, string_expr_response("hello")).await; + + let client = ReadNodeClient::new(server.uri()); + let result = client.get_data::("code".into()).await; + assert!( + matches!(result, Err(ReadNodeError::Deserialization(_))), + "expected Deserialization error, got {result:?}" + ); + } + + // ------------------------------------------------------- + // get_data_or_none tests + // ------------------------------------------------------- + + #[tokio::test] + async fn test_get_data_or_none_returns_some_on_success() { + let server = MockServer::start().await; + mount_explore_deploy(&server, string_expr_response("hello")).await; + + let client = ReadNodeClient::new(server.uri()); + let result: Option = client + .get_data_or_none("code".into()) + .await + .expect("should succeed"); + assert_eq!(result, Some("hello".into())); + } + + #[tokio::test] + async fn test_get_data_or_none_returns_none_on_missing_value() { + let server = MockServer::start().await; + mount_explore_deploy(&server, empty_expr_response()).await; + + let client = ReadNodeClient::new(server.uri()); + let result: Option = client + .get_data_or_none("code".into()) + .await + .expect("should succeed"); + assert_eq!(result, None); + } + + #[tokio::test] + async fn test_get_data_or_none_propagates_api_error() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/api/explore-deploy")) + .respond_with(ResponseTemplate::new(500).set_body_string("error")) + .mount(&server) + .await; + + let client = ReadNodeClient::new(server.uri()); + let result = client.get_data_or_none::("code".into()).await; + assert!(matches!(result, Err(ReadNodeError::Api(_, _)))); + } + + // ------------------------------------------------------- + // get_data_with_retry tests + // ------------------------------------------------------- + + #[tokio::test] + async fn test_get_data_with_retry_succeeds_first_try() { + let server = MockServer::start().await; + mount_explore_deploy(&server, string_expr_response("ok")).await; + + let client = ReadNodeClient::new(server.uri()); + let result: String = client + .get_data_with_retry("code".into(), 3, Duration::from_millis(100)) + .await + .expect("should succeed"); + assert_eq!(result, "ok"); + } + + #[tokio::test] + async fn test_get_data_with_retry_retries_on_missing_then_succeeds() { + let server = MockServer::start().await; + + // First 2 calls return empty + Mock::given(method("POST")) + .and(path("/api/explore-deploy")) + .respond_with(ResponseTemplate::new(200).set_body_json(empty_expr_response())) + .up_to_n_times(2) + .expect(2) + .mount(&server) + .await; + + // 3rd call returns data + Mock::given(method("POST")) + .and(path("/api/explore-deploy")) + .respond_with(ResponseTemplate::new(200).set_body_json(string_expr_response("found"))) + .expect(1) + .mount(&server) + .await; + + let client = ReadNodeClient::new(server.uri()); + let result: String = client + .get_data_with_retry("code".into(), 5, Duration::from_millis(10)) + .await + .expect("should succeed after retries"); + assert_eq!(result, "found"); + } + + #[tokio::test] + async fn test_get_data_with_retry_no_retry_on_api_error() { + let server = MockServer::start().await; + + Mock::given(method("POST")) + .and(path("/api/explore-deploy")) + .respond_with(ResponseTemplate::new(500).set_body_string("fail")) + .expect(1) + .mount(&server) + .await; + + let client = ReadNodeClient::new(server.uri()); + let result = client + .get_data_with_retry::("code".into(), 5, Duration::from_millis(10)) + .await; + assert!( + matches!(result, Err(ReadNodeError::Api(_, _))), + "expected Api error without retry, got {result:?}" + ); + } + + #[tokio::test] + async fn test_get_data_with_retry_exhausts_retries() { + let server = MockServer::start().await; + + // Always return empty + Mock::given(method("POST")) + .and(path("/api/explore-deploy")) + .respond_with(ResponseTemplate::new(200).set_body_json(empty_expr_response())) + .mount(&server) + .await; + + let client = ReadNodeClient::new(server.uri()); + let result = client + .get_data_with_retry::("code".into(), 2, Duration::from_millis(10)) + .await; + + // After exhausting retries, backon returns the last error + assert!( + matches!( + result, + Err(ReadNodeError::ReturnValueMissing) | Err(ReadNodeError::Timeout(_)) + ), + "expected ReturnValueMissing or Timeout, got {result:?}" + ); + } + + /// Timeout test: we can't easily test the 45-second timeout with a real + /// HTTP server, so instead we verify that the timeout duration is correct + /// by directly testing the Timeout error variant construction. + #[test] + fn test_timeout_error_has_correct_duration() { + let timeout = Duration::from_secs(45); + let err = ReadNodeError::Timeout(timeout); + match err { + ReadNodeError::Timeout(d) => assert_eq!(d, Duration::from_secs(45)), + other => panic!("expected Timeout, got {other:?}"), + } + } + + // ------------------------------------------------------- + // get_data_or_none_with_retry tests + // ------------------------------------------------------- + + #[tokio::test] + async fn test_get_data_or_none_with_retry_returns_some_on_success() { + let server = MockServer::start().await; + mount_explore_deploy(&server, string_expr_response("data")).await; + + let client = ReadNodeClient::new(server.uri()); + let result: Option = client + .get_data_or_none_with_retry("code".into(), 3, Duration::from_millis(10)) + .await + .expect("should succeed"); + assert_eq!(result, Some("data".into())); + } + + #[tokio::test] + async fn test_get_data_or_none_with_retry_returns_none_on_exhausted() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/api/explore-deploy")) + .respond_with(ResponseTemplate::new(200).set_body_json(empty_expr_response())) + .mount(&server) + .await; + + let client = ReadNodeClient::new(server.uri()); + let result: Option = client + .get_data_or_none_with_retry("code".into(), 2, Duration::from_millis(10)) + .await + .expect("should return Ok(None) after retries"); + assert_eq!(result, None); + } + + #[tokio::test] + async fn test_get_data_or_none_with_retry_propagates_api_error() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/api/explore-deploy")) + .respond_with(ResponseTemplate::new(500).set_body_string("error")) + .mount(&server) + .await; + + let client = ReadNodeClient::new(server.uri()); + let result = client + .get_data_or_none_with_retry::("code".into(), 3, Duration::from_millis(10)) + .await; + assert!(matches!(result, Err(ReadNodeError::Api(_, _)))); + } + + /// The read client had NO request timeout at all, so a read node that accepts + /// the connection and then stalls at the HTTP layer would hang `get_data` + /// forever — including the periodic registry health check, which calls it + /// directly. (reqwest's default `tcp_user_timeout` does not help: it only + /// fires when data goes *unacked*.) This test hangs on the old code. + #[tokio::test] + async fn test_get_data_times_out_when_server_stalls() { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/api/explore-deploy")) + .respond_with( + ResponseTemplate::new(200) + .set_body_json(string_expr_response("never arrives")) + .set_delay(Duration::from_secs(30)), + ) + .mount(&server) + .await; + + let config = ReadNodeConfig { + request_timeout: Duration::from_millis(300), + ..ReadNodeConfig::default() + }; + let client = ReadNodeClient::with_config(server.uri(), config); + + let started = std::time::Instant::now(); + let result: Result = client.get_data("code".into()).await; + + assert!(result.is_err(), "a stalled read must error, not hang"); + assert!( + started.elapsed() < Duration::from_secs(5), + "must be bounded by request_timeout, took {:?}", + started.elapsed() + ); } } diff --git a/packages/firefly-client/src/traits.rs b/packages/firefly-client/src/traits.rs new file mode 100644 index 00000000..5fd49ec4 --- /dev/null +++ b/packages/firefly-client/src/traits.rs @@ -0,0 +1,82 @@ +use std::future::Future; +use std::time::Duration; + +use futures::Stream; +use secp256k1::SecretKey; + +use crate::errors::ReadNodeError; +use crate::models::{DeployData, DeployId, SignedCode, WalletAddress}; +use crate::node_events::DeployEvent; + +/// Trait abstracting read operations against a blockchain node. +/// +/// The concrete implementation is [`ReadNodeClient`](crate::ReadNodeClient), +/// which uses HTTP / explore-deploy under the hood. Extracting a trait +/// enables unit-testing domain logic with lightweight test doubles. +pub trait ReadNode: Clone + Send + Sync + 'static { + fn get_data( + &self, + rholang_code: String, + ) -> impl Future> + Send; + + fn get_data_or_none( + &self, + rholang_code: String, + ) -> impl Future, ReadNodeError>> + Send; + + fn get_data_with_retry( + &self, + rholang_code: String, + max_retries: u32, + delay: Duration, + ) -> impl Future> + Send; + + fn get_data_or_none_with_retry( + &self, + rholang_code: String, + max_retries: u32, + delay: Duration, + ) -> impl Future, ReadNodeError>> + Send; +} + +/// Trait abstracting write / deploy operations against a blockchain node. +/// +/// The concrete implementation is [`WriteNodeClient`](crate::WriteNodeClient), +/// which uses gRPC under the hood. +pub trait WriteNode: Clone + Send + Sync + 'static { + fn deploy( + &mut self, + key: &SecretKey, + deploy_data: DeployData, + ) -> impl Future> + Send; + + fn deploy_signed_contract( + &mut self, + contract: SignedCode, + ) -> impl Future> + Send; + + fn full_deploy( + &mut self, + key: &SecretKey, + deploy_data: DeployData, + ) -> impl Future> + Send; + + fn get_head_block_index(&mut self) -> impl Future> + Send; +} + +/// Trait abstracting event subscriptions from a blockchain node. +/// +/// The concrete implementation is [`NodeEvents`](crate::NodeEvents), +/// which uses WebSocket under the hood. +pub trait NodeEventSource: Clone + Send + Sync + 'static { + fn wait_for_deploy( + &self, + deploy_id: &DeployId, + max_wait: Duration, + ) -> impl Future> + Send; + + fn subscribe_for_deploys( + &self, + wallet_address: WalletAddress, + ) -> impl Stream + Send; +} diff --git a/packages/firefly-client/src/write_node_client.rs b/packages/firefly-client/src/write_node_client.rs index b1d31c76..8fc0e0b8 100644 --- a/packages/firefly-client/src/write_node_client.rs +++ b/packages/firefly-client/src/write_node_client.rs @@ -1,56 +1,190 @@ +use std::time::Duration; + use anyhow::{Context, anyhow}; use blake2::digest::consts::U32; use blake2::{Blake2b, Digest}; use futures::TryStreamExt; use prost::Message as _; use secp256k1::{Message, Secp256k1, SecretKey}; +use tonic::transport::{Channel, Endpoint}; use crate::helpers::FromExpr; use crate::models::casper::v1::deploy_service_client::DeployServiceClient; -use crate::models::casper::v1::propose_service_client::ProposeServiceClient; -use crate::models::casper::v1::{ - block_info_response, - deploy_response, - propose_response, - rho_data_response, -}; -use crate::models::casper::{BlocksQuery, DataAtNameByBlockQuery, DeployDataProto, ProposeQuery}; +use crate::models::casper::v1::{block_info_response, deploy_response, rho_data_response}; +use crate::models::casper::{BlocksQuery, DataAtNameByBlockQuery, DeployDataProto}; use crate::models::rhoapi::expr::ExprInstance; use crate::models::rhoapi::{Expr, Par}; use crate::models::{BlockId, DeployData, DeployId, SignedCode, ValidAfter}; +use crate::traits::WriteNode; + +/// Transport resilience settings for the deploy-service gRPC channel. +/// +/// These exist because a default `tonic::Endpoint` sets **none** of them, which +/// made the client wedge permanently: with no HTTP/2 keep-alive, a half-open +/// connection (NAT/conntrack eviction, container blip, peer restart without a +/// clean FIN) is never detected, so the h2 connection never reports an error — +/// and tonic's `Reconnect` only re-establishes on error. With no request +/// deadline, calls on that dead connection hang forever. Because every clone of +/// `Channel` shares one tower `Buffer` worker, a single hung call parks the +/// worker and *every* caller stalls. That is exactly the observed failure: all +/// `*/prepare` endpoints hung indefinitely while HTTP reads stayed fast, and +/// only a process restart cleared it. +#[derive(Clone, Copy, Debug)] +pub struct WriteNodeConfig { + /// Hard deadline for any single call. Bounds queue-wait + connect + headers + /// + body (see `deadline`). + pub request_timeout: Duration, + /// Bounds the TCP connect only (not the h2 handshake). + pub connect_timeout: Duration, + /// How often to send HTTP/2 PING frames while a request is in flight. + /// `None` disables h2 keep-alive entirely. + /// + /// Tune with care. Unlike TCP keepalives — which the peer's *kernel* answers + /// — an h2 PING must be acked by the node's *application* task, which can + /// stall for seconds under block-processing load. Too tight a window + /// therefore kills healthy connections: measured against a live f1r3node, an + /// interval of 10s with a 5s ack window produced spurious + /// `KeepAliveTimedOut` errors on a perfectly good connection. + pub http2_keep_alive_interval: Option, + /// How long to wait for a PING ack before declaring the connection dead. + pub keep_alive_timeout: Option, + /// Whether to ping while idle. Left off by default: grpc-java/Netty servers + /// default to `permitKeepAliveWithoutCalls=false` and answer idle pings with + /// `GOAWAY(ENHANCE_YOUR_CALM)`. This client must work against both the Scala + /// and Rust nodes, so the idle case is covered by `tcp_keepalive` instead. + pub keep_alive_while_idle: bool, + /// Kernel-level keepalive — invisible to gRPC, so no `ENHANCE_YOUR_CALM` risk. + /// Covers the idle-blackhole case that `keep_alive_while_idle = false` leaves + /// open. Mirrors reqwest's `tcp_user_timeout`, which is empirically why the + /// read (HTTP) client never wedged while this one did. + pub tcp_keepalive: Option, +} + +impl Default for WriteNodeConfig { + fn default() -> Self { + Self { + // ~1500x the observed healthy latency (6.5ms) and ~2 block times, so + // it rides out a block-production stall without spurious failures, + // while still failing fast enough that a blip cannot become a wedge. + request_timeout: Duration::from_secs(10), + // Must stay < request_timeout so a reconnect inside a request cannot + // consume the whole budget. + connect_timeout: Duration::from_secs(5), + // Generous on purpose: the node must ack these from its application + // task, and it can stall for seconds while processing a block. A dead + // peer is still detected within interval + timeout (~60s) and the + // channel then self-heals via tonic's `Reconnect`, while a merely-busy + // node is never torn down. (Measured: 10s/5s spuriously killed a + // healthy connection.) + http2_keep_alive_interval: Some(Duration::from_secs(30)), + keep_alive_timeout: Some(Duration::from_secs(30)), + keep_alive_while_idle: false, + tcp_keepalive: Some(Duration::from_secs(20)), + } + } +} #[derive(Clone)] pub struct WriteNodeClient { - deploy_client: DeployServiceClient, - propose_client: ProposeServiceClient, + deploy_client: DeployServiceClient, + request_timeout: Duration, +} + +/// Extract a deploy ID from the node's response string. +/// The Scala node returns `"Success! DeployId is: "` while the +/// Rust node returns `"Success!\nDeployId is: "`. +fn extract_deploy_id(response: &str) -> anyhow::Result { + response + .strip_prefix("Success! DeployId is: ") + .or_else(|| response.strip_prefix("Success!\nDeployId is: ")) + .map(|id| DeployId::from(id.to_owned())) + .context(format!( + "failed to extract deploy_id from response: {response:?}" + )) } impl WriteNodeClient { - pub async fn new( + pub async fn new(deploy_service_url: String) -> anyhow::Result { + Self::with_config(deploy_service_url, WriteNodeConfig::default()).await + } + + pub async fn with_config( deploy_service_url: String, - propose_service_url: String, + config: WriteNodeConfig, ) -> anyhow::Result { - let deploy_client = DeployServiceClient::connect(deploy_service_url) - .await - .context("failed to connect to deploy service")?; + let mut endpoint = Endpoint::new(deploy_service_url) + .context("invalid deploy service url")? + .connect_timeout(config.connect_timeout) + .timeout(config.request_timeout) + .keep_alive_while_idle(config.keep_alive_while_idle) + .tcp_keepalive(config.tcp_keepalive) + .tcp_nodelay(true); - let propose_client = ProposeServiceClient::connect(propose_service_url) + if let Some(interval) = config.http2_keep_alive_interval { + endpoint = endpoint.http2_keep_alive_interval(interval); + } + if let Some(timeout) = config.keep_alive_timeout { + endpoint = endpoint.keep_alive_timeout(timeout); + } + + // `connect_timeout` bounds only the TCP connect: the HTTP/2 handshake runs + // *after* the connector returns, so a peer that accepts TCP and then never + // speaks h2 would hang startup forever without this outer bound. + let channel = tokio::time::timeout(config.connect_timeout * 2, endpoint.connect()) .await - .context("failed to connect to propose service")?; + .map_err(|_| anyhow!("timed out connecting to deploy service"))? + .context("failed to connect to deploy service")?; Ok(Self { - deploy_client, - propose_client, + deploy_client: DeployServiceClient::new(channel), + request_timeout: config.request_timeout, }) } + /// Apply the hard per-call deadline. + /// + /// An outer `tokio::time::timeout` is required — `Endpoint::timeout` alone is + /// NOT sufficient, for two independent reasons: + /// + /// 1. tonic's `GrpcTimeout` layer sits *inside* the tower `Buffer`, so its + /// timer only starts once the buffer worker dispatches the request. If the + /// worker is head-of-line blocked by a stuck call, the request waits in the + /// queue with no timer at all — which is precisely how one hung call wedged + /// every caller. + /// 2. `GrpcTimeout` resolves as soon as response *headers* arrive and then + /// drops its sleep, leaving a streaming body unbounded — and + /// `get_head_block_index` reads a server-streaming body. + /// + /// This wrapper bounds queue-wait + connect + handshake + headers + body. + /// Dropping the future on expiry also frees the HTTP/2 stream slot, so zombie + /// streams cannot exhaust `max_concurrent_streams`. + async fn deadline( + timeout: Duration, + fut: impl Future>, + ) -> anyhow::Result { + tokio::time::timeout(timeout, fut) + .await + .map_err(|_| anyhow!("deploy service call timed out after {timeout:?}"))? + } + pub async fn deploy( &mut self, key: &SecretKey, deploy_data: DeployData, + ) -> anyhow::Result { + let timeout = self.request_timeout; + Self::deadline(timeout, self.deploy_inner(key, deploy_data)).await + } + + async fn deploy_inner( + &mut self, + key: &SecretKey, + deploy_data: DeployData, ) -> anyhow::Result { let valid_after_block_number = match deploy_data.valid_after_block_number { - ValidAfter::Head => self.get_head_block_index().await?, + // `_inner`: the public caller already holds the single deadline for + // this whole call, so we must not nest a second one here. + ValidAfter::Head => self.get_head_block_index_inner().await?, ValidAfter::Index(i) => i, }; @@ -72,11 +206,11 @@ impl WriteNodeClient { let signature = secp.sign_ecdsa(Message::from_digest(hash.into()), key); - msg.sig = signature.serialize_der().to_vec(); + msg.sig = signature.serialize_der().to_vec().into(); msg.sig_algorithm = "secp256k1".into(); let public_key = key.public_key(&secp); - msg.deployer = public_key.serialize_uncompressed().into(); + msg.deployer = public_key.serialize_uncompressed().to_vec().into(); let resp = self .deploy_client @@ -86,28 +220,29 @@ impl WriteNodeClient { .message .context("missing do_deploy responce")?; - let deploy_id = match resp { - deploy_response::Message::Result(deploy_id) => deploy_id, - deploy_response::Message::Error(err) => { - return Err(anyhow!("do_deploy error: {err:?}")); - } - }; - - deploy_id - .strip_prefix("Success! DeployId is: ") - .map(|id| DeployId::from(id.to_owned())) - .context("failed to extract deploy_id") + match resp { + deploy_response::Message::Result(msg) => extract_deploy_id(&msg), + deploy_response::Message::Error(err) => Err(anyhow!("do_deploy error: {err:?}")), + } } pub async fn deploy_signed_contract( &mut self, contract: SignedCode, + ) -> anyhow::Result { + let timeout = self.request_timeout; + Self::deadline(timeout, self.deploy_signed_contract_inner(contract)).await + } + + async fn deploy_signed_contract_inner( + &mut self, + contract: SignedCode, ) -> anyhow::Result { let mut msg = DeployDataProto::decode(contract.contract.as_slice())?; - msg.sig = contract.sig; + msg.sig = contract.sig.into(); msg.sig_algorithm = contract.sig_algorithm; - msg.deployer = contract.deployer; + msg.deployer = contract.deployer.into(); let resp = self .deploy_client @@ -117,51 +252,26 @@ impl WriteNodeClient { .message .context("missing do_deploy responce")?; - let deploy_id = match resp { - deploy_response::Message::Result(deploy_id) => deploy_id, - deploy_response::Message::Error(err) => { - return Err(anyhow!("do_deploy error: {err:?}")); - } - }; - - deploy_id - .strip_prefix("Success! DeployId is: ") - .map(|id| DeployId::from(id.to_owned())) - .context("failed to extract deploy_id") - } - - pub async fn propose(&mut self) -> anyhow::Result { - let resp = self - .propose_client - .propose(ProposeQuery { is_async: false }) - .await - .context("propose grpc error")? - .into_inner() - .message - .context("missing propose responce")?; - - let block_hash = match resp { - propose_response::Message::Result(block_hash) => block_hash, - propose_response::Message::Error(err) => return Err(anyhow!("propose error: {err:?}")), - }; - - block_hash - .strip_prefix("Success! Block ") - .and_then(|block_hash| block_hash.strip_suffix(" created and added.")) - .map(|id| BlockId::from(id.to_owned())) - .context("failed to extract block hash") + match resp { + deploy_response::Message::Result(msg) => extract_deploy_id(&msg), + deploy_response::Message::Error(err) => Err(anyhow!("do_deploy error: {err:?}")), + } } pub async fn full_deploy( &mut self, key: &SecretKey, deploy_data: DeployData, - ) -> anyhow::Result { - self.deploy(key, deploy_data).await?; - self.propose().await + ) -> anyhow::Result { + self.deploy(key, deploy_data).await } pub async fn get_head_block_index(&mut self) -> anyhow::Result { + let timeout = self.request_timeout; + Self::deadline(timeout, self.get_head_block_index_inner()).await + } + + async fn get_head_block_index_inner(&mut self) -> anyhow::Result { let mut stream = self .deploy_client .show_main_chain(BlocksQuery { depth: 1 }) @@ -187,6 +297,18 @@ impl WriteNodeClient { hash: BlockId, channel: String, ) -> anyhow::Result + where + T: FromExpr, + { + let timeout = self.request_timeout; + Self::deadline(timeout, self.get_channel_value_inner(hash, channel)).await + } + + async fn get_channel_value_inner( + &mut self, + hash: BlockId, + channel: String, + ) -> anyhow::Result where T: FromExpr, { @@ -232,3 +354,260 @@ impl WriteNodeClient { T::from(expr) } } + +impl WriteNode for WriteNodeClient { + async fn deploy( + &mut self, + key: &SecretKey, + deploy_data: DeployData, + ) -> anyhow::Result { + self.deploy(key, deploy_data).await + } + + async fn deploy_signed_contract(&mut self, contract: SignedCode) -> anyhow::Result { + self.deploy_signed_contract(contract).await + } + + async fn full_deploy( + &mut self, + key: &SecretKey, + deploy_data: DeployData, + ) -> anyhow::Result { + self.full_deploy(key, deploy_data).await + } + + async fn get_head_block_index(&mut self) -> anyhow::Result { + self.get_head_block_index().await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ------------------------------------------------------- + // extract_deploy_id tests (pure function) + // ------------------------------------------------------- + + #[test] + fn test_extract_deploy_id_scala_format() { + let result = extract_deploy_id("Success! DeployId is: abc123def456").expect("should parse"); + assert_eq!(result, DeployId::from("abc123def456".to_owned())); + } + + #[test] + fn test_extract_deploy_id_rust_node_format() { + let result = + extract_deploy_id("Success!\nDeployId is: abc123def456").expect("should parse"); + assert_eq!(result, DeployId::from("abc123def456".to_owned())); + } + + #[test] + fn test_extract_deploy_id_error_response() { + let result = extract_deploy_id("Error: something went wrong"); + assert!(result.is_err(), "expected error for error response"); + } + + #[test] + fn test_extract_deploy_id_empty() { + let result = extract_deploy_id(""); + assert!(result.is_err(), "expected error for empty string"); + } + + #[test] + fn test_extract_deploy_id_partial_prefix_no_trailing_space() { + // The prefix includes a trailing space: "Success! DeployId is: " + // Without that space, the prefix doesn't match + let result = extract_deploy_id("Success! DeployId is:"); + assert!( + result.is_err(), + "expected error when trailing space is missing" + ); + } + + #[test] + fn test_extract_deploy_id_with_whitespace() { + let result = extract_deploy_id("Success! DeployId is: spaced_id ") + .expect("should parse with surrounding spaces"); + assert_eq!(result, DeployId::from(" spaced_id ".to_owned())); + } + + // ------------------------------------------------------- + // DeployData construction tests + // ------------------------------------------------------- + + #[test] + fn test_deploy_data_builder_defaults() { + let now = chrono::Utc::now(); + let data = DeployData::builder("new Nil".into()).build(); + + assert_eq!(data.term, "new Nil"); + assert_eq!(data.phlo_limit, 5_000_000); + assert!(matches!(data.valid_after_block_number, ValidAfter::Head)); + // Timestamp should be very close to now + let diff = (data.timestamp - now).num_milliseconds().unsigned_abs(); + assert!( + diff < 1000, + "timestamp should be close to now, diff={diff}ms" + ); + } + + #[test] + fn test_deploy_data_builder_custom_values() { + let ts = chrono::DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z") + .unwrap() + .to_utc(); + let data = DeployData::builder("code".into()) + .phlo_limit(1_000_000) + .timestamp(ts) + .valid_after_block_number(ValidAfter::Index(42)) + .build(); + + assert_eq!(data.term, "code"); + assert_eq!(data.phlo_limit, 1_000_000); + assert_eq!(data.timestamp, ts); + assert!(matches!( + data.valid_after_block_number, + ValidAfter::Index(42) + )); + } + + // ------------------------------------------------------- + // SignedCode construction test + // ------------------------------------------------------- + + #[test] + fn test_signed_code_fields() { + let code = SignedCode { + contract: vec![1, 2, 3], + sig: vec![4, 5, 6], + sig_algorithm: "secp256k1".into(), + deployer: vec![7, 8, 9], + }; + assert_eq!(code.contract, vec![1, 2, 3]); + assert_eq!(code.sig, vec![4, 5, 6]); + assert_eq!(code.sig_algorithm, "secp256k1"); + assert_eq!(code.deployer, vec![7, 8, 9]); + } + + // ------------------------------------------------------- + // transport resilience (the "every */prepare hangs forever" wedge) + // ------------------------------------------------------- + + /// Every public gRPC call routes through `deadline`, so this is the guard + /// that a stuck call fails fast instead of hanging forever — which is what + /// parked the shared tower `Buffer` worker and wedged *all* callers. + #[tokio::test] + async fn test_deadline_bounds_a_hanging_call() { + let result: anyhow::Result<()> = WriteNodeClient::deadline( + Duration::from_millis(50), + std::future::pending::>(), + ) + .await; + + let err = result.expect_err("a never-completing call must hit the deadline"); + assert!( + err.to_string().contains("timed out"), + "expected a timeout error, got: {err}" + ); + } + + #[tokio::test] + async fn test_deadline_passes_through_a_fast_call() { + let result = WriteNodeClient::deadline(Duration::from_secs(5), async { Ok(42_u64) }).await; + assert_eq!(result.expect("a fast call must not time out"), 42); + } + + /// A peer that accepts TCP but never speaks HTTP/2 models the production + /// wedge: the socket looks alive, so tonic's `Reconnect` never fires (it only + /// re-establishes on a connection *error*), and with no deadline the call + /// hangs forever — which is exactly how every `*/prepare` endpoint came to + /// hang until the process was restarted. + /// + /// Note: `Endpoint::connect()` itself returns `Ok` here — it does not await + /// the peer's HTTP/2 SETTINGS — so the connect bound is *not* what saves us. + /// The per-request deadline is. This test hangs forever on the old code. + // `held` is intentionally write-only: it exists purely to keep the accepted + // sockets from being dropped (which would close them and defeat the test). + #[allow(clippy::collection_is_never_read)] + #[tokio::test] + async fn test_call_fails_fast_against_peer_that_never_speaks_http2() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind ephemeral port"); + let addr = listener.local_addr().expect("local_addr"); + + // Accept connections and then say nothing, ever. + tokio::spawn(async move { + let mut held = Vec::new(); + while let Ok((stream, _)) = listener.accept().await { + held.push(stream); // hold it open; never write a byte + } + }); + + let config = WriteNodeConfig { + connect_timeout: Duration::from_millis(200), + request_timeout: Duration::from_millis(300), + ..WriteNodeConfig::default() + }; + + let mut client = WriteNodeClient::with_config(format!("http://{addr}"), config) + .await + .expect("connect() completes without awaiting the peer's HTTP/2 SETTINGS"); + + let started = std::time::Instant::now(); + let result = client.get_head_block_index().await; + + // Either bound firing is a pass — the property under test is "bounded, + // not hung". Here tonic's inner `GrpcTimeout` happens to win the race + // (both are 300ms and nothing is blocking the buffer worker); the outer + // `deadline` is the backstop for the two cases `GrpcTimeout` provably + // cannot cover: queue-wait while the worker is head-of-line blocked, and + // a streaming body that stalls after headers. + let err = result.expect_err("a call to a peer that never speaks HTTP/2 must not hang"); + let msg = err.to_string().to_lowercase(); + assert!( + msg.contains("timed out") || msg.contains("timeout"), + "expected a timeout, got: {err}" + ); + assert!( + started.elapsed() < Duration::from_secs(5), + "must fail fast, took {:?}", + started.elapsed() + ); + } + + /// The resilience settings must actually be applied, and be internally + /// consistent: a reconnect attempt inside a request must not be able to eat + /// the entire request budget. + #[test] + fn test_default_config_is_internally_consistent() { + let config = WriteNodeConfig::default(); + + assert!( + config.connect_timeout < config.request_timeout, + "connect_timeout must be < request_timeout, so a reconnect attempt \ + inside a request cannot consume the whole budget" + ); + + // Regression guard for a real mistake: a 10s interval with a 5s ack window + // was *measured* to spuriously kill a healthy connection against a live + // node (`KeepAliveTimedOut`). Unlike a TCP keepalive, which the peer's + // kernel answers, an h2 PING must be acked by the node's application task + // — which stalls for seconds while processing a block. The ack window must + // therefore tolerate a busy-but-alive node. + if let Some(ack_window) = config.keep_alive_timeout { + assert!( + ack_window >= Duration::from_secs(20), + "h2 PING ack window must tolerate a busy node; {ack_window:?} is too tight" + ); + } + + assert!( + config.tcp_keepalive.is_some(), + "TCP keepalive is the false-positive-free self-heal (the kernel acks it) \ + and covers the idle case that keep_alive_while_idle = false leaves open" + ); + assert!(!config.keep_alive_while_idle); + } +} diff --git a/packages/state-sync/src/main.rs b/packages/state-sync/src/main.rs index b54ff468..5db2d194 100644 --- a/packages/state-sync/src/main.rs +++ b/packages/state-sync/src/main.rs @@ -9,7 +9,7 @@ use base64::prelude::BASE64_STANDARD; use clap::{Parser, Subcommand}; use firefly_client::helpers::FromExpr; use firefly_client::models::rhoapi::expr::ExprInstance; -use firefly_client::models::{BlockId, DeployData}; +use firefly_client::models::{BlockId, DeployData, DeployId}; use secp256k1::SecretKey; use serde::{Deserialize, Serialize}; use tokio::select; @@ -25,10 +25,6 @@ struct Args { #[arg(long)] deploy_service_url: String, - /// Firefly propose service url - #[arg(long)] - propose_service_url: String, - /// Globally unique service identifier #[arg(long)] service_id: String, @@ -65,9 +61,7 @@ enum Commands { async fn main() -> anyhow::Result<()> { let args = Args::parse(); - let mut client = - firefly_client::WriteNodeClient::new(args.deploy_service_url, args.propose_service_url) - .await?; + let mut client = firefly_client::WriteNodeClient::new(args.deploy_service_url).await?; match args.command { Commands::Upload { db_url, interval } => { @@ -86,24 +80,24 @@ async fn main() -> anyhow::Result<()> { let rho_code = rho_sql_dump_template(channel_name, sql); let deploy_data = DeployData::builder(rho_code).build(); - let hash = client.full_deploy(&args.wallet_key, deploy_data).await?; - println!("dump hash: {hash}"); + let deploy_id = client.full_deploy(&args.wallet_key, deploy_data).await?; + println!("dump deploy_id: {deploy_id}"); let rho_code = rho_save_hash_template( &args.service_id, &ServiceHash { - block_hash: hash, + deploy_id, channel_name, }, ); let deploy_data = DeployData::builder(rho_code).build(); - let hash = client.full_deploy(&args.wallet_key, deploy_data).await?; - println!("save hash: {hash}"); + let deploy_id = client.full_deploy(&args.wallet_key, deploy_data).await?; + println!("save deploy_id: {deploy_id}"); } } Commands::Download { hash } => { let entries: Vec = client - .get_channel_value(hash, format!("{}-hashes", args.service_id)) + .get_channel_value(hash.clone(), format!("{}-hashes", args.service_id)) .await?; let Some(entry) = entries.into_iter().next_back() else { @@ -111,7 +105,7 @@ async fn main() -> anyhow::Result<()> { }; let sql: String = client - .get_channel_value(entry.block_hash, entry.channel_name.to_string()) + .get_channel_value(hash, entry.channel_name.to_string()) .await?; let sql = BASE64_STANDARD.decode(sql)?; let sql = String::from_utf8(sql)?; @@ -120,8 +114,8 @@ async fn main() -> anyhow::Result<()> { Commands::Init => { let rho_code = rho_save_hash_contract(&args.service_id); let deploy_data = DeployData::builder(rho_code).build(); - let hash = client.full_deploy(&args.wallet_key, deploy_data).await?; - println!("{hash}"); + let deploy_id = client.full_deploy(&args.wallet_key, deploy_data).await?; + println!("{deploy_id}"); } } @@ -155,7 +149,7 @@ fn rho_save_hash_contract(service_id: &str) -> String { #[derive(Debug, Clone, Serialize, Deserialize)] struct ServiceHash { - block_hash: BlockId, + deploy_id: DeployId, channel_name: Uuid, } @@ -163,14 +157,14 @@ impl FromExpr for ServiceHash { fn from(val: ExprInstance) -> anyhow::Result { let mut map: HashMap = FromExpr::from(val)?; - let block_hash = map.remove("block_hash").context("block_hash is missing")?; + let deploy_id = map.remove("deploy_id").context("deploy_id is missing")?; let channel_name = map .remove("channel_name") .context("channel_name is missing")?; let channel_name: Uuid = channel_name.parse()?; Ok(Self { - block_hash: block_hash.into(), + deploy_id: deploy_id.into(), channel_name, }) } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..e0438eaf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly-2026-02-09"