From 55ddb91c1c524062bb1370521bf7037356e3c25e Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Fri, 19 May 2023 17:07:01 -0400 Subject: [PATCH] refactor: massive jump toward a legit homestar runtime (#133) * Adds runtime for singular workflows (multiple workflows incoming). Incldues: - workflow => Dag for sequential/promised and parallel compute - task scheduler (within workflow) - worker - integration between libp2p and workers for receipts and more - feature-gating ipfs for resources - adds retry/backoff for fetching resources * moves us toward tracing's logging and adds logfmt-formatted logs * removes much of early-early demo code Issues closed: - https://github.com/ipvm-wg/homestar/issues/76 - https://github.com/ipvm-wg/homestar/issues/79 - https://github.com/ipvm-wg/homestar/issues/86 (enough for now) --- .envrc | 4 +- .github/workflows/tests_and_checks.yml | 39 +- .gitignore | 3 + Cargo.lock | 2015 +++++++++++++---- Cargo.toml | 17 +- codecov.yml | 2 +- diesel.toml | 4 +- flake.nix | 6 +- homestar-core/Cargo.toml | 22 +- homestar-core/src/consts.rs | 2 +- homestar-core/src/lib.rs | 19 +- homestar-core/src/test_utils/mod.rs | 3 + homestar-core/src/test_utils/workflow.rs | 159 ++ homestar-core/src/workflow.rs | 34 + homestar-core/src/workflow/ability.rs | 4 +- homestar-core/src/workflow/input.rs | 123 +- homestar-core/src/workflow/instruction.rs | 363 +++ ...cation_result.rs => instruction_result.rs} | 61 +- homestar-core/src/workflow/invocation.rs | 249 +- homestar-core/src/workflow/issuer.rs | 69 + homestar-core/src/workflow/mod.rs | 19 - homestar-core/src/workflow/nonce.rs | 26 +- homestar-core/src/workflow/pointer.rs | 94 +- homestar-core/src/workflow/prf.rs | 3 +- homestar-core/src/workflow/receipt.rs | 181 +- homestar-core/src/workflow/task.rs | 447 ++-- homestar-guest-wasm/Cargo.toml | 8 +- homestar-guest-wasm/src/lib.rs | 171 +- homestar-guest-wasm/wits/test.wit | 11 +- homestar-runtime/Cargo.toml | 60 +- homestar-runtime/config/settings.toml | 4 + .../migrations}/.keep | 0 .../down.sql | 2 + .../2022-12-11-183928_create_receipts/up.sql | 12 + homestar-runtime/src/cli.rs | 39 +- homestar-runtime/src/db.rs | 128 +- homestar-runtime/src/db/schema.rs | 4 +- homestar-runtime/src/lib.rs | 30 +- homestar-runtime/src/logger.rs | 59 + homestar-runtime/src/main.rs | 261 +-- homestar-runtime/src/network/client.rs | 179 -- homestar-runtime/src/network/eventloop.rs | 532 +++-- homestar-runtime/src/network/ipfs.rs | 72 + homestar-runtime/src/network/mod.rs | 8 +- homestar-runtime/src/network/pubsub.rs | 14 +- homestar-runtime/src/network/swarm.rs | 176 +- homestar-runtime/src/network/ws.rs | 226 ++ homestar-runtime/src/receipt.rs | 274 ++- homestar-runtime/src/runtime.rs | 18 + homestar-runtime/src/scheduler.rs | 311 +++ homestar-runtime/src/schema.rs | 12 - homestar-runtime/src/settings.rs | 121 + homestar-runtime/src/tasks.rs | 33 + homestar-runtime/src/tasks/wasm.rs | 57 + homestar-runtime/src/test_utils/db.rs | 42 +- homestar-runtime/src/test_utils/mod.rs | 2 + homestar-runtime/src/test_utils/receipt.rs | 41 + homestar-runtime/src/worker.rs | 339 +++ homestar-runtime/src/workflow.rs | 587 +++++ homestar-runtime/src/workflow/settings.rs | 37 + homestar-wasm/Cargo.toml | 33 +- .../fixtures/homestar_guest_wasm.wasm | Bin 26930 -> 373878 bytes homestar-wasm/src/io.rs | 9 +- homestar-wasm/src/lib.rs | 18 +- homestar-wasm/src/test_utils/mod.rs | 3 +- homestar-wasm/src/wasmtime/config.rs | 2 + homestar-wasm/src/wasmtime/ipld.rs | 50 +- homestar-wasm/src/wasmtime/mod.rs | 9 +- homestar-wasm/src/wasmtime/world.rs | 108 +- homestar-wasm/tests/execute_wasm.rs | 251 +- .../down.sql | 2 - .../2022-12-11-183928_create_receipts/up.sql | 10 - rust-toolchain.toml | 2 +- 73 files changed, 5876 insertions(+), 2459 deletions(-) create mode 100644 homestar-core/src/test_utils/workflow.rs create mode 100644 homestar-core/src/workflow.rs create mode 100644 homestar-core/src/workflow/instruction.rs rename homestar-core/src/workflow/{invocation_result.rs => instruction_result.rs} (63%) create mode 100644 homestar-core/src/workflow/issuer.rs delete mode 100644 homestar-core/src/workflow/mod.rs create mode 100644 homestar-runtime/config/settings.toml rename {migrations => homestar-runtime/migrations}/.keep (100%) create mode 100644 homestar-runtime/migrations/2022-12-11-183928_create_receipts/down.sql create mode 100644 homestar-runtime/migrations/2022-12-11-183928_create_receipts/up.sql create mode 100644 homestar-runtime/src/logger.rs delete mode 100644 homestar-runtime/src/network/client.rs create mode 100644 homestar-runtime/src/network/ipfs.rs create mode 100644 homestar-runtime/src/network/ws.rs create mode 100644 homestar-runtime/src/runtime.rs create mode 100644 homestar-runtime/src/scheduler.rs delete mode 100644 homestar-runtime/src/schema.rs create mode 100644 homestar-runtime/src/settings.rs create mode 100644 homestar-runtime/src/tasks.rs create mode 100644 homestar-runtime/src/tasks/wasm.rs create mode 100644 homestar-runtime/src/test_utils/receipt.rs create mode 100644 homestar-runtime/src/worker.rs create mode 100644 homestar-runtime/src/workflow.rs create mode 100644 homestar-runtime/src/workflow/settings.rs delete mode 100644 migrations/2022-12-11-183928_create_receipts/down.sql delete mode 100644 migrations/2022-12-11-183928_create_receipts/up.sql diff --git a/.envrc b/.envrc index 8ff688e8..29dc2412 100644 --- a/.envrc +++ b/.envrc @@ -1,4 +1,4 @@ use_flake -export RUST_LOG=homestar=debug,sqlx=warn,atuin_client=warn,libp2p=debug -export RUST_BACKTRACE=1 +export RUST_LOG=homestar_runtime=debug,atuin_client=warn,libp2p=info +export RUST_BACKTRACE=full diff --git a/.github/workflows/tests_and_checks.yml b/.github/workflows/tests_and_checks.yml index 776f6d6e..6a4585e4 100644 --- a/.github/workflows/tests_and_checks.yml +++ b/.github/workflows/tests_and_checks.yml @@ -79,7 +79,7 @@ jobs: if: ${{ matrix.rust-toolchain == 'stable' && github.event_name == 'push' }} run: cargo build --release - run-tests: + run-tests-all-features: runs-on: ubuntu-latest strategy: fail-fast: false @@ -112,3 +112,40 @@ jobs: - name: Run Tests run: cargo test --all-features + + - name: Run Tests + run: cargo test --no-default-features + + run-tests-no-default-features: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rust-toolchain: + - stable + - nightly + steps: + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Install Environment Packages + run: | + sudo apt-get update -qqy + sudo apt-get install jq + + - name: Cache Project + uses: Swatinem/rust-cache@v2 + + - name: Install Rust Toolchain + uses: actions-rs/toolchain@v1 + with: + override: true + toolchain: ${{ matrix.rust-toolchain }} + + - name: Run Tests + run: cargo test --no-default-features diff --git a/.gitignore b/.gitignore index a17beb4a..ba1448f6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,12 @@ private *.temp *.db *.tmp +*.png +*.dot .history .DS_Store homestar-guest-wasm/out +homestar-wasm/out # locks homestar-wasm/Cargo.lock diff --git a/Cargo.lock b/Cargo.lock index 4a34834e..6ad22d99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,9 +152,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] @@ -165,63 +165,87 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anstream" -version = "0.2.6" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ "anstyle", "anstyle-parse", + "anstyle-query", "anstyle-wincon", - "concolor-override", - "concolor-query", + "colorchoice", "is-terminal", "utf8parse", ] [[package]] name = "anstyle" -version = "0.3.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" [[package]] name = "anstyle-parse" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" dependencies = [ "utf8parse", ] [[package]] -name = "anstyle-wincon" -version = "0.2.0" +name = "anstyle-query" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "anstyle", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] -name = "anyhow" -version = "1.0.70" +name = "anstyle-wincon" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] [[package]] -name = "arbitrary" -version = "1.3.0" +name = "anyhow" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +dependencies = [ + "backtrace", +] [[package]] name = "arc-swap" @@ -362,7 +386,7 @@ dependencies = [ "log", "parking", "polling", - "rustix 0.37.11", + "rustix 0.37.19", "slab", "socket2", "waker-fn", @@ -385,7 +409,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.16", ] [[package]] @@ -433,7 +457,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.16", ] [[package]] @@ -450,7 +474,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.16", ] [[package]] @@ -474,9 +498,9 @@ checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" [[package]] name = "atomic_refcell" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "857253367827bd9d0fd973f0ef15506a96e79e41b0ad7aa691203a4e3214f6c8" +checksum = "79d6dc922a2792b006573f60b2648076355daeae5ce9cb59507e5908c9625d31" [[package]] name = "atty" @@ -495,6 +519,74 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.21.0", + "bitflags", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite 0.2.9", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1 0.10.5", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.6.2", + "object", + "rustc-demangle", +] + [[package]] name = "base-x" version = "0.2.11" @@ -561,6 +653,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[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" version = "0.10.6" @@ -707,15 +811,24 @@ checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" + +[[package]] +name = "byte-unit" +version = "4.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c" +dependencies = [ + "utf8-width", +] [[package]] name = "bytecheck" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13fe11640a23eb24562225322cd3e452b93a3d4091d62fab69c70542fcd17d1f" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" dependencies = [ "bytecheck_derive", "ptr_meta", @@ -724,9 +837,9 @@ dependencies = [ [[package]] name = "bytecheck_derive" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" dependencies = [ "proc-macro2", "quote", @@ -753,9 +866,9 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cap-fs-ext" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80754c33c036aa2b682c4c2f6d10f43c5a9b68527e89169706027ce285b0ea30" +checksum = "58bc48200a1a0fa6fba138b1802ad7def18ec1cdd92f7b2a04e21f1bd887f7b9" dependencies = [ "cap-primitives", "cap-std", @@ -763,39 +876,28 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "cap-net-ext" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12eb3ab6abfc0e1b81b2a0f4cd14e09f96e59459a47a1442bf144bdc63dfc50a" -dependencies = [ - "cap-primitives", - "cap-std", - "rustix 0.37.11", -] - [[package]] name = "cap-primitives" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1b2aadbde9f86e045e309df5d707600254d45eda76df251a7b840f81681d72" +checksum = "a4b6df5b295dca8d56f35560be8c391d59f0420f72e546997154e24e765e6451" dependencies = [ "ambient-authority", - "fs-set-times", + "fs-set-times 0.19.1", "io-extras", "io-lifetimes", "ipnet", "maybe-owned", - "rustix 0.37.11", + "rustix 0.37.19", "windows-sys 0.48.0", "winx", ] [[package]] name = "cap-rand" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4213970e64bba7b90bd25158955f024221dd45c0751cfd0c42f2745e9b177c1" +checksum = "4d25555efacb0b5244cf1d35833d55d21abc916fff0eaad254b8e2453ea9b8ab" dependencies = [ "ambient-authority", "rand 0.8.5", @@ -803,25 +905,25 @@ dependencies = [ [[package]] name = "cap-std" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a60db4439f80165589d00673a086e9e106e224944dd09cdf5553cedfbc90fe5c" +checksum = "3373a62accd150b4fcba056d4c5f3b552127f0ec86d3c8c102d60b978174a012" dependencies = [ "cap-primitives", "io-extras", "io-lifetimes", - "rustix 0.37.11", + "rustix 0.37.19", ] [[package]] name = "cap-time-ext" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afa389ffcd0c66daca4497d1a9992d18b985eff6b747ee8b4c86c2beae1f708" +checksum = "e95002993b7baee6b66c8950470e59e5226a23b3af39fc59c47fe416dd39821a" dependencies = [ "cap-primitives", "once_cell", - "rustix 0.37.11", + "rustix 0.37.19", "winx", ] @@ -891,11 +993,24 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +dependencies = [ + "iana-time-zone", + "num-integer", + "num-traits", + "serde", + "winapi", +] + [[package]] name = "ciborium" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" dependencies = [ "ciborium-io", "ciborium-ll", @@ -904,15 +1019,15 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" [[package]] name = "ciborium-ll" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" dependencies = [ "ciborium-io", "half 1.8.2", @@ -932,20 +1047,6 @@ dependencies = [ "unsigned-varint", ] -[[package]] -name = "cid" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b68e3193982cd54187d71afdb2a271ad4cf8af157858e9cb911b91321de143" -dependencies = [ - "core2", - "multibase", - "multihash 0.17.0", - "serde", - "serde_bytes", - "unsigned-varint", -] - [[package]] name = "cid" version = "0.10.1" @@ -954,7 +1055,7 @@ checksum = "fd94671561e36e4e7de75f753f577edafb0e7c05d6e4547229fdf7938fbcd2c3" dependencies = [ "core2", "multibase", - "multihash 0.18.0", + "multihash 0.18.1", "serde", "serde_bytes", "unsigned-varint", @@ -990,9 +1091,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.23" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "bitflags", "clap_lex 0.2.4", @@ -1002,9 +1103,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.2.1" +version = "4.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3" +checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" dependencies = [ "clap_builder", "clap_derive", @@ -1013,9 +1114,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.2.1" +version = "4.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" +checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" dependencies = [ "anstream", "anstyle", @@ -1033,7 +1134,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.16", ] [[package]] @@ -1057,6 +1158,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "common-multipart-rfc7578" version = "0.6.0" @@ -1074,27 +1181,74 @@ dependencies = [ ] [[package]] -name = "concolor-override" -version = "1.0.0" +name = "concat-in-place" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f" +checksum = "c5b80dba65d26e0c4b692ad0312b837f1177e8175031af57fd1de4f3bc36b430" [[package]] -name = "concolor-query" -version = "0.3.3" +name = "concurrent-queue" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" dependencies = [ - "windows-sys 0.45.0", + "crossbeam-utils", ] [[package]] -name = "concurrent-queue" -version = "2.2.0" +name = "config" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "console-api" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ab2224a0311582eb03adba4caaf18644f7b1f10a760803a803b9b605187fc7" dependencies = [ + "console-api", + "crossbeam-channel", "crossbeam-utils", + "futures", + "hdrhistogram", + "humantime", + "parking_lot 0.12.1", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", ] [[package]] @@ -1145,31 +1299,32 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] [[package]] name = "cranelift-bforest" -version = "0.96.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1277fbfa94bc82c8ec4af2ded3e639d49ca5f7f3c7eeab2c66accd135ece4e70" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-codegen" -version = "0.96.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e8c31ad3b2270e9aeec38723888fe1b0ace3bea2b06b3f749ccf46661d3220" dependencies = [ "bumpalo", "cranelift-bforest", "cranelift-codegen-meta", "cranelift-codegen-shared", - "cranelift-control", "cranelift-entity", "cranelift-isle", "gimli", @@ -1182,37 +1337,33 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.96.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ac5ac30d62b2d66f12651f6b606dbdfd9c2cfd0908de6b387560a277c5c9da" dependencies = [ "cranelift-codegen-shared", ] [[package]] name = "cranelift-codegen-shared" -version = "0.96.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" - -[[package]] -name = "cranelift-control" -version = "0.96.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" -dependencies = [ - "arbitrary", -] +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd82b8b376247834b59ed9bdc0ddeb50f517452827d4a11bccf5937b213748b8" [[package]] name = "cranelift-entity" -version = "0.96.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40099d38061b37e505e63f89bab52199037a72b931ad4868d9089ff7268660b0" dependencies = [ "serde", ] [[package]] name = "cranelift-frontend" -version = "0.96.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a25d9d0a0ae3079c463c34115ec59507b4707175454f0eee0891e83e30e82d" dependencies = [ "cranelift-codegen", "log", @@ -1222,13 +1373,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.96.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80de6a7d0486e4acbd5f9f87ec49912bf4c8fb6aea00087b989685460d4469ba" [[package]] name = "cranelift-native" -version = "0.96.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6b03e0e03801c4b3fd8ce0758a94750c07a44e7944cc0ffbf0d3f2e7c79b00" dependencies = [ "cranelift-codegen", "libc", @@ -1237,8 +1390,9 @@ dependencies = [ [[package]] name = "cranelift-wasm" -version = "0.96.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff3220489a3d928ad91e59dd7aeaa8b3de18afb554a6211213673a71c90737ac" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -1284,7 +1438,7 @@ dependencies = [ "atty", "cast", "ciborium", - "clap 3.2.23", + "clap 3.2.25", "criterion-plot", "itertools", "lazy_static", @@ -1310,6 +1464,20 @@ dependencies = [ "itertools", ] +[[package]] +name = "crossbeam" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -1344,6 +1512,16 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.15" @@ -1420,17 +1598,6 @@ dependencies = [ "cipher 0.4.4", ] -[[package]] -name = "cuckoofilter" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b810a8449931679f64cd7eef1bbd0fa315801b6d5d9cdc1ace2804d6529eee18" -dependencies = [ - "byteorder", - "fnv", - "rand 0.7.3", -] - [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -1458,14 +1625,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "dagga" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee7c9e23428d020bc4597fa94fba2a84d71ddb6aec22797ff4dc1f9a6a0dd320" +dependencies = [ + "dot2", + "log", + "rustc-hash", + "snafu", +] + [[package]] name = "darling" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" +dependencies = [ + "darling_core 0.20.1", + "darling_macro 0.20.1", ] [[package]] @@ -1482,17 +1671,42 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.16", +] + [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core", + "darling_core 0.14.4", "quote", "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" +dependencies = [ + "darling_core 0.20.1", + "quote", + "syn 2.0.16", +] + [[package]] name = "data-encoding" version = "2.3.3" @@ -1573,7 +1787,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" dependencies = [ - "darling", + "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", @@ -1591,12 +1805,13 @@ dependencies = [ [[package]] name = "diesel" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4391a22b19c916e50bec4d6140f29bdda3e3bb187223fe6e3ea0b6e4d1021c04" +checksum = "72eb77396836a4505da85bae0712fa324b74acfe1876d7c2f7e694ef3d0ee373" dependencies = [ "diesel_derives", "libsqlite3-sys", + "r2d2", ] [[package]] @@ -1691,15 +1906,33 @@ checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" [[package]] name = "displaydoc" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "dot2" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "855423f2158bcc73798b3b9a666ec4204597a72370dc91dbdb8e7f9519de8cc3" + [[package]] name = "dotenvy" version = "0.15.7" @@ -1885,7 +2118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ae6b3d9530211fb3b12a95374b8b0823be812f53d09e18c5675c0146b09642" dependencies = [ "cfg-if", - "rustix 0.37.11", + "rustix 0.37.19", "windows-sys 0.48.0", ] @@ -1926,13 +2159,13 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide 0.6.2", + "miniz_oxide 0.7.1", ] [[package]] @@ -1954,6 +2187,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[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.1.0" @@ -1963,6 +2211,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "857cf27edcb26c2a36d84b2954019573d335bb289876113aceacacdca47a4fd4" +dependencies = [ + "io-lifetimes", + "rustix 0.36.13", + "windows-sys 0.45.0", +] + [[package]] name = "fs-set-times" version = "0.19.1" @@ -1970,10 +2229,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7833d0f115a013d51c55950a3b09d30e4b057be9961b709acb9b5b17a1108861" dependencies = [ "io-lifetimes", - "rustix 0.37.11", + "rustix 0.37.19", "windows-sys 0.48.0", ] +[[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.28" @@ -2046,7 +2311,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.16", ] [[package]] @@ -2205,9 +2470,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" dependencies = [ "bytes", "fnv", @@ -2247,12 +2512,50 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.13.2" +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", +] + +[[package]] +name = "hdrhistogram" +version = "7.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8" +dependencies = [ + "base64 0.13.1", + "byteorder", + "flate2", + "nom", + "num-traits", +] + +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1 0.10.5", +] + +[[package]] +name = "headers-core" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" dependencies = [ - "ahash 0.8.3", + "http", ] [[package]] @@ -2338,9 +2641,9 @@ dependencies = [ "enum-as-inner", "enum-assoc", "generic-array", + "indexmap", "libipld", "proptest", - "semver 1.0.17", "serde", "signature 2.1.0", "thiserror", @@ -2362,27 +2665,48 @@ dependencies = [ name = "homestar-runtime" version = "0.1.0" dependencies = [ + "ansi_term", "anyhow", "async-trait", - "clap 4.2.1", + "axum", + "byte-unit", + "clap 4.2.7", + "concat-in-place", + "config", + "console-subscriber", "criterion", + "crossbeam", + "dagga", "diesel", "diesel_migrations", "dotenvy", - "env_logger", + "enum-assoc", + "futures", + "headers", "homestar-core", "homestar-wasm", + "http", + "http-serde", + "indexmap", "ipfs-api", "ipfs-api-backend-hyper", "itertools", + "json", "libipld", "libp2p", "libp2p-identity", "proptest", + "reqwest", + "semver 1.0.17", "serde", + "serde_with", + "thiserror", "tokio", + "tokio-tungstenite", "tracing", + "tracing-logfmt", "tracing-subscriber", + "tryhard", "url", ] @@ -2396,9 +2720,10 @@ dependencies = [ "enum-as-inner", "heck", "homestar-core", + "image", "itertools", "libipld", - "libipld-core 0.16.0", + "libipld-core", "rust_decimal", "serde_ipld_dagcbor", "stacker", @@ -2408,11 +2733,11 @@ dependencies = [ "tracing", "wasi-cap-std-sync", "wasi-common", - "wasmparser 0.101.1", + "wasmparser 0.104.0", "wasmtime", "wasmtime-component-util", "wat", - "wit-component", + "wit-component 0.8.2", ] [[package]] @@ -2448,6 +2773,16 @@ dependencies = [ "pin-project-lite 0.2.9", ] +[[package]] +name = "http-serde" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e272971f774ba29341db2f686255ff8a979365a26fb9e4277f6b6d9ec0cdd5e" +dependencies = [ + "http", + "serde", +] + [[package]] name = "httparse" version = "1.8.0" @@ -2468,9 +2803,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.25" +version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ "bytes", "futures-channel", @@ -2503,6 +2838,54 @@ dependencies = [ "hyper", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite 0.2.9", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows 0.48.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "id-arena" version = "2.2.1" @@ -2562,7 +2945,7 @@ dependencies = [ "rtnetlink", "system-configuration", "tokio", - "windows", + "windows 0.34.0", ] [[package]] @@ -2735,7 +3118,7 @@ checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", - "rustix 0.37.11", + "rustix 0.37.19", "windows-sys 0.48.0", ] @@ -2794,18 +3177,35 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "keccak" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" dependencies = [ "cpufeatures", ] @@ -2839,9 +3239,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.141" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libipld" @@ -2852,12 +3252,12 @@ dependencies = [ "fnv", "libipld-cbor", "libipld-cbor-derive", - "libipld-core 0.16.0", - "libipld-json 0.16.0", + "libipld-core", + "libipld-json", "libipld-macro", "libipld-pb", "log", - "multihash 0.18.0", + "multihash 0.18.1", "thiserror", ] @@ -2868,7 +3268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77d98c9d1747aa5eef1cf099cd648c3fd2d235249f5fed07522aaebc348e423b" dependencies = [ "byteorder", - "libipld-core 0.16.0", + "libipld-core", "thiserror", ] @@ -2885,21 +3285,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "libipld-core" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a704ba3b25dee9e7a2361fae2c7c19defae2a92e69ae96ffb203996705cd7c" -dependencies = [ - "anyhow", - "cid 0.9.0", - "core2", - "multibase", - "multihash 0.17.0", - "serde", - "thiserror", -] - [[package]] name = "libipld-core" version = "0.16.0" @@ -2910,31 +3295,19 @@ dependencies = [ "cid 0.10.1", "core2", "multibase", - "multihash 0.18.0", + "multihash 0.18.1", "serde", "thiserror", ] -[[package]] -name = "libipld-json" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc549d7c70f9a401031fcb6d3bf7eccfe91bcad938f7485f71ee8ba9f79c1e79" -dependencies = [ - "libipld-core 0.15.0", - "multihash 0.17.0", - "serde", - "serde_json", -] - [[package]] name = "libipld-json" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25856def940047b07b25c33d4e66d248597049ab0202085215dc4dca0487731c" dependencies = [ - "libipld-core 0.16.0", - "multihash 0.18.0", + "libipld-core", + "multihash 0.18.1", "serde", "serde_json", ] @@ -2945,7 +3318,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71171c54214f866ae6722f3027f81dff0931e600e5a61e6b1b6a49ca0b5ed4ae" dependencies = [ - "libipld-core 0.16.0", + "libipld-core", ] [[package]] @@ -2955,7 +3328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f2d0f866c4cd5dc9aa8068c429ba478d2882a3a4b70ab56f7e9a0eddf5d16f" dependencies = [ "bytes", - "libipld-core 0.16.0", + "libipld-core", "quick-protobuf", "thiserror", ] @@ -2968,9 +3341,9 @@ checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" [[package]] name = "libm" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libp2p" @@ -2987,13 +3360,13 @@ dependencies = [ "libp2p-connection-limits", "libp2p-core", "libp2p-dns", - "libp2p-floodsub", "libp2p-gossipsub", "libp2p-identify", "libp2p-identity", "libp2p-kad", "libp2p-mdns", "libp2p-metrics", + "libp2p-mplex", "libp2p-noise", "libp2p-quic", "libp2p-request-response", @@ -3008,9 +3381,9 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c54073648b20bb47335164544a4e5694434f44530f47a4f6618f5f585f3ff5" +checksum = "510daa05efbc25184458db837f6f9a5143888f1caa742426d92e1833ddd38a50" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3032,9 +3405,9 @@ dependencies = [ [[package]] name = "libp2p-core" -version = "0.39.1" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7f8b7d65c070a5a1b5f8f0510648189da08f787b8963f8e21219e0710733af" +checksum = "3c1df63c0b582aa434fb09b2d86897fa2b419ffeccf934b36f87fcedc8e835c2" dependencies = [ "either", "fnv", @@ -3072,37 +3445,17 @@ dependencies = [ "trust-dns-resolver", ] -[[package]] -name = "libp2p-floodsub" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "089336308101c0f5507e2aae8a693b0997bd3b31d88564530de1596d31a9b87a" -dependencies = [ - "asynchronous-codec", - "cuckoofilter", - "fnv", - "futures", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "log", - "quick-protobuf", - "quick-protobuf-codec", - "rand 0.8.5", - "smallvec", - "thiserror", -] - [[package]] name = "libp2p-gossipsub" -version = "0.44.3" +version = "0.44.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac213adad69bd9866fe87c37fbf241626715e5cd454fb6df9841aa2b02440ee" +checksum = "70b34b6da8165c0bde35c82db8efda39b824776537e73973549e76cadb3a77c5" dependencies = [ "asynchronous-codec", "base64 0.21.0", "byteorder", "bytes", + "either", "fnv", "futures", "hex_fmt", @@ -3120,14 +3473,15 @@ dependencies = [ "smallvec", "thiserror", "unsigned-varint", + "void", "wasm-timer", ] [[package]] name = "libp2p-identify" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40d1da1f75baf824cfdc80f6aced51f7cbf8dc14e32363e9443570a80d4ee337" +checksum = "5455f472243e63b9c497ff320ded0314254a9eb751799a39c283c6f20b793f3c" dependencies = [ "asynchronous-codec", "either", @@ -3147,27 +3501,27 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8ea433ae0cea7e3315354305237b9897afe45278b2118a7a57ca744e70fd27" +checksum = "9e2d584751cecb2aabaa56106be6be91338a60a0f4e420cf2af639204f596fc1" dependencies = [ "bs58", "ed25519-dalek", "log", "multiaddr", "multihash 0.17.0", - "prost", "quick-protobuf", "rand 0.8.5", + "sha2 0.10.6", "thiserror", "zeroize", ] [[package]] name = "libp2p-kad" -version = "0.43.2" +version = "0.43.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9647c76e63c4d0e9a10369cef9b929a2e5e8f03008b2097ab365fc4cb4e5318f" +checksum = "39d5ef876a2b2323d63c258e63c2f8e36f205fe5a11f0b3095d59635650790ff" dependencies = [ "arrayvec", "asynchronous-codec", @@ -3226,11 +3580,29 @@ dependencies = [ "prometheus-client", ] +[[package]] +name = "libp2p-mplex" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d34780b514b159e6f3fd70ba3e72664ec89da28dca2d1e7856ee55e2c7031ba" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures", + "libp2p-core", + "log", + "nohash-hasher", + "parking_lot 0.12.1", + "rand 0.8.5", + "smallvec", + "unsigned-varint", +] + [[package]] name = "libp2p-noise" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c87c2803deffeae94108072a0387f8c9ff494af68a4908454c11c811e27b5e5" +checksum = "9c3673da89d29936bc6435bafc638e2f184180d554ce844db65915113f86ec5e" dependencies = [ "bytes", "curve25519-dalek 3.2.0", @@ -3273,27 +3645,25 @@ dependencies = [ [[package]] name = "libp2p-request-response" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872b9d63fed44d9f81110c30be6c7ca5593a093576d2a95c5d018051e294d2e9" +checksum = "7ffdb374267d42dc5ed5bc53f6e601d4a64ac5964779c6e40bb9e4f14c1e30d5" dependencies = [ "async-trait", - "bytes", "futures", "instant", "libp2p-core", + "libp2p-identity", "libp2p-swarm", - "log", "rand 0.8.5", "smallvec", - "unsigned-varint", ] [[package]] name = "libp2p-swarm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd1e223f02fcd7e3790f9b954e2e81791810e3512daeb27fa97df7652e946bc2" +checksum = "903b3d592d7694e56204d211f29d31bc004be99386644ba8731fc3e3ef27b296" dependencies = [ "either", "fnv", @@ -3408,23 +3778,22 @@ dependencies = [ [[package]] name = "libp2p-yamux" -version = "0.43.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d048cd82f72c8800655aa1ee9b808ea8c9d523a649d3579b3ef0cfe23952d7fd" +checksum = "4dcd21d950662700a385d4c6d68e2f5f54d778e97068cdd718522222ef513bda" dependencies = [ "futures", "libp2p-core", "log", - "parking_lot 0.12.1", "thiserror", "yamux", ] [[package]] name = "libsqlite3-sys" -version = "0.25.2" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ "pkg-config", "vcpkg", @@ -3432,9 +3801,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" dependencies = [ "cc", "pkg-config", @@ -3455,9 +3824,9 @@ checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "linux-raw-sys" -version = "0.3.1" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" [[package]] name = "lock_api" @@ -3481,9 +3850,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e7d46de488603ffdd5f30afbc64fbba2378214a2c3a2fb83abf3d33126df17" +checksum = "03f1160296536f10c833a82dca22267d5486734230d47bf00bf435885814ba1e" dependencies = [ "hashbrown 0.13.2", ] @@ -3512,12 +3881,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" + [[package]] name = "maybe-owned" version = "0.3.4" @@ -3551,7 +3935,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc89ccdc6e10d6907450f753537ebc5c5d3460d2e4e62ea74bd571db62c0f9e" dependencies = [ - "rustix 0.37.11", + "rustix 0.37.19", ] [[package]] @@ -3695,24 +4079,18 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835d6ff01d610179fbce3de1694d007e500bf33a7f29689838941d6bf783ae40" dependencies = [ - "blake2b_simd", - "blake2s_simd", - "blake3", "core2", "digest 0.10.6", "multihash-derive", - "serde", - "serde-big-array", "sha2 0.10.6", - "sha3", "unsigned-varint", ] [[package]] name = "multihash" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e5d911412e631e1de11eb313e4dd71f73fd964401102aab23d6c8327c431ba" +checksum = "cfd8a792c1694c6da4f68db0a9d707c72bd260994da179e6030a5dcee00bb815" dependencies = [ "blake2b_simd", "blake2s_simd", @@ -3764,6 +4142,24 @@ dependencies = [ "getrandom 0.2.9", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "netlink-packet-core" version = "0.4.2" @@ -3907,7 +4303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", - "libm 0.2.6", + "libm 0.2.7", ] [[package]] @@ -3968,6 +4364,60 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" +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.16", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + [[package]] name = "os_str_bytes" version = "6.5.0" @@ -4072,6 +4522,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pem" version = "1.1.1" @@ -4096,24 +4552,68 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "pest" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "pest_meta" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.6", +] + [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", ] [[package]] @@ -4146,9 +4646,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "platforms" @@ -4199,9 +4699,9 @@ dependencies = [ [[package]] name = "polling" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be1c66a6add46bff50935c313dae30a5030cf8385c5206e8a95e9e9def974aa" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", "bitflags", @@ -4299,9 +4799,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" dependencies = [ "unicode-ident", ] @@ -4344,7 +4844,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax", + "regex-syntax 0.6.29", "rusty-fork", "tempfile", "unarray", @@ -4373,6 +4873,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prost-types" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +dependencies = [ + "prost", +] + [[package]] name = "psm" version = "0.1.21" @@ -4487,13 +4996,30 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot 0.12.1", + "scheduled-thread-pool", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -4664,13 +5190,22 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.3" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.1", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -4679,6 +5214,12 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "regex-syntax" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" + [[package]] name = "rend" version = "0.4.0" @@ -4688,6 +5229,43 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.0", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite 0.2.9", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.10.1", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -4726,29 +5304,43 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.41" +version = "0.7.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21499ed91807f07ae081880aabb2ccc0235e9d88011867d984525e9a4c3cfa3e" +checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" dependencies = [ + "bitvec", "bytecheck", "hashbrown 0.12.3", "ptr_meta", "rend", "rkyv_derive", "seahash", + "tinyvec", + "uuid", ] [[package]] name = "rkyv_derive" -version = "0.7.41" +version = "0.7.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1c672430eb41556291981f45ca900a0239ad007242d1cb4b4167af842db666" +checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags", + "serde", +] + [[package]] name = "rtcp" version = "0.7.2" @@ -4789,6 +5381,16 @@ dependencies = [ "webrtc-util", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rust_decimal" version = "1.29.1" @@ -4809,9 +5411,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" @@ -4848,9 +5450,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.12" +version = "0.36.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0af200a3324fa5bcd922e84e9b55a298ea9f431a489f01961acdebc6e908f25" +checksum = "3a38f9520be93aba504e8ca974197f46158de5dcaa9fa04b57c57cd6a679d658" dependencies = [ "bitflags", "errno", @@ -4862,16 +5464,16 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.11" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags", "errno", "io-lifetimes", "itoa", "libc", - "linux-raw-sys 0.3.1", + "linux-raw-sys 0.3.7", "once_cell", "windows-sys 0.48.0", ] @@ -4945,6 +5547,24 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot 0.12.1", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -5003,6 +5623,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.9.0" @@ -5026,9 +5669,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" dependencies = [ "serde_derive", ] @@ -5053,13 +5696,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.16", ] [[package]] @@ -5085,6 +5728,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5097,6 +5749,34 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" +dependencies = [ + "base64 0.13.1", + "chrono", + "hex", + "indexmap", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" +dependencies = [ + "darling 0.20.1", + "proc-macro2", + "quote", + "syn 2.0.16", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -5162,9 +5842,9 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c2bb1a323307527314a36bfb73f24febb08ce2b8a554bf4ffd6f51ad15198c" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ "digest 0.10.6", "keccak", @@ -5179,6 +5859,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -5227,9 +5916,9 @@ dependencies = [ [[package]] name = "slice-group-by" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b634d87b960ab1a38c4fe143b508576f075e7c978bfad18217645ebfdfa2ec" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" [[package]] name = "smallvec" @@ -5237,6 +5926,28 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "snafu" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0656e7e3ffb70f6c39b3c2a86332bb74aa3c679da781642590f3c1118c5045" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "475b3bbe5245c26f2d8a6f62d67c1f30eb9fffeccee721c45d162c3ebbdf81b2" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "snow" version = "0.9.2" @@ -5453,15 +6164,21 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "synstructure" version = "0.12.6" @@ -5489,9 +6206,9 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75182f12f490e953596550b65ee31bda7c8e043d9386174b353bda50838c3fd" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags", "core-foundation", @@ -5510,25 +6227,31 @@ dependencies = [ [[package]] name = "system-interface" -version = "0.25.6" +version = "0.25.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e1ab6a74e204b606bf397944fa991f3b01046113cc0a4ac269be3ef067cc24b" +checksum = "928ebd55ab758962e230f51ca63735c5b283f26292297c81404289cda5d78631" dependencies = [ "bitflags", "cap-fs-ext", "cap-std", "fd-lock", "io-lifetimes", - "rustix 0.37.11", + "rustix 0.37.19", "windows-sys 0.48.0", "winx", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" -version = "0.12.6" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae9980cab1db3fceee2f6c6f643d5d8de2997c58ee8d25fb0cc8a9e9e7348e5" +checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" [[package]] name = "tempfile" @@ -5539,7 +6262,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.3.5", - "rustix 0.37.11", + "rustix 0.37.19", "windows-sys 0.45.0", ] @@ -5575,7 +6298,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.16", ] [[package]] @@ -5601,9 +6324,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ "itoa", "serde", @@ -5613,15 +6336,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] @@ -5653,9 +6376,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.27.0" +version = "1.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" dependencies = [ "autocfg", "bytes", @@ -5667,25 +6390,46 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.45.0", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite 0.2.9", + "tokio", ] [[package]] name = "tokio-macros" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.16", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite 0.2.9", @@ -5705,11 +6449,23 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "tokio-tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", @@ -5729,6 +6485,60 @@ dependencies = [ "serde", ] +[[package]] +name = "tonic" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +dependencies = [ + "async-trait", + "axum", + "base64 0.21.0", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project", + "pin-project-lite 0.2.9", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -5750,20 +6560,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", @@ -5780,16 +6590,33 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-logfmt" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2fb2783ed7727b30a78ebecb49d59c98102b1f384105aa27d632487875a67b" +dependencies = [ + "time", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "parking_lot 0.12.1", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -5846,6 +6673,36 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tryhard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a30c54ab5d67a3aee4be0ffb0cc035426a06ffc3ef1ba979b3c658168688691" +dependencies = [ + "futures", + "pin-project-lite 0.2.9", + "tokio", +] + +[[package]] +name = "tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1 0.10.5", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "turn" version = "0.6.1" @@ -5884,22 +6741,22 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "ucan" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd5d89f3eb6d7e91ebcb733ceb95d186fa3479d264b4bacd971a25b3d5d336a" +checksum = "f45b08aa0818ab9be5d922d3931ea893a6c2e84d1a3bd6433ff2ca3e04a23425" dependencies = [ "anyhow", "async-recursion", "async-std", "async-trait", - "base64 0.13.1", + "base64 0.21.0", "bs58", - "cid 0.9.0", + "cid 0.10.1", "futures", "getrandom 0.2.9", "instant", - "libipld-core 0.15.0", - "libipld-json 0.15.0", + "libipld-core", + "libipld-json", "log", "rand 0.8.5", "serde", @@ -5910,6 +6767,12 @@ dependencies = [ "url", ] +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + [[package]] name = "uint" version = "0.9.5" @@ -6004,8 +6867,6 @@ checksum = "d86a8dc7f45e4c1b0d30e43038c38f274e77af056aa5f74b93c2cf9eb3c1c836" dependencies = [ "asynchronous-codec", "bytes", - "futures-io", - "futures-util", ] [[package]] @@ -6025,6 +6886,18 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" + [[package]] name = "utf8parse" version = "0.2.1" @@ -6033,9 +6906,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.3.1" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" dependencies = [ "getrandom 0.2.9", ] @@ -6132,55 +7005,53 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi-cap-std-sync" -version = "0.0.0" -source = "git+https://github.com/bytecodealliance/preview2-prototyping#d3bd2bc36a9caed047002e62561fc439c391df47" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612510e6c7b6681f7d29ce70ef26e18349c26acd39b7d89f1727d90b7f58b20e" dependencies = [ "anyhow", "async-trait", "cap-fs-ext", - "cap-net-ext", "cap-rand", "cap-std", "cap-time-ext", - "fs-set-times", + "fs-set-times 0.18.1", "io-extras", "io-lifetimes", - "ipnet", "is-terminal", "once_cell", - "rustix 0.37.11", + "rustix 0.36.13", "system-interface", "tracing", "wasi-common", - "windows-sys 0.48.0", + "windows-sys 0.45.0", ] [[package]] name = "wasi-common" -version = "0.0.0" -source = "git+https://github.com/bytecodealliance/preview2-prototyping#d3bd2bc36a9caed047002e62561fc439c391df47" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008136464e438c5049a614b6ea1bae9f6c4d354ce9ee2b4d9a1ac6e73f31aafc" dependencies = [ "anyhow", - "async-trait", "bitflags", - "cap-fs-ext", - "cap-net-ext", "cap-rand", "cap-std", "io-extras", - "ipnet", - "rustix 0.37.11", - "system-interface", + "log", + "rustix 0.36.13", "thiserror", "tracing", - "windows-sys 0.48.0", + "wasmtime", + "wiggle", + "windows-sys 0.45.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -6188,24 +7059,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" dependencies = [ "cfg-if", "js-sys", @@ -6215,9 +7086,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6225,22 +7096,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "wasm-encoder" @@ -6251,6 +7122,24 @@ dependencies = [ "leb128", ] +[[package]] +name = "wasm-encoder" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05d0b6fcd0aeb98adf16e7975331b3c17222aa815148f5b976370ce589d80ef" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasm-encoder" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77053dc709db790691d3732cfc458adc5acc881dec524965c608effdcd9c581" +dependencies = [ + "leb128", +] + [[package]] name = "wasm-metadata" version = "0.3.1" @@ -6260,10 +7149,23 @@ dependencies = [ "anyhow", "indexmap", "serde", - "wasm-encoder", + "wasm-encoder 0.25.0", "wasmparser 0.102.0", ] +[[package]] +name = "wasm-metadata" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbdef99fafff010c57fabb7bc703f0903ec16fcee49207a22dcc4f78ea44562f" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "wasm-encoder 0.26.0", + "wasmparser 0.104.0", +] + [[package]] name = "wasm-timer" version = "0.2.5" @@ -6281,9 +7183,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.101.1" +version = "0.102.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf2f22ef84ac5666544afa52f326f13e16f3d019d2e61e704fd8091c9358b130" +checksum = "48134de3d7598219ab9eaf6b91b15d8e50d31da76b8519fe4ecfcec2cf35104b" dependencies = [ "indexmap", "url", @@ -6291,9 +7193,19 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.102.0" +version = "0.104.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48134de3d7598219ab9eaf6b91b15d8e50d31da76b8519fe4ecfcec2cf35104b" +checksum = "6a396af81a7c56ad976131d6a35e4b693b78a1ea0357843bd436b4577e254a7d" +dependencies = [ + "indexmap", + "url", +] + +[[package]] +name = "wasmparser" +version = "0.105.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83be9e0b3f9570dc1979a33ae7b89d032c73211564232b99976553e5c155ec32" dependencies = [ "indexmap", "url", @@ -6301,18 +7213,19 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.2.54" +version = "0.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc17ae63836d010a2bf001c26a5fedbb9a05e5f71117fb63e0ab878bfbe1ca3" +checksum = "50b0e5ed7a74a065637f0d7798ce5f29cadb064980d24b0c82af5200122fa0d8" dependencies = [ "anyhow", - "wasmparser 0.102.0", + "wasmparser 0.105.0", ] [[package]] name = "wasmtime" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f907fdead3153cb9bfb7a93bbd5b62629472dc06dee83605358c64c52ed3dda9" dependencies = [ "anyhow", "async-trait", @@ -6338,23 +7251,24 @@ dependencies = [ "wasmtime-fiber", "wasmtime-jit", "wasmtime-runtime", - "wasmtime-winch", "wat", "windows-sys 0.45.0", ] [[package]] name = "wasmtime-asm-macros" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b9daa7c14cd4fa3edbf69de994408d5f4b7b0959ac13fa69d465f6597f810d" dependencies = [ "cfg-if", ] [[package]] name = "wasmtime-cache" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86437fa68626fe896e5afc69234bb2b5894949083586535f200385adfd71213" dependencies = [ "anyhow", "base64 0.21.0", @@ -6362,7 +7276,7 @@ dependencies = [ "directories-next", "file-per-thread-logger", "log", - "rustix 0.36.12", + "rustix 0.36.13", "serde", "sha2 0.10.6", "toml", @@ -6372,8 +7286,9 @@ dependencies = [ [[package]] name = "wasmtime-component-macro" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267096ed7cc93b4ab15d3daa4f195e04dbb7e71c7e5c6457ae7d52e9dd9c3607" dependencies = [ "anyhow", "proc-macro2", @@ -6381,22 +7296,23 @@ dependencies = [ "syn 1.0.109", "wasmtime-component-util", "wasmtime-wit-bindgen", - "wit-parser", + "wit-parser 0.6.4", ] [[package]] name = "wasmtime-component-util" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74e02ca7a4a3c69d72b88f26f0192e333958df6892415ac9ab84dcc42c9000c2" [[package]] name = "wasmtime-cranelift" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1cefde0cce8cb700b1b21b6298a3837dba46521affd7b8c38a9ee2c869eee04" dependencies = [ "anyhow", "cranelift-codegen", - "cranelift-control", "cranelift-entity", "cranelift-frontend", "cranelift-native", @@ -6413,12 +7329,12 @@ dependencies = [ [[package]] name = "wasmtime-cranelift-shared" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd041e382ef5aea1b9fc78442394f1a4f6d676ce457e7076ca4cb3f397882f8b" dependencies = [ "anyhow", "cranelift-codegen", - "cranelift-control", "cranelift-native", "gimli", "object", @@ -6428,8 +7344,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a990198cee4197423045235bf89d3359e69bd2ea031005f4c2d901125955c949" dependencies = [ "anyhow", "cranelift-entity", @@ -6440,7 +7357,7 @@ dependencies = [ "serde", "target-lexicon", "thiserror", - "wasm-encoder", + "wasm-encoder 0.25.0", "wasmparser 0.102.0", "wasmprinter", "wasmtime-component-util", @@ -6449,20 +7366,22 @@ dependencies = [ [[package]] name = "wasmtime-fiber" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab182d5ab6273a133ab88db94d8ca86dc3e57e43d70baaa4d98f94ddbd7d10a" dependencies = [ "cc", "cfg-if", - "rustix 0.36.12", + "rustix 0.36.13", "wasmtime-asm-macros", "windows-sys 0.45.0", ] [[package]] name = "wasmtime-jit" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de48df552cfca1c9b750002d3e07b45772dd033b0b206d5c0968496abf31244" dependencies = [ "addr2line", "anyhow", @@ -6485,18 +7404,20 @@ dependencies = [ [[package]] name = "wasmtime-jit-debug" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0554b84c15a27d76281d06838aed94e13a77d7bf604bbbaf548aa20eb93846" dependencies = [ "object", "once_cell", - "rustix 0.36.12", + "rustix 0.36.13", ] [[package]] name = "wasmtime-jit-icache-coherence" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecae978b13f7f67efb23bd827373ace4578f2137ec110bbf6a4a7cde4121bbd" dependencies = [ "cfg-if", "libc", @@ -6505,8 +7426,9 @@ dependencies = [ [[package]] name = "wasmtime-runtime" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658cf6f325232b6760e202e5255d823da5e348fdea827eff0a2a22319000b441" dependencies = [ "anyhow", "cc", @@ -6520,7 +7442,7 @@ dependencies = [ "memoffset 0.8.0", "paste", "rand 0.8.5", - "rustix 0.36.12", + "rustix 0.36.13", "wasmtime-asm-macros", "wasmtime-environ", "wasmtime-fiber", @@ -6530,8 +7452,9 @@ dependencies = [ [[package]] name = "wasmtime-types" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f6fffd2a1011887d57f07654dd112791e872e3ff4a2e626aee8059ee17f06f" dependencies = [ "cranelift-entity", "serde", @@ -6540,58 +7463,51 @@ dependencies = [ ] [[package]] -name = "wasmtime-winch" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +name = "wasmtime-wit-bindgen" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "983db9cc294d1adaa892a53ff6a0dc6605fc0ab1a4da5d8a2d2d4bde871ff7dd" dependencies = [ "anyhow", - "cranelift-codegen", - "gimli", - "object", - "target-lexicon", - "wasmparser 0.102.0", - "wasmtime-cranelift-shared", - "wasmtime-environ", - "winch-codegen", - "winch-environ", + "heck", + "wit-parser 0.6.4", ] [[package]] -name = "wasmtime-wit-bindgen" -version = "9.0.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" dependencies = [ - "anyhow", - "heck", - "wit-parser", + "leb128", ] [[package]] name = "wast" -version = "55.0.0" +version = "58.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4984d3e1406571f4930ba5cf79bd70f75f41d0e87e17506e0bd19b0e5d085f05" +checksum = "372eecae2d10a5091c2005b32377d7ecd6feecdf2c05838056d02d8b4f07c429" dependencies = [ "leb128", "memchr", "unicode-width", - "wasm-encoder", + "wasm-encoder 0.27.0", ] [[package]] name = "wat" -version = "1.0.61" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af2b53f4da14db05d32e70e9c617abdf6620c575bd5dd972b7400037b4df2091" +checksum = "6d47446190e112ab1579ab40b3ad7e319d859d74e5134683f04e9f0747bf4173" dependencies = [ - "wast", + "wast 58.0.0", ] [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" dependencies = [ "js-sys", "wasm-bindgen", @@ -6762,18 +7678,15 @@ dependencies = [ [[package]] name = "webrtc-media" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a3c157a040324e5049bcbd644ffc9079e6738fa2cfab2bcff64e5cc4c00d7" +checksum = "f72e1650a8ae006017d1a5280efb49e2610c19ccc3c0905b03b648aee9554991" dependencies = [ "byteorder", "bytes", - "derive_builder", - "displaydoc", "rand 0.8.5", "rtp", "thiserror", - "webrtc-util", ] [[package]] @@ -6850,6 +7763,48 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" +[[package]] +name = "wiggle" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b16a7462893c46c6d3dd2a1f99925953bdbb921080606e1a4c9344864492fa4" +dependencies = [ + "anyhow", + "async-trait", + "bitflags", + "thiserror", + "tracing", + "wasmtime", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489499e186ab24c8ac6d89e9934c54ced6f19bd473730e6a74f533bd67ecd905" +dependencies = [ + "anyhow", + "heck", + "proc-macro2", + "quote", + "shellexpand", + "syn 1.0.109", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9142e7fce24a4344c85a43c8b719ef434fc6155223bade553e186cb4183b6cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "wiggle-generate", +] + [[package]] name = "winapi" version = "0.3.9" @@ -6881,30 +7836,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "winch-codegen" -version = "0.7.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" -dependencies = [ - "anyhow", - "cranelift-codegen", - "gimli", - "regalloc2", - "smallvec", - "target-lexicon", - "wasmparser 0.102.0", -] - -[[package]] -name = "winch-environ" -version = "0.7.0" -source = "git+https://github.com/bytecodealliance/wasmtime#91e36f3449055c19195552c668474e4a1c5e4cf4" -dependencies = [ - "wasmparser 0.102.0", - "wasmtime-environ", - "winch-codegen", -] - [[package]] name = "windows" version = "0.34.0" @@ -6918,6 +7849,30 @@ dependencies = [ "windows_x86_64_msvc 0.34.0", ] +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -7126,8 +8081,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef177b73007d86c720931d0e2ea7e30eb8c9776e58361717743fc1e83cfacfe5" dependencies = [ "anyhow", - "wit-component", - "wit-parser", + "wit-component 0.7.4", + "wit-parser 0.6.4", ] [[package]] @@ -7137,10 +8092,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efdf5b00935b7b52d0e56cae1960f8ac13019a285f5aa762ff6bd7139a5c28a2" dependencies = [ "heck", - "wasm-metadata", + "wasm-metadata 0.3.1", "wit-bindgen-core", "wit-bindgen-rust-lib", - "wit-component", + "wit-component 0.7.4", ] [[package]] @@ -7164,7 +8119,7 @@ dependencies = [ "syn 1.0.109", "wit-bindgen-core", "wit-bindgen-rust", - "wit-component", + "wit-component 0.7.4", ] [[package]] @@ -7178,10 +8133,27 @@ dependencies = [ "indexmap", "log", "url", - "wasm-encoder", - "wasm-metadata", + "wasm-encoder 0.25.0", + "wasm-metadata 0.3.1", "wasmparser 0.102.0", - "wit-parser", + "wit-parser 0.6.4", +] + +[[package]] +name = "wit-component" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e291ff83cb9c8e59963cc6922bdda77ed8f5517d6835f0c98070c4e7f1ae4996" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "url", + "wasm-encoder 0.26.0", + "wasm-metadata 0.5.0", + "wasmparser 0.104.0", + "wit-parser 0.7.1", ] [[package]] @@ -7199,6 +8171,42 @@ dependencies = [ "url", ] +[[package]] +name = "wit-parser" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca2581061573ef6d1754983d7a9b3ed5871ef859d52708ea9a0f5af32919172" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "pulldown-cmark", + "unicode-xid", + "url", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror", + "wast 35.0.2", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x25519-dalek" version = "1.1.1" @@ -7274,6 +8282,15 @@ dependencies = [ "winreg 0.8.0", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yamux" version = "0.10.2" @@ -7314,7 +8331,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.16", ] [[package]] @@ -7349,9 +8366,9 @@ dependencies = [ [[package]] name = "zune-inflate" -version = "0.2.53" +version = "0.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440a08fd59c6442e4b846ea9b10386c38307eae728b216e1ab2c305d1c9daaf8" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" dependencies = [ "simd-adler32", ] diff --git a/Cargo.toml b/Cargo.toml index 71ff2382..c08b520d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "homestar-core", "homestar-guest-wasm", @@ -7,27 +8,38 @@ members = [ ] [workspace.package] -authors = ["Brooklyn Zelenka ", "Zeeshan Lakhani "] +authors = [ + "Zeeshan Lakhani ", + "Brooklyn Zelenka " +] edition = "2021" license = "Apache" rust-version = "1.66.0" +[workspace.dependencies] +anyhow = { version = "1.0", features = ["backtrace"] } +tracing = "0.1" + # Speedup build on macOS # See https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#splitting-debug-information [profile.dev] +debug-assertions = true split-debuginfo = "unpacked" [profile.release.package.homestar-core] # Tell `rustc` to optimize for small code size. opt-level = "s" +debug-assertions = false [profile.release.package.homestar-runtime] # Tell `rustc` to optimize for small code size. -opt-level = "s" +opt-level = "z" +debug-assertions = false [profile.release.package.homestar-wasm] # Tell `rustc` to optimize for small code size. opt-level = "s" +debug-assertions = false [profile.release.package.homestar-guest-wasm] # Perform optimizations on all codegen units. @@ -40,3 +52,4 @@ strip = "symbols" # Amount of debug information. # 0/false: no debug info at all; 1: line tables only; 2/true: full debug info debug = false +debug-assertions = false diff --git a/codecov.yml b/codecov.yml index 7f7e931b..e2fc69c4 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,5 +1,5 @@ ignore: - - "homestar/tests" + - "*/tests/*" - "homestar/src/test_utils" - "homestar-guest-wasm" - "homestar-wasm/src/test_utils" diff --git a/diesel.toml b/diesel.toml index 00b317bf..72072950 100644 --- a/diesel.toml +++ b/diesel.toml @@ -2,7 +2,7 @@ # see https://diesel.rs/guides/configuring-diesel-cli [print_schema] -file = "homestar/src/schema.rs" +file = "homestar-runtime/src/db/schema.rs" [migrations_directory] -dir = "migrations" +dir = "homestar-runtime/migrations" diff --git a/flake.nix b/flake.nix index a0de0d60..a3cf75f4 100644 --- a/flake.nix +++ b/flake.nix @@ -80,16 +80,16 @@ packages.irust = pkgs.rustPlatform.buildRustPackage rec { pname = "irust"; - version = "1.66.0"; + version = "1.70.0"; src = pkgs.fetchFromGitHub { owner = "sigmaSd"; repo = "IRust"; rev = "v${version}"; - sha256 = "sha256-tqNTh8ojTT80kl9jDCMq289jSfPz82gQ37UFAmnfsOw="; + sha256 = "sha256-chZKesbmvGHXwhnJRZbXyX7B8OwJL9dJh0O1Axz/n2E="; }; doCheck = false; - cargoSha256 = "sha256-LXBQkiNU4vP6PIW+5iaEzonNGrpwnTMeVaosqLJoGCg="; + cargoSha256 = "sha256-FmsD3ajMqpPrTkXCX2anC+cmm0a2xuP+3FHqzj56Ma4="; }; } ); diff --git a/homestar-core/Cargo.toml b/homestar-core/Cargo.toml index aabf985a..f0d6e9d0 100644 --- a/homestar-core/Cargo.toml +++ b/homestar-core/Cargo.toml @@ -1,16 +1,15 @@ [package] name = "homestar-core" version = "0.1.0" -description = "" -keywords = [] -categories = [] - +description = "Homestar core library for working with tasks, instructions, etc" +keywords = ["ipld", "tasks", "receipts", "ipvm"] +categories = ["workflow-engines", "core", "libraries"] include = ["/src", "README.md", "LICENSE"] license = { workspace = true } readme = "README.md" edition = { workspace = true } rust-version = { workspace = true } -documentation = "https://docs.rs/homestar" +documentation = "https://docs.rs/homestar-core" repository = "https://github.com/ipvm-wg/homestar" authors = { workspace = true } @@ -21,18 +20,20 @@ doctest = true crate-type = ["cdylib", "rlib"] [dependencies] -anyhow = "1.0" +# return to version.workspace = true after the following issue is fixed: +# https://github.com/DevinR528/cargo-sort/issues/47 +anyhow = { workspace = true } diesel = { version = "2.0", features = ["sqlite"] } enum-as-inner = "0.5" enum-assoc = "0.4" generic-array = "0.14" +indexmap = "1.9" libipld = "0.16" proptest = { version = "1.1", optional = true } -semver = "1.0" serde = { version = "1.0", features = ["derive"] } signature = "2.0" thiserror = "1.0" -tracing = "0.1" +tracing = { workspace = true } ucan = "0.1" url = "2.3" xid = "1.0" @@ -43,3 +44,8 @@ criterion = "0.4" [features] default = [] test_utils = ["proptest"] + +[package.metadata.docs.rs] +all-features = true +# defines the configuration attribute `docsrs` +rustdoc-args = ["--cfg", "docsrs"] diff --git a/homestar-core/src/consts.rs b/homestar-core/src/consts.rs index 867da354..df94e5b3 100644 --- a/homestar-core/src/consts.rs +++ b/homestar-core/src/consts.rs @@ -1,4 +1,4 @@ //! Exported global constants. /// SemVer-formatted version of the UCAN Invocation Specification. -pub const VERSION: &str = "0.2.0"; +pub const INVOCATION_VERSION: &str = "0.2.0"; diff --git a/homestar-core/src/lib.rs b/homestar-core/src/lib.rs index 113af6fd..4596d257 100644 --- a/homestar-core/src/lib.rs +++ b/homestar-core/src/lib.rs @@ -2,13 +2,22 @@ #![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] #![deny(unreachable_pub, private_in_public)] -//! Homestar is a determistic Wasm runtime and effectful job system intended to -//! embed inside IPFS. -//! You can find a more complete description [here]. +//! `homestar-core` is the underlying foundation for all homestar +//! packages and implements much of the [Ucan invocation] and [IPVM] +//! specifications, among other useful library features. //! -//! [here]: https://github.com/ipvm-wg/spec. +//! +//! Related crates/packages: +//! +//! - [homestar-runtime] +//! - [homestar-wasm] +//! +//! [homestar-runtime]: +//! [homestar-wasm]: +//! [IPVM]: +//! [Ucan invocation]: -mod consts; +pub mod consts; #[cfg(any(test, feature = "test_utils"))] #[cfg_attr(docsrs, doc(cfg(feature = "test_utils")))] pub mod test_utils; diff --git a/homestar-core/src/test_utils/mod.rs b/homestar-core/src/test_utils/mod.rs index 3635b6eb..991f3bab 100644 --- a/homestar-core/src/test_utils/mod.rs +++ b/homestar-core/src/test_utils/mod.rs @@ -3,5 +3,8 @@ /// Random value generator for sampling data. #[cfg(feature = "test_utils")] mod rvg; +#[cfg(feature = "test_utils")] +pub mod workflow; + #[cfg(feature = "test_utils")] pub use rvg::*; diff --git a/homestar-core/src/test_utils/workflow.rs b/homestar-core/src/test_utils/workflow.rs new file mode 100644 index 00000000..be5a2ae3 --- /dev/null +++ b/homestar-core/src/test_utils/workflow.rs @@ -0,0 +1,159 @@ +//! Test utilities for working with [workflow] items. +//! +//! [workflow]: crate::workflow + +use crate::workflow::{ + pointer::{Await, AwaitResult}, + prf::UcanPrf, + Ability, Input, Instruction, InstructionResult, Nonce, Pointer, Receipt, +}; +use libipld::{ + cid::Cid, + multihash::{Code, MultihashDigest}, + Ipld, Link, +}; +use std::collections::BTreeMap; +use url::Url; + +const RAW: u64 = 0x55; + +type NonceBytes = Vec; + +/// Return a `mocked` `wasm/run` [Instruction]. +pub fn wasm_instruction<'a, T>() -> Instruction<'a, T> { + let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); + let resource = Url::parse(format!("ipfs://{wasm}").as_str()).unwrap(); + + Instruction::new( + resource, + Ability::from("wasm/run"), + Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("add_one".to_string())), + ("args".into(), Ipld::List(vec![Ipld::Integer(1)])), + ]))), + ) +} + +/// Return `mocked` `wasm/run` [Instruction]'s, where the second is dependent on +/// the first. +pub fn related_wasm_instructions<'a, T>( +) -> (Instruction<'a, T>, Instruction<'a, T>, Instruction<'a, T>) +where + Ipld: From, + T: Clone, +{ + let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); + let resource = Url::parse(format!("ipfs://{wasm}").as_str()).unwrap(); + + let instr = Instruction::new( + resource.clone(), + Ability::from("wasm/run"), + Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("add_one".to_string())), + ("args".into(), Ipld::List(vec![Ipld::Integer(1)])), + ]))), + ); + + let promise = Await::new( + Pointer::new(Cid::try_from(instr.clone()).unwrap()), + AwaitResult::Ok, + ); + + let dep_instr1 = Instruction::new( + resource.clone(), + Ability::from("wasm/run"), + Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("add_one".to_string())), + ( + "args".into(), + Ipld::List(vec![Ipld::try_from(promise.clone()).unwrap()]), + ), + ]))), + ); + + let another_promise = Await::new( + Pointer::new(Cid::try_from(dep_instr1.clone()).unwrap()), + AwaitResult::Ok, + ); + + let dep_instr2 = Instruction::new( + resource, + Ability::from("wasm/run"), + Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("add_three".to_string())), + ( + "args".into(), + Ipld::List(vec![ + Ipld::try_from(another_promise).unwrap(), + Ipld::try_from(promise).unwrap(), + Ipld::Integer(42), + ]), + ), + ]))), + ); + + (instr, dep_instr1, dep_instr2) +} + +/// Return a `mocked` `wasm/run` [Instruction], along with it's [Nonce] as bytes. +pub fn wasm_instruction_with_nonce<'a, T>() -> (Instruction<'a, T>, NonceBytes) { + let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); + let resource = Url::parse(format!("ipfs://{wasm}").as_str()).unwrap(); + let nonce = Nonce::generate(); + + ( + Instruction::new_with_nonce( + resource, + Ability::from("wasm/run"), + Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("add_one".to_string())), + ("args".into(), Ipld::List(vec![Ipld::Integer(1)])), + ]))), + nonce.clone(), + ), + nonce.as_nonce96().unwrap().to_vec(), + ) +} + +/// Return a `mocked` [Instruction]. +pub fn instruction<'a, T>() -> Instruction<'a, T> { + let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); + let resource = Url::parse(format!("ipfs://{wasm}").as_str()).unwrap(); + + Instruction::new( + resource, + Ability::from("ipld/fun"), + Input::Ipld(Ipld::List(vec![Ipld::Bool(true)])), + ) +} + +/// Return a `mocked` [Instruction], along with it's [Nonce] as bytes. +pub fn instruction_with_nonce<'a, T>() -> (Instruction<'a, T>, NonceBytes) { + let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); + let resource = Url::parse(format!("ipfs://{wasm}").as_str()).unwrap(); + let nonce = Nonce::generate(); + + ( + Instruction::new_with_nonce( + resource, + Ability::from("ipld/fun"), + Input::Ipld(Ipld::List(vec![Ipld::Bool(true)])), + nonce.clone(), + ), + nonce.as_nonce96().unwrap().to_vec(), + ) +} + +/// Return a `mocked` [Receipt] with an [Ipld] [InstructionResult]. +pub fn receipt() -> Receipt { + let h = Code::Blake3_256.digest(b"beep boop"); + let cid = Cid::new_v1(RAW, h); + let link: Link = Link::new(cid); + Receipt::new( + Pointer::new_from_link(link), + InstructionResult::Ok(Ipld::Bool(true)), + Ipld::Null, + None, + UcanPrf::default(), + ) +} diff --git a/homestar-core/src/workflow.rs b/homestar-core/src/workflow.rs new file mode 100644 index 00000000..68a34ca1 --- /dev/null +++ b/homestar-core/src/workflow.rs @@ -0,0 +1,34 @@ +//! Workflow and [Ucan invocation] componets for building Homestar pipelines. +//! +//! [Ucan invocation]: + +mod ability; +pub mod config; +pub mod input; +pub mod instruction; +mod instruction_result; +mod invocation; +mod issuer; +mod nonce; +pub mod pointer; +pub mod prf; +pub mod receipt; +pub mod task; + +pub use ability::*; +pub use input::Input; +pub use instruction::Instruction; +pub use instruction_result::*; +pub use invocation::*; +pub use issuer::Issuer; +pub use nonce::*; +pub use pointer::Pointer; +pub use receipt::Receipt; +pub use task::Task; + +/// Generic link, cid => T [IndexMap] for storing +/// invoked, raw values in-memory and using them to +/// resolve other steps within a runtime's workflow. +/// +/// [IndexMap]: indexmap::IndexMap +pub type LinkMap = indexmap::IndexMap; diff --git a/homestar-core/src/workflow/ability.rs b/homestar-core/src/workflow/ability.rs index 2385de76..421a6b41 100644 --- a/homestar-core/src/workflow/ability.rs +++ b/homestar-core/src/workflow/ability.rs @@ -1,7 +1,7 @@ //! [UCAN Ability] for a given [Resource]. //! //! [Resource]: url::Url -//! [UCAN Ability]: https://github.com/ucan-wg/spec/#23-ability +//! [UCAN Ability]: use libipld::{serde::from_ipld, Ipld}; use serde::{Deserialize, Serialize}; @@ -34,7 +34,7 @@ use std::{borrow::Cow, fmt}; /// assert_eq!(ability.to_string(), "example/test".to_string()); /// ``` /// -/// [UCAN Ability]: https://github.com/ucan-wg/spec/#23-ability +/// [UCAN Ability]: #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct Ability(String); diff --git a/homestar-core/src/workflow/input.rs b/homestar-core/src/workflow/input.rs index 12ebb4dd..174ce4b9 100644 --- a/homestar-core/src/workflow/input.rs +++ b/homestar-core/src/workflow/input.rs @@ -1,25 +1,17 @@ -//! Input paramters for [Task] execution and means to +//! Input paramters for [Instruction] execution and means to //! generally [parse] and [resolve] them. //! -//! [Task]: super::Task +//! [Instruction]: super::Instruction //! [parse]: Parse::parse //! [resolve]: Args::resolve use super::{ - pointer::{Await, AwaitResult, InvokedTaskPointer, ERR_BRANCH, OK_BRANCH, PTR_BRANCH}, - InvocationResult, + pointer::{Await, AwaitResult, ERR_BRANCH, OK_BRANCH, PTR_BRANCH}, + InstructionResult, Pointer, }; use anyhow::anyhow; use libipld::{serde::from_ipld, Cid, Ipld}; -use std::{ - collections::{btree_map::BTreeMap, HashMap}, - result::Result, -}; - -/// Generic link, cid => T [HashMap] for storing -/// invoked, raw values in-memory and using them to -/// resolve other steps within a runtime's workflow. -pub type LinkMap = HashMap; +use std::{collections::btree_map::BTreeMap, result::Result}; /// Parsed [Args] consisting of [Inputs] for execution flows, as well as an /// optional function name/definition. @@ -47,6 +39,21 @@ impl Parsed { fun: Some(fun), } } + + /// Parsed arguments. + pub fn args(&self) -> &Args { + &self.args + } + + /// Turn [Parsed] structure into owned [Args]. + pub fn into_args(self) -> Args { + self.args + } + + /// Parsed function named. + pub fn fun(&self) -> Option { + self.fun.as_ref().map(|f| f.to_string()) + } } impl From> for Args { @@ -55,15 +62,15 @@ impl From> for Args { } } -/// Interface for [Task] implementations, relying on `core` -/// to implement for custom parsing specifics. +/// Interface for [Instruction] implementations, relying on `homestore-core` +/// to implement custom parsing specifics. /// /// # Example /// /// ``` /// use homestar_core::{ /// workflow::{ -/// input::{Args, Parse}, Ability, Input, Task, +/// input::{Args, Parse}, Ability, Input, Instruction, /// }, /// Unit, /// }; @@ -73,19 +80,19 @@ impl From> for Args { /// let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); /// let resource = Url::parse(format!("ipfs://{wasm}").as_str()).unwrap(); /// -/// let task = Task::unique( +/// let inst = Instruction::unique( /// resource, /// Ability::from("wasm/run"), /// Input::::Ipld(Ipld::List(vec![Ipld::Bool(true)])) /// ); /// -/// let parsed = task.input().parse().unwrap(); +/// let parsed = inst.input().parse().unwrap(); /// /// // turn into Args for invocation: /// let args: Args = parsed.try_into().unwrap(); /// ``` /// -/// [Task]: super::Task +/// [Instruction]: super::Instruction pub trait Parse { /// Function returning [Parsed] structure for execution/invocation. /// @@ -98,10 +105,13 @@ pub trait Parse { #[derive(Clone, Debug, PartialEq)] pub struct Args(Vec>); -impl Args { +impl Args +where + T: std::fmt::Debug, +{ /// Create an [Args] [Vec]-type. pub fn new(args: Vec>) -> Self { - Args(args) + Self(args) } /// Return wrapped [Vec] of [inputs]. @@ -118,6 +128,18 @@ impl Args { &self.0 } + /// Return *only* deferred/awaited inputs. + pub fn deferreds(&self) -> Vec { + self.0.iter().fold(vec![], |mut acc, input| { + if let Input::Deferred(awaited_promise) = input { + acc.push(awaited_promise.instruction_cid()); + acc + } else { + acc + } + }) + } + /// Resolve [awaited promises] of [inputs] into task-specific [Input::Arg]'s, /// given a successful lookup function; otherwise, return [Input::Deferred] /// for unresolved promises, or just return [Input::Ipld], @@ -128,7 +150,7 @@ impl Args { /// [resolving Ipld links]: resolve_links pub fn resolve(self, lookup_fn: F) -> anyhow::Result where - F: Fn(Cid) -> anyhow::Result> + Clone, + F: Fn(Cid) -> anyhow::Result> + Clone, Ipld: From, T: Clone, { @@ -149,7 +171,7 @@ where impl TryFrom for Args where - InvocationResult: TryFrom, + InstructionResult: TryFrom, { type Error = anyhow::Error; @@ -158,7 +180,7 @@ where let args = vec .into_iter() .fold(Vec::>::new(), |mut acc, ipld| { - if let Ok(invocation_result) = InvocationResult::try_from(ipld.to_owned()) { + if let Ok(invocation_result) = InstructionResult::try_from(ipld.to_owned()) { acc.push(Input::Arg(invocation_result)); } else if let Ok(await_result) = Await::try_from(ipld.to_owned()) { acc.push(Input::Deferred(await_result)); @@ -185,15 +207,15 @@ where pub enum Input { /// [Ipld] Literals. Ipld(Ipld), - /// Promise-[links] awaiting the output of another [Task]'s invocation, - /// directly. + /// Promise-[links] awaiting the output of another [Instruction]'s + /// invocation, directly. /// - /// [links]: InvokedTaskPointer - /// [Task]: super::Task + /// [links]: Pointer + /// [Instruction]: super::Instruction Deferred(Await), - /// General argument, wrapping an [InvocationResult] over a task-specific + /// General argument, wrapping an [InstructionResult] over a task-specific /// implementation's own input type(s). - Arg(InvocationResult), + Arg(InstructionResult), } impl Input { @@ -208,13 +230,13 @@ impl Input { /// [resolving Ipld links]: resolve_links pub fn resolve(self, lookup_fn: F) -> Input where - F: Fn(Cid) -> anyhow::Result> + Clone, + F: Fn(Cid) -> anyhow::Result> + Clone, Ipld: From, { match self { Input::Ipld(ipld) => { if let Ok(await_promise) = Await::try_from(&ipld) { - if let Ok(func_ret) = lookup_fn(await_promise.task_cid()) { + if let Ok(func_ret) = lookup_fn(await_promise.instruction_cid()) { Input::Arg(func_ret) } else { Input::Deferred(await_promise) @@ -225,7 +247,7 @@ impl Input { } Input::Arg(ref _arg) => self, Input::Deferred(await_promise) => { - if let Ok(func_ret) = lookup_fn(await_promise.task_cid()) { + if let Ok(func_ret) = lookup_fn(await_promise.instruction_cid()) { Input::Arg(func_ret) } else { Input::Deferred(await_promise) @@ -273,15 +295,15 @@ where .or_else(|| map.get_key_value(ERR_BRANCH)) .or_else(|| map.get_key_value(PTR_BRANCH)) .map_or( - if let Ok(invocation_result) = InvocationResult::try_from(ipld.to_owned()) { + if let Ok(invocation_result) = InstructionResult::try_from(ipld.to_owned()) { Ok(Input::Arg(invocation_result)) } else { Ok(Input::Ipld(ipld)) }, |(branch, ipld)| { - let invoked_task = InvokedTaskPointer::try_from(ipld)?; + let instruction = Pointer::try_from(ipld)?; Ok(Input::Deferred(Await::new( - invoked_task, + instruction, AwaitResult::result(branch) .ok_or_else(|| anyhow!("wrong branch name: {branch}"))?, ))) @@ -292,7 +314,7 @@ where fn resolve_args(args: Vec>, lookup_fn: F) -> Vec> where - F: Fn(Cid) -> anyhow::Result> + Clone, + F: Fn(Cid) -> anyhow::Result> + Clone, Ipld: From, { let args = args.into_iter().map(|v| v.resolve(lookup_fn.clone())); @@ -304,7 +326,7 @@ where /// [awaited promises]: Await pub fn resolve_links(ipld: Ipld, lookup_fn: F) -> Ipld where - F: Fn(Cid) -> anyhow::Result> + Clone, + F: Fn(Cid) -> anyhow::Result> + Clone, Ipld: From, { match ipld { @@ -357,24 +379,7 @@ where #[cfg(test)] mod test { use super::*; - use crate::{ - workflow::{Ability, Nonce, Task}, - Unit, - }; - use url::Url; - - fn task<'a, T>() -> Task<'a, T> { - let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); - let resource = Url::parse(format!("ipfs://{wasm}").as_str()).unwrap(); - let nonce = Nonce::generate(); - - Task::new( - resource, - Ability::from("wasm/run"), - Input::Ipld(Ipld::List(vec![Ipld::Integer(88)])), - Some(nonce), - ) - } + use crate::{test_utils, Unit}; #[test] fn input_ipld_ipld_rountrip() { @@ -387,8 +392,8 @@ mod test { #[test] fn input_deferred_ipld_rountrip() { - let task: Task<'_, Unit> = task(); - let ptr: InvokedTaskPointer = task.try_into().unwrap(); + let instruction = test_utils::workflow::instruction::(); + let ptr: Pointer = instruction.try_into().unwrap(); let input: Input = Input::Deferred(Await::new(ptr.clone(), AwaitResult::Ptr)); let ipld = Ipld::from(input.clone()); @@ -401,7 +406,7 @@ mod test { #[test] fn input_arg_ipld_rountrip() { - let input: Input = Input::Arg(InvocationResult::Just(Ipld::Bool(false))); + let input: Input = Input::Arg(InstructionResult::Just(Ipld::Bool(false))); let ipld = Ipld::from(input.clone()); assert_eq!( diff --git a/homestar-core/src/workflow/instruction.rs b/homestar-core/src/workflow/instruction.rs new file mode 100644 index 00000000..efe66403 --- /dev/null +++ b/homestar-core/src/workflow/instruction.rs @@ -0,0 +1,363 @@ +//! An [Instruction] is the smallest unit of work that can be requested from a +//! UCAN, described via `resource`, `ability`. + +use super::{Ability, Input, Nonce, Pointer}; +use anyhow::anyhow; +use libipld::{ + cbor::DagCborCodec, + cid::{ + multibase::Base, + multihash::{Code, MultihashDigest}, + Cid, + }, + prelude::Codec, + serde::from_ipld, + Ipld, +}; +use std::{borrow::Cow, collections::BTreeMap, fmt}; +use url::Url; + +const DAG_CBOR: u64 = 0x71; +const RESOURCE_KEY: &str = "rsc"; +const OP_KEY: &str = "op"; +const INPUT_KEY: &str = "input"; +const NNC_KEY: &str = "nnc"; + +/// Enumerator for `either` an expanded [Instruction] structure or +/// an [Pointer] ([Cid] wrapper). +#[derive(Debug, Clone, PartialEq)] +pub enum RunInstruction<'a, T> { + /// [Instruction] as an expanded structure. + Expanded(Instruction<'a, T>), + /// [Instruction] as a pointer. + Ptr(Pointer), +} + +impl<'a, T> From> for RunInstruction<'a, T> { + fn from(instruction: Instruction<'a, T>) -> Self { + RunInstruction::Expanded(instruction) + } +} + +impl<'a, T> TryFrom> for Instruction<'a, T> +where + T: fmt::Debug, +{ + type Error = anyhow::Error; + + fn try_from(run: RunInstruction<'a, T>) -> Result { + match run { + RunInstruction::Expanded(instruction) => Ok(instruction), + e => Err(anyhow!("wrong discriminant: {e:?}")), + } + } +} + +impl From for RunInstruction<'_, T> { + fn from(ptr: Pointer) -> Self { + RunInstruction::Ptr(ptr) + } +} + +impl<'a, T> TryFrom> for Pointer +where + T: fmt::Debug, +{ + type Error = anyhow::Error; + + fn try_from(run: RunInstruction<'a, T>) -> Result { + match run { + RunInstruction::Ptr(ptr) => Ok(ptr), + e => Err(anyhow!("wrong discriminant: {e:?}")), + } + } +} + +impl<'a, 'b, T> TryFrom<&'b RunInstruction<'a, T>> for &'b Pointer +where + T: fmt::Debug, +{ + type Error = anyhow::Error; + + fn try_from(run: &'b RunInstruction<'a, T>) -> Result { + match run { + RunInstruction::Ptr(ptr) => Ok(ptr), + e => Err(anyhow!("wrong discriminant: {e:?}")), + } + } +} + +impl<'a, 'b, T> TryFrom<&'b RunInstruction<'a, T>> for Pointer +where + T: fmt::Debug, +{ + type Error = anyhow::Error; + + fn try_from(run: &'b RunInstruction<'a, T>) -> Result { + match run { + RunInstruction::Ptr(ptr) => Ok(ptr.to_owned()), + e => Err(anyhow!("wrong discriminant: {e:?}")), + } + } +} + +impl From> for Ipld +where + Ipld: From, +{ + fn from(run: RunInstruction<'_, T>) -> Self { + match run { + RunInstruction::Expanded(instruction) => instruction.into(), + RunInstruction::Ptr(instruction_ptr) => instruction_ptr.into(), + } + } +} + +impl TryFrom for RunInstruction<'_, T> +where + T: From, +{ + type Error = anyhow::Error; + + fn try_from<'a>(ipld: Ipld) -> Result { + match ipld { + Ipld::Map(_) => Ok(RunInstruction::Expanded(Instruction::try_from(ipld)?)), + Ipld::Link(_) => Ok(RunInstruction::Ptr(Pointer::try_from(ipld)?)), + _ => Err(anyhow!("unexpected conversion type")), + } + } +} + +/// +/// +/// # Example +/// +/// ``` +/// use homestar_core::{Unit, workflow::{Ability, Input, Instruction}}; +/// use libipld::Ipld; +/// use url::Url; +/// +/// let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); +/// let resource = Url::parse(format!("ipfs://{wasm}").as_str()).unwrap(); +/// +/// let instr = Instruction::unique( +/// resource, +/// Ability::from("wasm/run"), +/// Input::::Ipld(Ipld::List(vec![Ipld::Bool(true)])) +/// ); +/// ``` +/// +/// We can also set-up an [Instruction] with a Deferred input to await on: +/// ``` +/// use homestar_core::{ +/// workflow::{ +/// pointer::{Await, AwaitResult}, +/// Ability, Input, Instruction, Nonce, Pointer, +/// }, +/// Unit, +/// }; +/// use libipld::{cid::{multihash::{Code, MultihashDigest}, Cid}, Ipld, Link}; +/// use url::Url; + +/// let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); +/// let resource = Url::parse(format!("ipfs://{wasm}").as_str()).expect("IPFS URL"); +/// let h = Code::Blake3_256.digest(b"beep boop"); +/// let cid = Cid::new_v1(0x55, h); +/// let link: Link = Link::new(cid); +/// let awaited_instr = Pointer::new_from_link(link); +/// +/// let instr = Instruction::new_with_nonce( +/// resource, +/// Ability::from("wasm/run"), +/// Input::::Deferred(Await::new(awaited_instr, AwaitResult::Ok)), +/// Nonce::generate() +/// ); +/// +/// // And covert it to a pointer: +/// let ptr = Pointer::try_from(instr).unwrap(); +/// ``` +/// [deferred promise]: super::pointer::Await +#[derive(Clone, Debug, PartialEq)] +pub struct Instruction<'a, T> { + rsc: Url, + op: Cow<'a, Ability>, + input: Input, + nnc: Nonce, +} + +impl Instruction<'_, T> { + /// Create a new [Instruction] with an empty Nonce. + pub fn new(rsc: Url, ability: Ability, input: Input) -> Self { + Self { + rsc, + op: Cow::from(ability), + input, + nnc: Nonce::Empty, + } + } + + /// Create a new [Instruction] with a given [Nonce]. + pub fn new_with_nonce(rsc: Url, ability: Ability, input: Input, nnc: Nonce) -> Self { + Self { + rsc, + op: Cow::from(ability), + input, + nnc, + } + } + + /// Create a unique [Instruction], with a default [Nonce] generator. + pub fn unique(rsc: Url, ability: Ability, input: Input) -> Self { + Self { + rsc, + op: Cow::from(ability), + input, + nnc: Nonce::generate(), + } + } + + /// Return [Instruction] resource, i.e. [Url]. + pub fn resource(&self) -> &Url { + &self.rsc + } + + /// Return [Ability] associated with `op`. + pub fn op(&self) -> &Ability { + &self.op + } + + /// Return [Instruction] [Input]. + pub fn input(&self) -> &Input { + &self.input + } + + /// Return [Nonce] reference. + pub fn nonce(&self) -> &Nonce { + &self.nnc + } +} + +impl TryFrom> for Pointer +where + Ipld: From, +{ + type Error = anyhow::Error; + + fn try_from(instruction: Instruction<'_, T>) -> Result { + Ok(Pointer::new(Cid::try_from(instruction)?)) + } +} + +impl TryFrom> for Cid +where + Ipld: From, +{ + type Error = anyhow::Error; + + fn try_from(instruction: Instruction<'_, T>) -> Result { + let ipld: Ipld = instruction.into(); + let bytes = DagCborCodec.encode(&ipld)?; + let hash = Code::Sha3_256.digest(&bytes); + Ok(Cid::new_v1(DAG_CBOR, hash)) + } +} + +impl From> for Ipld +where + Ipld: From, +{ + fn from(instruction: Instruction<'_, T>) -> Self { + Ipld::Map(BTreeMap::from([ + (RESOURCE_KEY.into(), instruction.rsc.to_string().into()), + (OP_KEY.into(), instruction.op.to_string().into()), + (INPUT_KEY.into(), instruction.input.into()), + (NNC_KEY.into(), instruction.nnc.into()), + ])) + } +} + +impl TryFrom<&Ipld> for Instruction<'_, T> +where + T: From, +{ + type Error = anyhow::Error; + + fn try_from(ipld: &Ipld) -> Result { + TryFrom::try_from(ipld.to_owned()) + } +} + +impl TryFrom for Instruction<'_, T> +where + T: From, +{ + type Error = anyhow::Error; + + fn try_from(ipld: Ipld) -> Result { + let map = from_ipld::>(ipld)?; + + let rsc = match map.get(RESOURCE_KEY) { + Some(Ipld::Link(cid)) => cid + .to_string_of_base(Base::Base32Lower) + .map_err(|e| anyhow!("failed to encode CID into multibase string: {e}")) + .and_then(|txt| { + Url::parse(format!("{}{}", "ipfs://", txt).as_str()) + .map_err(|e| anyhow!("failed to parse URL: {e}")) + }), + Some(Ipld::String(txt)) => { + Url::parse(txt.as_str()).map_err(|e| anyhow!("failed to parse URL: {e}")) + } + _ => Err(anyhow!("no resource/with set.")), + }?; + + Ok(Self { + rsc, + op: from_ipld( + map.get(OP_KEY) + .ok_or_else(|| anyhow!("no `op` field set"))? + .to_owned(), + )?, + input: Input::try_from( + map.get(INPUT_KEY) + .ok_or_else(|| anyhow!("no `input` field set"))? + .to_owned(), + )?, + nnc: Nonce::try_from( + map.get(NNC_KEY) + .unwrap_or(&Ipld::String("".to_string())) + .to_owned(), + )?, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{test_utils, Unit}; + + #[test] + fn ipld_roundtrip() { + let (instruction, bytes) = test_utils::workflow::instruction_with_nonce::(); + let ipld = Ipld::from(instruction.clone()); + + assert_eq!( + ipld, + Ipld::Map(BTreeMap::from([ + ( + RESOURCE_KEY.into(), + Ipld::String( + "ipfs://bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".into() + ) + ), + (OP_KEY.into(), Ipld::String("ipld/fun".to_string())), + (INPUT_KEY.into(), Ipld::List(vec![Ipld::Bool(true)])), + ( + NNC_KEY.into(), + Ipld::List(vec![Ipld::Integer(0), Ipld::Bytes(bytes)]) + ) + ])) + ); + assert_eq!(instruction, ipld.try_into().unwrap()) + } +} diff --git a/homestar-core/src/workflow/invocation_result.rs b/homestar-core/src/workflow/instruction_result.rs similarity index 63% rename from homestar-core/src/workflow/invocation_result.rs rename to homestar-core/src/workflow/instruction_result.rs index 25529f37..0823f72e 100644 --- a/homestar-core/src/workflow/invocation_result.rs +++ b/homestar-core/src/workflow/instruction_result.rs @@ -1,7 +1,7 @@ -//! The output `Result` of a [Task], as a `success` (`Ok`) / `failure` (`Error`) -//! state. +//! The output `Result` of an [Instruction], tagged as a `success` (`Ok`) or +//! `failure` (`Error`), or returned/inlined directly. //! -//! [Task]: super::Task +//! [Instruction]: super::Instruction use anyhow::anyhow; use diesel::{ @@ -19,30 +19,31 @@ const OK: &str = "ok"; const ERR: &str = "error"; const JUST: &str = "just"; -/// Resultant output of an executed [Task]. +/// Resultant output of an executed [Instruction]. /// -/// [Task]: super::Task +/// [Instruction]: super::Instruction #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, AsExpression, FromSqlRow)] #[diesel(sql_type = Binary)] -pub enum InvocationResult { +pub enum InstructionResult { /// `Ok` branch. Ok(T), /// `Error` branch. Error(T), /// `Just` branch, meaning `just the value`. Used for - /// not incorporating ok/error into arg/param. + /// not incorporating unwrapped ok/error into arg/param, where a + /// result may show up directly. Just(T), } -impl InvocationResult { +impl InstructionResult { /// Owned, inner result of a [Task] invocation. /// /// [Task]: super::Task pub fn into_inner(self) -> T { match self { - InvocationResult::Ok(inner) => inner, - InvocationResult::Error(inner) => inner, - InvocationResult::Just(inner) => inner, + InstructionResult::Ok(inner) => inner, + InstructionResult::Error(inner) => inner, + InstructionResult::Just(inner) => inner, } } @@ -51,27 +52,27 @@ impl InvocationResult { /// [Task]: super::Task pub fn inner(&self) -> &T { match self { - InvocationResult::Ok(inner) => inner, - InvocationResult::Error(inner) => inner, - InvocationResult::Just(inner) => inner, + InstructionResult::Ok(inner) => inner, + InstructionResult::Error(inner) => inner, + InstructionResult::Just(inner) => inner, } } } -impl From> for Ipld +impl From> for Ipld where Ipld: From, { - fn from(result: InvocationResult) -> Self { + fn from(result: InstructionResult) -> Self { match result { - InvocationResult::Ok(res) => Ipld::List(vec![OK.into(), res.into()]), - InvocationResult::Error(res) => Ipld::List(vec![ERR.into(), res.into()]), - InvocationResult::Just(res) => Ipld::List(vec![JUST.into(), res.into()]), + InstructionResult::Ok(res) => Ipld::List(vec![OK.into(), res.into()]), + InstructionResult::Error(res) => Ipld::List(vec![ERR.into(), res.into()]), + InstructionResult::Just(res) => Ipld::List(vec![JUST.into(), res.into()]), } } } -impl TryFrom for InvocationResult +impl TryFrom for InstructionResult where T: From, { @@ -81,13 +82,13 @@ where if let Ipld::List(v) = ipld { match &v[..] { [Ipld::String(result), res] if result == OK => { - Ok(InvocationResult::Ok(res.to_owned().try_into()?)) + Ok(InstructionResult::Ok(res.to_owned().try_into()?)) } [Ipld::String(result), res] if result == ERR => { - Ok(InvocationResult::Error(res.to_owned().try_into()?)) + Ok(InstructionResult::Error(res.to_owned().try_into()?)) } [Ipld::String(result), res] if result == JUST => { - Ok(InvocationResult::Just(res.to_owned().try_into()?)) + Ok(InstructionResult::Just(res.to_owned().try_into()?)) } _ => Err(anyhow!("unexpected conversion type")), } @@ -97,7 +98,7 @@ where } } -impl TryFrom<&Ipld> for InvocationResult +impl TryFrom<&Ipld> for InstructionResult where T: From, { @@ -109,7 +110,7 @@ where } /// Diesel, [Sqlite] [ToSql] implementation. -impl ToSql for InvocationResult +impl ToSql for InstructionResult where [u8]: ToSql, { @@ -121,12 +122,12 @@ where } /// Diesel, [Sqlite] [FromSql] implementation. -impl FromSql for InvocationResult { +impl FromSql for InstructionResult { fn from_sql(bytes: RawValue<'_, Sqlite>) -> deserialize::Result { let raw_bytes = <*const [u8] as FromSql>::from_sql(bytes)?; let raw_bytes: &[u8] = unsafe { &*raw_bytes }; let decoded: Ipld = DagCborCodec.decode(raw_bytes)?; - Ok(InvocationResult::try_from(decoded)?) + Ok(InstructionResult::try_from(decoded)?) } } @@ -136,9 +137,9 @@ mod test { #[test] fn ipld_roundtrip() { - let res1 = InvocationResult::Error(Ipld::String("bad stuff".to_string())); - let res2 = InvocationResult::Ok(Ipld::String("ok stuff".to_string())); - let res3 = InvocationResult::Just(Ipld::String("just the right stuff".to_string())); + let res1 = InstructionResult::Error(Ipld::String("bad stuff".to_string())); + let res2 = InstructionResult::Ok(Ipld::String("ok stuff".to_string())); + let res3 = InstructionResult::Just(Ipld::String("just the right stuff".to_string())); let ipld1 = Ipld::from(res1.clone()); let ipld2 = Ipld::from(res2.clone()); let ipld3 = Ipld::from(res3.clone()); diff --git a/homestar-core/src/workflow/invocation.rs b/homestar-core/src/workflow/invocation.rs index 2f599f0d..e3e75810 100644 --- a/homestar-core/src/workflow/invocation.rs +++ b/homestar-core/src/workflow/invocation.rs @@ -1,14 +1,8 @@ -//! [Invocation] container for running a [Task] or Task(s). +//! [Invocation] is a signed [Task]. //! //! [Task]: super::Task -use crate::{ - consts::VERSION, - workflow::{ - pointer::{InvocationPointer, InvokedTaskPointer}, - prf::UcanPrf, - task::RunTask, - }, -}; + +use super::{Pointer, Task}; use anyhow::anyhow; use libipld::{ cbor::DagCborCodec, @@ -20,87 +14,33 @@ use libipld::{ serde::from_ipld, Ipld, }; -use semver::Version; use std::collections::BTreeMap; -const VERSION_KEY: &str = "v"; -const RUN_KEY: &str = "run"; -const CAUSE_KEY: &str = "cause"; -const METADATA_KEY: &str = "meta"; -const PROOF_KEY: &str = "prf"; +const DAG_CBOR: u64 = 0x71; +const TASK_KEY: &str = "task"; -/// An Invocation is an instruction to the [Executor] to perform enclosed -/// [Task]. -/// -/// Invocations are not executable until they have been provided provable -/// authority (in form of UCANs in the [prf] field) and an Authorization. -/// -/// [Executor]: https://github.com/ucan-wg/invocation#212-executor -/// [Task]: super::Task -/// [prf]: super::prf +/// A signed [Task] wrapper/container. #[derive(Debug, Clone, PartialEq)] pub struct Invocation<'a, T> { - v: Version, - run: RunTask<'a, T>, - cause: Option, - meta: Ipld, - prf: UcanPrf, + task: Task<'a, T>, } -impl<'a, T> Invocation<'a, T> +impl<'a, T> From> for Invocation<'a, T> where Ipld: From, - T: Clone, { - /// Generate a new [Invocation] to run, with metadata, and `prf`. - pub fn new(run: RunTask<'a, T>, meta: Ipld, prf: UcanPrf) -> anyhow::Result { - let invok = Invocation { - v: Version::parse(VERSION)?, - run, - cause: None, - meta, - prf, - }; - - Ok(invok) - } - - /// Generate a new [Invocation] to run, with metadata, given a [cause], and - /// `prf`. - /// - /// [cause]: https://github.com/ucan-wg/invocation#523-cause - pub fn new_with_cause( - run: RunTask<'a, T>, - meta: Ipld, - prf: UcanPrf, - cause: Option, - ) -> anyhow::Result { - let invok = Invocation { - v: Version::parse(VERSION)?, - run, - cause, - meta, - prf, - }; - - Ok(invok) - } - - /// Return a reference pointer to given [Task] to run. - /// - /// [Task]: super::Task - pub fn run(&self) -> &RunTask<'_, T> { - &self.run + fn from(task: Task<'a, T>) -> Self { + Invocation::new(task) } +} - /// Return the [Cid] of the [Task] to run. - /// - /// [Task]: super::Task - pub fn task_cid(&self) -> anyhow::Result { - match &self.run { - RunTask::Expanded(task) => Ok(InvokedTaskPointer::try_from(task.to_owned())?.cid()), - RunTask::Ptr(taskptr) => Ok(taskptr.cid()), - } +impl<'a, T> Invocation<'a, T> +where + Ipld: From, +{ + /// Create a new [Invocation] container. + pub fn new(task: Task<'a, T>) -> Self { + Self { task } } } @@ -111,16 +51,10 @@ where type Error = anyhow::Error; fn try_from(invocation: Invocation<'_, T>) -> Result { - let map = Ipld::Map(BTreeMap::from([ - (VERSION_KEY.into(), invocation.v.to_string().into()), - (RUN_KEY.into(), invocation.run.try_into()?), - ( - CAUSE_KEY.into(), - invocation.cause.map_or(Ok(Ipld::Null), Ipld::try_from)?, - ), - (METADATA_KEY.into(), invocation.meta), - (PROOF_KEY.into(), invocation.prf.into()), - ])); + let map = Ipld::Map(BTreeMap::from([( + TASK_KEY.into(), + invocation.task.try_into()?, + )])); Ok(map) } @@ -135,58 +69,24 @@ where fn try_from(ipld: Ipld) -> Result { let map = from_ipld::>(ipld)?; - Ok(Invocation { - v: from_ipld::( - map.get(VERSION_KEY) - .ok_or_else(|| anyhow!("no `version` field set"))? - .to_owned(), - ) - .map(|s| Version::parse(&s))??, - run: RunTask::try_from( - map.get(RUN_KEY) - .ok_or_else(|| anyhow!("no `run` set"))? - .to_owned(), - )?, - cause: map - .get(CAUSE_KEY) - .and_then(|ipld| match ipld { - Ipld::Null => None, - ipld => Some(ipld), - }) - .and_then(|ipld| ipld.try_into().ok()), - meta: map - .get(METADATA_KEY) - .ok_or_else(|| anyhow!("no `metadata` field set"))? - .to_owned(), - prf: UcanPrf::try_from( - map.get(PROOF_KEY) - .ok_or_else(|| anyhow!("no proof field set"))? + Ok(Self { + task: Task::try_from( + map.get(TASK_KEY) + .ok_or_else(|| anyhow!("no `task` set"))? .to_owned(), )?, }) } } -impl TryFrom<&Ipld> for Invocation<'_, T> -where - T: From, -{ - type Error = anyhow::Error; - - fn try_from<'a>(ipld: &Ipld) -> Result { - TryFrom::try_from(ipld.to_owned()) - } -} - -impl TryFrom> for InvocationPointer +impl TryFrom> for Pointer where Ipld: From, - T: Clone, { type Error = anyhow::Error; fn try_from(invocation: Invocation<'_, T>) -> Result { - Ok(InvocationPointer::new(Cid::try_from(invocation)?)) + Ok(Pointer::new(Cid::try_from(invocation)?)) } } @@ -200,7 +100,7 @@ where let ipld: Ipld = invocation.try_into()?; let bytes = DagCborCodec.encode(&ipld)?; let hash = Code::Sha3_256.digest(&bytes); - Ok(Cid::new_v1(0x71, hash)) + Ok(Cid::new_v1(DAG_CBOR, hash)) } } @@ -208,96 +108,23 @@ where mod test { use super::*; use crate::{ - workflow::{config::Resources, Ability, Input, Task}, - Unit, VERSION, + test_utils, + workflow::{config::Resources, instruction::RunInstruction, prf::UcanPrf, Task}, + Unit, }; - use url::Url; - - fn task<'a, T>() -> Task<'a, T> { - let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); - let resource = Url::parse(format!("ipfs://{wasm}").as_str()).unwrap(); - - Task::new( - resource, - Ability::from("wasm/run"), - Input::Ipld(Ipld::List(vec![Ipld::Bool(true)])), - None, - ) - } #[test] fn ipld_roundtrip() { - let task: Task<'_, Unit> = task(); - let config = Resources::default(); - let invocation1 = Invocation::new( - RunTask::Expanded(task.clone()), - config.clone().into(), - UcanPrf::default(), - ) - .unwrap(); - - let ipld1 = Ipld::try_from(invocation1.clone()).unwrap(); - - let ipld_task = Ipld::Map(BTreeMap::from([ - ( - "on".into(), - Ipld::String( - "ipfs://bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".into(), - ), - ), - ("call".into(), Ipld::String("wasm/run".to_string())), - ("input".into(), Ipld::List(vec![Ipld::Bool(true)])), - ("nnc".into(), Ipld::Null), - ])); - - assert_eq!( - ipld1, - Ipld::Map(BTreeMap::from([ - (VERSION_KEY.into(), Ipld::String(VERSION.into())), - (RUN_KEY.into(), ipld_task), - (CAUSE_KEY.into(), Ipld::Null), - ( - METADATA_KEY.into(), - Ipld::Map(BTreeMap::from([ - ("fuel".into(), Ipld::Integer(u64::MAX.into())), - ("time".into(), Ipld::Integer(100_000)) - ])) - ), - (PROOF_KEY.into(), Ipld::List(vec![])) - ])) - ); - - assert_eq!(invocation1, ipld1.try_into().unwrap()); - - let invocation2 = Invocation::new_with_cause( - RunTask::Ptr::(task.try_into().unwrap()), + let instruction = test_utils::workflow::instruction::(); + let task = Task::new( + RunInstruction::Expanded(instruction.clone()), config.into(), UcanPrf::default(), - Some(InvocationPointer::try_from(invocation1.clone()).unwrap()), - ) - .unwrap(); - - let ipld2 = Ipld::try_from(invocation2.clone()).unwrap(); - let invocation1_ptr: InvocationPointer = invocation1.try_into().unwrap(); - - assert_eq!( - ipld2, - Ipld::Map(BTreeMap::from([ - (VERSION_KEY.into(), Ipld::String(VERSION.into())), - (RUN_KEY.into(), Ipld::Link(invocation2.task_cid().unwrap())), - (CAUSE_KEY.into(), Ipld::Link(invocation1_ptr.cid())), - ( - METADATA_KEY.into(), - Ipld::Map(BTreeMap::from([ - ("fuel".into(), Ipld::Integer(u64::MAX.into())), - ("time".into(), Ipld::Integer(100_000)) - ])) - ), - (PROOF_KEY.into(), Ipld::List(vec![])) - ])) ); - assert_eq!(invocation2, ipld2.try_into().unwrap()); + let invocation = Invocation::new(task); + let ipld = Ipld::try_from(invocation.clone()).unwrap(); + assert_eq!(invocation, Invocation::try_from(ipld).unwrap()); } } diff --git a/homestar-core/src/workflow/issuer.rs b/homestar-core/src/workflow/issuer.rs new file mode 100644 index 00000000..c8574e24 --- /dev/null +++ b/homestar-core/src/workflow/issuer.rs @@ -0,0 +1,69 @@ +//! Issuer referring to a principal (principal of least authority) that issues +//! a receipt. + +use diesel::{ + backend::RawValue, + deserialize::{self, FromSql}, + serialize::{self, IsNull, Output, ToSql}, + sql_types::Text, + sqlite::Sqlite, + AsExpression, FromSqlRow, +}; +use libipld::{serde::from_ipld, Ipld}; +use serde::{Deserialize, Serialize}; +use std::{fmt, str::FromStr}; +use ucan::ipld::Principle; + +/// [Principal] issuer of a receipt. If omitted issuer is +/// inferred from the [invocation] [task] audience. +/// +/// [invocation]: super::Invocation +/// [task]: super::Task +/// [Principal]: Principle +#[derive(Clone, Debug, Deserialize, Serialize, AsExpression, FromSqlRow, PartialEq)] +#[diesel(sql_type = Text)] +pub struct Issuer(Principle); + +impl Issuer { + /// Create a new [Issuer], wrapping a [Principle]. + pub fn new(principle: Principle) -> Self { + Issuer(principle) + } +} + +impl fmt::Display for Issuer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let did_as_string = self.0.to_string(); + write!(f, "{did_as_string}") + } +} + +impl From for Ipld { + fn from(issuer: Issuer) -> Self { + let principle = issuer.0.to_string(); + Ipld::String(principle) + } +} + +impl TryFrom for Issuer { + type Error = anyhow::Error; + + fn try_from(ipld: Ipld) -> Result { + let s = from_ipld::(ipld)?; + Ok(Issuer(Principle::from_str(&s)?)) + } +} + +impl ToSql for Issuer { + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result { + out.set_value(self.0.to_string()); + Ok(IsNull::No) + } +} + +impl FromSql for Issuer { + fn from_sql(bytes: RawValue<'_, Sqlite>) -> deserialize::Result { + let s = >::from_sql(bytes)?; + Ok(Issuer(Principle::from_str(&s)?)) + } +} diff --git a/homestar-core/src/workflow/mod.rs b/homestar-core/src/workflow/mod.rs deleted file mode 100644 index 9f3c97bc..00000000 --- a/homestar-core/src/workflow/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Workflow componets for building Homestar pipelines. - -mod ability; -pub mod config; -pub mod input; -mod invocation; -mod invocation_result; -mod nonce; -pub mod pointer; -pub mod prf; -pub mod receipt; -pub mod task; - -pub use ability::*; -pub use input::Input; -pub use invocation::*; -pub use invocation_result::*; -pub use nonce::*; -pub use task::Task; diff --git a/homestar-core/src/workflow/nonce.rs b/homestar-core/src/workflow/nonce.rs index 78c97d52..4603236b 100644 --- a/homestar-core/src/workflow/nonce.rs +++ b/homestar-core/src/workflow/nonce.rs @@ -1,6 +1,6 @@ -//! [Task] Nonce parameter. +//! [Instruction] nonce parameter. //! -//! [Task]: super::Task +//! [Instruction]: super::Instruction use anyhow::anyhow; use enum_as_inner::EnumAsInner; @@ -8,7 +8,8 @@ use generic_array::{ typenum::consts::{U12, U16}, GenericArray, }; -use libipld::Ipld; +use libipld::{multibase::Base::Base32HexLower, Ipld}; +use std::fmt; type Nonce96 = GenericArray; type Nonce128 = GenericArray; @@ -20,6 +21,8 @@ pub enum Nonce { Nonce96(Nonce96), /// 129-bit, 16-byte nonce. Nonce128(Nonce128), + /// No Nonce attributed. + Empty, } impl Nonce { @@ -29,6 +32,20 @@ impl Nonce { } } +impl fmt::Display for Nonce { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Nonce::Nonce96(nonce) => { + write!(f, "{}", Base32HexLower.encode(nonce.as_slice())) + } + Nonce::Nonce128(nonce) => { + write!(f, "{}", Base32HexLower.encode(nonce.as_slice())) + } + Nonce::Empty => write!(f, ""), + } + } +} + impl From for Ipld { fn from(nonce: Nonce) -> Self { match nonce { @@ -38,6 +55,7 @@ impl From for Ipld { Nonce::Nonce128(nonce) => { Ipld::List(vec![Ipld::Integer(1), Ipld::Bytes(nonce.to_vec())]) } + Nonce::Empty => Ipld::String("".to_string()), } } } @@ -57,7 +75,7 @@ impl TryFrom for Nonce { _ => Err(anyhow!("unexpected conversion type")), } } else { - Err(anyhow!("mismatched conversion type: {ipld:?}")) + Ok(Nonce::Empty) } } } diff --git a/homestar-core/src/workflow/pointer.rs b/homestar-core/src/workflow/pointer.rs index 301c1a49..f6935808 100644 --- a/homestar-core/src/workflow/pointer.rs +++ b/homestar-core/src/workflow/pointer.rs @@ -1,9 +1,12 @@ #![allow(missing_docs)] -//! Pointers and references to [Invocations] and [Tasks]. +//! Pointers and references to [Invocations], [Tasks], [Instructions], and/or +//! [Receipts], as well as handling for the [Await]'ed promises of pointers. //! //! [Invocations]: super::Invocation //! [Tasks]: super::Task +//! [Instructions]: super::Instruction +//! [Receipts]: super::Receipt use anyhow::ensure; use diesel::{ @@ -23,20 +26,13 @@ use libipld::{ use serde::{Deserialize, Serialize}; use std::{borrow::Cow, collections::btree_map::BTreeMap, fmt, str::FromStr}; -/// `await/ok` branch for a task invocation. +/// `await/ok` branch for instruction result. pub const OK_BRANCH: &str = "await/ok"; -/// `await/error` branch for a task invocation. +/// `await/error` branch for instruction result. pub const ERR_BRANCH: &str = "await/error"; -/// `await/*` branch for a task invocation. +/// `await/*` branch for instruction result. pub const PTR_BRANCH: &str = "await/*"; -/// Type alias around [InvocationPointer] for [Task] pointers. -/// -/// Essentially, reusing [InvocationPointer] as a [Cid] wrapper. -/// -/// [Task]: super::Task -pub type InvokedTaskPointer = InvocationPointer; - /// Enumerated wrapper around resulting branches of a promise /// that's being awaited on. /// @@ -63,7 +59,7 @@ pub enum AwaitResult { #[assoc(branch = ERR_BRANCH)] #[assoc(result = ERR_BRANCH)] Error, - /// + /// Direct resulting branch, without unwrapping of success or failure. #[assoc(branch = PTR_BRANCH)] #[assoc(result = PTR_BRANCH)] Ptr, @@ -79,29 +75,29 @@ impl fmt::Display for AwaitResult { } } -/// Describes the eventual output of the referenced [Task invocation], either -/// resolving to [OK_BRANCH], [ERR_BRANCH], or [PTR_BRANCH]. +/// Describes the eventual output of the referenced [Instruction] as a +/// [Pointer], either resolving to a tagged [OK_BRANCH], [ERR_BRANCH], or direct +/// result of a [PTR_BRANCH]. /// -/// [Task invocation]: InvokedTaskPointer +/// [Instruction]: super::Instruction #[derive(Clone, Debug, PartialEq, Eq)] pub struct Await { - invoked_task: InvokedTaskPointer, + instruction: Pointer, result: AwaitResult, } impl Await { - /// A new `Promise` [Await]'ed on, resulting in a [InvokedTaskPointer] + /// A new `Promise` [Await]'ed on, resulting in a [Pointer] /// and [AwaitResult]. - pub fn new(invoked: InvokedTaskPointer, result: AwaitResult) -> Self { - Await { - invoked_task: invoked, + pub fn new(instruction: Pointer, result: AwaitResult) -> Self { + Self { + instruction, result, } } - /// Return [Cid] for [InvokedTaskPointer]. - pub fn task_cid(&self) -> Cid { - self.invoked_task.cid() + pub fn instruction_cid(&self) -> Cid { + self.instruction.cid() } /// Return [AwaitResult] branch. @@ -114,7 +110,7 @@ impl From for Ipld { fn from(await_promise: Await) -> Self { Ipld::Map(BTreeMap::from([( await_promise.result.branch().to_string(), - await_promise.invoked_task.into(), + await_promise.instruction.into(), )])) } } @@ -133,7 +129,7 @@ impl TryFrom for Await { ensure!(map.len() == 1, "unexpected keys inside awaited promise"); let (key, value) = map.into_iter().next().unwrap(); - let invoked_task = InvokedTaskPointer::try_from(value)?; + let instruction = Pointer::try_from(value)?; let result = match key.as_str() { OK_BRANCH => AwaitResult::Ok, @@ -142,7 +138,7 @@ impl TryFrom for Await { }; Ok(Await { - invoked_task, + instruction, result, }) } @@ -156,14 +152,18 @@ impl TryFrom<&Ipld> for Await { } } -/// References a specific [Invocation], always by Cid. +/// References a specific [Invocation], [Task], [Instruction], and/or +/// [Receipt], always wrapping a [Cid]. /// /// [Invocation]: super::Invocation +/// [Task]: super::Task +/// [Instruction]: super::Instruction +/// [Receipt]: super::Receipt #[derive(Clone, Debug, AsExpression, FromSqlRow, PartialEq, Eq, Serialize, Deserialize)] #[diesel(sql_type = Text)] -pub struct InvocationPointer(Cid); +pub struct Pointer(Cid); -impl fmt::Display for InvocationPointer { +impl fmt::Display for Pointer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let cid_as_string = self .0 @@ -174,39 +174,39 @@ impl fmt::Display for InvocationPointer { } } -impl InvocationPointer { - /// Return the `inner` [Cid] for the [InvocationPointer]. +impl Pointer { + /// Return the `inner` [Cid] for the [Pointer]. pub fn cid(&self) -> Cid { self.0 } - /// Wrap an [InvocationPointer] for a given [Cid]. + /// Wrap an [Pointer] for a given [Cid]. pub fn new(cid: Cid) -> Self { - InvocationPointer(cid) + Pointer(cid) } - /// Convert an [Ipld::Link] to an [InvocationPointer]. + /// Convert an [Ipld::Link] to an [Pointer]. pub fn new_from_link(link: Link) -> Self { - InvocationPointer(*link) + Pointer(*link) } } -impl From for Ipld { - fn from(ptr: InvocationPointer) -> Self { +impl From for Ipld { + fn from(ptr: Pointer) -> Self { Ipld::Link(ptr.cid()) } } -impl TryFrom for InvocationPointer { +impl TryFrom for Pointer { type Error = anyhow::Error; fn try_from(ipld: Ipld) -> Result { let s: Cid = from_ipld(ipld)?; - Ok(InvocationPointer(s)) + Ok(Pointer(s)) } } -impl TryFrom<&Ipld> for InvocationPointer { +impl TryFrom<&Ipld> for Pointer { type Error = anyhow::Error; fn try_from(ipld: &Ipld) -> Result { @@ -214,28 +214,28 @@ impl TryFrom<&Ipld> for InvocationPointer { } } -impl<'a> From for Cow<'a, InvocationPointer> { - fn from(ptr: InvocationPointer) -> Self { +impl<'a> From for Cow<'a, Pointer> { + fn from(ptr: Pointer) -> Self { Cow::Owned(ptr) } } -impl<'a> From<&'a InvocationPointer> for Cow<'a, InvocationPointer> { - fn from(ptr: &'a InvocationPointer) -> Self { +impl<'a> From<&'a Pointer> for Cow<'a, Pointer> { + fn from(ptr: &'a Pointer) -> Self { Cow::Borrowed(ptr) } } -impl ToSql for InvocationPointer { +impl ToSql for Pointer { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result { out.set_value(self.cid().to_string_of_base(Base::Base32Lower)?); Ok(IsNull::No) } } -impl FromSql for InvocationPointer { +impl FromSql for Pointer { fn from_sql(bytes: RawValue<'_, Sqlite>) -> deserialize::Result { let s = >::from_sql(bytes)?; - Ok(InvocationPointer::new(Cid::from_str(&s)?)) + Ok(Pointer::new(Cid::from_str(&s)?)) } } diff --git a/homestar-core/src/workflow/prf.rs b/homestar-core/src/workflow/prf.rs index a828520f..0be42167 100644 --- a/homestar-core/src/workflow/prf.rs +++ b/homestar-core/src/workflow/prf.rs @@ -14,9 +14,10 @@ use diesel::{ use libipld::{cbor::DagCborCodec, prelude::Codec, serde::from_ipld, Ipld, Link}; use ucan::ipld::UcanIpld; -/// Proof container, containing links to UCANs for a particular [Task]. +/// Proof container, containing links to UCANs for a particular [Task] or [Receipt]. /// /// [Task]: super::Task +/// [Receipt]: super::Receipt #[derive(Clone, Debug, Default, PartialEq, AsExpression, FromSqlRow)] #[diesel(sql_type = Binary)] pub struct UcanPrf(Vec>); diff --git a/homestar-core/src/workflow/receipt.rs b/homestar-core/src/workflow/receipt.rs index 302fb978..bd5d1550 100644 --- a/homestar-core/src/workflow/receipt.rs +++ b/homestar-core/src/workflow/receipt.rs @@ -1,14 +1,7 @@ //! Output of an invocation, referenced by its invocation pointer. -use super::{pointer::InvocationPointer, prf::UcanPrf, InvocationResult}; -use diesel::{ - backend::RawValue, - deserialize::{self, FromSql}, - serialize::{self, IsNull, Output, ToSql}, - sql_types::Text, - sqlite::Sqlite, - AsExpression, FromSqlRow, -}; +use super::{prf::UcanPrf, InstructionResult, Issuer, Pointer}; +use anyhow::anyhow; use libipld::{ cbor::DagCborCodec, cid::{ @@ -16,10 +9,10 @@ use libipld::{ Cid, }, prelude::Codec, + serde::from_ipld, Ipld, }; -use std::{borrow::Cow, collections::BTreeMap, fmt, str::FromStr}; -use ucan::ipld::Principle; +use std::collections::BTreeMap; const RAN_KEY: &str = "ran"; const OUT_KEY: &str = "out"; @@ -27,82 +20,51 @@ const ISSUER_KEY: &str = "iss"; const METADATA_KEY: &str = "meta"; const PROOF_KEY: &str = "prf"; -/// [Principal] that issued this receipt. If omitted issuer is -/// inferred from the [invocation] [task] audience. -/// -/// [invocation]: super::Invocation -/// [task]: suepr::Task -#[derive(Clone, Debug, AsExpression, FromSqlRow, PartialEq)] -#[diesel(sql_type = Text)] -pub struct Issuer(Principle); - -impl fmt::Display for Issuer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let did_as_string = self.0.to_string(); - write!(f, "{did_as_string}") - } -} - -impl Issuer { - /// Create a new [Issuer], wrapping a [Principle]. - pub fn new(principle: Principle) -> Self { - Issuer(principle) - } -} - -/// A Receipt is an attestation of the [Result] and requested [Effects] by a -/// [Task Invocation]. -/// -/// A Receipt MUST be signed by the Executor or it's delegate. If signed by the -/// delegate, the proof of delegation from the [Executor] to the delegate -/// MUST be provided in prf. +/// A Receipt is a cryptographically signed description of the [Invocation] +/// and its [resulting output] and requested effects. /// /// TODO: Effects et al. /// -/// [Result]: InvocationResult -/// [Effects]: https://github.com/ucan-wg/invocation#7-effect -/// [Task Invocation]: super::Invocation -/// [Executor]: Issuer +/// [resulting output]: InstructionResult +/// [Invocation]: super::Invocation #[derive(Debug, Clone, PartialEq)] -pub struct Receipt<'a, T> { - ran: Cow<'a, InvocationPointer>, - out: InvocationResult, +pub struct Receipt { + ran: Pointer, + out: InstructionResult, meta: Ipld, - iss: Option, + issuer: Option, prf: UcanPrf, } -impl<'a, T> Receipt<'a, T> { +impl Receipt { /// pub fn new( - ran: InvocationPointer, - result: InvocationResult, + ran: Pointer, + result: InstructionResult, metadata: Ipld, issuer: Option, proof: UcanPrf, ) -> Self { Self { - ran: Cow::from(ran), + ran, out: result, meta: metadata, - iss: issuer, + issuer, prf: proof, } } } -impl Receipt<'_, T> { - /// [InvocationPointer] for [Task] ran. +impl Receipt { + /// [Pointer] for [Invocation] ran. /// - /// [Task]: super::Task - pub fn ran(&self) -> &InvocationPointer { + /// [Invocation]: super::Invocation + pub fn ran(&self) -> &Pointer { &self.ran } - /// [InvocationResult] output from [Task] invocation/execution. - /// - /// [Task]: super::Task - pub fn out(&self) -> &InvocationResult { + /// [InstructionResult] output from invocation/execution. + pub fn out(&self) -> &InstructionResult { &self.out } @@ -113,7 +75,7 @@ impl Receipt<'_, T> { /// Optional [Issuer] for [Receipt]. pub fn issuer(&self) -> &Option { - &self.iss + &self.issuer } /// [UcanPrf] delegation chain. @@ -122,27 +84,36 @@ impl Receipt<'_, T> { } } -impl TryFrom> for Vec { +impl TryFrom> for Vec { type Error = anyhow::Error; - fn try_from(receipt: Receipt<'_, Ipld>) -> Result { + fn try_from(receipt: Receipt) -> Result { let receipt_ipld = Ipld::from(&receipt); DagCborCodec.encode(&receipt_ipld) } } -impl TryFrom> for Cid { +impl TryFrom> for Receipt { type Error = anyhow::Error; - fn try_from(receipt: Receipt<'_, Ipld>) -> Result { + fn try_from(bytes: Vec) -> Result { + let ipld: Ipld = DagCborCodec.decode(&bytes)?; + ipld.try_into() + } +} + +impl TryFrom> for Cid { + type Error = anyhow::Error; + + fn try_from(receipt: Receipt) -> Result { TryFrom::try_from(&receipt) } } -impl TryFrom<&Receipt<'_, Ipld>> for Cid { +impl TryFrom<&Receipt> for Cid { type Error = anyhow::Error; - fn try_from(receipt: &Receipt<'_, Ipld>) -> Result { + fn try_from(receipt: &Receipt) -> Result { let ipld = Ipld::from(receipt); let bytes = DagCborCodec.encode(&ipld)?; let hash = Code::Sha3_256.digest(&bytes); @@ -150,24 +121,18 @@ impl TryFrom<&Receipt<'_, Ipld>> for Cid { } } -impl From> for Ipld { - fn from(receipt: Receipt<'_, Ipld>) -> Self { - From::from(&receipt) - } -} - -impl From<&Receipt<'_, Ipld>> for Ipld { - fn from(receipt: &Receipt<'_, Ipld>) -> Self { +impl From<&Receipt> for Ipld { + fn from(receipt: &Receipt) -> Self { Ipld::Map(BTreeMap::from([ - (RAN_KEY.into(), receipt.ran.as_ref().to_owned().into()), + (RAN_KEY.into(), receipt.ran.to_owned().into()), (OUT_KEY.into(), receipt.out.to_owned().into()), (METADATA_KEY.into(), receipt.meta.to_owned()), ( ISSUER_KEY.into(), receipt - .iss + .issuer .as_ref() - .map(|iss| iss.to_string().into()) + .map(|issuer| issuer.to_string().into()) .unwrap_or(Ipld::Null), ), (PROOF_KEY.into(), receipt.prf.to_owned().into()), @@ -175,16 +140,58 @@ impl From<&Receipt<'_, Ipld>> for Ipld { } } -impl ToSql for Issuer { - fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result { - out.set_value(self.0.to_string()); - Ok(IsNull::No) +impl From> for Ipld { + fn from(receipt: Receipt) -> Self { + From::from(&receipt) } } -impl FromSql for Issuer { - fn from_sql(bytes: RawValue<'_, Sqlite>) -> deserialize::Result { - let s = >::from_sql(bytes)?; - Ok(Issuer(Principle::from_str(&s)?)) +impl TryFrom for Receipt { + type Error = anyhow::Error; + + fn try_from(ipld: Ipld) -> Result { + let map = from_ipld::>(ipld)?; + + let ran = map + .get(RAN_KEY) + .ok_or_else(|| anyhow!("missing {RAN_KEY}"))? + .try_into()?; + + let out = map + .get(OUT_KEY) + .ok_or_else(|| anyhow!("missing {OUT_KEY}"))?; + + let meta = map + .get(METADATA_KEY) + .ok_or_else(|| anyhow!("missing {METADATA_KEY}"))?; + + let issuer = map + .get(ISSUER_KEY) + .and_then(|ipld| match ipld { + Ipld::Null => None, + ipld => Some(ipld), + }) + .and_then(|ipld| from_ipld(ipld.to_owned()).ok()) + .map(Issuer::new); + + let prf = map + .get(PROOF_KEY) + .ok_or_else(|| anyhow!("missing {PROOF_KEY}"))?; + + Ok(Receipt { + ran, + out: InstructionResult::try_from(out)?, + meta: meta.to_owned(), + issuer, + prf: UcanPrf::try_from(prf)?, + }) + } +} + +impl TryFrom> for Pointer { + type Error = anyhow::Error; + + fn try_from(receipt: Receipt) -> Result { + Ok(Pointer::new(Cid::try_from(receipt)?)) } } diff --git a/homestar-core/src/workflow/task.rs b/homestar-core/src/workflow/task.rs index 7313110c..4115c744 100644 --- a/homestar-core/src/workflow/task.rs +++ b/homestar-core/src/workflow/task.rs @@ -1,14 +1,10 @@ //! A [Task] is the smallest unit of work that can be requested from a UCAN. -use super::{ - pointer::{InvocationPointer, InvokedTaskPointer}, - Ability, Input, Nonce, -}; +use super::{instruction::RunInstruction, prf::UcanPrf, Pointer}; use anyhow::anyhow; use libipld::{ cbor::DagCborCodec, cid::{ - multibase::Base, multihash::{Code, MultihashDigest}, Cid, }, @@ -16,225 +12,157 @@ use libipld::{ serde::from_ipld, Ipld, }; -use std::{borrow::Cow, collections::BTreeMap, fmt}; -use url::Url; +use std::collections::BTreeMap; const DAG_CBOR: u64 = 0x71; -const ON_KEY: &str = "on"; -const CALL_KEY: &str = "call"; -const INPUT_KEY: &str = "input"; -const NNC_KEY: &str = "nnc"; - -/// Enumerator for `either` an expanded [Task] structure or -/// an [InvokedTaskPointer] ([Cid] wrapper). -#[derive(Debug, Clone, PartialEq)] -pub enum RunTask<'a, T> { - /// [Task] as an expanded structure. - Expanded(Task<'a, T>), - /// [Task] as a pointer. - Ptr(InvokedTaskPointer), -} +const RUN_KEY: &str = "run"; +const CAUSE_KEY: &str = "cause"; +const METADATA_KEY: &str = "meta"; +const PROOF_KEY: &str = "prf"; -impl<'a, T> From> for RunTask<'a, T> { - fn from(task: Task<'a, T>) -> Self { - RunTask::Expanded(task) - } +/// Contains the [Instruction], configuration, and a possible +/// [Receipt] of the invocation that caused this task to run. +/// +/// [Instruction]: super::Instruction +/// [Receipt]: super::Receipt +#[derive(Clone, Debug, PartialEq)] +pub struct Task<'a, T> { + run: RunInstruction<'a, T>, + cause: Option, + meta: Ipld, + prf: UcanPrf, } -impl<'a, T> TryFrom> for Task<'a, T> +impl<'a, T> Task<'a, T> where - T: fmt::Debug, + Ipld: From, + T: Clone, { - type Error = anyhow::Error; - - fn try_from(run: RunTask<'a, T>) -> Result { - match run { - RunTask::Expanded(task) => Ok(task), - e => Err(anyhow!("wrong discriminant: {e:?}")), + /// Generate a new [Task] to run, with metadata, and `prf`. + pub fn new(run: RunInstruction<'a, T>, meta: Ipld, prf: UcanPrf) -> Self { + Self { + run, + cause: None, + meta, + prf, } } -} - -impl From for RunTask<'_, T> { - fn from(ptr: InvokedTaskPointer) -> Self { - RunTask::Ptr(ptr) - } -} - -impl<'a, T> TryFrom> for InvokedTaskPointer -where - T: fmt::Debug, -{ - type Error = anyhow::Error; - fn try_from(run: RunTask<'a, T>) -> Result { - match run { - RunTask::Ptr(ptr) => Ok(ptr), - e => Err(anyhow!("wrong discriminant: {e:?}")), + /// Generate a new [Task] to execute, with metadata, given a `cause`, and + /// `prf`. + pub fn new_with_cause( + run: RunInstruction<'a, T>, + meta: Ipld, + prf: UcanPrf, + cause: Option, + ) -> Self { + Self { + run, + cause, + meta, + prf, } } -} -impl<'a, 'b, T> TryFrom<&'b RunTask<'a, T>> for &'b InvokedTaskPointer -where - T: fmt::Debug, -{ - type Error = anyhow::Error; + /// Return a reference pointer to given [Instruction] to run. + /// + /// [Instruction]: super::Instruction + pub fn run(&self) -> &RunInstruction<'_, T> { + &self.run + } - fn try_from(run: &'b RunTask<'a, T>) -> Result { - match run { - RunTask::Ptr(ptr) => Ok(ptr), - e => Err(anyhow!("wrong discriminant: {e:?}")), - } + /// Get [Task] metadata in [Ipld] form. + pub fn meta(&self) -> &Ipld { + &self.meta } -} -impl<'a, 'b, T> TryFrom<&'b RunTask<'a, T>> for InvokedTaskPointer -where - T: fmt::Debug, -{ - type Error = anyhow::Error; + /// Turn [Task] into owned [RunInstruction]. + pub fn into_instruction(self) -> RunInstruction<'a, T> { + self.run + } - fn try_from(run: &'b RunTask<'a, T>) -> Result { - match run { - RunTask::Ptr(ptr) => Ok(ptr.to_owned()), - e => Err(anyhow!("wrong discriminant: {e:?}")), + /// Return the [Cid] of the contained [Instruction]. + /// + /// [Instruction]: super::Instruction + pub fn instruction_cid(&self) -> anyhow::Result { + match &self.run { + RunInstruction::Expanded(instruction) => Ok(Cid::try_from(instruction.to_owned())?), + RunInstruction::Ptr(instruction_ptr) => Ok(instruction_ptr.cid()), } } } -impl From> for Ipld +impl From> for Ipld where Ipld: From, { - fn from(run: RunTask<'_, T>) -> Self { - match run { - RunTask::Expanded(task) => task.into(), - RunTask::Ptr(taskptr) => taskptr.into(), - } + fn from(task: Task<'_, T>) -> Self { + Ipld::Map(BTreeMap::from([ + (RUN_KEY.into(), task.run.into()), + ( + CAUSE_KEY.into(), + task.cause.map_or(Ipld::Null, |cause| cause.into()), + ), + (METADATA_KEY.into(), task.meta), + (PROOF_KEY.into(), task.prf.into()), + ])) } } -impl TryFrom for RunTask<'_, T> +impl TryFrom for Task<'_, T> where T: From, { type Error = anyhow::Error; - fn try_from<'a>(ipld: Ipld) -> Result { - match ipld { - Ipld::Map(_) => Ok(RunTask::Expanded(Task::try_from(ipld)?)), - Ipld::Link(_) => Ok(RunTask::Ptr(InvokedTaskPointer::try_from(ipld)?)), - _ => Err(anyhow!("unexpected conversion type")), - } - } -} - -/// A Task is the smallest unit of work that can be requested from a UCAN. -/// It describes one (resource, ability, input) triple. The [Input] field is -/// free-form, and depend on the specific resource and ability being interacted -/// with. Inputs can be expressed as [Ipld] or as a [deferred promise]. -/// -/// -/// # Example -/// -/// ``` -/// use homestar_core::{Unit, workflow::{Ability, Input, Task}}; -/// use libipld::Ipld; -/// use url::Url; -/// -/// let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); -/// let resource = Url::parse(format!("ipfs://{wasm}").as_str()).unwrap(); -/// -/// let task = Task::unique( -/// resource, -/// Ability::from("wasm/run"), -/// Input::::Ipld(Ipld::List(vec![Ipld::Bool(true)])) -/// ); -/// ``` -/// -/// We can also set-up a [Task] with a Deferred input to await on: -/// ``` -/// use homestar_core::{ -/// workflow::{Ability, Input, Nonce, Task, -/// pointer::{Await, AwaitResult, InvocationPointer, InvokedTaskPointer}, -/// }, -/// Unit, -/// }; -/// use libipld::{cid::{multihash::{Code, MultihashDigest}, Cid}, Ipld, Link}; -/// use url::Url; - -/// let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); -/// let resource = Url::parse(format!("ipfs://{wasm}").as_str()).expect("IPFS URL"); -/// let h = Code::Blake3_256.digest(b"beep boop"); -/// let cid = Cid::new_v1(0x55, h); -/// let link: Link = Link::new(cid); -/// let invoked_task = InvocationPointer::new_from_link(link); -/// -/// let task = Task::new( -/// resource, -/// Ability::from("wasm/run"), -/// Input::::Deferred(Await::new(invoked_task, AwaitResult::Ok)), -/// Some(Nonce::generate()) -/// ); -/// -/// // And covert it to a pointer: -/// let ptr = InvokedTaskPointer::try_from(task).unwrap(); -/// ``` -/// [deferred promise]: super::pointer::Await -#[derive(Clone, Debug, PartialEq)] -pub struct Task<'a, T> { - on: Url, - call: Cow<'a, Ability>, - input: Input, - nnc: Option, -} - -impl Task<'_, T> { - /// Create a new [Task]. - pub fn new(on: Url, ability: Ability, input: Input, nnc: Option) -> Self { - Task { - on, - call: Cow::from(ability), - input, - nnc, - } - } - - /// Create a unique [Task], with a default [Nonce] generator. - pub fn unique(on: Url, ability: Ability, input: Input) -> Self { - Task { - on, - call: Cow::from(ability), - input, - nnc: Some(Nonce::generate()), - } - } + fn try_from(ipld: Ipld) -> Result { + let map = from_ipld::>(ipld)?; - /// Return [Task] resource, i.e. [Url]. - pub fn resource(&self) -> &Url { - &self.on + Ok(Self { + run: RunInstruction::try_from( + map.get(RUN_KEY) + .ok_or_else(|| anyhow!("no `run` set"))? + .to_owned(), + )?, + cause: map + .get(CAUSE_KEY) + .and_then(|ipld| match ipld { + Ipld::Null => None, + ipld => Some(ipld), + }) + .and_then(|ipld| ipld.to_owned().try_into().ok()), + meta: map + .get(METADATA_KEY) + .ok_or_else(|| anyhow!("no `metadata` field set"))? + .to_owned(), + prf: UcanPrf::try_from( + map.get(PROOF_KEY) + .ok_or_else(|| anyhow!("no proof field set"))? + .to_owned(), + )?, + }) } +} - /// Return [Ability] associated with `call`. - pub fn call(&self) -> &Ability { - &self.call - } +impl TryFrom<&Ipld> for Task<'_, T> +where + T: From, +{ + type Error = anyhow::Error; - /// Return [Task] [Input]. - pub fn input(&self) -> &Input { - &self.input + fn try_from<'a>(ipld: &Ipld) -> Result { + TryFrom::try_from(ipld.to_owned()) } } -impl TryFrom> for InvocationPointer +impl TryFrom> for Pointer where Ipld: From, { type Error = anyhow::Error; fn try_from(task: Task<'_, T>) -> Result { - Ok(InvocationPointer::new(Cid::try_from(task)?)) + Ok(Pointer::new(Cid::try_from(task)?)) } } @@ -252,120 +180,83 @@ where } } -impl From> for Ipld -where - Ipld: From, -{ - fn from(task: Task<'_, T>) -> Self { - Ipld::Map(BTreeMap::from([ - (ON_KEY.into(), task.on.to_string().into()), - (CALL_KEY.into(), task.call.to_string().into()), - (INPUT_KEY.into(), task.input.into()), - ( - NNC_KEY.into(), - task.nnc.map(|nnc| nnc.into()).unwrap_or(Ipld::Null), - ), - ])) - } -} - -impl TryFrom<&Ipld> for Task<'_, T> -where - T: From, -{ - type Error = anyhow::Error; - - fn try_from(ipld: &Ipld) -> Result { - TryFrom::try_from(ipld.to_owned()) - } -} +#[cfg(test)] +mod test { + use super::*; + use crate::{test_utils, workflow::config::Resources, Unit}; -impl TryFrom for Task<'_, T> -where - T: From, -{ - type Error = anyhow::Error; + #[test] + fn ipld_roundtrip() { + let config = Resources::default(); + let instruction = test_utils::workflow::instruction::(); + let task1 = Task::new( + RunInstruction::Expanded(instruction.clone()), + config.clone().into(), + UcanPrf::default(), + ); - fn try_from(ipld: Ipld) -> Result { - let map = from_ipld::>(ipld)?; + let ipld1 = Ipld::from(task1.clone()); - let on = match map.get(ON_KEY) { - Some(Ipld::Link(cid)) => cid - .to_string_of_base(Base::Base32Lower) - .map_err(|e| anyhow!("failed to encode CID into multibase string: {e}")) - .and_then(|txt| { - Url::parse(format!("{}{}", "ipfs://", txt).as_str()) - .map_err(|e| anyhow!("failed to parse URL: {e}")) - }), - Some(Ipld::String(txt)) => { - Url::parse(txt.as_str()).map_err(|e| anyhow!("failed to parse URL: {e}")) - } - _ => Err(anyhow!("no resource/with set.")), - }?; + let ipld_task = Ipld::Map(BTreeMap::from([ + ( + "rsc".into(), + Ipld::String( + "ipfs://bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".into(), + ), + ), + ("op".into(), Ipld::String("ipld/fun".to_string())), + ("input".into(), Ipld::List(vec![Ipld::Bool(true)])), + ("nnc".into(), Ipld::String("".to_string())), + ])); - Ok(Task { - on, - call: from_ipld( - map.get(CALL_KEY) - .ok_or_else(|| anyhow!("no `call` field set"))? - .to_owned(), - )?, - input: Input::try_from( - map.get(INPUT_KEY) - .ok_or_else(|| anyhow!("no `input` field set"))? - .to_owned(), - )?, - nnc: map.get(NNC_KEY).and_then(|ipld| match ipld { - Ipld::Null => None, - ipld => Nonce::try_from(ipld).ok(), - }), - }) - } -} + assert_eq!( + ipld1, + Ipld::Map(BTreeMap::from([ + (RUN_KEY.into(), ipld_task), + (CAUSE_KEY.into(), Ipld::Null), + ( + METADATA_KEY.into(), + Ipld::Map(BTreeMap::from([ + ("fuel".into(), Ipld::Integer(u64::MAX.into())), + ("time".into(), Ipld::Integer(100_000)) + ])) + ), + (PROOF_KEY.into(), Ipld::List(vec![])) + ])) + ); -#[cfg(test)] -mod test { - use super::*; - use crate::Unit; + assert_eq!(task1, ipld1.try_into().unwrap()); - fn task<'a, T>() -> (Task<'a, T>, Vec) { - let wasm = "bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".to_string(); - let resource = Url::parse(format!("ipfs://{wasm}").as_str()).unwrap(); - let nonce = Nonce::generate(); + let receipt = test_utils::workflow::receipt(); - ( - Task::new( - resource, - Ability::from("wasm/run"), - Input::Ipld(Ipld::List(vec![Ipld::Bool(true)])), - Some(nonce.clone()), - ), - nonce.as_nonce96().unwrap().to_vec(), - ) - } + let task2 = Task::new_with_cause( + RunInstruction::Ptr::(instruction.try_into().unwrap()), + config.into(), + UcanPrf::default(), + Some(receipt.clone().try_into().unwrap()), + ); - #[test] - fn ipld_roundtrip() { - let (task, bytes) = task::(); - let ipld = Ipld::from(task.clone()); + let ipld2 = Ipld::from(task2.clone()); assert_eq!( - ipld, + ipld2, Ipld::Map(BTreeMap::from([ + (RUN_KEY.into(), Ipld::Link(task2.instruction_cid().unwrap())), ( - ON_KEY.into(), - Ipld::String( - "ipfs://bafkreidztuwoszw2dfnzufjpsjmzj67x574qcdm2autnhnv43o3t4zmh7i".into() - ) + CAUSE_KEY.into(), + Ipld::Link(Cid::try_from(receipt).unwrap()) ), - (CALL_KEY.into(), Ipld::String("wasm/run".to_string())), - (INPUT_KEY.into(), Ipld::List(vec![Ipld::Bool(true)])), ( - NNC_KEY.into(), - Ipld::List(vec![Ipld::Integer(0), Ipld::Bytes(bytes)]) - ) + METADATA_KEY.into(), + Ipld::Map(BTreeMap::from([ + ("fuel".into(), Ipld::Integer(u64::MAX.into())), + ("time".into(), Ipld::Integer(100_000)) + ])) + ), + (PROOF_KEY.into(), Ipld::List(vec![])) ])) ); - assert_eq!(task, ipld.try_into().unwrap()) + + assert_eq!(task2, ipld2.try_into().unwrap()); } } diff --git a/homestar-guest-wasm/Cargo.toml b/homestar-guest-wasm/Cargo.toml index 8ff78700..5a70a01d 100644 --- a/homestar-guest-wasm/Cargo.toml +++ b/homestar-guest-wasm/Cargo.toml @@ -3,10 +3,16 @@ name = "homestar-guest-wasm" publish = false version = "0.1.0" edition = { workspace = true } +rust-version = { workspace = true } [dependencies] -image = "0.24" +image = { version = "0.24", default-features = false, features = ["png"] } wit-bindgen = "0.4" +[dev-dependencies] +image = "0.24" + [lib] +doc = false +bench = false crate-type = ["cdylib", "rlib"] diff --git a/homestar-guest-wasm/src/lib.rs b/homestar-guest-wasm/src/lib.rs index f8c255a5..0d86d22c 100644 --- a/homestar-guest-wasm/src/lib.rs +++ b/homestar-guest-wasm/src/lib.rs @@ -1,12 +1,11 @@ #![allow(clippy::too_many_arguments)] -use image::DynamicImage; - +use std::io::Cursor; wit_bindgen::generate!("test" in "./wits"); struct Component; -type Matrix = Vec>; +type Matrix = Vec>; impl Homestar for Component { fn add_one(a: i32) -> i32 { @@ -18,6 +17,10 @@ impl Homestar for Component { [a, b.to_string()].join("\n") } + fn join_strings(a: String, b: String) -> String { + [a, b].join("") + } + fn transpose(matrix: Matrix) -> Matrix { assert!(!matrix.is_empty()); let len = matrix[0].len(); @@ -26,41 +29,55 @@ impl Homestar for Component { .collect() } - fn blur(data: Vec, sigma: f32, width: u32, height: u32) -> Vec { - let img_buf = image::RgbImage::from_vec(width, height, data).unwrap(); + fn blur(data: Vec, sigma: f32) -> Vec { + let img = image::load_from_memory_with_format(&data, image::ImageFormat::Png).unwrap(); - let blurred = DynamicImage::ImageRgb8(img_buf).blur(sigma); - blurred.into_bytes() + let blurred = img.blur(sigma); + + let mut buffer: Vec = Vec::new(); + blurred + .write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png) + .unwrap(); + + buffer } - fn crop( - data: Vec, - x: u32, - y: u32, - target_width: u32, - target_height: u32, - width: u32, - height: u32, - ) -> Vec { - let img_buf = image::RgbImage::from_vec(width, height, data).unwrap(); + fn crop(data: Vec, x: u32, y: u32, target_width: u32, target_height: u32) -> Vec { + let mut img = image::load_from_memory_with_format(&data, image::ImageFormat::Png).unwrap(); // Crop this image delimited by the bounding rectangle - let cropped = DynamicImage::ImageRgb8(img_buf).crop(x, y, target_width, target_height); - cropped.into_bytes() + let cropped = img.crop(x, y, target_width, target_height); + + let mut buffer: Vec = Vec::new(); + cropped + .write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png) + .unwrap(); + + buffer } - fn grayscale(data: Vec, width: u32, height: u32) -> Vec { - let img_buf = image::RgbImage::from_vec(width, height, data).unwrap(); + fn grayscale(data: Vec) -> Vec { + let img = image::load_from_memory_with_format(&data, image::ImageFormat::Png).unwrap(); + let gray = img.grayscale(); + + let mut buffer: Vec = Vec::new(); + gray.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png) + .unwrap(); - let gray = DynamicImage::ImageRgb8(img_buf).grayscale(); - gray.to_rgb8().into_vec() + buffer } - fn rotate90(data: Vec, width: u32, height: u32) -> Vec { - let img_buf = image::RgbImage::from_vec(width, height, data).unwrap(); + fn rotate90(data: Vec) -> Vec { + let img = image::load_from_memory_with_format(&data, image::ImageFormat::Png).unwrap(); + + let rotated = img.rotate90(); - let rotated = DynamicImage::ImageRgb8(img_buf).rotate90(); - rotated.into_bytes() + let mut buffer: Vec = Vec::new(); + rotated + .write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png) + .unwrap(); + + buffer } } @@ -95,64 +112,98 @@ mod test { #[test] fn blur() { let img = image::open(Path::new("./fixtures/synthcat.jpg")).unwrap(); - let (width, height) = (img.width(), img.height()); - let img_vec = img.into_bytes(); + let mut buffer: Vec = Vec::new(); + img.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png) + .unwrap(); // Call component to blur the image - let result = Component::blur(img_vec, 1.0, width, height); + let result = Component::blur(buffer, 0.3); + + let png_img = image::io::Reader::new(Cursor::new(&result)) + .with_guessed_format() + .unwrap() + .decode() + .unwrap(); - let processed_buf = image::RgbImage::from_vec(width, height, result).unwrap(); - let processed = DynamicImage::ImageRgb8(processed_buf); - processed - .save("./out/blurred.jpg") - .expect("Failed to write cropped.jpg to filesystem"); + png_img + .save("./out/blurred.png") + .expect("Failed to write blurred.png to filesystem"); } #[test] fn crop() { let img = image::open(Path::new("./fixtures/synthcat.jpg")).unwrap(); - let (width, height) = (img.width(), img.height()); - let img_vec = img.into_bytes(); - - // Call component to crop the image to a 200x200 square - let result = Component::crop(img_vec, 150, 350, 400, 400, width, height); - - let processed_buf = image::RgbImage::from_vec(400, 400, result).unwrap(); - let processed = DynamicImage::ImageRgb8(processed_buf); - processed - .save("./out/cropped.jpg") - .expect("Failed to write cropped.jpg to filesystem"); + let mut buffer: Vec = Vec::new(); + img.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png) + .unwrap(); + + // Call component to crop the image to a 400x400 square + let result = Component::crop(buffer, 150, 350, 400, 400); + + let png_img = image::io::Reader::new(Cursor::new(&result)) + .with_guessed_format() + .unwrap() + .decode() + .unwrap(); + + png_img + .save("./out/cropped.png") + .expect("Failed to write cropped.png to filesystem"); } #[test] fn grayscale() { let img = image::open(Path::new("./fixtures/synthcat.jpg")).unwrap(); - let (width, height) = (img.width(), img.height()); - let img_vec = img.into_bytes(); + let mut buffer: Vec = Vec::new(); + img.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png) + .unwrap(); // Call component to grayscale the image - let result = Component::grayscale(img_vec, width, height); + let result = Component::grayscale(buffer); + + let png_img = image::io::Reader::new(Cursor::new(&result)) + .with_guessed_format() + .unwrap() + .decode() + .unwrap(); - let processed_buf = image::RgbImage::from_vec(width, height, result).unwrap(); - let processed = DynamicImage::ImageRgb8(processed_buf); - processed - .save("./out/graycat.jpg") + png_img + .save("./out/graycat.png") .expect("Failed to write graycat.jpg to filesystem"); } #[test] fn rotate() { let img = image::open(Path::new("./fixtures/synthcat.jpg")).unwrap(); - let (width, height) = (img.width(), img.height()); - let img_vec = img.into_bytes(); + let mut buffer: Vec = Vec::new(); + img.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png) + .unwrap(); // Call component to rotate the image 90 deg clockwise - let result = Component::rotate90(img_vec, width, height); + let result = Component::rotate90(buffer); - let processed_buf = image::RgbImage::from_vec(width, height, result).unwrap(); - let processed = DynamicImage::ImageRgb8(processed_buf); - processed - .save("./out/rotated.jpg") + let png_img = image::io::Reader::new(Cursor::new(&result)) + .with_guessed_format() + .unwrap() + .decode() + .unwrap(); + + png_img + .save("./out/rotated.png") .expect("Failed to write graycat.jpg to filesystem"); } + + #[test] + fn mixed() { + let img = image::open(Path::new("./fixtures/synthcat.jpg")).unwrap(); + let mut buffer: Vec = Vec::new(); + img.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png) + .unwrap(); + + // Call component to rotate the image 90 deg clockwise + let rotated = Component::rotate90(buffer); + let gray = Component::grayscale(rotated); + let cropped = Component::crop(gray, 150, 350, 400, 400); + Component::blur(cropped, 0.1); + } } diff --git a/homestar-guest-wasm/wits/test.wit b/homestar-guest-wasm/wits/test.wit index f21e556b..d12296a9 100644 --- a/homestar-guest-wasm/wits/test.wit +++ b/homestar-guest-wasm/wits/test.wit @@ -1,9 +1,10 @@ default world homestar { export add-one: func(a: s32) -> s32 export append-string: func(a: string) -> string - export transpose: func(matrix: list>) -> list> - export blur: func(data: list, sigma: float32, width: u32, height: u32) -> list - export crop: func(data: list, x: u32, y: u32, target-width: u32, target-height: u32, width: u32, height: u32) -> list - export grayscale: func(data: list, width: u32, height: u32) -> list - export rotate90: func(data: list, width: u32, height: u32) -> list + export join-strings: func(a: string, b: string) -> string + export transpose: func(matrix: list>) -> list> + export blur: func(data: list, sigma: float32) -> list + export crop: func(data: list, x: u32, y: u32, target-width: u32, target-height: u32) -> list + export grayscale: func(data: list) -> list + export rotate90: func(data: list) -> list } diff --git a/homestar-runtime/Cargo.toml b/homestar-runtime/Cargo.toml index 35362306..86d2c85d 100644 --- a/homestar-runtime/Cargo.toml +++ b/homestar-runtime/Cargo.toml @@ -1,10 +1,9 @@ [package] name = "homestar-runtime" version = "0.1.0" -description = "" -keywords = [] -categories = [] - +description = "Homestar runtime implementation" +keywords = ["ipfs", "workflows", "ipld", "ipvm"] +categories = ["workflow-engines", "distributed-systems", "runtimes", "networking"] include = ["/src", "README.md", "LICENSE"] license = { workspace = true } readme = "README.md" @@ -27,31 +26,64 @@ doc = false bench = false [dependencies] -anyhow = "1.0" +ansi_term = { version = "0.12", optional = true, default-features = false } +# return to version.workspace = true after the following issue is fixed: +# https://github.com/DevinR528/cargo-sort/issues/47 +anyhow = { workspace = true } async-trait = "0.1" +axum = { version = "0.6", features = ["ws", "headers"] } +byte-unit = { version = "4.0", default-features = false } clap = { version = "4.1", features = ["derive"] } -diesel = { version = "2.0", features = ["sqlite"] } +concat-in-place = "1.1" +config = "0.13" +console-subscriber = { version = "0.1", default-features = false, features = [ "parking_lot" ], optional = true } +crossbeam = "0.8" +dagga = "0.2" +diesel = { version = "2.0", features = ["sqlite", "r2d2", "returning_clauses_for_sqlite_3_35"] } diesel_migrations = "2.0" dotenvy = "0.15" -env_logger = "0.10" +enum-assoc = "0.4" +futures = "0.3" +headers = "0.3" homestar-core = { version = "0.1", path = "../homestar-core" } homestar-wasm = { version = "0.1", path = "../homestar-wasm" } -ipfs-api = "0.17" -ipfs-api-backend-hyper = { version = "0.6", features = ["with-builder"] } +http = "0.2" +http-serde = "1.1" +indexmap = "1.9" +ipfs-api = { version = "0.17", optional = true } +ipfs-api-backend-hyper = { version = "0.6", features = ["with-builder", "with-send-sync"], optional = true } itertools = "0.10" +json = "0.12" libipld = "0.16" -libp2p = { version = "0.51", features = ["kad", "request-response", "macros", "identify", "mdns", "floodsub", "gossipsub", "tokio", "dns", "tcp", "noise", "yamux", "websocket"] } +libp2p = { version = "0.51", features = ["kad", "request-response", "macros", "identify", "mdns", "gossipsub", "tokio", "dns", "mplex", "tcp", "noise", "yamux", "websocket"] } libp2p-identity = "0.1" proptest = { version = "1.1", optional = true } +reqwest = { version = "0.11", features = ["json"] } +semver = "1.0" serde = { version = "1.0", features = ["derive"] } -tokio = { version = "1.26", features = ["io-util", "io-std", "macros", "rt", "rt-multi-thread"] } -tracing = "0.1" -tracing-subscriber = "0.3" +serde_with = "2.3" +thiserror = "1.0" +tokio = { version = "1.26", features = ["fs", "io-util", "io-std", "macros", "rt", "rt-multi-thread"] } +tracing = { workspace = true } +tracing-logfmt = { version = "0.3", optional = true } +tracing-subscriber = { version = "0.3", features = ["env-filter", "parking_lot", "registry"] } +tryhard = "0.5" url = "2.3" [dev-dependencies] criterion = "0.4" +homestar-core = { version = "0.1", path = "../homestar-core", features = [ "test_utils" ] } +tokio-tungstenite = "0.18" [features] -default = [] +default = ["console", "ipfs", "logfmt"] +ansi-logs = ["ansi_term"] +console = ["console-subscriber"] +ipfs = ["ipfs-api", "ipfs-api-backend-hyper"] +logfmt = ["tracing-logfmt"] test_utils = ["proptest"] + +[package.metadata.docs.rs] +all-features = true +# defines the configuration attribute `docsrs` +rustdoc-args = ["--cfg", "docsrs"] diff --git a/homestar-runtime/config/settings.toml b/homestar-runtime/config/settings.toml new file mode 100644 index 00000000..528e63c6 --- /dev/null +++ b/homestar-runtime/config/settings.toml @@ -0,0 +1,4 @@ +[monitoring] +process_collector_interval = 10 + +[node] diff --git a/migrations/.keep b/homestar-runtime/migrations/.keep similarity index 100% rename from migrations/.keep rename to homestar-runtime/migrations/.keep diff --git a/homestar-runtime/migrations/2022-12-11-183928_create_receipts/down.sql b/homestar-runtime/migrations/2022-12-11-183928_create_receipts/down.sql new file mode 100644 index 00000000..518332ca --- /dev/null +++ b/homestar-runtime/migrations/2022-12-11-183928_create_receipts/down.sql @@ -0,0 +1,2 @@ +DROP TABLE receipts; +DROP INDEX instruction_index; diff --git a/homestar-runtime/migrations/2022-12-11-183928_create_receipts/up.sql b/homestar-runtime/migrations/2022-12-11-183928_create_receipts/up.sql new file mode 100644 index 00000000..e5d0dd40 --- /dev/null +++ b/homestar-runtime/migrations/2022-12-11-183928_create_receipts/up.sql @@ -0,0 +1,12 @@ +CREATE TABLE receipts ( + cid TEXT NOT NULL PRIMARY KEY, + ran TEXT NOT NULL, + instruction TEXT NOT NULL, + out BLOB NOT NULL, + meta BLOB NOT NULL, + issuer TEXT, + prf BLOB NOT NULL, + version TEXT NOT NULL +); + +CREATE INDEX instruction_index ON receipts (instruction); diff --git a/homestar-runtime/src/cli.rs b/homestar-runtime/src/cli.rs index 12e35eb2..ef9c308c 100644 --- a/homestar-runtime/src/cli.rs +++ b/homestar-runtime/src/cli.rs @@ -1,50 +1,25 @@ //! CLI commands/arguments. use clap::Parser; -use libp2p::core::Multiaddr; /// CLI arguments. #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct Args { - /// Peer address. - #[arg(long)] - pub peer: Option, - - /// Listen address. - #[arg(long)] - pub listen: Option, - /// Ipvm-specific [Argument]. #[clap(subcommand)] pub argument: Argument, } -/// An Ipvm-specific CLI argument. +/// CLI Argument types. #[derive(Debug, Parser)] pub enum Argument { - /// Provider arguments. - Provide { - /// Wasm or WAT [Cid]. - /// - /// [Cid]: libipld::cid::Cid - #[arg(short, long)] - wasm: String, - - /// Function name within Wasm module. + /// TODO: Run [Workflow] given a file. + /// + /// [Workflow]: crate::Workflow + Run { + /// Configuration file for *homestar* node settings. #[arg(short, long)] - fun: String, - - /// Parameters / arguments to Wasm function. - #[arg(short, long, num_args(0..))] - args: Vec, - }, - /// GET/read arguments. - Get { - #[clap(long)] - /// [Cid] name/pointer to content. - /// - /// [Cid]: libipld::cid::Cid - name: String, + runtime_config: Option, }, } diff --git a/homestar-runtime/src/db.rs b/homestar-runtime/src/db.rs index b0b0f889..d5f2c128 100644 --- a/homestar-runtime/src/db.rs +++ b/homestar-runtime/src/db.rs @@ -3,14 +3,124 @@ #[allow(missing_docs, unused_imports)] pub mod schema; -use diesel::prelude::*; +use crate::{settings, Receipt}; +use anyhow::Result; +use byte_unit::{AdjustedByte, Byte, ByteUnit}; +use diesel::{prelude::*, r2d2}; use dotenvy::dotenv; -use std::env; - -/// Establish connection to Sqlite database. -pub fn establish_connection() -> SqliteConnection { - dotenv().ok(); - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - SqliteConnection::establish(&database_url) - .unwrap_or_else(|_| panic!("Error connecting to {database_url}")) +use homestar_core::workflow::Pointer; +use std::{env, sync::Arc, time::Duration}; +use tokio::fs; + +/// A Sqlite connection [pool]. +/// +/// [pool]: r2d2::Pool +pub type Pool = r2d2::Pool>; +/// A [connection] from the Sqlite connection [pool]. +/// +/// [connection]: r2d2::PooledConnection +/// [pool]: r2d2::Pool +pub type Connection = r2d2::PooledConnection>; + +/// The database object, which wraps an inner [Arc] to the connection pool. +#[derive(Debug)] +pub struct Db(Arc); + +impl Clone for Db { + fn clone(&self) -> Self { + Db(Arc::clone(&self.0)) + } +} + +impl Db { + fn url() -> String { + dotenv().ok(); + env::var("DATABASE_URL").expect("DATABASE_URL must be set") + } + + /// Get size of SQlite file in megabytes (via async call). + pub async fn size() -> Result { + let len = fs::metadata(Db::url()).await?.len(); + let byte = Byte::from_bytes(len); + let byte_unit = byte.get_adjusted_unit(ByteUnit::MB); + Ok(byte_unit) + } +} + +/// Database trait for working with different Sqlite [pool] and [connection] +/// configurations. +/// +/// [pool]: Pool +/// [connection]: Connection +pub trait Database { + /// Establish a pooled connection to Sqlite database. + fn setup_connection_pool(settings: &settings::Node) -> Self; + /// Get a [pooled connection] for the database. + /// + /// [pooled connection]: Connection + fn conn(&self) -> Result; + /// Store receipt given a [Connection] to the DB [Pool]. + /// + /// On conflicts, do nothing. + fn store_receipt(receipt: Receipt, conn: &mut Connection) -> Result { + diesel::insert_into(schema::receipts::table) + .values(&receipt) + .on_conflict(schema::receipts::cid) + .do_nothing() + .get_result(conn) + .map_err(Into::into) + } + + /// Store receipts given a [Connection] to the DB [Pool]. + fn store_receipts(receipts: Vec, conn: &mut Connection) -> Result { + diesel::insert_into(schema::receipts::table) + .values(&receipts) + .execute(conn) + .map_err(Into::into) + } + + /// Find receipt for a given [Instruction] [Pointer], which is indexed. + /// + /// This *should* always return one receipt, but sometimes it's nicer to + /// work across vecs/arrays. + /// + /// [Instruction]: homestar_core::workflow::Instruction + fn find_instructions(pointers: Vec, conn: &mut Connection) -> Result> { + let found_receipts = schema::receipts::dsl::receipts + .filter(schema::receipts::instruction.eq_any(pointers)) + .load(conn)?; + Ok(found_receipts) + } + + /// Find receipt for a given [Instruction] [Pointer], which is indexed. + /// + /// [Instruction]: homestar_core::workflow::Instruction + fn find_instruction(pointer: Pointer, conn: &mut Connection) -> Result { + let found_receipt = schema::receipts::dsl::receipts + .filter(schema::receipts::instruction.eq(pointer)) + .first(conn)?; + Ok(found_receipt) + } +} + +impl Database for Db { + fn setup_connection_pool(settings: &settings::Node) -> Self { + let manager = r2d2::ConnectionManager::::new(Db::url()); + + let pool = r2d2::Pool::builder() + // Max number of conns. + .max_size(settings.db.max_pool_size) + // Never maintain idle connections + .min_idle(Some(0)) + // Close connections after 30 seconds of idle time + .idle_timeout(Some(Duration::from_secs(30))) + .build(manager) + .expect("DATABASE_URL must be set to an SQLite DB file"); + Db(Arc::new(pool)) + } + + fn conn(&self) -> Result { + let conn = self.0.get()?; + Ok(conn) + } } diff --git a/homestar-runtime/src/db/schema.rs b/homestar-runtime/src/db/schema.rs index 92648aba..c892efdb 100644 --- a/homestar-runtime/src/db/schema.rs +++ b/homestar-runtime/src/db/schema.rs @@ -4,9 +4,11 @@ diesel::table! { receipts (cid) { cid -> Text, ran -> Text, + instruction -> Text, out -> Binary, meta -> Binary, - iss -> Nullable, + issuer -> Nullable, prf -> Binary, + version -> Text, } } diff --git a/homestar-runtime/src/lib.rs b/homestar-runtime/src/lib.rs index 2855f366..158c1336 100644 --- a/homestar-runtime/src/lib.rs +++ b/homestar-runtime/src/lib.rs @@ -2,18 +2,40 @@ #![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] #![deny(unreachable_pub, private_in_public)] -//! Homestar is a determistic Wasm runtime and effectful job system intended to -//! embed inside IPFS. +//! homestar-runtime is a determistic Wasm runtime and effectful workflow/job +//! system intended to be embedded inside or run alongside IPFS. +//! //! You can find a more complete description [here]. //! -//! [here]: https://github.com/ipvm-wg/spec. +//! +//! Related crates/packages: +//! +//! - [homestar-core] +//! - [homestar-wasm] +//! +//! [here]: +//! [homestar-core]: homestar_core +//! [homestar-wasm]: homestar_wasm pub mod cli; pub mod db; +pub mod logger; pub mod network; mod receipt; +mod runtime; +pub mod scheduler; +pub mod settings; +pub mod tasks; +mod worker; +pub mod workflow; -pub use receipt::*; +pub use db::Db; +#[cfg(feature = "ipfs")] +pub use network::ipfs::IpfsCli; +pub use receipt::Receipt; +pub use runtime::*; +pub use worker::Worker; +pub use workflow::Workflow; /// Test utilities. #[cfg(any(test, feature = "test_utils"))] diff --git a/homestar-runtime/src/logger.rs b/homestar-runtime/src/logger.rs new file mode 100644 index 00000000..9c4501e2 --- /dev/null +++ b/homestar-runtime/src/logger.rs @@ -0,0 +1,59 @@ +//! Logger initialization. + +use anyhow::Result; +#[cfg(not(feature = "logfmt"))] +use tracing_subscriber::prelude::*; +#[cfg(feature = "logfmt")] +use tracing_subscriber::{layer::SubscriberExt as _, prelude::*, EnvFilter}; + +/// Initialize a [tracing_subscriber::Registry] with a [logfmt] layer. +/// +/// [logfmt]: +#[cfg(feature = "logfmt")] +pub fn init() -> Result<()> { + let registry = tracing_subscriber::Registry::default() + .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) + .with(tracing_logfmt::layer()); + + #[cfg(all(feature = "console", tokio_unstable))] + #[cfg_attr(docsrs, doc(cfg(feature = "console")))] + { + let console_layer = console_subscriber::ConsoleLayer::builder() + .retention(Duration::from_secs(60)) + .spawn(); + + registry.with(console_layer).init(); + } + + #[cfg(any(not(feature = "console"), not(tokio_unstable)))] + { + registry.init(); + } + + Ok(()) +} + +/// Initialize a default [tracing_subscriber::FmtSubscriber]. +#[cfg(not(feature = "logfmt"))] +pub fn init() -> Result<()> { + let registry = tracing_subscriber::FmtSubscriber::builder() + .with_target(false) + .finish(); + + #[cfg(all(feature = "console", tokio_unstable))] + #[cfg_attr(docsrs, doc(cfg(feature = "console")))] + { + let console_layer = console_subscriber::ConsoleLayer::builder() + .retention(Duration::from_secs(60)) + .spawn(); + + registry.with(console_layer).init(); + } + + #[cfg(any(not(feature = "console"), not(tokio_unstable)))] + { + registry.init(); + } + + Ok(()) +} diff --git a/homestar-runtime/src/main.rs b/homestar-runtime/src/main.rs index 6711e546..f35593c7 100644 --- a/homestar-runtime/src/main.rs +++ b/homestar-runtime/src/main.rs @@ -1,235 +1,58 @@ -use anyhow::{anyhow, bail, Result}; +use anyhow::Result; use clap::Parser; -use diesel::RunQueryDsl; -use homestar_core::workflow::{ - config::Resources, input::Parse, prf::UcanPrf, receipt::Receipt as LocalReceipt, Ability, - Input, Invocation, InvocationResult, Task, -}; +#[cfg(feature = "ipfs")] +use homestar_runtime::network::ipfs::IpfsCli; use homestar_runtime::{ cli::{Args, Argument}, - db::{self, schema}, + db::{Database, Db}, + logger, network::{ - client::Client, - eventloop::{Event, RECEIPTS_TOPIC}, - swarm::{self, Topic, TopicMessage}, + eventloop::{EventLoop, RECEIPTS_TOPIC}, + swarm, + ws::WebSocket, }, - Receipt, -}; -use homestar_wasm::wasmtime; -use ipfs_api::{ - request::{DagCodec, DagPut}, - response::DagPutResponse, - IpfsApi, IpfsClient, -}; -use itertools::Itertools; -use libipld::{ - cid::{multibase::Base, Cid}, - Ipld, -}; -use libp2p::{ - futures::{future, TryStreamExt}, - identity::Keypair, - multiaddr::Protocol, + settings::Settings, }; -use libp2p_identity::PeerId; -use std::{ - collections::BTreeMap, - io::{stdout, Cursor, Write}, - str::{self, FromStr}, -}; -use url::Url; +use std::sync::Arc; -#[tokio::main] +#[tokio::main(flavor = "multi_thread")] async fn main() -> Result<()> { - env_logger::init(); + logger::init()?; let opts = Args::parse(); - let keypair = Keypair::generate_ed25519(); - - let mut swarm = swarm::new(keypair).await?; - - // subscribe to `receipts` topic - swarm.behaviour_mut().gossip_subscribe(RECEIPTS_TOPIC)?; - - let (mut client, mut events, event_loop) = Client::new(swarm).await?; - tokio::spawn(event_loop.run()); + #[cfg(feature = "ipfs")] + let ipfs = IpfsCli::default(); - if let Some(addr) = opts.peer { - let peer_id = match addr.iter().last() { - Some(Protocol::P2p(hash)) => PeerId::from_multihash(hash).expect("Valid hash."), - _ => bail!("Expect peer multiaddr to contain peer ID."), - }; - client.dial(peer_id, addr).await.expect("Dial to succeed."); - } - - match opts.listen { - Some(addr) => client - .start_listening(addr) - .await - .expect("Listening not to fail."), - - None => client - .start_listening("/ip4/0.0.0.0/tcp/0".parse()?) - .await - .expect("Listening not to fail."), - }; - - // TODO: abstraction for this and redo inner parts, around ownership, etc. - // TODO: cleanup-up use, clones, etc. match opts.argument { - Argument::Get { name } => { - let cid_name = Cid::from_str(&name)?; - let cid_string = cid_name.to_string_of_base(Base::Base32Lower)?; - let providers = client.get_providers(cid_string.clone()).await?; - - if providers.is_empty() { - Err(anyhow!("could not find provider for file {name}"))?; - } - - let requests = providers.into_iter().map(|p| { - let mut client = client.clone(); - let name = cid_string.clone(); - #[allow(unknown_lints, clippy::redundant_async_block)] - Box::pin(async move { client.request_file(p, name).await }) - }); - - let file_content = future::select_ok(requests) - .await - .map_err(|_| anyhow!("none of the providers returned file"))? - .0; - - stdout().write_all(&file_content)? - } - - Argument::Provide { wasm, fun, args } => { - let ipfs = IpfsClient::default(); - - // Pull Wasm (module) *out* of IPFS - let wasm_bytes = ipfs - .cat(wasm.as_str()) - .map_ok(|chunk| chunk.to_vec()) - .try_concat() - .await?; - - let wasm_args = - // Pull arg *out* of IPFS - future::try_join_all(args.iter().map(|arg| - ipfs - .cat(arg.as_str()) - .map_ok(|chunk| { - chunk.to_vec() - }) - .try_concat() - )).await?; - - // TODO: Don't read randomly from file. - // The interior of this is test specific code, - // unil we use a format for params, like Json. - let ipld_args = wasm_args - .iter() - .map(|a| { - if let Ok(arg) = str::from_utf8(a) { - match i32::from_str(arg) { - Ok(num) => Ok::(Ipld::from(num)), - Err(_e) => Ok::(Ipld::from(arg)), - } - } else { - Err(anyhow!("Unreadable input bytes: {a:?}")) - } - }) - .fold_ok(vec![], |mut acc, elem| { - acc.push(elem); - acc - })?; - - // TODO: Only works off happy path, but need to work with traps to - // capture error. - // TODO: State will derive from resources, other configuration. - let resource = Url::parse(format!("ipfs://{wasm}").as_str()).expect("IPFS URL"); - - let task = Task::new( - resource, - Ability::from("wasm/run"), - Input::Ipld(Ipld::Map(BTreeMap::from([( - "args".into(), - Ipld::List(ipld_args), - )]))), - None, - ); - let config = Resources::default(); - let invocation = Invocation::new( - task.clone().into(), - config.clone().into(), - UcanPrf::default(), - )?; - - let mut env = - wasmtime::World::instantiate(wasm_bytes, fun, wasmtime::State::default()).await?; - let res = env.execute(task.input().parse()?.try_into()?).await?; - - let local_receipt = LocalReceipt::new( - invocation.try_into()?, - InvocationResult::Ok(res.try_into()?), - Ipld::Null, - None, - UcanPrf::default(), - ); - let receipt = Receipt::try_from(&local_receipt)?; - - let receipt_bytes: Vec = local_receipt.try_into()?; - let dag_builder = DagPut::builder() - .input_codec(DagCodec::Cbor) - .hash("sha3-256") // sadly no support for blake3-256 - .build(); - let DagPutResponse { cid } = ipfs - .dag_put_with_options(Cursor::new(receipt_bytes.clone()), dag_builder) - .await - .expect("a CID"); - - // //Test for now - assert_eq!(cid.cid_string, receipt.cid()); - - let mut conn = db::establish_connection(); - // TODO: insert (or upsert via event handling when subscribed) - diesel::insert_into(schema::receipts::table) - .values(&receipt) - .on_conflict(schema::receipts::cid) - .do_nothing() - .execute(&mut conn) - .expect("Error saving new receipt"); - println!("stored: {receipt}"); - - let invoked_cid = receipt.ran(); - let output = receipt.output().clone(); - let async_client = client.clone(); - // We delay messages to make sure peers are within the mesh. - tokio::spawn(async move { - // TODO: make this configurable, but currently matching heartbeat. - tokio::time::sleep(std::time::Duration::from_secs(10)).await; - let _ = async_client - .publish_message( - Topic::new(RECEIPTS_TOPIC.to_string()), - TopicMessage::Receipt(receipt), - ) - .await; - }); - - let _ = client.start_providing(invoked_cid.clone()).await; - - loop { - match events.recv().await { - Some(Event::InboundRequest { request, channel }) => { - if request.eq(&invoked_cid) { - let output = format!("{output:?}"); - client.respond_file(output.into_bytes(), channel).await?; - } - } - e => todo!("{:?}", e), - } - } + Argument::Run { runtime_config } => { + let settings = if let Some(file) = runtime_config { + Settings::load_from_file(file) + } else { + Settings::load() + }?; + + let db = Db::setup_connection_pool(settings.node()); + let mut swarm = swarm::new(settings.node()).await?; + + // subscribe to `receipts` topic + swarm.behaviour_mut().gossip_subscribe(RECEIPTS_TOPIC)?; + + let (_tx, rx) = EventLoop::setup_channel(settings.node()); + // instantiate and start event-loop for events + let eventloop = EventLoop::new(swarm, rx, settings.node()); + + #[cfg(not(feature = "ipfs"))] + tokio::spawn(eventloop.run(db)); + + #[cfg(feature = "ipfs")] + tokio::spawn(eventloop.run(db, ipfs)); + + let (ws_tx, ws_rx) = WebSocket::setup_channel(settings.node()); + let ws_sender = Arc::new(ws_tx); + let ws_receiver = Arc::new(ws_rx); + WebSocket::start_server(ws_sender, ws_receiver, settings.node()).await?; + Ok(()) } } - - Ok(()) } diff --git a/homestar-runtime/src/network/client.rs b/homestar-runtime/src/network/client.rs deleted file mode 100644 index d7c2d179..00000000 --- a/homestar-runtime/src/network/client.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! - -use crate::network::{ - eventloop::{Event, EventLoop}, - swarm::{ComposedBehaviour, Topic, TopicMessage}, -}; -use anyhow::Result; -use libp2p::{request_response::ResponseChannel, Multiaddr, PeerId, Swarm}; -use std::collections::HashSet; -use tokio::sync::{mpsc, oneshot}; - -/// A client for interacting with the [libp2p] networking layer. -#[derive(Clone, Debug)] -pub struct Client { - sender: mpsc::Sender, -} - -impl Client { - /// Initialize a client with an event [mpsc::Receiver] and [EventLoop]. - pub async fn new( - swarm: Swarm, - ) -> Result<(Self, mpsc::Receiver, EventLoop)> { - let (command_sender, command_receiver) = mpsc::channel(1); - let (event_sender, event_receiver) = mpsc::channel(1); - - Ok(( - Client { - sender: command_sender, - }, - event_receiver, - EventLoop::new(swarm, command_receiver, event_sender), - )) - } - - /// Publish a [message] to a topic on a running pubsub protocol. - /// - /// [message]: TopicMessage - pub async fn publish_message(&self, topic: Topic, msg: TopicMessage) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.sender - .send(Command::PublishMessage { msg, sender, topic }) - .await?; - receiver.await? - } - - /// Listen for incoming connections on the given address. - pub async fn start_listening(&mut self, addr: Multiaddr) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.sender - .send(Command::StartListening { addr, sender }) - .await?; - receiver.await? - } - - /// Dial the given peer at the given address. - pub async fn dial(&mut self, peer_id: PeerId, peer_addr: Multiaddr) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.sender - .send(Command::Dial { - peer_id, - peer_addr, - sender, - }) - .await?; - receiver.await? - } - - /// Advertise the local node as the provider of the given file on the DHT. - pub async fn start_providing(&mut self, file_name: String) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - self.sender - .send(Command::StartProviding { file_name, sender }) - .await?; - receiver.await? - } - - /// Find the providers for the given file on the DHT. - pub async fn get_providers(&mut self, file_name: String) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.sender - .send(Command::GetProviders { file_name, sender }) - .await?; - receiver.await? - } - - /// Request the content of the given file from the given peer. - pub async fn request_file(&mut self, peer: PeerId, file_name: String) -> Result> { - let (sender, receiver) = oneshot::channel(); - self.sender - .send(Command::RequestFile { - file_name, - peer, - sender, - }) - .await?; - receiver.await? - } - - /// Respond with the provided file content to the given request. - pub async fn respond_file( - &mut self, - file: Vec, - channel: ResponseChannel, - ) -> Result<()> { - self.sender - .send(Command::RespondFile { file, channel }) - .await?; - Ok(()) - } -} - -/// Wrapper-type for file request name. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FileRequest(pub(crate) String); - -/// Wrapper-type for file response content/bytes. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FileResponse(pub(crate) Vec); - -#[derive(Debug)] -/// [Client] commands. -pub enum Command { - /// Start listening on an address. - StartListening { - /// Address to listen on. - addr: Multiaddr, - /// Channel to send on. - sender: oneshot::Sender>, - }, - /// Dial a peer in the cluster. - Dial { - /// Peer identifier. - peer_id: PeerId, - /// Peer address. - peer_addr: Multiaddr, - /// Channel to send on. - sender: oneshot::Sender>, - }, - /// Start providing content over channel. - StartProviding { - /// File. - file_name: String, - /// Channel to send on. - sender: oneshot::Sender>, - }, - /// Lookup providers for given key (file). - GetProviders { - /// File. - file_name: String, - /// Channel to send on. - sender: oneshot::Sender>>, - }, - /// Request file from peer. - /// TODO: File type(s)? - RequestFile { - /// File. - file_name: String, - /// Peer identifier. - peer: PeerId, - /// Channel to send on. - sender: oneshot::Sender>>, - }, - /// Respond with file content. - RespondFile { - /// File content. - file: Vec, - /// Channel to send on. - channel: ResponseChannel, - }, - /// Publish message on the topic name. - PublishMessage { - /// Pubsub (i.e. [libp2p::gossipsub]) topic. - topic: Topic, - /// [TopicMessage]. - msg: TopicMessage, - /// Channel to send on. - sender: oneshot::Sender>, - }, -} diff --git a/homestar-runtime/src/network/eventloop.rs b/homestar-runtime/src/network/eventloop.rs index 5572ffc0..ba62bea2 100644 --- a/homestar-runtime/src/network/eventloop.rs +++ b/homestar-runtime/src/network/eventloop.rs @@ -1,155 +1,328 @@ //! [EventLoop] implementation for handling network events and messages, as well //! as commands for the running [libp2p] node. +use super::swarm::TopicMessage; +#[cfg(feature = "ipfs")] +use crate::IpfsCli; use crate::{ - network::{ - client::{Command, FileRequest, FileResponse}, - swarm::{ComposedBehaviour, ComposedEvent}, - }, + db::{Database, Db}, + network::swarm::{ComposedBehaviour, ComposedEvent}, + settings, + workflow::WorkflowInfo, Receipt, }; use anyhow::{anyhow, Result}; +use concat_in_place::veccat; +use crossbeam::channel; +use homestar_core::{ + consts, + workflow::{Pointer, Receipt as InvocationReceipt}, +}; +use libipld::Cid; use libp2p::{ - floodsub::FloodsubEvent, futures::StreamExt, gossipsub, - kad::{GetProvidersOk, KademliaEvent, QueryId, QueryResult}, + kad::{ + record::Key, AddProviderOk, BootstrapOk, GetProvidersOk, GetRecordOk, KademliaEvent, + PeerRecord, PutRecordOk, QueryId, QueryResult, Quorum, Record, + }, mdns, multiaddr::Protocol, - request_response::{self, RequestId, ResponseChannel}, swarm::{Swarm, SwarmEvent}, }; -use libp2p_identity::PeerId; -use std::collections::{hash_map, HashMap, HashSet}; -use tokio::sync::{mpsc, oneshot}; +use std::{collections::HashMap, fmt, num::NonZeroUsize, str}; +use tokio::sync::mpsc; /// [Receipt]-related topic for pub(gossip)sub. /// /// [Receipt]: homestar_core::workflow::receipt pub const RECEIPTS_TOPIC: &str = "receipts"; +type WorkerSender = channel::Sender<(Cid, Receipt)>; + /// Event loop handler for [libp2p] network events and commands. #[allow(missing_debug_implementations)] pub struct EventLoop { + receiver: mpsc::Receiver, + receipt_quorum: usize, + worker_senders: HashMap, swarm: Swarm, - command_receiver: mpsc::Receiver, - event_sender: mpsc::Sender, - pending_dial: HashMap>>, - pending_start_providing: HashMap>>, - pending_get_providers: HashMap>>>, - pending_request_file: HashMap>>>, } impl EventLoop { + /// Setup bounded, MPSC channel for runtime to send and receive internal + /// events with workers. + pub fn setup_channel( + settings: &settings::Node, + ) -> (mpsc::Sender, mpsc::Receiver) { + mpsc::channel(settings.network.events_buffer_len) + } + /// Create an [EventLoop] with channel sender/receiver defaults. pub fn new( swarm: Swarm, - command_receiver: mpsc::Receiver, - event_sender: mpsc::Sender, + receiver: mpsc::Receiver, + settings: &settings::Node, ) -> Self { Self { + receiver, + receipt_quorum: settings.network.receipt_quorum, + worker_senders: HashMap::new(), swarm, - command_receiver, - event_sender, - pending_dial: Default::default(), - pending_start_providing: Default::default(), - pending_get_providers: Default::default(), - pending_request_file: Default::default(), } } /// Loop and select over swarm and pubsub [events] and client [commands]. /// /// [events]: SwarmEvent - /// [commands]: Command - pub async fn run(mut self) -> Result<()> { + #[cfg(not(feature = "ipfs"))] + pub async fn run(mut self, db: Db) -> Result<()> { loop { tokio::select! { - event = self.swarm.select_next_some() => self.handle_event(event).await, - command = self.command_receiver.recv() => if let Some(c) = command {self.handle_command(c).await} + swarm_event = self.swarm.select_next_some() => self.handle_event(swarm_event, db.clone()).await, + runtime_event = self.receiver.recv() => if let Some(ev) = runtime_event { self.handle_runtime_event(ev).await }, } } } - async fn handle_event( - &mut self, - event: SwarmEvent, - ) { + /// Loop and select over swarm and pubsub [events]. + /// + /// [events]: SwarmEvent + #[cfg(feature = "ipfs")] + pub async fn run(mut self, db: Db, ipfs: IpfsCli) -> Result<()> { + loop { + tokio::select! { + swarm_event = self.swarm.select_next_some() => self.handle_event(swarm_event, db.clone()).await, + runtime_event = self.receiver.recv() => if let Some(ev) = runtime_event { self.handle_runtime_event(ev, ipfs.clone()).await }, + } + } + } + + #[cfg(not(feature = "ipfs"))] + async fn handle_runtime_event(&mut self, event: Event) { match event { - SwarmEvent::Behaviour(ComposedEvent::Floodsub(FloodsubEvent::Message(message))) => { - match Receipt::try_from(message.data) { - Ok(receipt) => println!("got message: {receipt}"), + Event::CapturedReceipt(receipt, workflow_info) => { + match self.on_capture(receipt, workflow_info) { + Ok((cid, _bytes)) => { + tracing::debug!( + cid = cid, + "record replicated with quorum {}", + self.receipt_quorum + ) + } Err(err) => { - println!("cannot handle_message: {err}") + tracing::error!(error=?err, "error putting record on DHT with quorum {}", self.receipt_quorum) } } } - SwarmEvent::Behaviour(ComposedEvent::Floodsub(FloodsubEvent::Subscribed { - peer_id, - topic, - })) => { - println!("{peer_id} subscribed to topic {} over pubsub", topic.id()) + Event::FindReceipt(cid, sender) => self.on_find_receipt(cid, sender), + } + } + + #[cfg(feature = "ipfs")] + async fn handle_runtime_event(&mut self, event: Event, ipfs: IpfsCli) { + match event { + Event::CapturedReceipt(receipt, workflow_info) => { + match self.on_capture(receipt, workflow_info) { + Ok((cid, bytes)) => { + tracing::debug!( + cid = cid, + "record replicated with quorum {}", + self.receipt_quorum + ); + + // Spawn client call in background, without awaiting. + tokio::spawn(async move { + match ipfs.put_receipt_bytes(bytes.to_vec()).await { + Ok(put_cid) => { + tracing::info!(cid = put_cid, "IPLD DAG node stored"); + + #[cfg(debug_assertions)] + debug_assert_eq!(put_cid, cid); + } + Err(err) => { + tracing::info!(error=?err, cid=cid, "Failed to store IPLD DAG node") + } + } + }); + } + Err(err) => { + tracing::error!(error=?err, "error putting record on DHT with quorum {}", self.receipt_quorum) + } + } } - SwarmEvent::Behaviour(ComposedEvent::Floodsub(_)) => {} + Event::FindReceipt(cid, sender) => self.on_find_receipt(cid, sender), + } + } + + fn on_capture( + &mut self, + receipt: Receipt, + mut workflow_info: WorkflowInfo, + ) -> Result<(String, Vec)> { + let receipt_cid = receipt.cid(); + let invocation_receipt = InvocationReceipt::from(&receipt); + let instruction_bytes = receipt.instruction_cid_as_bytes(); + match self.swarm.behaviour_mut() + .gossip_publish(RECEIPTS_TOPIC, TopicMessage::CapturedReceipt(receipt)) { + Ok(msg_id) => + tracing::info!("message {msg_id} published on {RECEIPTS_TOPIC} for receipt with cid: {receipt_cid}"), + Err(err) => tracing::error!(error=?err, "message not published on {RECEIPTS_TOPIC} for receipt with cid: {receipt_cid}") + } + + let quorum = if self.receipt_quorum > 0 { + unsafe { Quorum::N(NonZeroUsize::new_unchecked(self.receipt_quorum)) } + } else { + Quorum::One + }; + + if let Ok(receipt_bytes) = Vec::try_from(invocation_receipt) { + let ref_bytes = &receipt_bytes; + let value = veccat!(consts::INVOCATION_VERSION.as_bytes() ref_bytes); + let _id = self + .swarm + .behaviour_mut() + .kademlia + .put_record(Record::new(instruction_bytes, value.to_vec()), quorum) + .map_err(anyhow::Error::msg)?; + + // increment progress with capture + workflow_info.increment_progress(); + let _id = self + .swarm + .behaviour_mut() + .kademlia + .put_record( + Record::new(workflow_info.cid.to_bytes(), Vec::try_from(workflow_info)?), + quorum, + ) + .map_err(anyhow::Error::msg)?; + + Ok((receipt_cid, receipt_bytes)) + } else { + Err(anyhow!("cannot convert receipt {receipt_cid} to bytes")) + } + } + + fn on_find_receipt(&mut self, instruction_cid: Cid, sender: WorkerSender) { + let id = self + .swarm + .behaviour_mut() + .kademlia + .get_record(Key::new(&instruction_cid.to_bytes())); + self.worker_senders.insert(id, sender); + } + + fn on_found_record(key_cid: Cid, value: Vec) -> Result { + if value.starts_with(consts::INVOCATION_VERSION.as_bytes()) { + let receipt_bytes = &value[consts::INVOCATION_VERSION.as_bytes().len()..]; + let invocation_receipt = InvocationReceipt::try_from(receipt_bytes.to_vec())?; + Receipt::try_with(Pointer::new(key_cid), &invocation_receipt) + } else { + Err(anyhow!( + "receipt version mismatch, current version: {}", + consts::INVOCATION_VERSION + )) + } + } + + async fn handle_event( + &mut self, + event: SwarmEvent, + db: Db, + ) { + match event { SwarmEvent::Behaviour(ComposedEvent::Gossipsub(gossipsub::Event::Message { message, propagation_source, message_id, - })) => - match Receipt::try_from(message.data) { - Ok(receipt) => println!( + })) => match Receipt::try_from(message.data) { + Ok(receipt) => { + tracing::info!( "got message: {receipt} from {propagation_source} with message id: {message_id}" - ), + ); - Err(err) => println!( - "cannot handle_message: {err}" - ) - } + // Store gossiped receipt. + let _ = db + .conn() + .as_mut() + .map(|conn| Db::store_receipt(receipt, conn)); + } + Err(err) => tracing::info!(err=?err, "cannot handle incoming event message"), + }, SwarmEvent::Behaviour(ComposedEvent::Gossipsub(gossipsub::Event::Subscribed { peer_id, topic, })) => { - println!("{peer_id} subscribed to topic {topic} over gossipsub") + tracing::debug!("{peer_id} subscribed to topic {topic} over gossipsub") } SwarmEvent::Behaviour(ComposedEvent::Gossipsub(_)) => {} SwarmEvent::Behaviour(ComposedEvent::Kademlia( - KademliaEvent::OutboundQueryProgressed { - id, - result: QueryResult::StartProviding(_), + KademliaEvent::OutboundQueryProgressed { id, result, .. }, + )) => match result { + QueryResult::Bootstrap(Ok(BootstrapOk { peer, .. })) => { + tracing::debug!("successfully bootstrapped peer: {peer}") + } + QueryResult::GetProviders(Ok(GetProvidersOk::FoundProviders { + key, + providers, .. - }, - )) => { - let sender = self - .pending_start_providing - .remove(&id) - .expect("Completed query to be previously pending"); - let _ = sender.send(Ok(())); - } - SwarmEvent::Behaviour(ComposedEvent::Kademlia( - KademliaEvent::OutboundQueryProgressed { - id, - result: - QueryResult::GetProviders(Ok(GetProvidersOk::FoundProviders { - providers, .. - })), + })) => { + for peer in providers { + tracing::debug!("peer {peer} provides key: {key:#?}"); + } + } + QueryResult::GetProviders(Err(err)) => { + tracing::error!("error retrieving outbound query providers: {err}") + } + QueryResult::GetRecord(Ok(GetRecordOk::FoundRecord(PeerRecord { + record: + Record { + key, + value, + publisher, + .. + }, .. - }, - )) => { - let _ = self - .pending_get_providers - .remove(&id) - .expect("Completed query to be previously pending") - .send(Ok(providers)); - } + }))) => { + tracing::debug!("found record {key:#?}, published by {publisher:?}"); + if let Ok(cid) = Cid::try_from(key.as_ref()) { + match Self::on_found_record(cid, value) { + Ok(receipt) => { + tracing::info!("found receipt: {receipt}"); + if let Some(sender) = self.worker_senders.remove(&id) { + let _ = sender.send((cid, receipt)); + } else { + tracing::error!("error converting key {key:#?} to cid") + } + } + Err(err) => tracing::error!(err=?err, "error retrieving receipt"), + } + } + } + QueryResult::GetRecord(Ok(_)) => {} + QueryResult::GetRecord(Err(err)) => { + tracing::error!("error retrieving record: {err}"); + } + QueryResult::PutRecord(Ok(PutRecordOk { key })) => { + tracing::debug!("successfully put record {key:#?}"); + } + QueryResult::PutRecord(Err(err)) => { + tracing::error!("error putting record: {err}") + } + QueryResult::StartProviding(Ok(AddProviderOk { key })) => { + tracing::debug!("successfully put provider record {key:#?}"); + } + QueryResult::StartProviding(Err(err)) => { + tracing::error!("error putting provider record: {err}"); + } + _ => {} + }, SwarmEvent::Behaviour(ComposedEvent::Kademlia(_)) => {} SwarmEvent::Behaviour(ComposedEvent::Mdns(mdns::Event::Discovered(list))) => { for (peer_id, _multiaddr) in list { - println!("mDNS discovered a new peer: {peer_id}"); - self.swarm - .behaviour_mut() - .floodsub - .add_node_to_partial_view(peer_id); + tracing::info!("mDNS discovered a new peer: {peer_id}"); self.swarm .behaviour_mut() @@ -159,12 +332,7 @@ impl EventLoop { } SwarmEvent::Behaviour(ComposedEvent::Mdns(mdns::Event::Expired(list))) => { for (peer_id, _multiaddr) in list { - println!("mDNS discover peer has expired: {peer_id}"); - - self.swarm - .behaviour_mut() - .floodsub - .remove_node_from_partial_view(&peer_id); + tracing::info!("mDNS discover peer has expired: {peer_id}"); self.swarm .behaviour_mut() @@ -172,174 +340,48 @@ impl EventLoop { .remove_explicit_peer(&peer_id); } } - SwarmEvent::Behaviour(ComposedEvent::RequestResponse( - request_response::Event::Message { message, .. }, - )) => match message { - request_response::Message::Request { - request, channel, .. - } => { - self.event_sender - .send(Event::InboundRequest { - request: request.0, - channel, - }) - .await - .expect("Event receiver not to be dropped"); - } - request_response::Message::Response { - request_id, - response, - } => { - let _ = self - .pending_request_file - .remove(&request_id) - .expect("Request to still be pending") - .send(Ok(response.0)); - } - }, - SwarmEvent::Behaviour(ComposedEvent::RequestResponse( - request_response::Event::InboundFailure { - request_id: _, - error: _, - .. - }, - )) => {} - SwarmEvent::Behaviour(ComposedEvent::RequestResponse( - request_response::Event::OutboundFailure { - request_id, error, .. - }, - )) => { - let _ = self - .pending_request_file - .remove(&request_id) - .expect("Request to still be pending") - .send(Err(anyhow!(error))); - } - SwarmEvent::Behaviour(ComposedEvent::RequestResponse( - request_response::Event::ResponseSent { .. }, - )) => {} SwarmEvent::NewListenAddr { address, .. } => { let local_peer_id = *self.swarm.local_peer_id(); - println!( + tracing::info!( "local node is listening on {:?}", address.with(Protocol::P2p(local_peer_id.into())) ); } SwarmEvent::IncomingConnection { .. } => {} - SwarmEvent::ConnectionEstablished { - peer_id, endpoint, .. - } => { - if endpoint.is_dialer() { - if let Some(sender) = self.pending_dial.remove(&peer_id) { - let _ = sender.send(Ok(())); - } - } - } - SwarmEvent::ConnectionClosed { .. } => {} - SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { - if let Some(peer_id) = peer_id { - if let Some(sender) = self.pending_dial.remove(&peer_id) { - let _ = sender.send(Err(anyhow!(error))); - } - } - } - SwarmEvent::IncomingConnectionError { .. } => {} - SwarmEvent::Dialing(peer_id) => println!("dialing {peer_id}"), - e => panic!("{e:?}"), - } - } - - async fn handle_command(&mut self, command: Command) { - match command { - Command::PublishMessage { topic, msg, sender } => { - let _ = match self - .swarm - .behaviour_mut() - .gossip_publish(&topic.to_string(), msg) - { - Ok(_) => sender.send(Ok(())), - Err(e) => sender.send(Err(anyhow!(e))), - }; - } - Command::StartListening { addr, sender } => { - let _ = match self.swarm.listen_on(addr) { - Ok(_) => sender.send(Ok(())), - Err(e) => sender.send(Err(anyhow!(e))), - }; - } - Command::Dial { - peer_id, - peer_addr, - sender, - } => { - if let hash_map::Entry::Vacant(e) = self.pending_dial.entry(peer_id) { - self.swarm - .behaviour_mut() - .kademlia - .add_address(&peer_id, peer_addr.clone()); - match self - .swarm - .dial(peer_addr.with(Protocol::P2p(peer_id.into()))) - { - Ok(()) => { - e.insert(sender); - } - Err(e) => { - let _ = sender.send(Err(anyhow!(e))); - } - } - } else { - todo!("Already dialing peer."); - } - } - Command::StartProviding { file_name, sender } => { - let query_id = self - .swarm - .behaviour_mut() - .kademlia - .start_providing(file_name.into_bytes().into()) - .expect("No store error"); - self.pending_start_providing.insert(query_id, sender); - } - Command::GetProviders { file_name, sender } => { - let query_id = self - .swarm - .behaviour_mut() - .kademlia - .get_providers(file_name.into_bytes().into()); - self.pending_get_providers.insert(query_id, sender); - } - Command::RequestFile { - file_name, - peer, - sender, - } => { - let request_id = self - .swarm - .behaviour_mut() - .request_response - .send_request(&peer, FileRequest(file_name)); - self.pending_request_file.insert(request_id, sender); - } - Command::RespondFile { file, channel } => { - self.swarm - .behaviour_mut() - .request_response - .send_response(channel, FileResponse(file)) - .expect("Connection to peer to be still open"); - } + _ => {} } } } /// Internal events to capture. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Event { - /// Information about a received and handled inbound request. - InboundRequest { - /// Request name or [libipld::cid::Cid]. - request: String, - /// Response channel. - channel: ResponseChannel, - }, + /// [Receipt] stored and captured event. + CapturedReceipt(Receipt, WorkflowInfo), + /// Find a [Receipt] stored in the DHT. + /// + /// [Receipt]: InvocationReceipt + FindReceipt(Cid, WorkerSender), +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_utils; + + #[test] + fn found_record() { + let (invocation_receipt, receipt) = test_utils::receipt::receipts(); + let instruction_bytes = receipt.instruction_cid_as_bytes(); + let bytes = Vec::try_from(invocation_receipt).unwrap(); + let ref_bytes = &bytes; + let value = veccat!(consts::INVOCATION_VERSION.as_bytes() ref_bytes); + let record = Record::new(instruction_bytes, value.to_vec()); + let record_value = record.value; + let found_receipt = + EventLoop::on_found_record(Cid::try_from(receipt.instruction()).unwrap(), record_value) + .unwrap(); + + assert_eq!(found_receipt, receipt); + } } diff --git a/homestar-runtime/src/network/ipfs.rs b/homestar-runtime/src/network/ipfs.rs new file mode 100644 index 00000000..1db3f458 --- /dev/null +++ b/homestar-runtime/src/network/ipfs.rs @@ -0,0 +1,72 @@ +//! Ipfs Client container for an [Arc]'ed [IpfsClient]. + +use anyhow::Result; +use futures::TryStreamExt; +use homestar_core::workflow::Receipt; +use ipfs_api::{ + request::{DagCodec, DagPut}, + response::DagPutResponse, + IpfsApi, IpfsClient, +}; +use libipld::{Cid, Ipld}; +use std::{io::Cursor, sync::Arc}; +use url::Url; + +const SHA3_256: &str = "sha3-256"; + +/// [IpfsClient]-wrapper. +#[allow(missing_debug_implementations)] +pub struct IpfsCli(Arc); + +impl Clone for IpfsCli { + fn clone(&self) -> Self { + IpfsCli(Arc::clone(&self.0)) + } +} + +impl Default for IpfsCli { + fn default() -> Self { + Self(Arc::new(IpfsClient::default())) + } +} + +impl IpfsCli { + /// Retrieve content from a [Url]. + pub async fn get_resource(&self, url: &Url) -> Result> { + let cid = Cid::try_from(url.to_string())?; + self.get_cid(cid).await + } + + /// Retrieve content from a [Cid]. + pub async fn get_cid(&self, cid: Cid) -> Result> { + self.0 + .cat(&cid.to_string()) + .map_ok(|chunk| chunk.to_vec()) + .try_concat() + .await + .map_err(Into::into) + } + + /// Put/Write [Receipt] into IPFS. + pub async fn put_receipt(&self, receipt: Receipt) -> Result { + let receipt_bytes: Vec = receipt.try_into()?; + self.put_receipt_bytes(receipt_bytes).await + } + + /// Put/Write [Receipt], as bytes, into IPFS. + pub async fn put_receipt_bytes(&self, receipt_bytes: Vec) -> Result { + let dag_builder = DagPut::builder() + .store_codec(DagCodec::Cbor) + .input_codec(DagCodec::Cbor) + .hash(SHA3_256) // sadly no support for blake3-256 + .build(); + + let DagPutResponse { cid } = self + .0 + .dag_put_with_options(Cursor::new(receipt_bytes.clone()), dag_builder) + .await + .expect("a CID"); + + Ok(cid.cid_string) + } +} diff --git a/homestar-runtime/src/network/mod.rs b/homestar-runtime/src/network/mod.rs index 6eb9f348..27d0bbb4 100644 --- a/homestar-runtime/src/network/mod.rs +++ b/homestar-runtime/src/network/mod.rs @@ -1,8 +1,12 @@ -//! [libp2p] networking interface. +//! [libp2p], [websocket], and [ipfs] networking interfaces. //! //! [libp2p]: +//! [websocket]: ws +//! [ipfs]: ipfs -pub mod client; pub mod eventloop; +#[cfg(feature = "ipfs")] +pub mod ipfs; pub mod pubsub; pub mod swarm; +pub mod ws; diff --git a/homestar-runtime/src/network/pubsub.rs b/homestar-runtime/src/network/pubsub.rs index bfa399e4..876334aa 100644 --- a/homestar-runtime/src/network/pubsub.rs +++ b/homestar-runtime/src/network/pubsub.rs @@ -1,11 +1,9 @@ -//! [gossipsub] and [Floodsub] initializers for PubSub across connected peers. +//! [gossipsub] initializer for PubSub across connected peers. use anyhow::Result; use libp2p::{ - floodsub::Floodsub, gossipsub::{self, ConfigBuilder, Message, MessageAuthenticity, MessageId, ValidationMode}, identity::Keypair, - PeerId, }; use std::{ collections::hash_map::DefaultHasher, @@ -13,13 +11,10 @@ use std::{ time::Duration, }; -/// Setup direct [Floodsub] protocol with a given [PeerId]. -pub fn new_floodsub(peer_id: PeerId) -> Floodsub { - Floodsub::new(peer_id) -} +use crate::settings; /// Setup [gossipsub] mesh protocol with default configuration. -pub fn new_gossipsub(keypair: Keypair) -> Result { +pub fn new(keypair: Keypair, settings: &settings::Node) -> Result { // To content-address message, we can take the hash of message and use it as an ID. let message_id_fn = |message: &Message| { let mut s = DefaultHasher::new(); @@ -27,9 +22,8 @@ pub fn new_gossipsub(keypair: Keypair) -> Result { MessageId::from(s.finish().to_string()) }; - // TODO: Make configurable let gossipsub_config = ConfigBuilder::default() - .heartbeat_interval(Duration::from_secs(10)) + .heartbeat_interval(Duration::from_secs(settings.network.pubsub_heartbeat_secs)) // This sets the kind of message validation. The default is Strict (enforce message signing). .validation_mode(ValidationMode::Strict) .mesh_n_low(1) diff --git a/homestar-runtime/src/network/swarm.rs b/homestar-runtime/src/network/swarm.rs index acebd9d6..0961e801 100644 --- a/homestar-runtime/src/network/swarm.rs +++ b/homestar-runtime/src/network/swarm.rs @@ -1,60 +1,49 @@ //! Sets up a [libp2p] [Swarm], containing the state of the network and the way //! it should behave. -use crate::{ - network::{ - client::{FileRequest, FileResponse}, - pubsub, - }, - Receipt, -}; +use crate::{network::pubsub, settings, Receipt}; use anyhow::{anyhow, Result}; -use async_trait::async_trait; use libp2p::{ - core::upgrade::{self, read_length_prefixed, write_length_prefixed, ProtocolName}, - floodsub::{self, Floodsub, FloodsubEvent}, - futures::{AsyncRead, AsyncWrite, AsyncWriteExt}, + core::upgrade, gossipsub::{self, MessageId, SubscriptionError, TopicHash}, identity::Keypair, kad::{record::store::MemoryStore, Kademlia, KademliaEvent}, mdns, noise, - request_response::{self, ProtocolSupport}, swarm::{NetworkBehaviour, Swarm, SwarmBuilder}, - tcp, - yamux::YamuxConfig, - Transport, + tcp, yamux, Transport, }; -use std::{fmt, io, iter}; +use std::fmt; /// Build a new [Swarm] with a given transport and a tokio executor. -pub async fn new(keypair: Keypair) -> Result> { +pub async fn new(settings: &settings::Node) -> Result> { + let keypair = Keypair::generate_ed25519(); let peer_id = keypair.public().to_peer_id(); let transport = tcp::tokio::Transport::new(tcp::Config::default().nodelay(true)) - .upgrade(upgrade::Version::V1) + .upgrade(upgrade::Version::V1Lazy) .authenticate( - noise::NoiseAuthenticated::xx(&keypair) - .expect("Signing libp2p-noise static DH keypair failed"), + noise::Config::new(&keypair).expect("Signing libp2p-noise static DH keypair failed"), ) - .multiplex(YamuxConfig::default()) + .multiplex(yamux::Config::default()) + // TODO: configure + //.timeout(Duration::from_secs(5)) .boxed(); - Ok(SwarmBuilder::with_tokio_executor( + let mut swarm = SwarmBuilder::with_tokio_executor( transport, ComposedBehaviour { - floodsub: pubsub::new_floodsub(peer_id), - gossipsub: pubsub::new_gossipsub(keypair)?, + gossipsub: pubsub::new(keypair, settings)?, kademlia: Kademlia::new(peer_id, MemoryStore::new(peer_id)), mdns: mdns::Behaviour::new(mdns::Config::default(), peer_id)?, - request_response: request_response::Behaviour::new( - FileExchangeCodec(), - iter::once((FileExchangeProtocol(), ProtocolSupport::Full)), - Default::default(), - ), }, peer_id, ) - .build()) + .build(); + + // Listen-on given address + swarm.listen_on(settings.network.listen_address.to_string().parse()?)?; + + Ok(swarm) } /// Custom event types to listen for and respond to. @@ -62,14 +51,10 @@ pub async fn new(keypair: Keypair) -> Result> { pub enum ComposedEvent { /// [gossipsub::Event] event. Gossipsub(gossipsub::Event), - /// [floodsub::FloodsubEvent] event. - Floodsub(FloodsubEvent), /// [KademliaEvent] event. Kademlia(KademliaEvent), /// [mdns::Event] event. Mdns(mdns::Event), - /// [request_response::Event] event. - RequestResponse(request_response::Event), } /// Message topic. @@ -93,7 +78,7 @@ impl Topic { #[derive(Debug)] pub enum TopicMessage { /// Receipt topic, wrapping [Receipt]. - Receipt(Receipt), + CapturedReceipt(Receipt), } /// Custom behaviours for [Swarm]. @@ -103,33 +88,13 @@ pub enum TopicMessage { pub struct ComposedBehaviour { /// [gossipsub::Behaviour] behaviour. pub gossipsub: gossipsub::Behaviour, - /// [floodsub::Floodsub] behaviour. - pub floodsub: Floodsub, /// In-memory [kademlia: Kademlia] behaviour. pub kademlia: Kademlia, /// [mdns::tokio::Behaviour] behaviour. pub mdns: mdns::tokio::Behaviour, - /// [request_response::Behaviour] behaviour. - pub request_response: request_response::Behaviour, } impl ComposedBehaviour { - /// Subscribe to [Floodsub] topic. - pub fn subscribe(&mut self, topic: &str) -> bool { - let topic = floodsub::Topic::new(topic); - self.floodsub.subscribe(topic) - } - - /// Serialize [TopicMessage] and publish to [Floodsub] topic. - pub fn publish(&mut self, topic: &str, msg: TopicMessage) -> Result<()> { - let id_topic = floodsub::Topic::new(topic); - // Make this an or msg to match on other topics. - let TopicMessage::Receipt(receipt) = msg; - let msg_bytes: Vec = receipt.try_into()?; - self.floodsub.publish(id_topic, msg_bytes); - Ok(()) - } - /// Subscribe to [gossipsub] topic. pub fn gossip_subscribe(&mut self, topic: &str) -> Result { let topic = gossipsub::IdentTopic::new(topic); @@ -139,8 +104,8 @@ impl ComposedBehaviour { /// Serialize [TopicMessage] and publish to [gossipsub] topic. pub fn gossip_publish(&mut self, topic: &str, msg: TopicMessage) -> Result { let id_topic = gossipsub::IdentTopic::new(topic); - // Make this an or msg to match on other topics. - let TopicMessage::Receipt(receipt) = msg; + // Make this a match once we have other topics. + let TopicMessage::CapturedReceipt(receipt) = msg; let msg_bytes: Vec = receipt.try_into()?; if self .gossipsub @@ -165,12 +130,6 @@ impl From for ComposedEvent { } } -impl From for ComposedEvent { - fn from(event: FloodsubEvent) -> Self { - ComposedEvent::Floodsub(event) - } -} - impl From for ComposedEvent { fn from(event: KademliaEvent) -> Self { ComposedEvent::Kademlia(event) @@ -182,94 +141,3 @@ impl From for ComposedEvent { ComposedEvent::Mdns(event) } } - -impl From> for ComposedEvent { - fn from(event: request_response::Event) -> Self { - ComposedEvent::RequestResponse(event) - } -} - -/// Simple file-exchange protocol. -#[derive(Debug, Clone)] -pub struct FileExchangeProtocol(); - -/// File-exchange codec. -#[derive(Debug, Clone)] -pub struct FileExchangeCodec(); - -impl ProtocolName for FileExchangeProtocol { - fn protocol_name(&self) -> &[u8] { - "/file-exchange/1".as_bytes() - } -} - -#[async_trait] -impl request_response::codec::Codec for FileExchangeCodec { - type Protocol = FileExchangeProtocol; - type Request = FileRequest; - type Response = FileResponse; - - async fn read_request( - &mut self, - _: &FileExchangeProtocol, - io: &mut T, - ) -> io::Result - where - T: AsyncRead + Unpin + Send, - { - let vec = read_length_prefixed(io, 1_000_000).await?; - - if vec.is_empty() { - return Err(io::ErrorKind::UnexpectedEof.into()); - } - - Ok(FileRequest(String::from_utf8(vec).unwrap())) - } - - async fn read_response( - &mut self, - _: &FileExchangeProtocol, - io: &mut T, - ) -> io::Result - where - T: AsyncRead + Unpin + Send, - { - let vec = read_length_prefixed(io, 500_000_000).await?; // update transfer maximum - - if vec.is_empty() { - return Err(io::ErrorKind::UnexpectedEof.into()); - } - - Ok(FileResponse(vec)) - } - - async fn write_request( - &mut self, - _: &FileExchangeProtocol, - io: &mut T, - FileRequest(data): FileRequest, - ) -> io::Result<()> - where - T: AsyncWrite + Unpin + Send, - { - write_length_prefixed(io, data).await?; - io.close().await?; - - Ok(()) - } - - async fn write_response( - &mut self, - _: &FileExchangeProtocol, - io: &mut T, - FileResponse(data): FileResponse, - ) -> io::Result<()> - where - T: AsyncWrite + Unpin + Send, - { - write_length_prefixed(io, data).await?; - io.close().await?; - - Ok(()) - } -} diff --git a/homestar-runtime/src/network/ws.rs b/homestar-runtime/src/network/ws.rs new file mode 100644 index 00000000..9a233370 --- /dev/null +++ b/homestar-runtime/src/network/ws.rs @@ -0,0 +1,226 @@ +//! Sets up a websocket server for sending and receiving messages from browser +//! clients. + +use crate::settings; +use anyhow::{anyhow, Result}; +use axum::{ + extract::{ + ws::{self, Message, WebSocketUpgrade}, + ConnectInfo, State, TypedHeader, + }, + response::IntoResponse, + routing::get, + Router, +}; +use futures::{stream::StreamExt, SinkExt}; +use std::{ + net::{IpAddr, SocketAddr, TcpListener}, + ops::ControlFlow, + str::FromStr, + sync::Arc, +}; +use tokio::sync::broadcast; + +/// WebSocket state information. +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub struct WebSocket { + addr: SocketAddr, + sender: Arc>, + receiver: Arc>, +} + +impl WebSocket { + /// Setup bounded, MPMC channel for runtime to send and received messages + /// through the websocket connection(s). + pub fn setup_channel( + settings: &settings::Node, + ) -> (broadcast::Sender, broadcast::Receiver) { + broadcast::channel(settings.network.websocket_capacity) + } + + /// Start the websocket server given settings. + pub async fn start_server( + sender: Arc>, + receiver: Arc>, + settings: &settings::Node, + ) -> Result<()> { + let host = IpAddr::from_str(&settings.network.websocket_host.to_string())?; + let addr = if port_available(host, settings.network.websocket_port) { + SocketAddr::from((host, settings.network.websocket_port)) + } else { + let port = (settings.network.websocket_port..settings.network.websocket_port + 1000) + .find(|port| port_available(host, *port)) + .ok_or_else(|| anyhow!("no free TCP ports available"))?; + SocketAddr::from((host, port)) + }; + + let ws_state = Self { + addr, + sender, + receiver, + }; + let app = Router::new().route("/", get(ws_handler).with_state(ws_state)); + + tokio::spawn(async move { + axum::Server::bind(&addr) + .serve(app.into_make_service_with_connect_info::()) + .await + .expect("Websocket server to start"); + }); + + tracing::info!("websocket server starting on {addr}"); + + Ok(()) + } +} + +async fn ws_handler( + ws: WebSocketUpgrade, + user_agent: Option>, + State(state): State, + ConnectInfo(addr): ConnectInfo, +) -> impl IntoResponse { + let user_agent = if let Some(TypedHeader(user_agent)) = user_agent { + user_agent.to_string() + } else { + String::from("Unknown browser") + }; + tracing::info!("`{user_agent}` at {addr} connected."); + + // Finalize the upgrade process by returning upgrade callback. + // We can customize the callback by sending additional info such as address. + ws.on_upgrade(move |socket| handle_socket(socket, state)) +} + +async fn handle_socket(mut socket: ws::WebSocket, state: WebSocket) { + let addr = state.addr; + + // Send a ping (unsupported by some browsers) just to kick things off and + // get a response. + if socket.send(Message::Ping(vec![1, 2, 3])).await.is_ok() { + tracing::debug!("Pinged {}...", addr); + } else { + tracing::info!("Could not send ping {}!", addr); + // no Error here since the only thing we can do is to close the connection. + // If we can not send messages, there is no way to salvage the statemachine anyway. + return; + } + + // Receive single message from a client (we can either receive or send with + // the socket). This will likely be the Pong for our Ping or a processed + // message from client. + // Waiting for message from a client will block this task, but will not + // block other client's connections. + if let Some(msg) = socket.recv().await { + if let Ok(msg) = msg { + if process_message(msg, addr).await.is_break() { + return; + } + } else { + tracing::info!("client {} abruptly disconnected", state.addr); + return; + } + } + + // By splitting socket we can send and receive at the same time. + let (mut socket_sender, mut socket_receiver) = socket.split(); + let mut subscribed_rx = state.sender.subscribe(); + + let mut send_task = tokio::spawn(async move { + while let Ok(msg) = subscribed_rx.recv().await { + // In any websocket error, break loop. + if socket_sender + .send(Message::Binary(msg.into())) + .await + .is_err() + { + break; + } + } + }); + + let mut recv_task = tokio::spawn(async move { + let mut cnt = 0; + while let Some(Ok(msg)) = socket_receiver.next().await { + cnt += 1; + if process_message(msg, addr).await.is_break() { + break; + } + } + cnt + }); + + // If any one of the tasks exit, abort the other. + tokio::select! { + _ = (&mut send_task) => recv_task.abort(), + _ = (&mut recv_task) => send_task.abort(), + }; + + tracing::info!("Websocket context {} destroyed", addr); +} + +/// Process [messages]. +/// +/// [messages]: Message +async fn process_message(msg: Message, addr: SocketAddr) -> ControlFlow<(), ()> { + match msg { + Message::Text(t) => { + tracing::info!(">>> {} sent str: {:?}", addr, t); + } + Message::Binary(d) => { + tracing::info!(">>> {} sent {} bytes: {:?}", addr, d.len(), d); + } + Message::Close(c) => { + if let Some(cf) = c { + tracing::info!( + ">>> {} sent close with code {} and reason `{}`", + addr, + cf.code, + cf.reason + ); + } else { + tracing::info!(">>> {} somehow sent close message without CloseFrame", addr); + } + return ControlFlow::Break(()); + } + + Message::Pong(v) => { + tracing::info!(">>> {} sent pong with {:?}", addr, v); + } + // You should never need to manually handle Message::Ping, as axum's websocket library + // will do so for you automagically by replying with Pong and copying the v according to + // spec. But if you need the contents of the pings you can see them here. + Message::Ping(v) => { + tracing::info!(">>> {} sent ping with {:?}", addr, v); + } + } + ControlFlow::Continue(()) +} + +fn port_available(host: IpAddr, port: u16) -> bool { + TcpListener::bind((host.to_string(), port)).is_ok() +} + +#[cfg(test)] +mod test { + use crate::settings::Settings; + + use super::*; + + #[tokio::test] + async fn ws_connect() { + let (tx, rx) = broadcast::channel(1); + let sender = Arc::new(tx); + let receiver = Arc::new(rx); + let settings = Settings::load().unwrap(); + + WebSocket::start_server(Arc::clone(&sender), Arc::clone(&receiver), settings.node()) + .await + .unwrap(); + + tokio_tungstenite::connect_async("ws://localhost:1337".to_string()) + .await + .unwrap(); + } +} diff --git a/homestar-runtime/src/receipt.rs b/homestar-runtime/src/receipt.rs index 6a4d0d8b..8a1490c7 100644 --- a/homestar-runtime/src/receipt.rs +++ b/homestar-runtime/src/receipt.rs @@ -10,81 +10,157 @@ use diesel::{ sqlite::Sqlite, AsExpression, FromSqlRow, Insertable, Queryable, }; -use homestar_core::workflow::{ - pointer::InvocationPointer, - prf::UcanPrf, - receipt::{Issuer, Receipt as LocalReceipt}, - InvocationResult, +use homestar_core::{ + consts, + workflow::{prf::UcanPrf, InstructionResult, Issuer, Pointer, Receipt as InvocationReceipt}, }; -use libipld::{cbor::DagCborCodec, cid::Cid, prelude::Codec, serde::from_ipld, Ipld}; +use homestar_wasm::io::Arg; +use libipld::{ + cbor::DagCborCodec, cid::Cid, json::DagJsonCodec, prelude::Codec, serde::from_ipld, Ipld, +}; +use semver::Version; use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, fmt}; +const CID_KEY: &str = "cid"; +const INSTRUCTION_KEY: &str = "instruction"; const RAN_KEY: &str = "ran"; const OUT_KEY: &str = "out"; const ISSUER_KEY: &str = "iss"; const METADATA_KEY: &str = "meta"; const PROOF_KEY: &str = "prf"; -const CID_KEY: &str = "cid"; +const VERSION_KEY: &str = "version"; -/// Receipt for [Invocation], including it's own [Cid]. +/// Receipt for [Invocation], including it's own [Cid] and a [Cid] for an [Instruction]. /// -/// `@See` [LocalReceipt] for more info on the internal fields. +/// `@See` [homestar_core::workflow::Receipt] for more info on some internal +/// fields. /// /// [Invocation]: homestar_core::workflow::Invocation +/// [Instruction]: homestar_core::workflow::Instruction #[derive(Debug, Clone, PartialEq, Queryable, Insertable)] pub struct Receipt { - cid: InvocationPointer, - ran: InvocationPointer, - out: InvocationResult, + cid: Pointer, + ran: Pointer, + instruction: Pointer, + out: InstructionResult, meta: LocalIpld, - iss: Option, + issuer: Option, prf: UcanPrf, + version: String, } impl fmt::Display for Receipt { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "Receipt: [cid: {}, ran: {}, output: {:?}, metadata: {:?}, issuer: {:?}]", - self.cid, self.ran, self.out, self.meta.0, self.iss + "Receipt: [cid: {}, instruction: {}, ran: {}, output: {:?}, metadata: {:?}, issuer: {:?}]", + self.cid, self.instruction, self.ran, self.out, self.meta.0, self.issuer ) } } impl Receipt { /// Generate a receipt. - pub fn new(cid: Cid, local: &LocalReceipt<'_, Ipld>) -> Self { + pub fn new( + cid: Cid, + instruction: Pointer, + invocation_receipt: &InvocationReceipt, + ) -> Self { Self { - ran: local.ran().to_owned(), - out: local.out().to_owned(), - meta: LocalIpld(local.meta().to_owned()), - iss: local.issuer().to_owned(), - prf: local.prf().to_owned(), - cid: InvocationPointer::new(cid), + cid: Pointer::new(cid), + ran: invocation_receipt.ran().to_owned(), + instruction, + out: invocation_receipt.out().to_owned(), + meta: LocalIpld(invocation_receipt.meta().to_owned()), + issuer: invocation_receipt.issuer().to_owned(), + prf: invocation_receipt.prf().to_owned(), + version: consts::INVOCATION_VERSION.to_string(), } } + /// Return a runtime [Receipt] given an [Instruction] [Pointer] and + /// [UCAN Invocation Receipt]. + /// + /// [Instruction]: homestar_core::workflow::Instruction + /// [UCAN Invocation Receipt]: homestar_core::workflow::Receipt + pub fn try_with( + instruction: Pointer, + invocation_receipt: &InvocationReceipt, + ) -> anyhow::Result { + let cid = Cid::try_from(invocation_receipt)?; + Ok(Receipt::new(cid, instruction, invocation_receipt)) + } + + /// Get [Ipld] metadata on a [Receipt]. + pub fn meta(&self) -> &Ipld { + self.meta.inner() + } + + /// Set [Ipld] metadata on a [Receipt]. + pub fn set_meta(&mut self, meta: Ipld) { + self.meta = LocalIpld(meta) + } + /// Get unique identifier of receipt. pub fn cid(&self) -> String { self.cid.to_string() } + /// Get inner [Cid] as bytes. + pub fn cid_as_bytes(&self) -> Vec { + self.cid.cid().to_bytes() + } + + /// Return the [Cid] of the [Receipt]'s associated [Instruction]. + /// + /// [Instruction]: homestar_core::workflow::Instruction + pub fn instruction(&self) -> String { + self.instruction.to_string() + } + + /// Get instruction [Pointer] inner [Cid] as bytes. + pub fn instruction_cid_as_bytes(&self) -> Vec { + self.instruction.cid().to_bytes() + } + /// Get [Cid] in [Receipt] as a [String]. pub fn ran(&self) -> String { self.ran.to_string() } /// Get executed result/value in [Receipt] as [Ipld]. - pub fn output(&self) -> &InvocationResult { + pub fn output(&self) -> &InstructionResult { &self.out } + /// Return [InstructionResult] output as [Arg] for execution. + pub fn output_as_arg(&self) -> InstructionResult { + match self.out.to_owned() { + InstructionResult::Ok(res) => InstructionResult::Ok(res.into()), + InstructionResult::Error(res) => InstructionResult::Error(res.into()), + InstructionResult::Just(res) => InstructionResult::Just(res.into()), + } + } + /// Get executed result/value in [Receipt] as encoded Cbor. pub fn output_encoded(&self) -> anyhow::Result> { let ipld = Ipld::from(self.out.to_owned()); DagCborCodec.encode(&ipld) } + + /// Return semver [Version] of [Receipt]. + pub fn version(&self) -> Result { + Version::parse(&self.version) + } + + /// Return runtime receipt as stringified Json. + pub fn to_json(&self) -> anyhow::Result { + let encoded = DagJsonCodec.encode(&Ipld::from(self.to_owned()))?; + let s = std::str::from_utf8(&encoded) + .map_err(|e| anyhow!("cannot stringify encoded value: {e}"))?; + Ok(s.to_string()) + } } impl TryFrom for Vec { @@ -101,37 +177,51 @@ impl TryFrom> for Receipt { fn try_from(bytes: Vec) -> Result { let ipld: Ipld = DagCborCodec.decode(&bytes)?; - Receipt::try_from(ipld) + ipld.try_into() } } -impl From for LocalReceipt<'_, Ipld> { +impl From for InvocationReceipt { fn from(receipt: Receipt) -> Self { - LocalReceipt::new( + InvocationReceipt::new( receipt.ran, receipt.out, receipt.meta.0, - receipt.iss, + receipt.issuer, receipt.prf, ) } } +impl From<&Receipt> for InvocationReceipt { + fn from(receipt: &Receipt) -> Self { + InvocationReceipt::new( + receipt.ran.clone(), + receipt.out.clone(), + receipt.meta.0.clone(), + receipt.issuer.clone(), + receipt.prf.clone(), + ) + } +} + impl From for Ipld { fn from(receipt: Receipt) -> Self { Ipld::Map(BTreeMap::from([ + (CID_KEY.into(), receipt.cid.into()), (RAN_KEY.into(), receipt.ran.into()), + (INSTRUCTION_KEY.into(), receipt.instruction.into()), (OUT_KEY.into(), receipt.out.into()), (METADATA_KEY.into(), receipt.meta.0), ( ISSUER_KEY.into(), receipt - .iss - .map(|iss| iss.to_string().into()) + .issuer + .map(|issuer| issuer.to_string().into()) .unwrap_or(Ipld::Null), ), (PROOF_KEY.into(), receipt.prf.into()), - (CID_KEY.into(), receipt.cid.into()), + (VERSION_KEY.into(), receipt.version.into()), ])) } } @@ -142,11 +232,22 @@ impl TryFrom for Receipt { fn try_from(ipld: Ipld) -> Result { let map = from_ipld::>(ipld)?; + let cid = from_ipld( + map.get(CID_KEY) + .ok_or_else(|| anyhow!("missing {CID_KEY}"))? + .to_owned(), + )?; + let ran = map .get(RAN_KEY) .ok_or_else(|| anyhow!("missing {RAN_KEY}"))? .try_into()?; + let instruction = map + .get(INSTRUCTION_KEY) + .ok_or_else(|| anyhow!("missing {INSTRUCTION_KEY}"))? + .try_into()?; + let out = map .get(OUT_KEY) .ok_or_else(|| anyhow!("missing {OUT_KEY}"))?; @@ -155,7 +256,7 @@ impl TryFrom for Receipt { .get(METADATA_KEY) .ok_or_else(|| anyhow!("missing {METADATA_KEY}"))?; - let iss = map + let issuer = map .get(ISSUER_KEY) .and_then(|ipld| match ipld { Ipld::Null => None, @@ -168,45 +269,42 @@ impl TryFrom for Receipt { .get(PROOF_KEY) .ok_or_else(|| anyhow!("missing {PROOF_KEY}"))?; - let cid = from_ipld( - map.get(CID_KEY) - .ok_or_else(|| anyhow!("missing {CID_KEY}"))? + let version = from_ipld( + map.get(VERSION_KEY) + .ok_or_else(|| anyhow!("missing {VERSION_KEY}"))? .to_owned(), )?; Ok(Receipt { + cid: Pointer::new(cid), ran, - out: InvocationResult::try_from(out)?, + instruction, + out: InstructionResult::try_from(out)?, meta: LocalIpld(meta.to_owned()), - iss, + issuer, prf: UcanPrf::try_from(prf)?, - cid: InvocationPointer::new(cid), + version, }) } } -impl TryFrom> for Receipt { - type Error = anyhow::Error; +/// Wrapper-type for [Ipld] in order integrate to/from for local storage/db. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, AsExpression, FromSqlRow)] +#[diesel(sql_type = Binary)] +pub struct LocalIpld(Ipld); - fn try_from(receipt: LocalReceipt<'_, Ipld>) -> Result { - TryFrom::try_from(&receipt) +impl LocalIpld { + /// Convert into owned, inner [Ipld]. + pub fn into_inner(self) -> Ipld { + self.0 } -} -impl TryFrom<&LocalReceipt<'_, Ipld>> for Receipt { - type Error = anyhow::Error; - - fn try_from(receipt: &LocalReceipt<'_, Ipld>) -> Result { - let cid = Cid::try_from(receipt)?; - Ok(Receipt::new(cid, receipt)) + /// Convert into referenced, inner [Ipld]. + pub fn inner(&self) -> &Ipld { + &self.0 } } -/// Wrapper-type for [Ipld] in order integrate to/from for local storage/db. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, AsExpression, FromSqlRow)] -#[diesel(sql_type = Binary)] -pub struct LocalIpld(pub Ipld); - impl ToSql for LocalIpld where [u8]: ToSql, @@ -229,49 +327,36 @@ impl FromSql for LocalIpld { #[cfg(test)] mod test { use super::*; - use crate::{db::schema, receipt::receipts, test_utils::db}; - use diesel::prelude::*; - use libipld::{ - multihash::{Code, MultihashDigest}, - Link, + use crate::{ + db::{schema, Database}, + receipt::receipts, + settings::Settings, + test_utils, }; - const RAW: u64 = 0x55; - - fn receipt<'a>() -> (LocalReceipt<'a, Ipld>, Receipt) { - let h = Code::Blake3_256.digest(b"beep boop"); - let cid = Cid::new_v1(RAW, h); - let link: Link = Link::new(cid); - let local = LocalReceipt::new( - InvocationPointer::new_from_link(link), - InvocationResult::Ok(Ipld::Bool(true)), - Ipld::Null, - None, - UcanPrf::default(), - ); - let receipt = Receipt::try_from(&local).unwrap(); - (local, receipt) - } + use diesel::prelude::*; #[test] - fn local_into_receipt() { - let (local, receipt) = receipt(); - assert_eq!(local.ran().to_string(), receipt.ran()); - assert_eq!(local.out(), receipt.output()); - assert_eq!(local.meta(), &receipt.meta.0); - assert_eq!(local.issuer(), &receipt.iss); - assert_eq!(local.prf(), &receipt.prf); + fn invocation_into_receipt() { + let (invocation, receipt) = test_utils::receipt::receipts(); + assert_eq!(invocation.ran().to_string(), receipt.ran()); + assert_eq!(invocation.out(), receipt.output()); + assert_eq!(invocation.meta(), &receipt.meta.0); + assert_eq!(invocation.issuer(), &receipt.issuer); + assert_eq!(invocation.prf(), &receipt.prf); let output_bytes = DagCborCodec - .encode::(&local.out().clone().into()) + .encode::(&invocation.out().clone().into()) .unwrap(); assert_eq!(output_bytes, receipt.output_encoded().unwrap()); } #[test] fn receipt_sql_roundtrip() { - let mut conn = db::setup().unwrap(); - - let (_, receipt) = receipt(); + let mut conn = + test_utils::db::MemoryDb::setup_connection_pool(Settings::load().unwrap().node()) + .conn() + .unwrap(); + let (_, receipt) = test_utils::receipt::receipts(); let rows_inserted = diesel::insert_into(schema::receipts::table) .values(&receipt) @@ -282,12 +367,27 @@ mod test { let inserted_receipt = receipts::table.load::(&mut conn).unwrap(); - assert_eq!(vec![receipt], inserted_receipt); + assert_eq!(vec![receipt.clone()], inserted_receipt); + } + + #[test] + fn receipt_to_json() { + let (_, receipt) = test_utils::receipt::receipts(); + assert_eq!( + receipt.to_json().unwrap(), + format!( + r#"{{"cid":{{"/":"{}"}},"instruction":{{"/":"{}"}},"iss":null,"meta":null,"out":["ok",true],"prf":[],"ran":{{"/":"{}"}},"version":"{}"}}"#, + receipt.cid(), + receipt.instruction(), + receipt.ran(), + consts::INVOCATION_VERSION + ) + ); } #[test] fn receipt_bytes_roundtrip() { - let (_, receipt) = receipt(); + let (_, receipt) = test_utils::receipt::receipts(); let bytes: Vec = receipt.clone().try_into().unwrap(); let from_bytes = Receipt::try_from(bytes).unwrap(); diff --git a/homestar-runtime/src/runtime.rs b/homestar-runtime/src/runtime.rs new file mode 100644 index 00000000..11bdad2d --- /dev/null +++ b/homestar-runtime/src/runtime.rs @@ -0,0 +1,18 @@ +//! General [Runtime] for working across multiple workers +//! and workflows. +//! +//! TODO: Fill this out. + +use homestar_wasm::io::Arg; +use tokio::task::JoinSet; + +/// Runtime for starting workers on workflows. +#[allow(dead_code)] +#[derive(Debug)] +pub struct Runtime { + /// The set of [workers] for [workflows] + /// + /// [workers]: crate::Worker + /// [workflows]: crate::Workflow + pub(crate) workers: JoinSet>, +} diff --git a/homestar-runtime/src/scheduler.rs b/homestar-runtime/src/scheduler.rs new file mode 100644 index 00000000..dc547922 --- /dev/null +++ b/homestar-runtime/src/scheduler.rs @@ -0,0 +1,311 @@ +//! [Scheduler] module for executing a series of tasks for a given +//! [Workflow]. +//! +//! [Scheduler]: TaskScheduler + +use crate::{ + db::{Connection, Database}, + workflow::{Resource, Vertex}, + Db, Workflow, +}; +use anyhow::Result; +use dagga::Node; +use futures::future::BoxFuture; +use homestar_core::workflow::{InstructionResult, LinkMap, Pointer}; +use homestar_wasm::io::Arg; +use indexmap::IndexMap; +use libipld::Cid; +use std::{ops::ControlFlow, str::FromStr}; + +type Schedule<'a> = Vec, usize>>>; + +/// Type for [instruction]-based, batched, execution graph and set of task +/// resources. +/// +/// [instruction]: homestar_core::workflow::Instruction +#[allow(missing_debug_implementations)] +pub struct ExecutionGraph<'a> { + /// A built-up [Dag] [Schedule] of batches. + /// + /// [Dag]: dagga::Dag + pub(crate) schedule: Schedule<'a>, + /// Vector of [resources] to fetch for executing functions in [Workflow]. + /// + /// [resources]: Resource + pub(crate) resources: Vec, +} + +/// Scheduler for a series of tasks, including what's run, +/// what's left to run, and data structures to track resources +/// and what's been executed in memory. +#[allow(dead_code)] +#[derive(Debug, Default)] +pub struct TaskScheduler<'a> { + /// In-memory map of task/instruction results. + pub(crate) linkmap: LinkMap>, + /// [ExecutionGraph] of what's been run so far for a [Workflow] of [Tasks]. + /// + /// [Tasks]: homestar_core::workflow::Task + pub(crate) ran: Option>, + + /// [ExecutionGraph] of what's left to run for a [Workflow] of [Tasks]. + /// + /// [Tasks]: homestar_core::workflow::Task + pub(crate) run: Schedule<'a>, + + /// Step/batch to resume from. + pub(crate) resume_step: Option, + + /// Resources that tasks within a [Workflow] rely on, retrieved + /// through IPFS Client or the DHT directly ahead-of-time. + /// + /// This is transferred from the [ExecutionGraph] for actually executing the + /// schedule. + pub(crate) resources: IndexMap>, +} + +impl<'a> TaskScheduler<'a> { + /// Initialize Task Scheduler, given [Receipt] cache. + /// + /// [Receipt]: crate::Receipt + pub async fn init( + workflow: Workflow<'a, Arg>, + mut conn: Connection, + fetch_fn: F, + ) -> Result> + where + F: FnOnce(Vec) -> BoxFuture<'a, Result>>>, + { + let graph = workflow.graph()?; + let mut schedule = graph.schedule; + let schedule_length = schedule.len(); + let fetched = fetch_fn(graph.resources).await?; + + let resume = schedule + .iter() + .enumerate() + .rev() + .try_for_each(|(idx, vec)| { + let folded_pointers = vec.iter().try_fold(vec![], |mut ptrs, node| { + ptrs.push(Pointer::new(Cid::from_str(node.name())?)); + Ok::<_, anyhow::Error>(ptrs) + }); + + if let Ok(pointers) = folded_pointers { + match Db::find_instructions(pointers, &mut conn) { + Ok(found) => { + let linkmap = found.iter().fold( + LinkMap::>::new(), + |mut map, receipt| { + if let Ok(cid) = receipt.instruction().try_into() { + let _ = map.insert(cid, receipt.output_as_arg()); + } + map + }, + ); + + if found.len() == vec.len() { + ControlFlow::Break((idx + 1, linkmap)) + } else if !found.is_empty() && found.len() < vec.len() { + ControlFlow::Break((idx, linkmap)) + } else { + ControlFlow::Continue(()) + } + } + _ => ControlFlow::Continue(()), + } + } else { + ControlFlow::Continue(()) + } + }); + + match resume { + ControlFlow::Break((idx, linkmap)) => { + let pivot = schedule.split_off(idx); + let step = if idx >= schedule_length || idx == 0 { + None + } else { + Some(idx) + }; + + Ok(Self { + linkmap, + ran: Some(schedule), + run: pivot, + resume_step: step, + resources: fetched, + }) + } + _ => Ok(Self { + linkmap: LinkMap::>::new(), + ran: None, + run: schedule, + resume_step: None, + resources: fetched, + }), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{db::Database, settings::Settings, test_utils, Receipt}; + use futures::FutureExt; + use homestar_core::{ + test_utils::workflow as workflow_test_utils, + workflow::{ + config::Resources, instruction::RunInstruction, prf::UcanPrf, Invocation, + Receipt as InvocationReceipt, Task, + }, + }; + use libipld::Ipld; + + #[tokio::test] + async fn initialize_task_scheduler() { + let config = Resources::default(); + let (instruction1, instruction2, _) = + workflow_test_utils::related_wasm_instructions::(); + let task1 = Task::new( + RunInstruction::Expanded(instruction1), + config.clone().into(), + UcanPrf::default(), + ); + let task2 = Task::new( + RunInstruction::Expanded(instruction2), + config.into(), + UcanPrf::default(), + ); + + let db = test_utils::db::MemoryDb::setup_connection_pool(Settings::load().unwrap().node()); + let conn = db.conn().unwrap(); + let workflow = Workflow::new(vec![task1.clone(), task2.clone()]); + let fetch_fn = |_rscs: Vec| { async { Ok(IndexMap::default()) } }.boxed(); + let scheduler = TaskScheduler::init(workflow, conn, fetch_fn).await.unwrap(); + + assert!(scheduler.linkmap.is_empty()); + assert!(scheduler.ran.is_none()); + assert_eq!(scheduler.run.len(), 2); + assert_eq!(scheduler.resume_step, None); + } + + #[tokio::test] + async fn initialize_task_scheduler_with_receipted_instruction() { + let config = Resources::default(); + let (instruction1, instruction2, _) = + workflow_test_utils::related_wasm_instructions::(); + let task1 = Task::new( + RunInstruction::Expanded(instruction1.clone()), + config.clone().into(), + UcanPrf::default(), + ); + let task2 = Task::new( + RunInstruction::Expanded(instruction2), + config.into(), + UcanPrf::default(), + ); + + let invocation_receipt = InvocationReceipt::new( + Invocation::new(task1.clone()).try_into().unwrap(), + InstructionResult::Ok(Ipld::Integer(4)), + Ipld::Null, + None, + UcanPrf::default(), + ); + let receipt = Receipt::try_with( + instruction1.clone().try_into().unwrap(), + &invocation_receipt, + ) + .unwrap(); + + let db = test_utils::db::MemoryDb::setup_connection_pool(Settings::load().unwrap().node()); + let mut conn = db.conn().unwrap(); + + let stored_receipt = + test_utils::db::MemoryDb::store_receipt(receipt.clone(), &mut conn).unwrap(); + + assert_eq!(receipt, stored_receipt); + + let workflow = Workflow::new(vec![task1.clone(), task2.clone()]); + let fetch_fn = |_rscs: Vec| { async { Ok(IndexMap::default()) } }.boxed(); + let scheduler = TaskScheduler::init(workflow, conn, fetch_fn).await.unwrap(); + let ran = scheduler.ran.as_ref().unwrap(); + + assert_eq!(scheduler.linkmap.len(), 1); + assert!(scheduler + .linkmap + .contains_key(&Cid::try_from(instruction1).unwrap())); + assert_eq!(ran.len(), 1); + assert_eq!(scheduler.run.len(), 1); + assert_eq!(scheduler.resume_step, Some(1)); + } + + #[tokio::test] + async fn initialize_task_scheduler_with_all_receipted_instruction() { + let config = Resources::default(); + let (instruction1, instruction2, _) = + workflow_test_utils::related_wasm_instructions::(); + + let task1 = Task::new( + RunInstruction::Expanded(instruction1.clone()), + config.clone().into(), + UcanPrf::default(), + ); + + let task2 = Task::new( + RunInstruction::Expanded(instruction2.clone()), + config.into(), + UcanPrf::default(), + ); + + let invocation_receipt1 = InvocationReceipt::new( + Invocation::new(task1.clone()).try_into().unwrap(), + InstructionResult::Ok(Ipld::Integer(4)), + Ipld::Null, + None, + UcanPrf::default(), + ); + let receipt1 = Receipt::try_with( + instruction1.clone().try_into().unwrap(), + &invocation_receipt1, + ) + .unwrap(); + + let invocation_receipt2 = InvocationReceipt::new( + Invocation::new(task2.clone()).try_into().unwrap(), + InstructionResult::Ok(Ipld::Integer(44)), + Ipld::Null, + None, + UcanPrf::default(), + ); + let receipt2 = Receipt::try_with( + instruction2.clone().try_into().unwrap(), + &invocation_receipt2, + ) + .unwrap(); + + let db = test_utils::db::MemoryDb::setup_connection_pool(Settings::load().unwrap().node()); + let mut conn = db.conn().unwrap(); + + let rows_inserted = + test_utils::db::MemoryDb::store_receipts(vec![receipt1, receipt2], &mut conn).unwrap(); + + assert_eq!(2, rows_inserted); + + let workflow = Workflow::new(vec![task1.clone(), task2.clone()]); + let fetch_fn = |_rscs: Vec| { async { Ok(IndexMap::default()) } }.boxed(); + let scheduler = TaskScheduler::init(workflow, conn, fetch_fn).await.unwrap(); + let ran = scheduler.ran.as_ref().unwrap(); + + assert_eq!(scheduler.linkmap.len(), 1); + assert!(!scheduler + .linkmap + .contains_key(&Cid::try_from(instruction1).unwrap()),); + assert!(scheduler + .linkmap + .contains_key(&Cid::try_from(instruction2).unwrap())); + assert_eq!(ran.len(), 2); + assert!(scheduler.run.is_empty()); + assert_eq!(scheduler.resume_step, None); + } +} diff --git a/homestar-runtime/src/schema.rs b/homestar-runtime/src/schema.rs deleted file mode 100644 index 92648aba..00000000 --- a/homestar-runtime/src/schema.rs +++ /dev/null @@ -1,12 +0,0 @@ -// @generated automatically by Diesel CLI. - -diesel::table! { - receipts (cid) { - cid -> Text, - ran -> Text, - out -> Binary, - meta -> Binary, - iss -> Nullable, - prf -> Binary, - } -} diff --git a/homestar-runtime/src/settings.rs b/homestar-runtime/src/settings.rs new file mode 100644 index 00000000..9cda09b0 --- /dev/null +++ b/homestar-runtime/src/settings.rs @@ -0,0 +1,121 @@ +//! Settings / Configuration. + +use config::{Config, ConfigError, Environment, File}; +use http::Uri; +use serde::Deserialize; +use std::path::PathBuf; + +/// Server settings. +#[derive(Clone, Debug, Deserialize)] +pub struct Node { + #[serde(default)] + pub(crate) network: Network, + #[serde(default)] + pub(crate) db: Database, +} + +/// Process monitoring settings. +#[derive(Clone, Debug, Deserialize)] +pub struct Monitoring { + /// Monitoring collection interval. + #[allow(dead_code)] + process_collector_interval: u64, +} + +#[derive(Debug, Deserialize)] +/// Application settings. +pub struct Settings { + monitoring: Monitoring, + node: Node, +} + +impl Settings { + /// Monitoring settings getter. + pub fn monitoring(&self) -> &Monitoring { + &self.monitoring + } + + /// Node + pub fn node(&self) -> &Node { + &self.node + } +} + +/// Network-related settings for a homestar node. +#[derive(Clone, Debug, Deserialize)] +pub struct Network { + /// + pub(crate) events_buffer_len: usize, + /// Address for [Swarm] to listen on. + /// + /// [Swarm]: libp2p::swarm::Swarm + #[serde(with = "http_serde::uri")] + pub(crate) listen_address: Uri, + /// Pub/sub hearbeat interval for mesh configuration. + pub(crate) pubsub_heartbeat_secs: u64, + /// Quorum for receipt records on the DHT. + pub(crate) receipt_quorum: usize, + /// Websocket-server host address. + #[serde(with = "http_serde::uri")] + pub(crate) websocket_host: Uri, + /// Websocket-server port. + pub(crate) websocket_port: u16, + /// Number of *bounded* clients to send messages to, used for a + /// [tokio::sync::broadcast::channel] + pub(crate) websocket_capacity: usize, +} + +/// Database-related settings for a homestar node. +#[derive(Clone, Debug, Deserialize)] +pub struct Database { + /// Maximum number of connections managed by the [pool]. + /// + /// [pool]: crate::db::Pool + pub(crate) max_pool_size: u32, +} + +impl Default for Network { + fn default() -> Self { + Self { + events_buffer_len: 100, + listen_address: Uri::from_static("/ip4/0.0.0.0/tcp/0"), + pubsub_heartbeat_secs: 10, + receipt_quorum: 2, + websocket_host: Uri::from_static("127.0.0.1"), + websocket_port: 1337, + websocket_capacity: 100, + } + } +} + +impl Default for Database { + fn default() -> Self { + Self { max_pool_size: 100 } + } +} + +impl Settings { + /// Load settings. + pub fn load() -> Result { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config/settings.toml"); + // inject environment variables naming them properly on the settings + // e.g. [database] url="foo" + // would be injected with environment variable HOMESTAR_DATABASE_URL="foo" + // use one underscore as defined by the separator below + Self::build(path) + } + + /// Load settings from file string that must conform to a [PathBuf]. + pub fn load_from_file(file: String) -> Result { + let path = PathBuf::from(file); + Self::build(path) + } + + fn build(path: PathBuf) -> Result { + let s = Config::builder() + .add_source(File::with_name(&path.as_path().display().to_string())) + .add_source(Environment::with_prefix("HOMESTAR").separator("__")) + .build()?; + s.try_deserialize() + } +} diff --git a/homestar-runtime/src/tasks.rs b/homestar-runtime/src/tasks.rs new file mode 100644 index 00000000..94d7aa9b --- /dev/null +++ b/homestar-runtime/src/tasks.rs @@ -0,0 +1,33 @@ +#![allow(missing_docs)] + +//! Module for working with task-types and task-specific functionality. + +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use enum_assoc::Assoc; +use std::path::PathBuf; + +mod wasm; + +pub(crate) use wasm::*; + +const WASM_OP: &str = "wasm/run"; + +/// First-class registered task-types. +#[allow(missing_debug_implementations)] +#[derive(Assoc)] +#[func(pub fn ability(s: &str) -> Option)] +pub enum RegisteredTasks { + /// Basic `wasm/run` task-type. + #[assoc(ability = WASM_OP)] + WasmRun, +} + +/// Trait for loading files for different task-types directly. +#[async_trait] +pub trait FileLoad { + /// Load file asynchronously. + async fn load(file: PathBuf) -> Result> { + tokio::fs::read(file).await.map_err(|e| anyhow!(e)) + } +} diff --git a/homestar-runtime/src/tasks/wasm.rs b/homestar-runtime/src/tasks/wasm.rs new file mode 100644 index 00000000..939ac691 --- /dev/null +++ b/homestar-runtime/src/tasks/wasm.rs @@ -0,0 +1,57 @@ +use super::FileLoad; +use anyhow::Result; +use async_trait::async_trait; +use homestar_core::workflow::input::Args; +use homestar_wasm::{ + io::{Arg, Output}, + wasmtime::{world::Env, State, World}, +}; + +#[allow(missing_debug_implementations)] +pub(crate) struct WasmContext { + env: Env, +} + +impl WasmContext { + pub(crate) fn new(data: State) -> Result { + Ok(Self { + env: World::default(data)?, + }) + } + + /// Instantiate environment via [World] and execute on [Args]. + pub(crate) async fn run<'a>( + &mut self, + bytes: Vec, + fun_name: &'a str, + args: Args, + ) -> Result { + let env = World::instantiate_with_current_env(bytes, fun_name, &mut self.env).await?; + env.execute(args).await + } +} + +#[async_trait] +impl FileLoad for WasmContext {} + +#[cfg(test)] +mod test { + use super::*; + use std::path::PathBuf; + + fn fixtures(file: &str) -> PathBuf { + PathBuf::from(format!( + "{}/../homestar-wasm/fixtures/{file}", + env!("CARGO_MANIFEST_DIR") + )) + } + + #[tokio::test] + async fn load_wasm_file_as_bytes() { + let wat = WasmContext::load(fixtures("add_one_component.wat")) + .await + .unwrap(); + + assert!(!wat.is_empty()); + } +} diff --git a/homestar-runtime/src/test_utils/db.rs b/homestar-runtime/src/test_utils/db.rs index c35abd56..b5dbdcb5 100644 --- a/homestar-runtime/src/test_utils/db.rs +++ b/homestar-runtime/src/test_utils/db.rs @@ -1,13 +1,33 @@ -use diesel::{connection::SimpleConnection, Connection, SqliteConnection}; - -pub fn setup() -> anyhow::Result { - let mut conn = diesel::sqlite::SqliteConnection::establish(":memory:").unwrap(); - let source = diesel_migrations::FileBasedMigrations::find_migrations_directory()?; - let _ = diesel_migrations::MigrationHarness::run_pending_migrations(&mut conn, source).unwrap(); - begin(&mut conn)?; - Ok(conn) -} +use crate::{ + db::{Connection, Database, Pool}, + settings, +}; +use diesel::r2d2; +use std::sync::Arc; + +/// Sqlite in-memory [Database] [Pool]. +#[derive(Debug)] +pub struct MemoryDb(Arc); + +impl Database for MemoryDb { + fn setup_connection_pool(_settings: &settings::Node) -> Self { + let manager = r2d2::ConnectionManager::::new(":memory:"); + let pool = r2d2::Pool::builder() + .max_size(1) + .build(manager) + .expect("DATABASE_URL must be set to an SQLite DB file"); + + let source = diesel_migrations::FileBasedMigrations::find_migrations_directory().unwrap(); + let _ = diesel_migrations::MigrationHarness::run_pending_migrations( + &mut pool.get().unwrap(), + source, + ) + .unwrap(); + MemoryDb(Arc::new(pool)) + } -pub fn begin(conn: &mut SqliteConnection) -> Result<(), diesel::result::Error> { - conn.batch_execute("BEGIN;") + fn conn(&self) -> anyhow::Result { + let conn = self.0.get()?; + Ok(conn) + } } diff --git a/homestar-runtime/src/test_utils/mod.rs b/homestar-runtime/src/test_utils/mod.rs index b3f5a8d2..d041ac17 100644 --- a/homestar-runtime/src/test_utils/mod.rs +++ b/homestar-runtime/src/test_utils/mod.rs @@ -1,2 +1,4 @@ #[cfg(test)] pub mod db; +#[cfg(test)] +pub mod receipt; diff --git a/homestar-runtime/src/test_utils/receipt.rs b/homestar-runtime/src/test_utils/receipt.rs new file mode 100644 index 00000000..ac1fa9a1 --- /dev/null +++ b/homestar-runtime/src/test_utils/receipt.rs @@ -0,0 +1,41 @@ +//! Test utilities for working with [receipts]. +//! +//! [receipts]: crate::Receipt + +use crate::Receipt; +use homestar_core::{ + test_utils, + workflow::{prf::UcanPrf, receipt::Receipt as InvocationReceipt, InstructionResult, Pointer}, +}; +use libipld::{ + cid::Cid, + multihash::{Code, MultihashDigest}, + Ipld, Link, +}; + +const RAW: u64 = 0x55; + +/// Return both a `mocked` [Ucan Invocation Receipt] and a runtime [Receipt] +/// +/// [UCAN Invocation Receipt]: homestar_core::workflow::Receipt +pub fn receipts() -> (InvocationReceipt, Receipt) { + let h = Code::Blake3_256.digest(b"beep boop"); + let cid = Cid::new_v1(RAW, h); + let link: Link = Link::new(cid); + let local = InvocationReceipt::new( + Pointer::new_from_link(link), + InstructionResult::Ok(Ipld::Bool(true)), + Ipld::Null, + None, + UcanPrf::default(), + ); + let receipt = Receipt::try_with( + test_utils::workflow::instruction::() + .try_into() + .unwrap(), + &local, + ) + .unwrap(); + + (local, receipt) +} diff --git a/homestar-runtime/src/worker.rs b/homestar-runtime/src/worker.rs new file mode 100644 index 00000000..b7aa3da6 --- /dev/null +++ b/homestar-runtime/src/worker.rs @@ -0,0 +1,339 @@ +#[cfg(feature = "ipfs")] +use crate::workflow::settings::BackoffStrategy; +#[cfg(feature = "ipfs")] +use crate::IpfsCli; +use crate::{ + db::{Connection, Database}, + network::eventloop::Event, + scheduler::TaskScheduler, + tasks::{RegisteredTasks, WasmContext}, + workflow::{settings::Settings, Resource, WorkflowInfo}, + Db, Receipt, Workflow, +}; +use anyhow::{anyhow, bail, Result}; +use crossbeam::channel; +use futures::FutureExt; +#[cfg(feature = "ipfs")] +use futures::StreamExt; +use homestar_core::workflow::{ + prf::UcanPrf, InstructionResult, Pointer, Receipt as InvocationReceipt, +}; +use homestar_wasm::{io::Arg, wasmtime::State}; +use indexmap::IndexMap; +use libipld::{Cid, Ipld}; +use std::{ + collections::BTreeMap, + sync::Arc, + time::{Duration, Instant}, +}; +use tokio::{sync::mpsc, task::JoinSet}; +#[cfg(feature = "ipfs")] +use tryhard::RetryFutureConfig; + +/// Worker that operates over a given [TaskScheduler]. +#[derive(Debug)] +pub struct Worker<'a> { + pub(crate) scheduler: TaskScheduler<'a>, + pub(crate) workflow_info: WorkflowInfo, +} + +impl<'a> Worker<'a> { + /// Instantiate a new [Worker] for a [Workflow]. + #[cfg(not(feature = "ipfs"))] + pub async fn new( + workflow: Workflow<'a, Arg>, + workflow_settings: &'a Settings, + conn: Connection, + ) -> Result> { + let fetch_fn = |rscs: Vec| { + async { Self::get_resources(rscs, workflow_settings).await }.boxed() + }; + + let scheduler = TaskScheduler::init(workflow.clone(), conn, fetch_fn).await?; + let workflow_len = workflow.len(); + let workflow_cid = Cid::try_from(workflow)?; + let workflow_info = WorkflowInfo::new( + workflow_cid, + scheduler.resume_step.map_or(0, |step| step), + workflow_len, + ); + Ok(Self { + scheduler, + workflow_info, + }) + } + + /// Instantiate a new [Worker] for a [Workflow]. + #[cfg(feature = "ipfs")] + pub async fn new( + workflow: Workflow<'a, Arg>, + workflow_settings: &'a Settings, + conn: Connection, + ipfs: &'a IpfsCli, + ) -> Result> { + let fetch_fn = |rscs: Vec| { + async { Self::get_resources(rscs, workflow_settings, ipfs).await }.boxed() + }; + + let scheduler = TaskScheduler::init(workflow.clone(), conn, fetch_fn).await?; + let workflow_len = workflow.len(); + let workflow_cid = Cid::try_from(workflow)?; + let workflow_info = WorkflowInfo::new( + workflow_cid, + scheduler.resume_step.map_or(0, |step| step), + workflow_len, + ); + Ok(Self { + scheduler, + workflow_info, + }) + } + + /// Run [Worker]'s tasks in task-queue with access to the [Db] object + /// to use a connection from the Database pool per run. + pub async fn run( + self, + db: Db, + event_sender: Arc>, + settings: Settings, + ) -> Result<()> { + self.run_queue(db, event_sender, settings).await + } + + #[cfg(feature = "ipfs")] + async fn get_resources( + resources: Vec, + settings: &'a Settings, + ipfs: &'a IpfsCli, + ) -> Result>> { + async fn fetch(rsc: Resource, client: IpfsCli) -> (Resource, Result>) { + match rsc { + Resource::Url(url) => { + let bytes = match (url.scheme(), url.domain(), url.path()) { + ("ipfs", _, _) => client.get_resource(&url).await, + (_, Some("ipfs.io"), _) => client.get_resource(&url).await, + (_, _, path) if path.contains("/ipfs/") || path.contains("/ipns/") => { + client.get_resource(&url).await + } + (_, Some(domain), _) => { + let split: Vec<&str> = domain.splitn(3, '.').collect(); + // subdomain-gateway case: + // + if let (Ok(_cid), "ipfs") = (Cid::try_from(split[0]), split[1]) { + client.get_resource(&url).await + } else { + // TODO: reqwest call + todo!() + } + } + // TODO: reqwest call + (_, _, _) => todo!(), + }; + (Resource::Url(url), bytes) + } + + Resource::Cid(cid) => { + let bytes = client.get_cid(cid).await; + (Resource::Cid(cid), bytes) + } + } + } + let num_requests = resources.len(); + futures::stream::iter(resources.into_iter().map(|rsc| async move { + // Have to enumerate configs here, as type variants are different + // and cannot be matched on. + match settings.retry_backoff_strategy { + BackoffStrategy::Exponential => { + tryhard::retry_fn(|| { + let rsc = rsc.clone(); + let client = ipfs.clone(); + tokio::spawn(async move { fetch(rsc, client).await }) + }) + .with_config( + RetryFutureConfig::new(settings.retries) + .exponential_backoff(Duration::from_millis( + settings.retry_initial_delay_ms, + )) + .max_delay(Duration::from_secs(settings.retry_max_delay_secs)), + ) + .await + } + BackoffStrategy::Fixed => { + tryhard::retry_fn(|| { + let rsc = rsc.clone(); + let client = ipfs.clone(); + tokio::spawn(async move { fetch(rsc, client).await }) + }) + .with_config( + RetryFutureConfig::new(settings.retries) + .fixed_backoff(Duration::from_millis(settings.retry_initial_delay_ms)) + .max_delay(Duration::from_secs(settings.retry_max_delay_secs)), + ) + .await + } + BackoffStrategy::Linear => { + tryhard::retry_fn(|| { + let rsc = rsc.clone(); + let client = ipfs.clone(); + tokio::spawn(async move { fetch(rsc, client).await }) + }) + .with_config( + RetryFutureConfig::new(settings.retries) + .linear_backoff(Duration::from_millis(settings.retry_initial_delay_ms)) + .max_delay(Duration::from_secs(settings.retry_max_delay_secs)), + ) + .await + } + BackoffStrategy::None => { + tryhard::retry_fn(|| { + let rsc = rsc.clone(); + let client = ipfs.clone(); + tokio::spawn(async move { fetch(rsc, client).await }) + }) + .with_config(RetryFutureConfig::new(settings.retries).no_backoff()) + .await + } + } + })) + .buffer_unordered(num_requests) + .collect::>() + .await + .into_iter() + .try_fold(IndexMap::default(), |mut acc, res| { + let inner = res?; + let answer = inner.1?; + acc.insert(inner.0, answer); + Ok::<_, anyhow::Error>(acc) + }) + } + + /// TODO: Client calls (only) over http(s). + #[cfg(not(feature = "ipfs"))] + async fn get_resources( + _resources: Vec, + _settings: &'a Settings, + ) -> Result> { + Ok(IndexMap::default()) + } + + async fn run_queue( + mut self, + db: Db, + event_sender: Arc>, + settings: Settings, + ) -> Result<()> { + for batch in self.scheduler.run.into_iter() { + let (mut set, _handles) = batch.into_iter().try_fold( + (JoinSet::new(), vec![]), + |(mut set, mut handles), node| { + let vertice = node.into_inner(); + let invocation_ptr = vertice.invocation; + let instruction = vertice.instruction; + let rsc = instruction.resource(); + let parsed = vertice.parsed; + let fun = parsed.fun().ok_or_else(|| anyhow!("no function defined"))?; + + let args = parsed.into_args(); + let meta = Ipld::Map(BTreeMap::from([ + ("op".into(), fun.to_string().into()), + ("workflow".into(), self.workflow_info.cid().into()) + ])); + + match RegisteredTasks::ability(&instruction.op().to_string()) { + Some(RegisteredTasks::WasmRun) => { + let wasm = self + .scheduler + .resources + .get(&Resource::Url(rsc.to_owned())) + .ok_or_else(|| anyhow!("resource not available"))? + .to_owned(); + + let instruction_ptr = Pointer::try_from(instruction)?; + let state = State::default(); + let mut wasm_ctx = WasmContext::new(state)?; + let resolved = + args.resolve(|cid| match self.scheduler.linkmap.get(&cid) { + Some(result) => Ok(result.to_owned()), + None => match Db::find_instruction( + Pointer::new(cid), + &mut db.conn()?, + ) { + Ok(found) => Ok(found.output_as_arg()), + Err(_e) => { + tracing::debug!( + "no related instruction receipt found in the DB" + ); + let (sender, receiver) = channel::bounded(1); + event_sender.blocking_send(Event::FindReceipt( + cid, + sender, + ))?; + let found = match receiver.recv_deadline( + Instant::now() + Duration::from_secs(settings.p2p_timeout_secs), + ) { + Ok((found_cid, found)) if found_cid == cid => { + found + } + Ok(_) => bail!("only one worker channel per worker"), + Err(err) => bail!("error returning invocation receipt for {cid}: {err}"), + }; + + Ok(found.output_as_arg()) + } + }, + })?; + + let handle = set.spawn(async move { + match wasm_ctx.run(wasm, &fun, resolved).await { + Ok(output) => { + Ok((output, instruction_ptr, invocation_ptr, meta)) + } + Err(e) => Err(anyhow!("cannot execute wasm module: {e}")), + } + }); + handles.push(handle); + } + None => tracing::error!( + "no valid task/instruction-type referenced by operation: {}", + instruction.op() + ), + }; + + Ok::<_, anyhow::Error>((set, handles)) + }, + )?; + + while let Some(res) = set.join_next().await { + let (executed, instruction_ptr, invocation_ptr, meta) = res??; + let output_to_store = Ipld::try_from(executed)?; + + let invocation_receipt = InvocationReceipt::new( + invocation_ptr, + InstructionResult::Ok(output_to_store), + Ipld::Null, + None, + UcanPrf::default(), + ); + + let mut receipt = Receipt::try_with(instruction_ptr, &invocation_receipt)?; + self.scheduler.linkmap.insert( + Cid::try_from(receipt.instruction())?, + receipt.output_as_arg(), + ); + + receipt.set_meta(meta); + + let stored_receipt = Db::store_receipt(receipt, &mut db.conn()?)?; + + // send internal event + event_sender + .send(Event::CapturedReceipt( + stored_receipt, + self.workflow_info.clone(), + )) + .await?; + } + } + Ok(()) + } +} diff --git a/homestar-runtime/src/workflow.rs b/homestar-runtime/src/workflow.rs new file mode 100644 index 00000000..eebf1100 --- /dev/null +++ b/homestar-runtime/src/workflow.rs @@ -0,0 +1,587 @@ +//! A [Workflow] is a declarative configuration of a series of +//! [UCAN Invocation] `Tasks`. +//! +//! [UCAN Invocation]: + +use crate::scheduler::ExecutionGraph; +use anyhow::{anyhow, bail}; +use dagga::{self, dot::DagLegend, Node}; +use homestar_core::workflow::{ + input::{Parse, Parsed}, + instruction::RunInstruction, + Instruction, Invocation, Pointer, Task, +}; +use homestar_wasm::io::Arg; +use indexmap::IndexMap; +use libipld::{ + cbor::DagCborCodec, + json::DagJsonCodec, + multihash::{Code, MultihashDigest}, + prelude::Codec, + serde::from_ipld, + Cid, Ipld, +}; +use std::{collections::BTreeMap, path::Path}; +use url::Url; + +pub(crate) mod settings; + +type Dag<'a> = dagga::Dag, usize>; + +const DAG_CBOR: u64 = 0x71; +const CID_KEY: &str = "cid"; +const TASKS_KEY: &str = "tasks"; +const PROGRESS_KEY: &str = "progress"; +const NUM_TASKS_KEY: &str = "num_tasks"; + +/// A resource can refer to a [URI] or [Cid] +/// being accessed. +/// +/// [URI]: +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub enum Resource { + /// Resource fetched by [Url]. + Url(Url), + /// Resource fetched by [Cid]. + Cid(Cid), +} + +/// Ahead-of-time (AOT) context object, which includes the given +/// [Workflow] as a executable [Dag] (directed acyclic graph) and +/// the [Task] resources retrieved through IPFS Client or the DHT directly +/// ahead-of-time. +/// +/// [Dag]: dagga::Dag +#[derive(Debug, Clone)] +pub struct AOTContext<'a> { + dag: Dag<'a>, + resources: Vec, +} + +impl AOTContext<'static> { + /// Convert [Dag] to a [dot] file, to be read by graphviz, etc. + /// + /// [Dag]: dagga::Dag + /// [dot]: + pub fn dot(&self, name: &str, path: &Path) -> anyhow::Result<()> { + DagLegend::new(self.dag.nodes()) + .with_name(name) + .save_to( + path.to_str() + .ok_or_else(|| anyhow!("path is not correctly formatted"))?, + ) + .map_err(|e| anyhow!(e)) + } +} + +/// Vertex information for [Dag] [Node]. +/// +/// [Dag]: dagga::Dag +#[derive(Debug, Clone, PartialEq)] +pub struct Vertex<'a> { + pub(crate) instruction: Instruction<'a, Arg>, + pub(crate) parsed: Parsed, + pub(crate) invocation: Pointer, +} + +impl<'a> Vertex<'a> { + fn new( + instruction: Instruction<'a, Arg>, + parsed: Parsed, + invocation: Pointer, + ) -> Vertex<'a> { + Vertex { + instruction, + parsed, + invocation, + } + } +} + +/// Associated [Workflow] information, separated from [Workflow] struct in order +/// to relate to it as a key-value relationship of (workflow) +/// cid => [WorkflowInfo]. +/// +/// TODO: map of task cids completed +#[derive(Debug, Clone, PartialEq)] +pub struct WorkflowInfo { + pub(crate) cid: Cid, + pub(crate) progress: usize, + pub(crate) num_tasks: usize, +} + +impl WorkflowInfo { + /// Create a new [WorkflowInfo] given a [Cid], progress / step, and number + /// of tasks. + pub fn new(cid: Cid, progress: usize, num_tasks: usize) -> Self { + Self { + cid, + progress, + num_tasks, + } + } + + /// Create a default [WorkflowInfo] given a [Cid] and number of tasks. + pub fn default(cid: Cid, num_tasks: usize) -> Self { + Self { + cid, + progress: 0, + num_tasks, + } + } + + /// Get the [Cid] of a [Workflow] as a [String]. + pub fn cid(&self) -> String { + self.cid.to_string() + } + + /// Set the progress / step of the [WorkflowInfo]. + pub fn set_progress(&mut self, progress: usize) { + self.progress = progress; + } + + /// Increment the progress / step of the [WorkflowInfo]. + pub fn increment_progress(&mut self) { + self.progress += 1; + } +} + +impl From for Ipld { + fn from(workflow: WorkflowInfo) -> Self { + Ipld::Map(BTreeMap::from([ + (CID_KEY.into(), Ipld::Link(workflow.cid)), + ( + PROGRESS_KEY.into(), + Ipld::Integer(workflow.progress as i128), + ), + ( + NUM_TASKS_KEY.into(), + Ipld::Integer(workflow.num_tasks as i128), + ), + ])) + } +} + +impl TryFrom for WorkflowInfo { + type Error = anyhow::Error; + + fn try_from(ipld: Ipld) -> Result { + let map = from_ipld::>(ipld)?; + let cid = from_ipld( + map.get(CID_KEY) + .ok_or_else(|| anyhow!("no `cid` set"))? + .to_owned(), + )?; + let progress = from_ipld( + map.get(PROGRESS_KEY) + .ok_or_else(|| anyhow!("no `progress` set"))? + .to_owned(), + )?; + let num_tasks = from_ipld( + map.get(NUM_TASKS_KEY) + .ok_or_else(|| anyhow!("no `num_tasks` set"))? + .to_owned(), + )?; + + Ok(Self { + cid, + progress, + num_tasks, + }) + } +} + +impl TryFrom for Vec { + type Error = anyhow::Error; + + fn try_from(receipt: WorkflowInfo) -> Result { + let receipt_ipld = Ipld::from(receipt); + DagCborCodec.encode(&receipt_ipld) + } +} + +impl TryFrom> for WorkflowInfo { + type Error = anyhow::Error; + + fn try_from(bytes: Vec) -> Result { + let ipld: Ipld = DagCborCodec.decode(&bytes)?; + ipld.try_into() + } +} + +/// Workflow composed of [tasks]. +/// +/// [tasks]: Task +#[derive(Debug, Clone, PartialEq)] +pub struct Workflow<'a, T> { + tasks: Vec>, +} + +impl<'a> Workflow<'a, Arg> { + /// Create a new [Workflow] given a set of tasks. + pub fn new(tasks: Vec>) -> Self { + Self { tasks } + } + + /// Length of workflow given a series of [tasks]. + /// + /// [tasks]: Task + pub fn len(&self) -> usize { + self.tasks.len() + } + + /// Whether [Workflow] contains [tasks] or not. + /// + /// [tasks]: Task + pub fn is_empty(&self) -> bool { + self.tasks.is_empty() + } + + /// Convert the [Workflow] into an batch-separated [ExecutionGraph]. + pub fn graph(self) -> anyhow::Result> { + let aot = self.aot()?; + match aot.dag.build_schedule() { + Ok(schedule) => Ok(ExecutionGraph { + schedule: schedule.batches, + resources: aot.resources, + }), + Err(e) => bail!("schedule could not be built from given workflow: {e}"), + } + } + + /// Return workflow as stringified Json. + pub fn to_json(&self) -> anyhow::Result { + let encoded = DagJsonCodec.encode(&Ipld::from(self.to_owned()))?; + let s = std::str::from_utf8(&encoded) + .map_err(|e| anyhow!("cannot stringify encoded value: {e}"))?; + Ok(s.to_string()) + } + + fn aot(self) -> anyhow::Result> { + let lookup_table = self.lookup_table()?; + + let (dag, resources) = + self.tasks.into_iter().enumerate().try_fold( + (Dag::default(), vec![]), + |(mut dag, mut resources), (i, task)| { + // Clone as we're owning the struct going backward. + let ptr: Pointer = Invocation::::from(task.clone()).try_into()?; + let instr_cid = task.instruction_cid()?.to_string(); + + let RunInstruction::Expanded(instr) = task.into_instruction() else { + bail!("workflow tasks/instructions must be expanded / inlined") + }; + + // TODO: check if op is runnable on current node + // TODO LATER: check if op is registered on the network + + resources.push(Resource::Url(instr.resource().to_owned())); + + let parsed = instr.input().parse()?; + let reads = parsed.args().deferreds().into_iter().fold( + vec![], + |mut in_flow_reads, cid| { + if let Some(v) = lookup_table.get(&cid) { + in_flow_reads.push(*v) + } else { + resources.push(Resource::Url(instr.resource().to_owned())); + } + in_flow_reads + }, + ); + + let node = Node::new(Vertex::new(instr.to_owned(), parsed, ptr)) + .with_name(instr_cid) + .with_result(i); + + dag.add_node(node.with_reads(reads)); + Ok::<_, anyhow::Error>((dag, resources)) + }, + )?; + + Ok(AOTContext { dag, resources }) + } + + /// Generate an [IndexMap] lookup table of task instruction CIDs to a + /// unique enumeration. + fn lookup_table(&self) -> anyhow::Result> { + self.tasks + .iter() + .enumerate() + .try_fold(IndexMap::new(), |mut acc, (i, t)| { + acc.insert(t.instruction_cid()?, i); + Ok::<_, anyhow::Error>(acc) + }) + } +} + +impl From> for Ipld { + fn from(workflow: Workflow<'_, Arg>) -> Self { + Ipld::Map(BTreeMap::from([( + TASKS_KEY.into(), + Ipld::List( + workflow + .tasks + .into_iter() + .map(Ipld::from) + .collect::>(), + ), + )])) + } +} + +impl TryFrom for Workflow<'_, Arg> { + type Error = anyhow::Error; + + fn try_from(ipld: Ipld) -> Result { + let map = from_ipld::>(ipld)?; + let ipld = map + .get(TASKS_KEY) + .ok_or_else(|| anyhow!("no `tasks` set"))?; + + let tasks = if let Ipld::List(tasks) = ipld { + let tasks = tasks.iter().fold(vec![], |mut acc, ipld| { + acc.push(ipld.try_into().unwrap()); + acc + }); + tasks + } else { + bail!("unexpected conversion type") + }; + + Ok(Self { tasks }) + } +} + +impl TryFrom> for Cid { + type Error = anyhow::Error; + + fn try_from(workflow: Workflow<'_, Arg>) -> Result { + let ipld: Ipld = workflow.into(); + let bytes = DagCborCodec.encode(&ipld)?; + let hash = Code::Sha3_256.digest(&bytes); + Ok(Cid::new_v1(DAG_CBOR, hash)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use homestar_core::{ + test_utils, + workflow::{config::Resources, instruction::RunInstruction, prf::UcanPrf}, + }; + use std::path::Path; + + #[test] + fn dag_to_dot() { + let config = Resources::default(); + let instruction1 = test_utils::workflow::wasm_instruction::(); + let (instruction2, _) = test_utils::workflow::wasm_instruction_with_nonce::(); + let task1 = Task::new( + RunInstruction::Expanded(instruction1), + config.clone().into(), + UcanPrf::default(), + ); + let task2 = Task::new( + RunInstruction::Expanded(instruction2), + config.into(), + UcanPrf::default(), + ); + + let workflow = Workflow::new(vec![task1, task2]); + let aot = workflow.aot().unwrap(); + + aot.dot("test", Path::new("test.dot")).unwrap(); + assert!(Path::new("test.dot").exists()); + } + + #[test] + fn build_parallel_schedule() { + let config = Resources::default(); + let instruction1 = test_utils::workflow::wasm_instruction::(); + let (instruction2, _) = test_utils::workflow::wasm_instruction_with_nonce::(); + let task1 = Task::new( + RunInstruction::Expanded(instruction1), + config.clone().into(), + UcanPrf::default(), + ); + let task2 = Task::new( + RunInstruction::Expanded(instruction2), + config.into(), + UcanPrf::default(), + ); + + let tasks = vec![task1.clone(), task2.clone()]; + + let workflow = Workflow::new(tasks); + let dag = workflow.aot().unwrap().dag; + + let instr1 = task1.instruction_cid().unwrap().to_string(); + let instr2 = task2.instruction_cid().unwrap().to_string(); + + dagga::assert_batches(&[format!("{}, {}", instr2, instr1).as_str()], dag); + } + + #[test] + fn build_seq_schedule() { + let config = Resources::default(); + let (instruction1, instruction2, _) = + test_utils::workflow::related_wasm_instructions::(); + let task1 = Task::new( + RunInstruction::Expanded(instruction1), + config.clone().into(), + UcanPrf::default(), + ); + let task2 = Task::new( + RunInstruction::Expanded(instruction2), + config.into(), + UcanPrf::default(), + ); + + let workflow = Workflow::new(vec![task1.clone(), task2.clone()]); + let dag = workflow.aot().unwrap().dag; + + let instr1 = task1.instruction_cid().unwrap().to_string(); + let instr2 = task2.instruction_cid().unwrap().to_string(); + + // separate + dagga::assert_batches(&[&instr1, &instr2], dag); + } + + #[test] + fn build_mixed_graph() { + let config = Resources::default(); + let (instruction1, instruction2, instruction3) = + test_utils::workflow::related_wasm_instructions::(); + let task1 = Task::new( + RunInstruction::Expanded(instruction1), + config.clone().into(), + UcanPrf::default(), + ); + let task2 = Task::new( + RunInstruction::Expanded(instruction2), + config.clone().into(), + UcanPrf::default(), + ); + let task3 = Task::new( + RunInstruction::Expanded(instruction3), + config.clone().into(), + UcanPrf::default(), + ); + + let (instruction4, _) = test_utils::workflow::wasm_instruction_with_nonce::(); + let task4 = Task::new( + RunInstruction::Expanded(instruction4), + config.into(), + UcanPrf::default(), + ); + + let tasks = vec![task1.clone(), task2.clone(), task3.clone(), task4.clone()]; + let workflow = Workflow::new(tasks); + + let instr1 = task1.instruction_cid().unwrap().to_string(); + let instr2 = task2.instruction_cid().unwrap().to_string(); + let instr3 = task3.instruction_cid().unwrap().to_string(); + let instr4 = task4.instruction_cid().unwrap().to_string(); + + let schedule = workflow.graph().unwrap().schedule; + let nodes = schedule + .into_iter() + .fold(vec![], |mut acc: Vec, vec| { + if vec.len() == 1 { + acc.push(vec.first().unwrap().name().to_string()) + } else { + let mut tmp = vec![]; + for node in vec { + tmp.push(node.name().to_string()); + } + acc.push(tmp.join(", ")) + } + + acc + }); + + assert!( + nodes + == vec![ + format!("{}, {}", instr1, instr4), + instr2.clone(), + instr3.clone() + ] + || nodes == vec![format!("{}, {}", instr4, instr1), instr2, instr3] + ); + } + + #[test] + fn workflow_to_json() { + let config = Resources::default(); + let (instruction1, instruction2, _) = + test_utils::workflow::related_wasm_instructions::(); + let task1 = Task::new( + RunInstruction::Expanded(instruction1), + config.clone().into(), + UcanPrf::default(), + ); + let task2 = Task::new( + RunInstruction::Expanded(instruction2), + config.into(), + UcanPrf::default(), + ); + + let workflow = Workflow::new(vec![task1.clone(), task2.clone()]); + + let json_string = workflow.to_json().unwrap(); + + let json_val = json::from(json_string.clone()); + assert_eq!(json_string, json_val.to_string()); + } + + #[test] + fn ipld_roundtrip_workflow() { + let config = Resources::default(); + let (instruction1, instruction2, _) = + test_utils::workflow::related_wasm_instructions::(); + let task1 = Task::new( + RunInstruction::Expanded(instruction1), + config.clone().into(), + UcanPrf::default(), + ); + let task2 = Task::new( + RunInstruction::Expanded(instruction2), + config.into(), + UcanPrf::default(), + ); + + let workflow = Workflow::new(vec![task1.clone(), task2.clone()]); + let ipld = Ipld::from(workflow.clone()); + + assert_eq!(workflow, ipld.try_into().unwrap()) + } + + #[test] + fn ipld_roundtrip_workflow_info() { + let config = Resources::default(); + let (instruction1, instruction2, _) = + test_utils::workflow::related_wasm_instructions::(); + let task1 = Task::new( + RunInstruction::Expanded(instruction1), + config.clone().into(), + UcanPrf::default(), + ); + let task2 = Task::new( + RunInstruction::Expanded(instruction2), + config.into(), + UcanPrf::default(), + ); + + let workflow = Workflow::new(vec![task1.clone(), task2.clone()]); + let mut workflow_info = + WorkflowInfo::default(Cid::try_from(workflow.clone()).unwrap(), workflow.len()); + let ipld = Ipld::from(workflow_info.clone()); + assert_eq!(workflow_info, ipld.try_into().unwrap()); + workflow_info.increment_progress(); + workflow_info.increment_progress(); + assert_eq!(workflow_info.progress, 2); + } +} diff --git a/homestar-runtime/src/workflow/settings.rs b/homestar-runtime/src/workflow/settings.rs new file mode 100644 index 00000000..d928144d --- /dev/null +++ b/homestar-runtime/src/workflow/settings.rs @@ -0,0 +1,37 @@ +//! Workflow settings for a worker's run/execution. + +/// Workflow settings. +#[derive(Debug, Clone, PartialEq)] +pub struct Settings { + pub(crate) retries: u32, + pub(crate) retry_backoff_strategy: BackoffStrategy, + pub(crate) retry_max_delay_secs: u64, + pub(crate) retry_initial_delay_ms: u64, + pub(crate) p2p_timeout_secs: u64, +} + +impl Default for Settings { + fn default() -> Self { + Self { + retries: 10, + retry_backoff_strategy: BackoffStrategy::Exponential, + retry_max_delay_secs: 60, + retry_initial_delay_ms: 500, + p2p_timeout_secs: 60, + } + } +} + +/// Backoff strategies supported for workflows. +#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum BackoffStrategy { + /// Exponential backoff: the delay will double each time. + Exponential, + /// Fixed backoff: the delay wont change between attempts. + Fixed, + /// Linear backoff: the delay will scale linearly with the number of attempts. + Linear, + /// No backoff: forcing just leveraging retries. + None, +} diff --git a/homestar-wasm/Cargo.toml b/homestar-wasm/Cargo.toml index fc99346a..7c747061 100644 --- a/homestar-wasm/Cargo.toml +++ b/homestar-wasm/Cargo.toml @@ -1,12 +1,11 @@ [package] name = "homestar-wasm" version = "0.1.0" -description = "" -keywords = [] -categories = [] - +description = "Homestar Wasm / Wasmtime implementation and IPLD <=> WIT interpreter" +keywords = ["wasm", "wasmtime", "wit", "ipld", "ipvm"] +categories = ["wasm", "execution-engines"] include = ["/src", "README.md", "LICENSE"] -license = "Apache" +license = { workspace = true } readme = "README.md" edition = { workspace = true } rust-version = { workspace = true } @@ -21,7 +20,9 @@ doctest = true crate-type = ["cdylib", "rlib"] [dependencies] -anyhow = "1.0" +# return to version.workspace = true after the following issue is fixed: +# https://github.com/DevinR528/cargo-sort/issues/47 +anyhow = { workspace = true } atomic_refcell = "0.1" enum-as-inner = "0.5" heck = "0.4" @@ -33,19 +34,25 @@ rust_decimal = "1.28" serde_ipld_dagcbor = "0.2" stacker = "0.1" thiserror = "1.0" -tracing = "0.1" -wasi-cap-std-sync = { git = "https://github.com/bytecodealliance/preview2-prototyping" } -wasi-common = { git = "https://github.com/bytecodealliance/preview2-prototyping" } -wasmparser = "0.101" -wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", features = ["async", "component-model", "default"] } -wasmtime-component-util = { git = "https://github.com/bytecodealliance/wasmtime" } +tracing = { workspace = true } +wasi-cap-std-sync = "8.0" +wasi-common = "8.0" +wasmparser = "0.104" +wasmtime = { version = "8.0", features = ["async", "component-model", "default"] } +wasmtime-component-util = "8.0" wat = "1.0" -wit-component = "0.7" +wit-component = "0.8" [dev-dependencies] criterion = "0.4" +image = "0.24" tokio = { version = "1.25", default_features = false } tokio-test = "0.4" [features] default = [] + +[package.metadata.docs.rs] +all-features = true +# defines the configuration attribute `docsrs` +rustdoc-args = ["--cfg", "docsrs"] diff --git a/homestar-wasm/fixtures/homestar_guest_wasm.wasm b/homestar-wasm/fixtures/homestar_guest_wasm.wasm index 0ca7d7e29838d54e7e8123e64daf3cade746c735..b96fd6196a0b2a5b711f380709402e23d25a6609 100755 GIT binary patch literal 373878 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
DRjPsoD2$Z{l8PR3-}2Z7|K}me3*mXrsMS2A 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Ξt!Q${i3AuBNo2vGa3)8_pk4iE*rFxrIWmgq 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: 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%0Aay{|kgd?-C$#Vum>z&>&Nw2~7tit6?iG!DOC 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 literal 26930 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 diff --git a/homestar-wasm/src/io.rs b/homestar-wasm/src/io.rs index 98831728..e6fda776 100644 --- a/homestar-wasm/src/io.rs +++ b/homestar-wasm/src/io.rs @@ -1,3 +1,5 @@ +//! IO (input/output) types for the Wasm execution. + use anyhow::anyhow; use enum_as_inner::EnumAsInner; use homestar_core::workflow::{ @@ -53,12 +55,17 @@ impl input::Parse for Input { fn parse(&self) -> anyhow::Result> { if let Input::Ipld(ref ipld) = self { let map = from_ipld::>(ipld.to_owned())?; + + let func = map + .get("func") + .ok_or_else(|| anyhow!("wrong task input format: {ipld:?}"))?; + let wasm_args = map .get("args") .ok_or_else(|| anyhow!("wrong task input format: {ipld:?}"))?; let args: Args = wasm_args.to_owned().try_into()?; - Ok(Parsed::with(args)) + Ok(Parsed::with_fn(from_ipld::(func.to_owned())?, args)) } else { Err(anyhow!("unexpected task input")) } diff --git a/homestar-wasm/src/lib.rs b/homestar-wasm/src/lib.rs index 86f153f0..d2b14177 100644 --- a/homestar-wasm/src/lib.rs +++ b/homestar-wasm/src/lib.rs @@ -2,14 +2,18 @@ #![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] #![deny(unreachable_pub, private_in_public)] -//! homestar-wasm is enables a Wasm runtime and execution engine for Homestar. +//! homestar-wasm wraps and extends a [Wasmtime] runtime and acts as the defacto +//! execution engine for Homestar. +//! +//! Related crates/packages: +//! +//! - [homestar-core] +//! - [homestar-runtime] +//! +//! [homestar-core]: homestar_core +//! [homestar-runtime]: +//! [Wasmtime]: -/// pub mod io; -/// Test utilities. pub mod test_utils; -/// All interaction with [wasmtime] runtime, types, and values. pub mod wasmtime; - -/// -pub use homestar_core; diff --git a/homestar-wasm/src/test_utils/mod.rs b/homestar-wasm/src/test_utils/mod.rs index 67ed9115..70feb9ea 100644 --- a/homestar-wasm/src/test_utils/mod.rs +++ b/homestar-wasm/src/test_utils/mod.rs @@ -1,2 +1,3 @@ -/// Test-utilities for Wasm components. +//! Test-utilities for Wasm components. + pub mod component; diff --git a/homestar-wasm/src/wasmtime/config.rs b/homestar-wasm/src/wasmtime/config.rs index 07788997..a8ddbf6c 100644 --- a/homestar-wasm/src/wasmtime/config.rs +++ b/homestar-wasm/src/wasmtime/config.rs @@ -1,3 +1,5 @@ +//! Configuration for Wasm/wasmtime execution. + use crate::wasmtime; use homestar_core::workflow::config::Resources; diff --git a/homestar-wasm/src/wasmtime/ipld.rs b/homestar-wasm/src/wasmtime/ipld.rs index b878dee8..67c5be6c 100644 --- a/homestar-wasm/src/wasmtime/ipld.rs +++ b/homestar-wasm/src/wasmtime/ipld.rs @@ -1,5 +1,9 @@ //! Convert (bidirectionally) between [Ipld] and [wasmtime::component::Val]s //! and [wasmtime::component::Type]s. +//! +//! tl;dr: [Ipld] <=> [wasmtime::component::Val] IR. +//! +//! [Ipld]: libipld::Ipld use anyhow::{anyhow, Result}; use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; @@ -305,6 +309,22 @@ impl RuntimeVal { _ => RuntimeVal::new(Val::Float64(v)), }, Ipld::String(v) => RuntimeVal::new(Val::String(Box::from(v))), + Ipld::Bytes(v) if matches!(interface_ty.inner(), Some(Type::List(_))) => { + let inner = interface_ty + .inner() + .ok_or_else(|| anyhow!("component type mismatch: expected >"))?; + + // already pattern matched against + let list_inst = inner.unwrap_list(); + + let vec = v.into_iter().fold(vec![], |mut acc, elem| { + let RuntimeVal(value, _tags) = RuntimeVal::new(Val::U8(elem)); + acc.push(value); + acc + }); + + RuntimeVal::new(list_inst.new_val(vec.into_boxed_slice())?) + } Ipld::Bytes(v) => RuntimeVal::new(Val::String(Box::from(Base::Base64.encode(v)))), Ipld::Link(v) => match v.version() { cid::Version::V0 => RuntimeVal::new(Val::String(Box::from( @@ -454,15 +474,29 @@ impl TryFrom for Ipld { })?; Ipld::Map(inner) } - RuntimeVal(Val::List(v), _) => { - let inner = v.iter().try_fold(vec![], |mut acc, elem| { - let ipld = Ipld::try_from(RuntimeVal::new(elem.to_owned()))?; - acc.push(ipld); - Ok::<_, Self::Error>(acc) - })?; + RuntimeVal(Val::List(v), _) => match v.first() { + Some(Val::U8(_)) => { + let inner = v.iter().try_fold(vec![], |mut acc, elem| { + if let Val::U8(v) = elem { + acc.push(v.to_owned()); + Ok::<_, Self::Error>(acc) + } else { + Err(anyhow!("expected all u8 types"))? + } + })?; - Ipld::List(inner) - } + Ipld::Bytes(inner) + } + Some(_) => { + let inner = v.iter().try_fold(vec![], |mut acc, elem| { + let ipld = Ipld::try_from(RuntimeVal::new(elem.to_owned()))?; + acc.push(ipld); + Ok::<_, Self::Error>(acc) + })?; + Ipld::List(inner) + } + None => Ipld::List(vec![]), + }, RuntimeVal(Val::Union(u), tags) if !tags.empty() => { let inner = Ipld::try_from(RuntimeVal::new(u.payload().to_owned()))?; diff --git a/homestar-wasm/src/wasmtime/mod.rs b/homestar-wasm/src/wasmtime/mod.rs index 44dfeea8..b7e69b7d 100644 --- a/homestar-wasm/src/wasmtime/mod.rs +++ b/homestar-wasm/src/wasmtime/mod.rs @@ -4,13 +4,8 @@ //! [Wasmtime]: //! [Ipld]: libipld::Ipld -/// pub mod config; -/// [Ipld] <=> [wasmtime::component::Val] IR. -/// -/// [Ipld]: libipld::Ipld pub mod ipld; -/// Wasmtime component initialzation and execution of Wasm function(s). -mod world; +pub mod world; -pub use world::*; +pub use world::{State, World}; diff --git a/homestar-wasm/src/wasmtime/world.rs b/homestar-wasm/src/wasmtime/world.rs index 0ac55e2c..39903972 100644 --- a/homestar-wasm/src/wasmtime/world.rs +++ b/homestar-wasm/src/wasmtime/world.rs @@ -1,7 +1,7 @@ //! [Wasmtime] shim for parsing Wasm components, instantiating //! a module, and executing a Wasm function dynamically. //! -//! [Wasmtime]: https://docs.rs/wasmtime/latest/wasmtime/ +//! [Wasmtime]: use super::ipld::{InterfaceType, RuntimeVal}; use crate::io::{Arg, Output}; @@ -22,7 +22,7 @@ const UNIT_OF_COMPUTE_INSTRUCTIONS: u64 = 100_000; // our error set. /// Incoming `state` from host runtime. -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct State { fuel: u64, } @@ -49,80 +49,93 @@ impl State { /// wasmtime [Instance], [Engine], [Linker], and [Store]. #[allow(missing_debug_implementations)] pub struct Env { - bindings: World, + bindings: Option, engine: Engine, - instance: Instance, + instance: Option, linker: Linker, store: Store, } impl Env { - fn new( - bindings: World, - engine: Engine, - instance: Instance, - linker: Linker, - store: Store, - ) -> Env { - Env { - bindings, + fn new(engine: Engine, linker: Linker, store: Store) -> Env { + Self { + bindings: None, engine, - instance, + instance: None, linker, store, } } fn set_bindings(&mut self, bindings: World) { - self.bindings = bindings; + self.bindings = Some(bindings); } fn set_instance(&mut self, instance: Instance) { - self.instance = instance; + self.instance = Some(instance); } /// Execute Wasm function dynamically given a list ([Args]) of [Ipld] or /// [wasmtime::component::Val] arguments and returning [Output] results. /// Types must conform to [Wit] IDL types when Wasm was compiled/generated. /// - /// [Wit]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md + /// [Wit]: /// [Ipld]: libipld::Ipld pub async fn execute(&mut self, args: Args) -> Result where T: Send, { - let param_typs = self.bindings.func().params(&self.store); - let result_typs = self.bindings.func().results(&self.store); + let param_types = self + .bindings + .as_mut() + .ok_or_else(|| anyhow!("bindings not yet instantiated for wasm environment"))? + .func() + .params(&self.store); + let result_types = self + .bindings + .as_mut() + .ok_or_else(|| anyhow!("bindings not yet instantiated for wasm environment"))? + .func() + .results(&self.store); let params: Vec = iter::zip( - param_typs.iter(), + param_types.iter(), args.into_inner().into_iter(), ) .try_fold(vec![], |mut acc, (typ, arg)| { let v = match arg { Input::Ipld(ipld) => RuntimeVal::try_from(ipld, &InterfaceType::from(typ))?.value(), - // TODO: Match within `InvocationResult`(s). - Input::Arg(val) => val.into_inner().into_value().map_err(|e| anyhow!(e))?, + Input::Arg(val) => match val.into_inner() { + Arg::Ipld(ipld) => { + RuntimeVal::try_from(ipld, &InterfaceType::from(typ))?.value() + } + Arg::Value(v) => v, + }, Input::Deferred(await_promise) => bail!(anyhow!( "deferred task not yet resolved for {}: {}", await_promise.result(), - await_promise.task_cid() + await_promise.instruction_cid() )), }; acc.push(v); Ok::<_, anyhow::Error>(acc) })?; - let mut results_alloc: Vec = result_typs + let mut results_alloc: Vec = result_types .iter() .map(|_res| component::Val::Bool(false)) .collect(); self.bindings + .as_mut() + .ok_or_else(|| anyhow!("bindings not yet instantiated for wasm environment"))? .func() .call_async(&mut self.store, ¶ms, &mut results_alloc) .await?; + self.bindings + .as_mut() + .ok_or_else(|| anyhow!("bindings not yet instantiated for wasm environment"))? .func() .post_return_async(&mut self.store) .await?; @@ -137,12 +150,12 @@ impl Env { } /// Return `wasmtime` bindings. - pub fn bindings(&self) -> &World { + pub fn bindings(&self) -> &Option { &self.bindings } /// Return the initialized [wasmtime::component::Instance]. - pub fn instance(&self) -> Instance { + pub fn instance(&self) -> Option { self.instance } @@ -164,17 +177,33 @@ impl Env { pub struct World(Func); impl World { + /// Instantiate a default [environment] given a configuration + /// for a [World], given [State]. + /// + /// [environment]: Env + pub fn default(data: State) -> Result> { + let config = Self::configure(); + let engine = Engine::new(&config)?; + let linker = Self::define_linker(&engine); + + let mut store = Store::new(&engine, data); + store.add_fuel(store.data().fuel)?; + + // Configures a `Store` to yield execution of async WebAssembly code + // periodically and not cause extended polling. + store.out_of_fuel_async_yield(u64::MAX, UNIT_OF_COMPUTE_INSTRUCTIONS); + + let env = Env::new(engine, linker, store); + Ok(env) + } + /// Instantiates the provided `module` using the specified /// parameters, wrapping up the result in a [Env] structure /// that translates between wasm and the host, and gives access /// for future invocations to use the already-initialized linker, store. /// /// Used when first initiating a module of a workflow. - pub async fn instantiate<'a>( - bytes: Vec, - fun_name: String, - data: State, - ) -> Result> { + pub async fn instantiate(bytes: Vec, fun_name: &str, data: State) -> Result> { let config = Self::configure(); let engine = Engine::new(&config)?; let linker = Self::define_linker(&engine); @@ -191,7 +220,9 @@ impl World { let instance = linker.instantiate_async(&mut store, &component).await?; let bindings = Self::new(&mut store, &instance, fun_name)?; - let env = Env::new(bindings, engine, instance, linker, store); + let mut env = Env::new(engine, linker, store); + env.set_bindings(bindings); + env.set_instance(instance); Ok(env) } @@ -201,11 +232,11 @@ impl World { /// the instance for the Wasm component. /// /// [environment]: Env - pub async fn instantiate_with_current_env( + pub async fn instantiate_with_current_env<'a, T>( bytes: Vec, - fun_name: String, - env: &mut Env, - ) -> Result<&mut Env> + fun_name: &'a str, + env: &'a mut Env, + ) -> Result<&'a mut Env> where T: Send, { @@ -232,6 +263,7 @@ impl World { config.wasm_component_model(true); config.async_support(true); config.cranelift_nan_canonicalization(true); + config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); // Most Wasm instructions consume 1 unit of fuel. // Some instructions, such as nop, drop, block, and loop, consume 0 @@ -258,13 +290,13 @@ impl World { fn new( mut store: impl wasmtime::AsContextMut, instance: &Instance, - fun_name: String, + fun_name: &str, ) -> Result { let mut store_ctx = store.as_context_mut(); let mut exports = instance.exports(&mut store_ctx); let mut __exports = exports.root(); let func = __exports - .func(&fun_name) + .func(fun_name) .or_else(|| __exports.func(&fun_name.to_kebab_case())) .or_else(|| __exports.func(&fun_name.to_snake_case())) .ok_or_else(|| anyhow!("function not found"))?; diff --git a/homestar-wasm/tests/execute_wasm.rs b/homestar-wasm/tests/execute_wasm.rs index d52ec5de..03c4d182 100644 --- a/homestar-wasm/tests/execute_wasm.rs +++ b/homestar-wasm/tests/execute_wasm.rs @@ -1,9 +1,9 @@ +use homestar_core::workflow::{ + input::{Args, Parse}, + pointer::{Await, AwaitResult}, + Input, InstructionResult, Pointer, +}; use homestar_wasm::{ - homestar_core::workflow::{ - input::{Args, Parse}, - pointer::{Await, AwaitResult, InvocationPointer}, - Input, InvocationResult, - }, io::{Arg, Output}, wasmtime::{State, World}, }; @@ -22,13 +22,13 @@ fn fixtures(file: &str) -> PathBuf { #[tokio::test] async fn test_execute_wat() { - let ipld = Input::Ipld(Ipld::Map(BTreeMap::from([( - "args".into(), - Ipld::List(vec![Ipld::Integer(1)]), - )]))); + let ipld = Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("add_one".to_string())), + ("args".into(), Ipld::List(vec![Ipld::Integer(1)])), + ]))); let wat = fs::read(fixtures("add_one_component.wat")).unwrap(); - let mut env = World::instantiate(wat, "add-one".to_string(), State::default()) + let mut env = World::instantiate(wat, "add-one", State::default()) .await .unwrap(); let res = env @@ -41,19 +41,19 @@ async fn test_execute_wat() { #[tokio::test] async fn test_execute_wat_from_non_component() { let wat = fs::read(fixtures("add_one.wat")).unwrap(); - let env = World::instantiate(wat, "add_one".to_string(), State::default()).await; + let env = World::instantiate(wat, "add_one", State::default()).await; assert!(env.is_err()); } #[tokio::test] async fn test_execute_wasm_underscore() { - let ipld = Input::Ipld(Ipld::Map(BTreeMap::from([( - "args".into(), - Ipld::List(vec![Ipld::Integer(1)]), - )]))); + let ipld = Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("add_one".to_string())), + ("args".into(), Ipld::List(vec![Ipld::Integer(1)])), + ]))); let wasm = fs::read(fixtures("add_one.wasm")).unwrap(); - let mut env = World::instantiate(wasm, "add_one".to_string(), State::default()) + let mut env = World::instantiate(wasm, "add_one", State::default()) .await .unwrap(); let res = env @@ -65,13 +65,13 @@ async fn test_execute_wasm_underscore() { #[tokio::test] async fn test_execute_wasm_hyphen() { - let ipld = Input::Ipld(Ipld::Map(BTreeMap::from([( - "args".into(), - Ipld::List(vec![Ipld::Integer(10)]), - )]))); + let ipld = Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("add_one".to_string())), + ("args".into(), Ipld::List(vec![Ipld::Integer(10)])), + ]))); let wasm = fs::read(fixtures("add_one.wasm")).unwrap(); - let mut env = World::instantiate(wasm, "add-one".to_string(), State::default()) + let mut env = World::instantiate(wasm, "add-one", State::default()) .await .unwrap(); let res = env @@ -84,19 +84,22 @@ async fn test_execute_wasm_hyphen() { #[tokio::test] async fn test_wasm_wrong_fun() { let wasm = fs::read(fixtures("add_one.wasm")).unwrap(); - let env = World::instantiate(wasm, "add-onez".to_string(), State::default()).await; + let env = World::instantiate(wasm, "add-onez", State::default()).await; assert!(env.is_err()); } #[tokio::test] async fn test_append_string() { - let ipld = Input::Ipld(Ipld::Map(BTreeMap::from([( - "args".into(), - Ipld::List(vec![Ipld::String("Natural Science".to_string())]), - )]))); + let ipld = Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("append-string".to_string())), + ( + "args".into(), + Ipld::List(vec![Ipld::String("Natural Science".to_string())]), + ), + ]))); let wasm = fs::read(fixtures("homestar_guest_wasm.wasm")).unwrap(); - let mut env = World::instantiate(wasm, "append-string".to_string(), State::default()) + let mut env = World::instantiate(wasm, "append-string", State::default()) .await .unwrap(); @@ -120,13 +123,13 @@ async fn test_matrix_transpose() { Ipld::List(vec![Ipld::Integer(4), Ipld::Integer(5), Ipld::Integer(6)]), Ipld::List(vec![Ipld::Integer(7), Ipld::Integer(8), Ipld::Integer(9)]), ]); - let ipld = Input::Ipld(Ipld::Map(BTreeMap::from([( - "args".into(), - Ipld::List(vec![ipld_inner.clone()]), - )]))); + let ipld = Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("transpose".to_string())), + ("args".into(), Ipld::List(vec![ipld_inner.clone()])), + ]))); let wasm = fs::read(fixtures("homestar_guest_wasm.wasm")).unwrap(); - let mut env = World::instantiate(wasm, "transpose".to_string(), State::default()) + let mut env = World::instantiate(wasm, "transpose", State::default()) .await .unwrap(); @@ -139,10 +142,10 @@ async fn test_matrix_transpose() { assert_ne!(transposed_ipld, ipld_inner); - let ipld_transposed_map = Input::Ipld(Ipld::Map(BTreeMap::from([( - "args".into(), - Ipld::List(vec![transposed_ipld]), - )]))); + let ipld_transposed_map = Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("transpose".to_string())), + ("args".into(), Ipld::List(vec![transposed_ipld])), + ]))); let retransposed = env .execute(ipld_transposed_map.parse().unwrap().try_into().unwrap()) @@ -156,20 +159,23 @@ async fn test_matrix_transpose() { #[tokio::test] async fn test_execute_wasms_in_seq() { - let ipld_int = Input::Ipld(Ipld::Map(BTreeMap::from([( - "args".into(), - Ipld::List(vec![Ipld::Integer(1)]), - )]))); - - let ipld_str = Input::Ipld(Ipld::Map(BTreeMap::from([( - "args".into(), - Ipld::List(vec![Ipld::String("Natural Science".to_string())]), - )]))); + let ipld_int = Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("add_one".to_string())), + ("args".into(), Ipld::List(vec![Ipld::Integer(1)])), + ]))); + + let ipld_str = Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("append_string".to_string())), + ( + "args".into(), + Ipld::List(vec![Ipld::String("Natural Science".to_string())]), + ), + ]))); let wasm1 = fs::read(fixtures("add_one.wasm")).unwrap(); let wasm2 = fs::read(fixtures("homestar_guest_wasm.wasm")).unwrap(); - let mut env = World::instantiate(wasm1, "add_one".to_string(), State::default()) + let mut env = World::instantiate(wasm1, "add_one", State::default()) .await .unwrap(); @@ -180,7 +186,7 @@ async fn test_execute_wasms_in_seq() { assert_eq!(res, Output::Value(wasmtime::component::Val::S32(2))); - let env2 = World::instantiate_with_current_env(wasm2, "append_string".to_string(), &mut env) + let env2 = World::instantiate_with_current_env(wasm2, "append_string", &mut env) .await .unwrap(); @@ -197,28 +203,70 @@ async fn test_execute_wasms_in_seq() { ); } +#[tokio::test] +async fn test_multiple_args() { + let wasm = fs::read(fixtures("homestar_guest_wasm.wasm")).unwrap(); + + let ipld_str = Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("join-strings".to_string())), + ( + "args".into(), + Ipld::List(vec![ + Ipld::String("Round".to_string()), + Ipld::String("about".to_string()), + ]), + ), + ]))); + + let mut env = World::instantiate(wasm, "join-strings", State::default()) + .await + .unwrap(); + + let res = env + .execute(ipld_str.parse().unwrap().try_into().unwrap()) + .await + .unwrap(); + + assert_eq!( + res, + Output::Value(wasmtime::component::Val::String("Roundabout".into())) + ); +} + #[tokio::test] async fn test_execute_wasms_in_seq_with_threaded_result() { - let ipld_step_1 = Input::Ipld(Ipld::Map(BTreeMap::from([( - "args".into(), - Ipld::List(vec![Ipld::Integer(1)]), - )]))); + let ipld_step_1 = Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("join-strings".to_string())), + ( + "args".into(), + Ipld::List(vec![ + Ipld::String("Round".to_string()), + Ipld::String("about".to_string()), + ]), + ), + ]))); let h = Code::Blake3_256.digest(b"beep boop"); let cid = Cid::new_v1(0x55, h); let link: Link = Link::new(cid); - let invoked_task = InvocationPointer::new_from_link(link); + let invoked_instr = Pointer::new_from_link(link); - let promise = Await::new(invoked_task, AwaitResult::Ok); + let promise = Await::new(invoked_instr, AwaitResult::Ok); - let ipld_step_2 = Input::::Ipld(Ipld::Map(BTreeMap::from([( - "args".into(), - Ipld::List(vec![Ipld::try_from(promise).unwrap()]), - )]))); + let ipld_step_2 = Input::::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("join-strings".to_string())), + ( + "args".into(), + Ipld::List(vec![ + Ipld::try_from(promise).unwrap(), + Ipld::String("about".to_string()), + ]), + ), + ]))); - let wasm1 = fs::read(fixtures("add_one.wasm")).unwrap(); + let wasm1 = fs::read(fixtures("homestar_guest_wasm.wasm")).unwrap(); - let mut env = World::instantiate(wasm1.clone(), "add_one".to_string(), State::default()) + let mut env = World::instantiate(wasm1.clone(), "join-strings", State::default()) .await .unwrap(); @@ -227,9 +275,12 @@ async fn test_execute_wasms_in_seq_with_threaded_result() { .await .unwrap(); - assert_eq!(res, Output::Value(wasmtime::component::Val::S32(2))); + assert_eq!( + res, + Output::Value(wasmtime::component::Val::String("Roundabout".into())) + ); - let env2 = World::instantiate_with_current_env(wasm1, "add-one".to_string(), &mut env) + let env2 = World::instantiate_with_current_env(wasm1, "join-strings", &mut env) .await .unwrap(); @@ -238,12 +289,84 @@ async fn test_execute_wasms_in_seq_with_threaded_result() { // Short-circuit resolve with known value. let resolved = parsed .resolve(|_| { - Ok(InvocationResult::Ok(Arg::Value( - wasmtime::component::Val::S32(2), + Ok(InstructionResult::Ok(Arg::Value( + wasmtime::component::Val::String("RoundRound".into()), ))) }) .unwrap(); let res2 = env2.execute(resolved).await.unwrap(); - assert_eq!(res2, Output::Value(wasmtime::component::Val::S32(3))); + assert_eq!( + res2, + Output::Value(wasmtime::component::Val::String("RoundRoundabout".into())) + ); +} + +#[tokio::test] +async fn test_execute_wasms_with_multiple_inits() { + let ipld_step_1 = Input::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("join-strings".to_string())), + ( + "args".into(), + Ipld::List(vec![ + Ipld::String("Round".to_string()), + Ipld::String("about".to_string()), + ]), + ), + ]))); + + let h = Code::Blake3_256.digest(b"beep boop"); + let cid = Cid::new_v1(0x55, h); + let link: Link = Link::new(cid); + let invoked_instr = Pointer::new_from_link(link); + + let promise = Await::new(invoked_instr, AwaitResult::Ok); + + let ipld_step_2 = Input::::Ipld(Ipld::Map(BTreeMap::from([ + ("func".into(), Ipld::String("join-strings".to_string())), + ( + "args".into(), + Ipld::List(vec![ + Ipld::try_from(promise).unwrap(), + Ipld::String("about".to_string()), + ]), + ), + ]))); + + let wasm1 = fs::read(fixtures("homestar_guest_wasm.wasm")).unwrap(); + + let mut env = World::instantiate(wasm1.clone(), "join-strings", State::default()) + .await + .unwrap(); + + let res = env + .execute(ipld_step_1.parse().unwrap().try_into().unwrap()) + .await + .unwrap(); + + assert_eq!( + res, + Output::Value(wasmtime::component::Val::String("Roundabout".into())) + ); + + let mut env2 = World::instantiate(wasm1, "join-strings", State::default()) + .await + .unwrap(); + + let parsed: Args = ipld_step_2.parse().unwrap().try_into().unwrap(); + + // Short-circuit resolve with known value. + let resolved = parsed + .resolve(|_| { + Ok(InstructionResult::Ok(Arg::Ipld(Ipld::String( + "RoundRound".into(), + )))) + }) + .unwrap(); + + let res2 = env2.execute(resolved).await.unwrap(); + assert_eq!( + res2, + Output::Value(wasmtime::component::Val::String("RoundRoundabout".into())) + ); } diff --git a/migrations/2022-12-11-183928_create_receipts/down.sql b/migrations/2022-12-11-183928_create_receipts/down.sql deleted file mode 100644 index 92d4139a..00000000 --- a/migrations/2022-12-11-183928_create_receipts/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE receipts; -DROP INDEX ran_index; diff --git a/migrations/2022-12-11-183928_create_receipts/up.sql b/migrations/2022-12-11-183928_create_receipts/up.sql deleted file mode 100644 index b5aa656d..00000000 --- a/migrations/2022-12-11-183928_create_receipts/up.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TABLE receipts ( - cid TEXT NOT NULL PRIMARY KEY, - ran TEXT NOT NULL, - out BLOB NOT NULL, - meta BLOB NOT NULL, - iss TEXT, - prf BLOB NOT NULL -); - -CREATE INDEX ran_index ON receipts (ran); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 292fe499..5d56faf9 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "stable" +channel = "nightly"