diff --git a/.gitmodules b/.gitmodules index e6f8e3a6..de6b6f05 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "vendor/smoldot"] path = vendor/smoldot - url = https://github.com/ermalkaleci/smoldot.git + url = https://github.com/smol-dot/smoldot.git diff --git a/configs/acala.yml b/configs/acala.yml index 0110b0dd..53ba858b 100644 --- a/configs/acala.yml +++ b/configs/acala.yml @@ -5,6 +5,15 @@ mock-signature-host: true block: ${env.ACALA_BLOCK_NUMBER} db: ./db.sqlite +p2p: + genesisBlockHash: '0xfc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c' + bootnodes: + - /dns/acala-bootnode-4.aca-api.network/tcp/30334/ws/p2p/12D3KooWBLwm4oKY5fsbkdSdipHzYJJHSHhuoyb1eTrH31cidrnY + - /dns/acala-bootnode-5.aca-api.network/tcp/443/wss/p2p/12D3KooWN6ZZ2LFSJo2vDci3hqmmcvqMcKJAbREvuYCdvoBvV2D4 + - /dns/acala-bootnode-6.aca-api.network/tcp/80/ws/p2p/12D3KooWEBniruZHpoVj8RUtAFPahaN8UaGP6UtQb5Bdp4MVYbLc + - /dns/acala-bootnode-6.aca-api.network/tcp/443/wss/p2p/12D3KooWEBniruZHpoVj8RUtAFPahaN8UaGP6UtQb5Bdp4MVYbLc + - /dns/acala-bootnode-7.aca-api.network/tcp/80/ws/p2p/12D3KooWMq7AtHFx3ZboMT92HQw8BvhZFzJh8UrPCZeMB3yFLe1V + import-storage: Sudo: Key: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY # Alice diff --git a/configs/polkadot.yml b/configs/polkadot.yml index 0b329c0e..ad109438 100644 --- a/configs/polkadot.yml +++ b/configs/polkadot.yml @@ -6,6 +6,15 @@ block: ${env.POLKADOT_BLOCK_NUMBER} db: ./db.sqlite # wasm-override: polkadot_runtime.compact.compressed.wasm +p2p: + genesisBlockHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3' + bootnodes: + - /dns/polkadot-boot.dwellir.com/tcp/30334/ws/p2p/12D3KooWKvdDyRKqUfSAaUCbYiLwKY8uK3wDWpCuy2FiDLbkPTDJ + - /dns/polkadot-bootnode-0.polkadot.io/tcp/30334/ws/p2p/12D3KooWSz8r2WyCdsfWHgPyvD8GKQdJ1UAiRmrcrs8sQB3fe2KU + - /dns/polkadot-bootnode-0.polkadot.io/tcp/443/wss/p2p/12D3KooWSz8r2WyCdsfWHgPyvD8GKQdJ1UAiRmrcrs8sQB3fe2KU + - /dns/polkadot-bootnode-1.polkadot.io/tcp/30334/ws/p2p/12D3KooWFN2mhgpkJsDBuNuE5427AcDrsib8EoqGMZmkxWwx3Md4 + - /dns/polkadot-bootnode-1.polkadot.io/tcp/443/wss/p2p/12D3KooWFN2mhgpkJsDBuNuE5427AcDrsib8EoqGMZmkxWwx3Md4 + import-storage: System: Account: diff --git a/executor/Cargo.lock b/executor/Cargo.lock index 84f1ab35..89ba07f3 100644 --- a/executor/Cargo.lock +++ b/executor/Cargo.lock @@ -26,9 +26,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "arrayref" @@ -51,6 +51,71 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "async-channel" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" +dependencies = [ + "concurrent-queue", + "event-listener 5.3.0", + "event-listener-strategy 0.5.1", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" +dependencies = [ + "async-lock 3.3.0", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + [[package]] name = "async-lock" version = "3.3.0" @@ -58,21 +123,82 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ "event-listener 4.0.3", - "event-listener-strategy", + "event-listener-strategy 0.4.0", "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad07b3443bfa10dcddf86a452ec48949e8e7fedf7392d82de3969fda99e90ed" +dependencies = [ + "async-channel", + "async-io", + "async-lock 3.3.0", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.3.0", + "futures-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-signal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +dependencies = [ + "async-io", + "async-lock 2.8.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-task" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" + [[package]] name = "atomic-take" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "base64" @@ -101,6 +227,18 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "blake2-rfc" version = "0.2.18" @@ -129,6 +267,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel", + "async-lock 3.3.0", + "async-task", + "fastrand", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + [[package]] name = "bs58" version = "0.5.1" @@ -140,9 +294,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -152,9 +306,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cfg-if" @@ -178,17 +332,28 @@ name = "chopsticks-executor" version = "0.0.0" dependencies = [ "arrayvec 0.7.4", + "async-channel", "console_error_panic_hook", "console_log", + "derive_more", + "event-listener 4.0.3", + "fnv", + "futures-lite", "getrandom", + "hashbrown", "hex", "hex-literal", "js-sys", "log", + "no-std-net", + "nom", + "pin-project", "serde", "serde-wasm-bindgen", "serde_json", + "slab", "smoldot", + "smoldot-light", "wasm-bindgen", "wasm-bindgen-futures", ] @@ -320,7 +485,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.60", ] [[package]] @@ -358,9 +523,9 @@ dependencies = [ [[package]] name = "downcast-rs" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "ed25519" @@ -388,9 +553,25 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" @@ -399,16 +580,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", + "parking", "pin-project-lite", ] [[package]] name = "event-listener" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" dependencies = [ "concurrent-queue", + "parking", "pin-project-lite", ] @@ -422,11 +605,27 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "event-listener-strategy" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" +dependencies = [ + "event-listener 5.3.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" + [[package]] name = "fiat-crypto" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" +checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f" [[package]] name = "fnv" @@ -476,7 +675,10 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ + "fastrand", "futures-core", + "futures-io", + "parking", "pin-project-lite", ] @@ -488,7 +690,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.60", ] [[package]] @@ -533,9 +735,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "js-sys", @@ -564,6 +766,12 @@ dependencies = [ "serde", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hex" version = "0.4.3" @@ -638,9 +846,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" @@ -720,17 +928,42 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "merlin" @@ -797,7 +1030,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.60", ] [[package]] @@ -842,6 +1075,35 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + [[package]] name = "paste" version = "1.0.14" @@ -874,14 +1136,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.60", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -889,11 +1151,37 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "platforms" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" +checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" + +[[package]] +name = "polling" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] [[package]] name = "poly1305" @@ -914,18 +1202,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -960,6 +1248,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -969,6 +1266,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "ruzstd" version = "0.6.0" @@ -1004,6 +1314,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.22" @@ -1012,9 +1328,9 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] @@ -1041,20 +1357,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.60", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -1106,6 +1422,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -1133,12 +1458,29 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smol" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e635339259e51ef85ac7aa29a1cd991b957047507288697a690e80ab97d07cad" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock 3.3.0", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + [[package]] name = "smoldot" version = "0.17.0" dependencies = [ "arrayvec 0.7.4", - "async-lock", + "async-lock 3.3.0", "atomic-take", "base64 0.22.0", "bip39", @@ -1149,7 +1491,7 @@ dependencies = [ "derive_more", "ed25519-zebra", "either", - "event-listener 5.2.0", + "event-listener 5.3.0", "fnv", "futures-lite", "futures-util", @@ -1160,7 +1502,6 @@ dependencies = [ "libm", "libsecp256k1", "merlin", - "no-std-net", "nom", "num-bigint", "num-rational", @@ -1186,6 +1527,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "smoldot-light" +version = "0.15.0" +dependencies = [ + "async-channel", + "async-lock 3.3.0", + "base64 0.22.0", + "blake2-rfc", + "bs58", + "derive_more", + "either", + "event-listener 5.3.0", + "fnv", + "futures-channel", + "futures-lite", + "futures-util", + "hashbrown", + "hex", + "itertools", + "log", + "lru", + "parking_lot", + "pin-project", + "rand", + "rand_chacha", + "serde", + "serde_json", + "siphasher", + "slab", + "smol", + "smoldot", + "zeroize", +] + [[package]] name = "soketto" version = "0.8.0" @@ -1232,9 +1607,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -1256,6 +1631,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" + [[package]] name = "twox-hash" version = "1.6.3" @@ -1321,7 +1712,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.60", "wasm-bindgen-shared", ] @@ -1355,7 +1746,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1420,6 +1811,145 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -1449,7 +1979,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.60", ] [[package]] @@ -1469,5 +1999,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.60", ] diff --git a/executor/Cargo.toml b/executor/Cargo.toml index 26e81275..3fee979b 100644 --- a/executor/Cargo.toml +++ b/executor/Cargo.toml @@ -27,10 +27,21 @@ console_error_panic_hook = "0.1" console_log = { version = "1.0" } smoldot = { path = '../vendor/smoldot/lib', default-features = false } +smoldot-light = { path = '../vendor/smoldot/light-base', default-features = false } +no-std-net = { version = "0.6.0", default-features = false } +futures-lite = { version = "2.0.0", default-features = false, features = ["alloc"]} +derive_more = "0.99.17" +event-listener = { version = "4.0.2", default-features = false } +nom = { version = "7.1.3", features = [] } +slab = { version = "0.4.9", features = [] } +fnv = { version = "1.0.7", default-features = false } +hashbrown = { version = "0.14.2", default-features = false } +pin-project = "1.1.3" +async-channel = { version = "2.1.1", default-features = false } [features] default = [] -std = ["smoldot/std"] +std = ["smoldot/std", "smoldot-light/std"] [profile.release] codegen-units = 1 diff --git a/executor/scripts/pack-wasm.cjs b/executor/scripts/pack-wasm.cjs index 9261fffb..9637ea02 100755 --- a/executor/scripts/pack-wasm.cjs +++ b/executor/scripts/pack-wasm.cjs @@ -19,6 +19,11 @@ const BYTES = '${base64}'; import { base64Decode, unzlibSync } from '@polkadot/wasm-util'; const WASM_BYTES = unzlibSync(base64Decode(BYTES, new Uint8Array(LEN_IN)), new Uint8Array(LEN_OUT)); +const startedAt = BigInt(Date.now() * 1000); +globalThis.monotonic = function() { + return startedAt + BigInt(Math.floor(globalThis.performance.now() * 1000)); +}; + import { initSync } from "./chopsticks_executor.js"; initSync(new WebAssembly.Module(WASM_BYTES)); diff --git a/executor/src/lib.rs b/executor/src/lib.rs index d6e43bf2..9820d63b 100644 --- a/executor/src/lib.rs +++ b/executor/src/lib.rs @@ -7,8 +7,11 @@ use smoldot::{ use std::{collections::BTreeMap, str::FromStr}; use wasm_bindgen::prelude::*; +mod light_client; +mod platform; mod proof; mod task; +mod timers; fn setup_console(level: Option) { console_error_panic_hook::set_once(); @@ -19,7 +22,7 @@ fn setup_console(level: Option) { #[wasm_bindgen(typescript_custom_section)] const _: &'static str = r#" type HexString = `0x${string}`; -export interface JsCallback { +export interface JsRuntimeCallback { getStorage: (key: HexString) => Promise getNextKey: (prefix: HexString, key: HexString) => Promise offchainGetStorage: (key: HexString) => Promise @@ -31,31 +34,34 @@ export interface JsCallback { #[wasm_bindgen] extern "C" { - #[wasm_bindgen(typescript_type = "JsCallback")] - pub type JsCallback; + #[wasm_bindgen(typescript_type = "JsRuntimeCallback")] + pub type JsRuntimeCallback; #[wasm_bindgen(catch, structural, method, js_name = "getStorage")] - pub async fn get_storage(this: &JsCallback, key: JsValue) -> Result; + pub async fn get_storage(this: &JsRuntimeCallback, key: JsValue) -> Result; #[wasm_bindgen(catch, structural, method, js_name = "getNextKey")] pub async fn get_next_key( - this: &JsCallback, + this: &JsRuntimeCallback, prefix: JsValue, key: JsValue, ) -> Result; #[wasm_bindgen(catch, structural, method, js_name = "offchainGetStorage")] - pub async fn offchain_get_storage(this: &JsCallback, key: JsValue) -> Result; + pub async fn offchain_get_storage( + this: &JsRuntimeCallback, + key: JsValue, + ) -> Result; #[wasm_bindgen(catch, structural, method, js_name = "offchainTimestamp")] - pub async fn offchain_timestamp(this: &JsCallback) -> Result; + pub async fn offchain_timestamp(this: &JsRuntimeCallback) -> Result; #[wasm_bindgen(catch, structural, method, js_name = "offchainRandomSeed")] - pub async fn offchain_random_seed(this: &JsCallback) -> Result; + pub async fn offchain_random_seed(this: &JsRuntimeCallback) -> Result; #[wasm_bindgen(catch, structural, method, js_name = "offchainSubmitTransaction")] pub async fn offchain_submit_transaction( - this: &JsCallback, + this: &JsRuntimeCallback, tx: JsValue, ) -> Result; } @@ -122,7 +128,7 @@ pub async fn create_proof(nodes: JsValue, updates: JsValue) -> Result, ) -> Result { setup_console(log_level); @@ -135,7 +141,7 @@ pub async fn run_task( } #[wasm_bindgen] -pub async fn testing(js: JsCallback, key: JsValue) -> Result { +pub async fn testing(js: JsRuntimeCallback, key: JsValue) -> Result { setup_console(None); js.get_storage(key).await diff --git a/executor/src/light_client.rs b/executor/src/light_client.rs new file mode 100644 index 00000000..ab08ddcc --- /dev/null +++ b/executor/src/light_client.rs @@ -0,0 +1,484 @@ +extern crate console_error_panic_hook; + +use serde::{Deserialize, Serialize}; +use smoldot::{ + json_rpc::methods::{HashHexString, HexString}, + libp2p::PeerId, + network::{ + self, + service::{Multiaddr, Role}, + }, +}; +use smoldot_light::network_service::{Config, ConfigChain, NetworkService, NetworkServiceChain}; +use std::{ + collections::BTreeMap, + num::NonZeroU32, + str::FromStr, + sync::{Arc, Mutex}, + time::Duration, +}; +use wasm_bindgen::prelude::*; + +use crate::{platform::JsPlatform, proof::inner_decode_proof, setup_console}; + +#[wasm_bindgen(typescript_custom_section)] +const _: &'static str = r#" +export interface JsLightClientCallback { + startTimer: (delay: number) => void + connect: (connectionId: number, address: string, cert: Uint8Array) => void + resetConnection: (connectionId: number) => void + messageSend: (connectionId: number, data: Uint8Array) => void + queryResponse: (requestId: number, response: Response) => void +} + +export type Request = + { + storage: { + hash: HexString + keys: HexString[] + } + } + | + { + block: { + number: number | null + hash: HexString | null + header: boolean + body: boolean + } + } + +export type Response = + { + storage: [HexString, HexString][] + } + | + { + block: { + hash: HexString + header: HexString + body: HexString[] + } + } + | + { + error: string + } +"#; + +#[wasm_bindgen] +extern "C" { + // global method retuning microsecond timestamp + #[wasm_bindgen(js_name = "monotonic")] + pub fn monotonic_clock_us() -> u64; + + #[wasm_bindgen(typescript_type = "JsLightClientCallback")] + #[derive(Debug)] + pub type JsLightClientCallback; + + #[wasm_bindgen(structural, method, js_name = "startTimer")] + pub fn start_timer(this: &JsLightClientCallback, delay: u64); + + #[wasm_bindgen(structural, method, js_name = "connect")] + pub fn connect(this: &JsLightClientCallback, conn_id: u32, address: String, cert: Vec); + + #[wasm_bindgen(structural, method, js_name = "messageSend")] + pub fn message_send(this: &JsLightClientCallback, conn_id: u32, data: Vec); + + #[wasm_bindgen(structural, method, js_name = "resetConnection")] + pub fn reset_connection(this: &JsLightClientCallback, conn_id: u32); + + #[wasm_bindgen(structural, method, js_name = "queryResponse")] + pub fn query_response(this: &JsLightClientCallback, request_id: usize, response: JsValue); +} + +unsafe impl Sync for JsLightClientCallback {} +unsafe impl Send for JsLightClientCallback {} + +struct Chain { + network_service: Arc>, + peers: Mutex>, +} + +impl Chain { + fn new(chain: Arc>) -> Self { + Self { + network_service: chain, + peers: Mutex::new(BTreeMap::new()), + } + } + + fn peers(&self) -> Vec<(PeerId, Role, u64, HashHexString)> { + self.peers + .lock() + .unwrap() + .iter() + .map(|(peer_id, (role, best_number, best_hash))| { + (peer_id.clone(), *role, *best_number, best_hash.clone()) + }) + .collect() + } + + fn peers_list(&self) -> Vec { + self.peers.lock().unwrap().keys().cloned().collect() + } + + fn is_connected(&self) -> bool { + !self.peers.lock().unwrap().is_empty() + } + + fn latest_block(&self) -> Option<(u64, HashHexString)> { + let mut values = self + .peers + .lock() + .unwrap() + .values() + .map(|(_, best_number, best_hash)| (*best_number, best_hash.clone())) + .collect::>(); + values.sort_by(|(a, _), (b, _)| b.cmp(a)); + values.first().map(|(number, hash)| (*number, hash.clone())) + } +} + +static CHAINS: Mutex>> = Mutex::new(BTreeMap::new()); + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct NetworkServiceConfig { + genesis_block_hash: HashHexString, + bootnodes: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +enum Request { + Storage { + hash: HashHexString, + keys: Vec, + }, + Block { + hash: Option, + number: Option, + header: bool, + body: bool, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +enum Response { + Storage(Vec<(HexString, HexString)>), + Block(Block), + Error(String), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Block { + hash: HashHexString, + header: HexString, + body: Vec, +} + +#[wasm_bindgen] +pub async fn start_network_service( + config: JsValue, + callback: JsLightClientCallback, +) -> Result { + setup_console(None); + + let platform = JsPlatform { + callback: Arc::new(callback), + }; + + let network_service = NetworkService::new(Config { + platform, + identify_agent_version: concat!(env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION")) + .to_owned(), + chains_capacity: 1, + connections_open_pool_size: 8, + connections_open_pool_restore_delay: Duration::from_secs(3), + }); + + let config = serde_wasm_bindgen::from_value::(config)?; + + let mut addrs = BTreeMap::>::new(); + for node in config.bootnodes { + let mut parts = node.split("/p2p/"); + + let addr = parts.next().ok_or("invalid bootstrap node format")?; + let peer_id = parts.next().ok_or("invalid bootstrap node format")?; + + if !addr.ends_with("/ws") && !addr.ends_with("/wss") { + return Err("invalid bootstrap node format, only websocket is supported".into()); + } + + let addr = Multiaddr::from_str(addr).map_err(|e| e.to_string())?; + let peer_id = PeerId::from_str(peer_id).map_err(|e| e.to_string())?; + + addrs.entry(peer_id).or_default().push(addr); + } + + let chain = network_service.add_chain(ConfigChain { + log_name: "chopsticks".to_string(), + num_out_slots: 10, + genesis_block_hash: config.genesis_block_hash.0, + best_block: (0, config.genesis_block_hash.0), + fork_id: None, + block_number_bytes: 4, + grandpa_protocol_finalized_block_height: None, + }); + + let events = chain.subscribe().await; + let (connected_tx, connected_rx) = async_channel::unbounded::<()>(); + + let chain_state = Arc::new(Chain::new(chain.clone())); + let state = chain_state.clone(); + + wasm_bindgen_futures::spawn_local(async move { + loop { + match events.recv().await { + Ok(smoldot_light::network_service::Event::Connected { + peer_id, + role, + best_block_number, + best_block_hash, + }) => { + if !connected_tx.is_closed() { + connected_tx.send(()).await.unwrap(); + connected_tx.close(); + } + let mut peers = state.peers.lock().unwrap(); + peers.insert( + peer_id, + (role, best_block_number, HashHexString(best_block_hash)), + ); + } + Ok(smoldot_light::network_service::Event::Disconnected { peer_id }) => { + let mut peers = state.peers.lock().unwrap(); + peers.remove(&peer_id); + } + Ok(_) => { + // ignore + } + Err(_) => break, + } + } + }); + + chain.discover(addrs, true).await; + connected_rx.recv().await.map_err(|e| e.to_string())?; + + let mut chains = CHAINS.lock().unwrap(); + let chain_id = chains.len(); + chains.insert(chain_id, chain_state); + + Ok(chain_id) +} + +#[wasm_bindgen] +pub fn query_chain( + chain_id: usize, + request_id: usize, + request: JsValue, + mut retries: usize, + callback: JsLightClientCallback, +) -> Result<(), JsValue> { + setup_console(None); + + let chains = CHAINS.lock().unwrap(); + let chain = chains.get(&chain_id).cloned().ok_or("chain not found")?; + drop(chains); + + let request = serde_wasm_bindgen::from_value::(request)?; + + wasm_bindgen_futures::spawn_local(async move { + loop { + if !chain.is_connected() { + let response = Response::Error("no peers".to_string()); + callback + .query_response(request_id, serde_wasm_bindgen::to_value(&response).unwrap()); + break; + } + let peers = chain.peers_list(); + let index = request_id.saturating_add(retries) % peers.len(); + let peer_id = peers.get(index).cloned().expect("index out of range"); + retries = retries.saturating_sub(1); + + match &request { + Request::Storage { hash, keys } => { + let proof = chain + .network_service + .clone() + .storage_proof_request( + peer_id, + network::codec::StorageProofRequestConfig { + block_hash: hash.0, + keys: keys.clone().into_iter().map(|x| x.0), + }, + Duration::from_secs(30), + ) + .await; + + match proof { + Ok(proof) => { + let result = inner_decode_proof( + smoldot::trie::proof_decode::Config { + proof: proof.decode().to_vec(), + }, + None, + ); + + match result { + Ok(result) => { + let response = Response::Storage(result); + callback.query_response( + request_id, + serde_wasm_bindgen::to_value(&response).unwrap(), + ); + break; + } + Err(reason) => { + log::debug!( + "storage proof decode failed with error {:?}, try next peer", + reason + ); + } + } + } + Err(err) => { + log::debug!( + "storage proof request failed with error {:?}, try next peer", + err + ); + } + } + } + Request::Block { + hash, + number, + header, + body, + } => { + let response = chain + .network_service + .clone() + .blocks_request( + peer_id, + network::codec::BlocksRequestConfig { + start: if hash.is_some() { + network::codec::BlocksRequestConfigStart::Hash( + hash.clone().unwrap().0, + ) + } else { + network::codec::BlocksRequestConfigStart::Number( + number.unwrap_or(0), + ) + }, + direction: network::codec::BlocksRequestDirection::Descending, + desired_count: NonZeroU32::new(1).unwrap(), + fields: network::codec::BlocksRequestFields { + header: *header, + body: *body, + justifications: false, + }, + }, + Duration::from_secs(30), + ) + .await; + + match response { + Ok(blocks) => { + let mut result = blocks + .into_iter() + .map(|block| Block { + hash: HashHexString(block.hash), + header: HexString(block.header.unwrap_or_default()), + body: block + .body + .unwrap_or_default() + .into_iter() + .map(HexString) + .collect(), + }) + .collect::>(); + if result.is_empty() { + log::debug!("blocks request returned empty result, try next peer"); + continue; + } + + let response = Response::Block(result.remove(0)); + callback.query_response( + request_id, + serde_wasm_bindgen::to_value(&response).unwrap(), + ); + break; + } + Err(err) => { + log::debug!( + "blocks request failed with error {:?}, try next peer", + err + ); + } + } + } + } + + if retries == 0 { + let response = Response::Error("query out of retries".to_string()); + callback + .query_response(request_id, serde_wasm_bindgen::to_value(&response).unwrap()); + break; + } + } + }); + + Ok(()) +} + +#[wasm_bindgen] +pub fn message_received(connection_id: u32, data: Vec) { + crate::platform::message_received(connection_id, data); +} + +#[wasm_bindgen] +pub fn connection_writable_bytes(connection_id: u32, num_bytes: u32) { + crate::platform::connection_writable_bytes(connection_id, num_bytes); +} + +#[wasm_bindgen] +pub fn connection_reset(connection_id: u32) { + crate::platform::connection_reset(connection_id); +} + +#[wasm_bindgen] +pub fn wake_up(callback: JsLightClientCallback) { + crate::timers::wake_up(Arc::new(callback)); +} + +#[wasm_bindgen] +pub fn peers_list(chain_id: usize) -> Result { + let chains = CHAINS.lock().unwrap(); + let chain = chains.get(&chain_id).cloned().ok_or("chain not found")?; + let peers = chain + .peers() + .into_iter() + .map(|(peer_id, role, best_number, best_hash)| { + ( + peer_id.to_string(), + format!("{role:?}"), + best_number, + best_hash, + ) + }) + .collect::>(); + serde_wasm_bindgen::to_value(&peers).map_err(|x| x.into()) +} + +#[wasm_bindgen] +pub fn latest_block(chain_id: usize) -> Result { + let chains = CHAINS.lock().unwrap(); + let chain = chains.get(&chain_id).cloned().ok_or("chain not found")?; + let latest = chain.latest_block().ok_or("no peers")?; + serde_wasm_bindgen::to_value(&latest).map_err(|x| x.into()) +} diff --git a/executor/src/platform.rs b/executor/src/platform.rs new file mode 100644 index 00000000..e8f14a68 --- /dev/null +++ b/executor/src/platform.rs @@ -0,0 +1,592 @@ +use core::{future, mem, ops, pin, str, task, time::Duration}; +use futures_lite::future::FutureExt as _; +use smoldot_light::platform::{read_write, SubstreamDirection}; +use std::{ + borrow::Cow, + collections::{BTreeMap, VecDeque}, + sync::{Arc, Mutex}, +}; + +use crate::{ + light_client::{monotonic_clock_us, JsLightClientCallback}, + timers::Delay, +}; + +#[derive(Debug, Clone)] +pub(crate) struct JsPlatform { + pub callback: Arc, +} + +unsafe impl Sync for JsPlatform {} +unsafe impl Send for JsPlatform {} + +impl smoldot_light::platform::PlatformRef for JsPlatform { + type Delay = Delay; + type Instant = Duration; + type MultiStream = MultiStreamWrapper; // Entry in the ̀`CONNECTIONS` map. + type Stream = StreamWrapper; + type ReadWriteAccess<'a> = ReadWriteAccess<'a>; + type StreamErrorRef<'a> = StreamError; + // Entry in the ̀`STREAMS` map and a read buffer. + type StreamConnectFuture = future::Ready; + type MultiStreamConnectFuture = pin::Pin< + Box< + dyn future::Future< + Output = smoldot_light::platform::MultiStreamWebRtcConnection< + Self::MultiStream, + >, + > + Send, + >, + >; + type StreamUpdateFuture<'a> = pin::Pin + Send + 'a>>; + type NextSubstreamFuture<'a> = pin::Pin< + Box> + Send + 'a>, + >; + + fn now_from_unix_epoch(&self) -> Duration { + Duration::from_micros(monotonic_clock_us()) + } + + fn now(&self) -> Self::Instant { + Duration::from_micros(monotonic_clock_us()) + } + + fn fill_random_bytes(&self, buffer: &mut [u8]) { + buffer[0..8].copy_from_slice(&js_sys::Math::random().to_le_bytes()); + buffer[8..16].copy_from_slice(&js_sys::Math::random().to_le_bytes()); + buffer[16..24].copy_from_slice(&js_sys::Math::random().to_le_bytes()); + buffer[24..32].copy_from_slice(&js_sys::Math::random().to_le_bytes()); + } + + fn sleep(&self, duration: Duration) -> Self::Delay { + Delay::new(duration, self.callback.clone()) + } + + fn sleep_until(&self, when: Self::Instant) -> Self::Delay { + Delay::new_at_monotonic_clock(when, self.callback.clone()) + } + + fn spawn_task( + &self, + task_name: Cow, + task: impl future::Future + Send + 'static, + ) { + // The code below processes tasks that have names. + #[pin_project::pin_project] + struct FutureAdapter { + name: String, + #[pin] + future: F, + } + + impl future::Future for FutureAdapter { + type Output = F::Output; + fn poll(self: pin::Pin<&mut Self>, cx: &mut task::Context) -> task::Poll { + let this = self.project(); + log::trace!("current_task_entered {}", this.name); + let out = this.future.poll(cx); + log::trace!("current_task_exit {}", this.name); + out + } + } + + let task = FutureAdapter { + name: task_name.into_owned(), + future: task, + }; + + wasm_bindgen_futures::spawn_local(task); + } + + fn log<'a>( + &self, + _log_level: smoldot_light::platform::LogLevel, + _log_target: &'a str, + _message: &'a str, + _key_values: impl Iterator, + ) { + // TODO: + } + + fn client_name(&self) -> Cow { + env!("CARGO_PKG_NAME").into() + } + + fn client_version(&self) -> Cow { + env!("CARGO_PKG_VERSION").into() + } + + fn supports_connection_type( + &self, + connection_type: smoldot_light::platform::ConnectionType, + ) -> bool { + match connection_type { + smoldot_light::platform::ConnectionType::TcpIpv4 + | smoldot_light::platform::ConnectionType::TcpIpv6 + | smoldot_light::platform::ConnectionType::TcpDns => false, + smoldot_light::platform::ConnectionType::WebSocketIpv4 { .. } => true, + smoldot_light::platform::ConnectionType::WebSocketIpv6 { .. } => false, + smoldot_light::platform::ConnectionType::WebSocketDns { .. } => true, + smoldot_light::platform::ConnectionType::WebRtcIpv4 => false, + smoldot_light::platform::ConnectionType::WebRtcIpv6 => false, + } + } + + fn connect_stream( + &self, + address: smoldot_light::platform::Address, + ) -> Self::StreamConnectFuture { + let mut lock = STATE.try_lock().unwrap(); + + let connection_id = lock.next_connection_id; + lock.next_connection_id += 1; + + let encoded_address = match address { + smoldot_light::platform::Address::WebSocketIp { + ip: core::net::IpAddr::V4(ip), + port, + } => format!("ws://{ip}:{port}"), + smoldot_light::platform::Address::WebSocketIp { + ip: core::net::IpAddr::V6(ip), + port, + } => format!("ws://[{ip}]:{port}"), + smoldot_light::platform::Address::WebSocketDns { + hostname, + port, + secure: false, + } => format!("ws://{hostname}:{port}"), + smoldot_light::platform::Address::WebSocketDns { + hostname, + port, + secure: true, + } => format!("wss://{hostname}:{port}"), + _ => panic!("unsupported address type"), + }; + + let write_closable = match address { + smoldot_light::platform::Address::TcpIp { .. } + | smoldot_light::platform::Address::TcpDns { .. } => true, + smoldot_light::platform::Address::WebSocketIp { .. } + | smoldot_light::platform::Address::WebSocketDns { .. } => false, + }; + + self.callback + .connect(connection_id, encoded_address, vec![]); + + let _prev_value = lock.connections.insert( + connection_id, + Connection { + inner: ConnectionInner::SingleStreamMsNoiseYamux, + something_happened: event_listener::Event::new(), + }, + ); + debug_assert!(_prev_value.is_none()); + + let _prev_value = lock.streams.insert( + connection_id, + Stream { + reset: None, + messages_queue: VecDeque::with_capacity(8), + messages_queue_total_size: 0, + something_happened: event_listener::Event::new(), + writable_bytes_extra: 0, + }, + ); + debug_assert!(_prev_value.is_none()); + + future::ready(StreamWrapper { + connection_id, + read_buffer: Vec::new(), + inner_expected_incoming_bytes: Some(1), + is_reset: None, + writable_bytes: 0, + write_closable, + write_closed: false, + when_wake_up: None, + callback: self.callback.clone(), + }) + } + + fn connect_multistream( + &self, + _address: smoldot_light::platform::MultiStreamAddress, + ) -> Self::MultiStreamConnectFuture { + unimplemented!() + } + + fn open_out_substream( + &self, + MultiStreamWrapper(_connection_id, _callback): &mut Self::MultiStream, + ) { + unimplemented!() + } + + fn next_substream<'a>( + &self, + MultiStreamWrapper(_connection_id, _callback): &'a mut Self::MultiStream, + ) -> Self::NextSubstreamFuture<'a> { + unimplemented!() + } + + fn read_write_access<'a>( + &self, + stream: pin::Pin<&'a mut Self::Stream>, + ) -> Result, Self::StreamErrorRef<'a>> { + let stream = stream.get_mut(); + + if let Some(message) = &stream.is_reset { + return Err(StreamError { + message: message.clone(), + }); + } + + Ok(ReadWriteAccess { + read_write: read_write::ReadWrite { + now: Duration::from_micros(monotonic_clock_us()), + incoming_buffer: mem::take(&mut stream.read_buffer), + expected_incoming_bytes: Some(0), + read_bytes: 0, + write_buffers: Vec::new(), + write_bytes_queued: 0, + write_bytes_queueable: if !stream.write_closed { + Some(stream.writable_bytes) + } else { + None + }, + wake_up_after: None, + }, + stream, + }) + } + + fn wait_read_write_again<'a>( + &self, + stream: pin::Pin<&'a mut Self::Stream>, + ) -> Self::StreamUpdateFuture<'a> { + Box::pin(async move { + let stream = stream.get_mut(); + + if stream.is_reset.is_some() { + future::pending::<()>().await; + } + + loop { + let listener = { + let mut lock = STATE.try_lock().unwrap(); + let stream_inner = lock.streams.get_mut(&stream.connection_id).unwrap(); + + if let Some(msg) = &stream_inner.reset { + stream.is_reset = Some(msg.clone()); + return; + } + + let mut shall_return = false; + + // Move the buffers from `STATE` into `read_buffer`. + if !stream_inner.messages_queue.is_empty() { + stream + .read_buffer + .reserve(stream_inner.messages_queue_total_size); + + while let Some(msg) = stream_inner.messages_queue.pop_front() { + stream_inner.messages_queue_total_size -= msg.len(); + // TODO: could be optimized by reworking the bindings + stream.read_buffer.extend_from_slice(&msg); + if stream + .inner_expected_incoming_bytes + .map_or(false, |expected| expected <= stream.read_buffer.len()) + { + shall_return = true; + break; + } + } + } + + if stream_inner.writable_bytes_extra != 0 { + // As documented, the number of writable bytes must never become + // exceedingly large (a few megabytes). As such, this can't overflow + // unless there is a bug on the JavaScript side. + stream.writable_bytes += stream_inner.writable_bytes_extra; + stream_inner.writable_bytes_extra = 0; + shall_return = true; + } + + if shall_return { + return; + } + + stream_inner.something_happened.listen() + }; + + let timer_stop = async move { + listener.await; + false + } + .or(async { + if let Some(when_wake_up) = stream.when_wake_up.as_mut() { + when_wake_up.await; + stream.when_wake_up = None; + true + } else { + future::pending().await + } + }) + .await; + + if timer_stop { + return; + } + } + }) + } +} + +pub(crate) struct ReadWriteAccess<'a> { + pub read_write: read_write::ReadWrite, + pub stream: &'a mut StreamWrapper, +} + +impl<'a> ops::Deref for ReadWriteAccess<'a> { + type Target = read_write::ReadWrite; + + fn deref(&self) -> &Self::Target { + &self.read_write + } +} + +impl<'a> ops::DerefMut for ReadWriteAccess<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.read_write + } +} + +impl<'a> Drop for ReadWriteAccess<'a> { + fn drop(&mut self) { + let mut lock = STATE.try_lock().unwrap(); + + let stream_inner = lock.streams.get_mut(&self.stream.connection_id).unwrap(); + + if (self.read_write.read_bytes != 0 + && self + .read_write + .expected_incoming_bytes + .map_or(false, |expected| { + expected >= self.read_write.incoming_buffer.len() + })) + || (self.read_write.write_bytes_queued != 0 + && self.read_write.write_bytes_queueable.is_some()) + { + self.read_write.wake_up_asap(); + } + + self.stream.when_wake_up = self + .read_write + .wake_up_after + .map(|until| Delay::new_at_monotonic_clock(until, self.stream.callback.clone())); + + self.stream.read_buffer = mem::take(&mut self.read_write.incoming_buffer); + + self.stream.inner_expected_incoming_bytes = self.read_write.expected_incoming_bytes; + + for buffer in self.read_write.write_buffers.drain(..) { + assert!(buffer.len() <= self.stream.writable_bytes); + self.stream.writable_bytes -= buffer.len(); + + if stream_inner.reset.is_none() { + self.stream + .callback + .message_send(self.stream.connection_id, buffer); + } + } + + if self.read_write.write_bytes_queueable.is_none() && !self.stream.write_closed { + self.stream.write_closed = true; + } + } +} + +pub struct StreamWrapper { + pub connection_id: u32, + pub read_buffer: Vec, + pub inner_expected_incoming_bytes: Option, + /// `Some` if the remote has reset the stream and `update_stream` has since then been called. + /// Contains the error message. + pub is_reset: Option, + pub writable_bytes: usize, + pub write_closable: bool, + pub write_closed: bool, + /// The stream should wake up after this delay. + pub when_wake_up: Option, + pub callback: Arc, +} + +unsafe impl Sync for StreamWrapper {} +unsafe impl Send for StreamWrapper {} + +impl Drop for StreamWrapper { + fn drop(&mut self) { + let mut lock = STATE.try_lock().unwrap(); + let lock = &mut *lock; + lock.connections.remove(&self.connection_id).unwrap(); + } +} + +pub(crate) struct MultiStreamWrapper(u32, Arc); + +impl Drop for MultiStreamWrapper { + fn drop(&mut self) { + let mut lock = STATE.try_lock().unwrap(); + + let connection = lock.connections.get_mut(&self.0).unwrap(); + let (remove_connection, reset_connection) = match &mut connection.inner { + ConnectionInner::SingleStreamMsNoiseYamux { .. } => { + unreachable!() + } + ConnectionInner::Reset => (true, false), + }; + + if remove_connection { + lock.connections.remove(&self.0).unwrap(); + } + if reset_connection { + self.1.reset_connection(self.0); + } + } +} + +#[derive(Debug, derive_more::Display, Clone)] +#[display(fmt = "{message}")] +pub(crate) struct StreamError { + message: String, +} + +static STATE: Mutex = Mutex::new(NetworkState { + next_connection_id: 0, + connections: hashbrown::HashMap::with_hasher(FnvBuildHasher), + streams: BTreeMap::new(), +}); + +// TODO: we use a custom `FnvBuildHasher` because it's not possible to create `fnv::FnvBuildHasher` in a `const` context +struct FnvBuildHasher; +impl core::hash::BuildHasher for FnvBuildHasher { + type Hasher = fnv::FnvHasher; + fn build_hasher(&self) -> fnv::FnvHasher { + fnv::FnvHasher::default() + } +} + +/// All the connections that are alive. +struct NetworkState { + next_connection_id: u32, + connections: hashbrown::HashMap, + streams: BTreeMap, +} + +#[derive(Debug)] +struct Connection { + /// Type of connection and extra fields that depend on the type. + inner: ConnectionInner, + /// Event notified whenever one of the fields above is modified. + something_happened: event_listener::Event, +} + +#[derive(Debug)] +enum ConnectionInner { + SingleStreamMsNoiseYamux, + /// [`bindings::connection_reset`] has been called + Reset, +} + +struct Stream { + /// `Some` if [`bindings::stream_reset`] has been called. Contains the error message. + reset: Option, + /// Sum of the writable bytes reported through [`bindings::stream_writable_bytes`] that + /// haven't been processed yet in a call to `update_stream`. + writable_bytes_extra: usize, + /// List of messages received through [`bindings::stream_message`]. Must never contain + /// empty messages. + messages_queue: VecDeque>, + /// Total size of all the messages stored in [`Stream::messages_queue`]. + messages_queue_total_size: usize, + /// Event notified whenever one of the fields above is modified, such as a new message being + /// queued. + something_happened: event_listener::Event, +} + +pub(crate) fn connection_writable_bytes(connection_id: u32, bytes: u32) { + let mut lock = STATE.try_lock().unwrap(); + if lock.connections.get_mut(&connection_id).is_none() { + return; + } + + let stream = lock.streams.get_mut(&connection_id).unwrap(); + debug_assert!(stream.reset.is_none()); + + // As documented, the number of writable bytes must never become exceedingly large (a few + // megabytes). As such, this can't overflow unless there is a bug on the JavaScript side. + stream.writable_bytes_extra += usize::try_from(bytes).unwrap(); + stream.something_happened.notify(usize::MAX); +} + +pub fn message_received(connection_id: u32, message: Vec) { + if message.is_empty() { + return; + } + let mut lock = STATE.try_lock().unwrap(); + // ensure connection is active + if lock.connections.get_mut(&connection_id).is_none() { + return; + } + + let stream = lock.streams.get_mut(&connection_id).unwrap(); + debug_assert!(stream.reset.is_none()); + + // There is unfortunately no way to instruct the browser to back-pressure connections to + // remotes. + // + // In order to avoid DoS attacks, we refuse to buffer more than a certain amount of data per + // connection. This limit is completely arbitrary, and this is in no way a robust solution + // because this limit isn't in sync with any other part of the code. In other words, it could + // be legitimate for the remote to buffer a large amount of data. + // + // This corner case is handled by discarding the messages that would go over the limit. While + // this is not a great solution, going over that limit can be considered as a fault from the + // remote, the same way as it would be a fault from the remote to forget to send some bytes, + // and thus should be handled in a similar way by the higher level code. + // + // A better way to handle this would be to kill the connection abruptly. However, this would + // add a lot of complex code in this module, and the effort is clearly not worth it for this + // niche situation. + // + // While this problem is specific to browsers (Deno and NodeJS have ways to back-pressure + // connections), we add this hack for all platforms, for consistency. If this limit is ever + // reached, we want to be sure to detect it, even when testing on NodeJS or Deno. + // + // See . + // TODO: do this properly eventually ^ + if stream.messages_queue_total_size >= 25 * 1024 * 1024 { + return; + } + + stream.messages_queue_total_size += message.len(); + stream.messages_queue.push_back(message.into_boxed_slice()); + stream.something_happened.notify(usize::MAX); +} + +pub fn connection_reset(connection_id: u32) { + let mut lock = STATE.try_lock().unwrap(); + let connection = lock.connections.get_mut(&connection_id); + let connection = match connection { + Some(connection) => connection, + None => return, + }; + + connection.inner = ConnectionInner::Reset; + + connection.something_happened.notify(usize::MAX); + + if let Some(stream) = lock.streams.get_mut(&connection_id) { + stream.reset = Some("connection reset".into()); + stream.something_happened.notify(usize::MAX); + } +} diff --git a/executor/src/proof.rs b/executor/src/proof.rs index 20df1c96..42766e69 100644 --- a/executor/src/proof.rs +++ b/executor/src/proof.rs @@ -16,22 +16,32 @@ pub fn decode_proof( let config = Config::> { proof: encode_proofs(nodes), }; + let entries = inner_decode_proof(config, Some(&trie_root_hash.0))?; + Ok(entries) +} + +pub fn inner_decode_proof( + config: Config>, + trie_root_hash: Option<&[u8; 32]>, +) -> Result, String> { let decoded = decode_and_verify_proof(config).map_err(|e| e.to_string())?; let entries = decoded .iter_ordered() - .filter(|(key, entry)| { - if !key.trie_root_hash.eq(&trie_root_hash.0) { - return false; + .filter(|(entry_key, proof_entry)| { + if let Some(trie_root_hash) = trie_root_hash { + if !entry_key.trie_root_hash.eq(trie_root_hash) { + return false; + } } matches!( - entry.trie_node_info.storage_value, + proof_entry.trie_node_info.storage_value, StorageValue::Known { .. } ) }) - .map(|(key, entry)| { - let key = HexString(nibbles_to_bytes_suffix_extend(key.key).collect::>()); - match entry.trie_node_info.storage_value { + .map(|(entry_key, proof_entry)| { + let key = HexString(nibbles_to_bytes_suffix_extend(entry_key.key).collect::>()); + match proof_entry.trie_node_info.storage_value { StorageValue::Known { value, .. } => (key, HexString(value.to_vec())), _ => unreachable!(), } @@ -251,10 +261,7 @@ fn create_proof_works() { let (hash, nodes) = create_proof(get_nodes(), updates).unwrap(); let decoded = decode_proof(hash, nodes.iter().map(|x| x.0.clone()).collect::>()).unwrap(); - assert!(decoded - .iter() - .find(|(key, _)| key == &dmq_mqc_head) - .is_none()); + assert!(!decoded.iter().any(|(key, _)| key == &dmq_mqc_head)); // current_slot is not changed let (_, value) = decoded diff --git a/executor/src/task.rs b/executor/src/task.rs index ea2c2f95..fad701a7 100644 --- a/executor/src/task.rs +++ b/executor/src/task.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; use smoldot::{ executor::{ - host::{Config, HeapPages, HostVmPrototype, LogEmitInfo}, - runtime_call::{self, OffchainContext, RuntimeCall}, + host::{Config, HeapPages, HostVmPrototype}, + runtime_call::{self, LogEmitInfo, OffchainContext, RuntimeCall}, storage_diff::TrieDiff, CoreVersionRef, }, @@ -117,7 +117,10 @@ fn handle_value(value: wasm_bindgen::JsValue) -> Result>, JsError } } -pub async fn run_task(task: TaskCall, js: crate::JsCallback) -> Result { +pub async fn run_task( + task: TaskCall, + js: crate::JsRuntimeCallback, +) -> Result { let mut storage_main_trie_changes = TrieDiff::default(); let mut storage_changes: BTreeMap, Option>> = Default::default(); let mut offchain_storage_changes: BTreeMap, Option>> = Default::default(); diff --git a/executor/src/timers.rs b/executor/src/timers.rs new file mode 100644 index 00000000..f5cbc00a --- /dev/null +++ b/executor/src/timers.rs @@ -0,0 +1,242 @@ +use core::{ + cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}, + future, mem, + pin::Pin, + task::{Context, Poll, Waker}, + time::Duration, +}; +use std::sync::Arc; +use std::{collections::BTreeSet, sync::Mutex}; + +use crate::light_client::{monotonic_clock_us, JsLightClientCallback}; + +pub(crate) fn wake_up(js_callback: Arc) { + process_timers(js_callback); +} + +/// `Future` that automatically wakes up after a certain amount of time has elapsed. +pub struct Delay { + /// Index in `TIMERS::timers`. Guaranteed to have `is_obsolete` equal to `false`. + /// If `None`, then this timer is already ready. + timer_id: Option, +} + +impl Delay { + pub fn new(after: Duration, js_callback: Arc) -> Self { + let now = Duration::from_micros(monotonic_clock_us()); + Self::new_inner(now + after, js_callback) + } + + pub fn new_at_monotonic_clock(when: Duration, js_callback: Arc) -> Self { + Self::new_inner(when, js_callback) + } + + fn new_inner(when: Duration, js_callback: Arc) -> Self { + let now = Duration::from_micros(monotonic_clock_us()); + // Small optimization because sleeps of 0 seconds are frequent. + if when <= now { + return Delay { timer_id: None }; + } + + // Because we're in a single-threaded environment, `try_lock()` should always succeed. + let mut lock = TIMERS.try_lock().unwrap(); + + let timer_id = lock.timers.insert(Timer { + is_finished: false, + is_obsolete: false, + waker: None, + }); + + lock.timers_queue.insert(QueuedTimer { when, timer_id }); + + // If the timer that has just been inserted is the one that ends the soonest, then + // actually start the callback that will process timers. + // Ideally we would instead cancel or update the deadline of the previous call to + // `start_timer`, but this isn't possible. + if lock + .timers_queue + .first() + .unwrap_or_else(|| unreachable!()) + .timer_id + == timer_id + { + js_callback.start_timer((when - now).as_millis() as u64); + } + + Delay { + timer_id: Some(timer_id), + } + } +} + +impl future::Future for Delay { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let timer_id = match self.timer_id { + Some(id) => id, + None => return Poll::Ready(()), + }; + + // Because we're in a single-threaded environment, `try_lock()` should always succeed. + let mut lock = TIMERS.try_lock().unwrap(); + debug_assert!(!lock.timers[timer_id].is_obsolete); + + if lock.timers[timer_id].is_finished { + lock.timers.remove(timer_id); + self.timer_id = None; + return Poll::Ready(()); + } + + lock.timers[timer_id].waker = Some(cx.waker().clone()); + Poll::Pending + } +} + +impl Drop for Delay { + fn drop(&mut self) { + let timer_id = match self.timer_id { + Some(id) => id, + None => return, + }; + + // Because we're in a single-threaded environment, `try_lock()` should always succeed. + let mut lock = TIMERS.try_lock().unwrap(); + debug_assert!(!lock.timers[timer_id].is_obsolete); + + if lock.timers[timer_id].is_finished { + lock.timers.remove(timer_id); + return; + } + + lock.timers[timer_id].is_obsolete = true; + lock.timers[timer_id].waker = None; + } +} + +static TIMERS: Mutex = Mutex::new(Timers { + timers_queue: BTreeSet::new(), + timers: slab::Slab::new(), +}); + +struct Timers { + /// Same entries as `timer`, but ordered based on when they're finished (from soonest to + /// latest). Items are only ever removed from [`process_timers`] when they finish, even if + /// the corresponding [`Delay`] is destroyed. + timers_queue: BTreeSet, + + /// List of all timers. + timers: slab::Slab, +} + +struct Timer { + /// If `true`, then this timer has elapsed. + is_finished: bool, + /// If `true`, then the corresponding `Delay` has been destroyed or no longer points to this + /// item. + is_obsolete: bool, + /// How to wake up the `Delay`. + waker: Option, +} + +struct QueuedTimer { + when: Duration, + + // Entry in `TIMERS::timers`. Guaranteed to always have `is_finished` equal to `false`. + timer_id: usize, +} + +impl PartialEq for QueuedTimer { + fn eq(&self, other: &Self) -> bool { + matches!(self.cmp(other), Ordering::Equal) + } +} + +impl Eq for QueuedTimer {} + +impl PartialOrd for QueuedTimer { + fn partial_cmp(&self, other: &Self) -> Option { + Some(Ord::cmp(self, other)) + } +} + +impl Ord for QueuedTimer { + fn cmp(&self, other: &Self) -> Ordering { + // `when` takes higher priority in the ordering. + match self.when.cmp(&other.when) { + Ordering::Equal => self.timer_id.cmp(&other.timer_id), + ord => ord, + } + } +} + +/// Marks as ready all the timers in `TIMERS` that are finished. +fn process_timers(js_callback: Arc) { + // Because we're in a single-threaded environment, `try_lock()` should always succeed. + let mut lock = TIMERS.try_lock().unwrap(); + let lock = &mut *lock; + let now = Duration::from_micros(monotonic_clock_us()); + + // Note that this function can be called spuriously. + // For example, `process_timers` can be scheduled twice from two different timers, and the + // first call leads to both timers being finished, after which the second call will be + // spurious. + + // We remove all the queued timers whose `when` is inferior to `now`. + let expired_timers = { + let timers_remaining = lock.timers_queue.split_off(&QueuedTimer { + when: now, + // Note that `split_off` returns values greater or equal, meaning that if a timer had + // a `timer_id` equal to `max_value()` it would erroneously be returned instead of being + // left in the collection as expected. For obvious reasons, a `timer_id` of + // `usize::max_value()` is impossible, so this isn't a problem. + timer_id: usize::max_value(), + }); + + mem::replace(&mut lock.timers_queue, timers_remaining) + }; + + // Wake up the expired timers. + for timer in expired_timers { + debug_assert!(timer.when <= now); + debug_assert!(!lock.timers[timer.timer_id].is_finished); + lock.timers[timer.timer_id].is_finished = true; + if let Some(waker) = lock.timers[timer.timer_id].waker.take() { + waker.wake(); + } + } + + // Figure out the next time we should call `process_timers`. + // + // This iterates through all the elements in `timers_queue` until a valid one is found. + let next_wakeup: Option = loop { + let next_timer = match lock.timers_queue.first() { + Some(t) => t, + None => break None, + }; + + // The `Delay` corresponding to the iterated timer has been destroyed. Removing it and + // `continue`. + if lock.timers[next_timer.timer_id].is_obsolete { + let next_timer_id = next_timer.timer_id; + lock.timers.remove(next_timer_id); + lock.timers_queue + .pop_first() + .unwrap_or_else(|| unreachable!()); + continue; + } + + // Iterated timer is not ready. + break Some(next_timer.when); + }; + + if let Some(next_wakeup) = next_wakeup { + let duration = next_wakeup - now; + js_callback.start_timer(duration.as_millis() as u64); + } else { + // Clean up memory a bit. Hopefully this doesn't impact performances too much. + if !lock.timers.is_empty() && lock.timers.capacity() > lock.timers.len() * 8 { + lock.timers.shrink_to_fit(); + } + } +} diff --git a/packages/chopsticks/src/context.ts b/packages/chopsticks/src/context.ts index 0f8b33ce..8d832a29 100644 --- a/packages/chopsticks/src/context.ts +++ b/packages/chopsticks/src/context.ts @@ -47,6 +47,7 @@ export const setupContext = async (argv: Config, overrideParent = false) => { offchainWorker: argv['offchain-worker'], maxMemoryBlockCount: argv['max-memory-block-count'], processQueuedMessages: argv['process-queued-messages'], + p2p: argv.p2p, }) // load block from db diff --git a/packages/chopsticks/src/schema/index.ts b/packages/chopsticks/src/schema/index.ts index cf69602c..95fc4a33 100644 --- a/packages/chopsticks/src/schema/index.ts +++ b/packages/chopsticks/src/schema/index.ts @@ -65,6 +65,12 @@ export const configSchema = z.object({ 'Produce extra block when queued messages are detected. Default to true. Set to false to disable it.', }) .optional(), + p2p: z + .object({ + genesisBlockHash: zHash, + bootnodes: z.array(z.string()), + }) + .optional(), }) export type Config = z.infer diff --git a/packages/chopsticks/src/schema/options.test.ts b/packages/chopsticks/src/schema/options.test.ts index af312a04..de2f6cc3 100644 --- a/packages/chopsticks/src/schema/options.test.ts +++ b/packages/chopsticks/src/schema/options.test.ts @@ -68,6 +68,12 @@ it('get yargs options from zod schema', () => { "description": "Enable offchain worker", "type": "boolean", }, + "p2p": { + "choices": undefined, + "demandOption": false, + "description": undefined, + "type": undefined, + }, "port": { "choices": undefined, "demandOption": false, diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index 36a290d3..53a545d7 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -6,6 +6,25 @@ import _ from 'lodash' import { ChainProperties, Header, SignedBlock } from './index.js' import { prefixedChildKey, splitChildKey, stripChildPrefix } from './utils/index.js' +export interface ApiT { + isReady: Promise + chain: Promise + chainProperties: Promise + signedExtensions: ExtDef + disconnect(): Promise + getSystemName(): Promise + getSystemProperties(): Promise + getSystemChain(): Promise + getBlockHash(blockNumber?: number): Promise + getHeader(hash?: string): Promise
+ getBlock(hash?: string): Promise + getStorage(key: string, hash?: string): Promise + getStorageBatch(prefix: HexString, keys: HexString[], hash?: HexString): Promise<[HexString, HexString | null][]> + getKeysPaged(prefix: string, pageSize: number, startKey: string, hash?: string): Promise + subscribeRemoteNewHeads(cb: ProviderInterfaceCallback): Promise + subscribeRemoteFinalizedHeads(cb: ProviderInterfaceCallback): Promise +} + /** * API class. Calls provider to get on-chain data. * Either `endpoint` or `genesis` porvider must be provided. @@ -18,7 +37,7 @@ import { prefixedChildKey, splitChildKey, stripChildPrefix } from './utils/index * await api.isReady * ``` */ -export class Api { +export class Api implements ApiT { #provider: ProviderInterface #ready: Promise | undefined #chain: Promise | undefined @@ -51,7 +70,7 @@ export class Api { } } - return this.#ready + return this.#ready! } get chain(): Promise { @@ -120,7 +139,7 @@ export class Api { if (hash) params.push(hash as HexString) return this.#provider .send('childstate_getKeysPaged', params, !!hash) - .then((keys) => keys.map((key) => prefixedChildKey(child, key))) + .then((keys) => keys.map((key) => prefixedChildKey(child, key) as HexString)) } else { // main storage key, use state_getKeysPaged const params = [prefix, pageSize, startKey] diff --git a/packages/core/src/blockchain/get-keys-paged.test.ts b/packages/core/src/blockchain/get-keys-paged.test.ts index 692c64c8..7ab5b176 100644 --- a/packages/core/src/blockchain/get-keys-paged.test.ts +++ b/packages/core/src/blockchain/get-keys-paged.test.ts @@ -1,4 +1,5 @@ import { Api } from '../api.js' +import { HexString } from '@polkadot/util/types' import { RemoteStorageLayer, StorageLayer, StorageValueKind } from './storage-layer.js' import { describe, expect, it, vi } from 'vitest' import _ from 'lodash' @@ -24,7 +25,7 @@ describe('getKeysPaged', () => { Api.prototype['getKeysPaged'] = vi.fn(async (prefix, pageSize, startKey) => { const withPrefix = allKeys.filter((k) => k.startsWith(prefix) && k > startKey) const result = withPrefix.slice(0, pageSize) - return result + return result as HexString[] }) const mockApi = new Api(undefined!) diff --git a/packages/core/src/blockchain/index.ts b/packages/core/src/blockchain/index.ts index 623dde05..98ea73d9 100644 --- a/packages/core/src/blockchain/index.ts +++ b/packages/core/src/blockchain/index.ts @@ -9,7 +9,7 @@ import _ from 'lodash' import type { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types' import type { TransactionValidity } from '@polkadot/types/interfaces/txqueue' -import { Api } from '../api.js' +import { ApiT } from '../api.js' import { Block } from './block.js' import { BuildBlockMode, BuildBlockParams, DownwardMessage, HorizontalMessage, TxPool } from './txpool.js' import { Database } from '../database.js' @@ -26,7 +26,7 @@ const logger = defaultLogger.child({ name: 'blockchain' }) export interface Options { /** API instance, for getting on-chain data. */ - api: Api + api: ApiT /** Build block mode. Default to Batch. */ buildBlockMode?: BuildBlockMode /** Inherent provider, for creating inherents. */ @@ -76,7 +76,7 @@ export interface Options { */ export class Blockchain { /** API instance, for getting on-chain data. */ - readonly api: Api + readonly api: ApiT /** Datasource for caching storage and blocks data. */ readonly db: Database | undefined /** Enable mock signature. Any signature starts with 0xdeadbeef and filled by 0xcd is considered valid */ diff --git a/packages/core/src/blockchain/storage-layer.ts b/packages/core/src/blockchain/storage-layer.ts index c6fddc30..6c706ba0 100644 --- a/packages/core/src/blockchain/storage-layer.ts +++ b/packages/core/src/blockchain/storage-layer.ts @@ -1,7 +1,7 @@ import { HexString } from '@polkadot/util/types' import _ from 'lodash' -import { Api } from '../api.js' +import { ApiT } from '../api.js' import { CHILD_PREFIX_LENGTH, PREFIX_LENGTH, isPrefixedChildKey } from '../utils/index.js' import { Database } from '../database.js' import { defaultLogger } from '../logger.js' @@ -38,13 +38,13 @@ export interface StorageLayerProvider { } export class RemoteStorageLayer implements StorageLayerProvider { - readonly #api: Api + readonly #api: ApiT readonly #at: string readonly #db: Database | undefined readonly #keyCache = new KeyCache(PREFIX_LENGTH) readonly #defaultChildKeyCache = new KeyCache(CHILD_PREFIX_LENGTH) - constructor(api: Api, at: string, db: Database | undefined) { + constructor(api: ApiT, at: string, db: Database | undefined) { this.#api = api this.#at = at this.#db = db @@ -58,6 +58,7 @@ export class RemoteStorageLayer implements StorageLayerProvider { } } logger.trace({ at: this.#at, key }, 'RemoteStorageLayer get') + const data = await this.#api.getStorage(key, this.#at) this.#db?.saveStorage(this.#at as HexString, key as HexString, data) return data ?? undefined diff --git a/packages/core/src/genesis-provider.ts b/packages/core/src/genesis-provider.ts index 6c5e0542..0285d261 100644 --- a/packages/core/src/genesis-provider.ts +++ b/packages/core/src/genesis-provider.ts @@ -8,7 +8,7 @@ import { } from '@polkadot/rpc-provider/types' import { Genesis, genesisSchema } from './schema/index.js' -import { JsCallback, calculateStateRoot, emptyTaskHandler } from './wasm-executor/index.js' +import { JsRuntimeCallback, calculateStateRoot, emptyTaskHandler } from './wasm-executor/index.js' import { defaultLogger, isPrefixedChildKey } from './index.js' /** @@ -117,7 +117,7 @@ export class GenesisProvider implements ProviderInterface { } } - get _jsCallback(): JsCallback { + get _jsCallback(): JsRuntimeCallback { const storage = this.#genesis.genesis.raw.top return { ...emptyTaskHandler, diff --git a/packages/core/src/p2p.ts b/packages/core/src/p2p.ts new file mode 100644 index 00000000..4d60ad93 --- /dev/null +++ b/packages/core/src/p2p.ts @@ -0,0 +1,130 @@ +import { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types' +import { HexString } from '@polkadot/util/types' +import { ProviderInterfaceCallback } from '@polkadot/rpc-provider/types' +import { TypeRegistry } from '@polkadot/types' +import { hexToU8a } from '@polkadot/util' + +import { ApiT } from './api.js' +import { ChainProperties, Header, SignedBlock } from './index.js' +import { LightClient, LightClientConfig } from './wasm-executor/light-client.js' + +export type { LightClientConfig } + +export class P2P extends LightClient implements ApiT { + // to decode header + #registry = new TypeRegistry() + + static async create(config: LightClientConfig, fallback: ApiT) { + await fallback.isReady + const client = new P2P(config, fallback) + await client.isReady + return client + } + + constructor( + config: LightClientConfig, + readonly fallback: ApiT, + ) { + super(config) + } + + get signedExtensions(): ExtDef { + return this.fallback.signedExtensions + } + + get chain() { + return this.fallback.chain + } + + get chainProperties() { + return this.fallback.chainProperties + } + + get isReady() { + return super.isReady + } + + async disconnect(): Promise { + return this.fallback.disconnect() + } + + async getSystemName(): Promise { + return this.fallback.getSystemName() + } + + async getSystemProperties(): Promise { + return this.fallback.getSystemProperties() + } + + async getSystemChain(): Promise { + return this.fallback.getSystemChain() + } + + async getBlockHash(blockNumber?: number): Promise { + if (!blockNumber && blockNumber != 0) { + ;[blockNumber] = await this.getLatestBlock() + } + try { + return this.queryBlock(blockNumber).then(({ hash }) => hash) + } catch (_) { + return this.fallback.getBlockHash(blockNumber) + } + } + + async getHeader(hash?: string): Promise
{ + if (!hash) { + ;[, hash] = await this.getLatestBlock() + } + try { + const block = await this.queryBlock(hash as HexString) + return this.#registry.createType('Header', hexToU8a(block.header)).toJSON() as Header + } catch (_) { + return this.fallback.getHeader(hash) + } + } + + async getBlock(hash?: string): Promise { + if (!hash) { + ;[, hash] = await this.getLatestBlock() + } + try { + const block = await this.queryBlock(hash as HexString) + const header = this.#registry.createType('Header', hexToU8a(block.header)).toJSON() as Header + return { block: { header, extrinsics: block.body }, justifications: [] } + } catch (_) { + return this.fallback.getBlock(hash) + } + } + + async getStorage(key: string, hash?: string): Promise { + if (!hash) { + ;[, hash] = await this.getLatestBlock() + } + const storage = await this.queryStorage([key as HexString], hash as HexString) + const pair = storage.find(([k]) => k === key) + return pair?.[1] || null + } + + async getStorageBatch( + _prefix: HexString, + keys: HexString[], + hash?: HexString, + ): Promise<[HexString, HexString | null][]> { + if (!hash) { + ;[, hash] = await this.getLatestBlock() + } + return this.queryStorage(keys, hash as HexString) + } + + async getKeysPaged(prefix: string, pageSize: number, startKey: string, hash?: string): Promise { + return this.fallback.getKeysPaged(prefix, pageSize, startKey, hash) + } + + async subscribeRemoteNewHeads(cb: ProviderInterfaceCallback): Promise { + return this.fallback.subscribeRemoteNewHeads(cb) + } + + async subscribeRemoteFinalizedHeads(cb: ProviderInterfaceCallback): Promise { + return this.fallback.subscribeRemoteFinalizedHeads(cb) + } +} diff --git a/packages/core/src/rpc/substrate/system.ts b/packages/core/src/rpc/substrate/system.ts index 1a4ff769..a106688b 100644 --- a/packages/core/src/rpc/substrate/system.ts +++ b/packages/core/src/rpc/substrate/system.ts @@ -1,21 +1,28 @@ import { HexString } from '@polkadot/util/types' import { Index } from '@polkadot/types/interfaces' import { hexToU8a } from '@polkadot/util' +import _ from 'lodash' import { ChainProperties } from '../../index.js' import { Handler } from '../shared.js' +import { LightClient } from '../../wasm-executor/light-client.js' export const system_localPeerId = async () => '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' export const system_nodeRoles = async () => ['Full'] export const system_localListenAddresses = async () => [] export const system_chain: Handler = async (context) => { - return context.chain.api.getSystemChain() + return context.chain.head.runtimeVersion.then((runtime) => _.capitalize(runtime.specName)) } export const system_properties: Handler = async (context) => { - return context.chain.api.getSystemProperties() + const meta = await context.chain.head.meta + const properties = meta.registry.getChainProperties() + if (!properties) { + throw new Error('No chain properties found') + } + return properties.toJSON() as ChainProperties } export const system_name: Handler = async (context) => { - return context.chain.api.getSystemName() + return context.chain.head.runtimeVersion.then((runtime) => _.capitalize(runtime.implName)) } export const system_version: Handler = async (_context) => { return 'chopsticks-v1' @@ -23,7 +30,15 @@ export const system_version: Handler = async (_context) => { export const system_chainType: Handler = async (_context) => { return 'Development' } -export const system_health = async () => { +export const system_health: Handler = async (context) => { + if (context.chain.api instanceof LightClient) { + const peers = await context.chain.api.getPeers() + return { + peers: peers.length, + isSyncing: false, + shouldHavePeers: true, + } + } return { peers: 0, isSyncing: false, @@ -31,6 +46,13 @@ export const system_health = async () => { } } +export const system_peers: Handler = async (context) => { + if (context.chain.api instanceof LightClient) { + return context.chain.api.getPeers() + } + return [] +} + /** * @param context * @param params - [`extrinsic`, `at`] diff --git a/packages/core/src/setup.ts b/packages/core/src/setup.ts index 64aa2b89..6d294e5b 100644 --- a/packages/core/src/setup.ts +++ b/packages/core/src/setup.ts @@ -6,11 +6,12 @@ import { ProviderInterface } from '@polkadot/rpc-provider/types' import { RegisteredTypes } from '@polkadot/types/types' import { compactAddLength } from '@polkadot/util' -import { Api } from './api.js' +import { Api, ApiT } from './api.js' import { Blockchain } from './blockchain/index.js' import { BuildBlockMode } from './blockchain/txpool.js' import { Database } from './database.js' import { GenesisProvider } from './genesis-provider.js' +import { LightClientConfig, P2P } from './p2p.js' import { defaultLogger } from './logger.js' import { getSlotDuration, setStorage } from './index.js' import { inherentProviders } from './blockchain/inherent/index.js' @@ -28,6 +29,7 @@ export type SetupOptions = { offchainWorker?: boolean maxMemoryBlockCount?: number processQueuedMessages?: boolean + p2p?: LightClientConfig } export const genesisSetup = async (chain: Blockchain, genesis: GenesisProvider) => { @@ -79,8 +81,14 @@ export const processOptions = async (options: SetupOptions) => { } else { provider = new WsProvider(options.endpoint, 3_000) } - const api = new Api(provider) - await api.isReady + + let api: ApiT + if (options.p2p) { + api = await P2P.create(options.p2p, new Api(provider)) + } else { + api = new Api(provider) + await api.isReady + } let blockHash: string if (options.block == null) { diff --git a/packages/core/src/wasm-executor/__snapshots__/executor.test.ts.snap b/packages/core/src/wasm-executor/__snapshots__/executor.test.ts.snap index 9447ecfb..6f9191bb 100644 --- a/packages/core/src/wasm-executor/__snapshots__/executor.test.ts.snap +++ b/packages/core/src/wasm-executor/__snapshots__/executor.test.ts.snap @@ -1,5 +1,63 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`wasm > LightClient works 1`] = ` +{ + "body": [ + "0x280401000b20da607f8c01", + "0x1dda041e0091033659d46f5f48cdf0f9ac5e49fbc27657ec676dbe3b83ecc5f38fd4926a4dff23d67a39016500d671b12ef935e683569a9aa96adb613850d26d74645a6e00682614a7aa3f51e59c63b61d273dc0649a9891d78334836a6abb47c0e152d51be8008a31ef880c066175726120c767750800000000045250535290dbcae8a8b71ee45cf5c0e1bfb812ef7c14583f40dd78a0269f60c9a4f9ec215d5ede72040561757261010190ffd9ef55c7a6429a3e549652e03348054e90d587bac2b12ab795d9221e4b43b59b8889f8c3936c724e0154a76e4f2b43636dc4d3d6b54f4d8816b6d75e0f8399b71c01fecbb2d1c841cb5ab9b89e315282a71ef3d640ac12de4f266ec2810c465353a8000050005501b456f5a4efb16ffa83d007000080b23ce24557b486acbe8f76e1dc4a51faf9f1afe079534ff5b8f0d38cf1b6f6e9e856f5a4efb16ffa83d0070000b42ce8030000d4070000d6070000d8070000db070000dc070000f0070000f2070000f3070000f50700003808000099015c61975d97255ddb070000d00700005501e803000000900100009001000000000000000000018e8b1b1010c04392c2f54f101f4af58087d49b6f3b3141a401792d4ed222300600e8764817000000000000000000000000e8764817000000000000000000000099015c8c2de8299067f3070000d00700005501e80300000090010000900100000000000000000001d98c6c97df04a1cd31bd86f44de6e2ac0752153cb43ef42320a3381472121d0b00e8764817000000000000000000000000e8764817000000000000000000000099015ce1ee506d55f8d0070000f30700005501e80300000090010000900100000000000000000001873f836ad8dd3a1ce112bdb9416ee94c7d36a6be429aa6f5fe84076fca995d6b00e8764817000000000000000000000000e876481700000000000000000000009d015d01c872d0ebaf85d0070000f50700005501e80300000090010000900100000000000000000001929da5da4bbef691330783c8693787afee14c7a121b8614608098e1dcf70593900e8764817000000000000000000000000e876481700000000000000000000009d015d0452a22bee61fad0070000f20700005501e80300000090010000900100000000000000000001881c4a68554ccea849d73154df80a742bf368a91d266102e6a4736f91e5ac64400e8764817000000000000000000000000e876481700000000000000000000009d015d04d2a15ab51127e8030000d00700005501e803000000900100009001000000000000000000017c1dc6ebd2be7529c8b80bbe345f7904e676756afae7e70045fa057ac3b6098400e8764817000000000000000000000000e876481700000000000000000000009d015d057a605f506cfcd4070000d00700005501e803000000900100009001000000000000000000014117a3a323ff75a02f2a537fb64af30ec1533e52adfa25e7f793066b4d0b1b0e00e8764817000000000000000000000000e876481700000000000000000000009d015d06ad4314650419d0070000f00700005501e803000000900100009001000000000000000000013c77bdaaeff3a48e612968456031e8ec30cf19e4a79eb04a4b385774a0fa099c00e8764817000000000000000000000000e876481700000000000000000000009d015d07edc4cbc65e03d0070000d40700005501e8030000009001000090010000000000000000000195d565fb5bec539d20211ce72d66519697517d4c04cbcf598438f17354a4d5d900e8764817000000000000000000000000e876481700000000000000000000009d015d099274c2ff3639d0070000d60700005501e8030000009001000090010000000000000000000167b262980d743ac4e2cb50f0aa79778f2f909c84b7e8956d8ad3dc15523a125800e8764817000000000000000000000000e876481700000000000000000000009d015d0a87461a6f93a938080000d00700005501e80300000090010000900100000000000000000001a5b55864a6db03420b9512ae322e62a2b24e314541d0853ab59761969e962e2000e8764817000000000000000000000000e876481700000000000000000000009d015d0b652b2ae6ed1ddc070000d00700005501e80300000090010000900100000000000000000001146e54f204e45061ffac1cde16bf97dbe18681ff2dd5c23b68f5c128a79ab22b00e8764817000000000000000000000000e876481700000000000000000000009d015d0bc334ef110d8af5070000d00700005501e80300000090010000900100000000000000000001e9b5d5c413d4e8b434475465438ed59afc3c931bd7da3915f4fdf50f1eefcff600e8764817000000000000000000000000e876481700000000000000000000009d015d0c472775baca93f0070000d00700005501e8030000009001000090010000000000000000000121bcdfeb3b1009571129ab828480ff59a556cfb7196426f7e7ef10fbdf6ca46c00e8764817000000000000000000000000e876481700000000000000000000009d015d0fcde7347851edd0070000380800005501e80300000090010000900100000000000000000001081ee805c616b44e60071b808d81f0ad2bf4c36c34537c9c51c8b641c3b6c54400e8764817000000000000000000000000e876481700000000000000000000009d015e046fae65527199f2070000d00700005501e80300000090010000900100000000000000000001f420e187719ff5b6dd89709d5fe1bce35f4d8716fe62bb1b18ae8e0acfe9ee1300e8764817000000000000000000000000e876481700000000000000000000009d015e1a4e21b9b6ce11d8070000d00700005501e803000000900100009001000000000000000000015063b45fb0f97475aae3195a3a5ee0b06ce909ecc8c856d42d51ebde6fc6773900e8764817000000000000000000000000e876481700000000000000000000009d015e36c65ca123d5fbd0070000dc0700005501e8030000009001000090010000000000000000000174394a76857cb1bdfc1d612f215e5e9c36319e2a5b04adf1e892b90bfb73809b00e8764817000000000000000000000000e87648170000000000000000000000c45e414cb008e0e61e46722aa60abdd6728010fda851987882185b68b499bf2924b220e810111782fc3802ce80cf547e45289d015e4f36708366b722d0070000e80300005501e80300000090010000900100000000000000000001a522607a82c4fba67f1015b1be833a2a6a0e18f9b92efc11324527533d8e8c9300e8764817000000000000000000000000e876481700000000000000000000009d015e55c8e02d73966fd6070000d00700005501e80300000090010000900100000000000000000001aabb7b5c60751031ef98e4321ab5c4eb3f381228c2d21d373bfc2f1f6da20a2a00e8764817000000000000000000000000e87648170000000000000000000000c85e77dfdb8adb10f78f10a5df8742c5458401ed0d04cc5a6ea9743cb8341585f8fc5563e6e6c50efdb6c70573004e097eba729d015ec412f496dcf830d0070000d80700005501e8030000009001000090010000000000000000000117bd272b0646c5d00cba5dfa84f4bcf40affc6e0901c0e116b3a679aeec62bab00e8764817000000000000000000000000e876481700000000000000000000009d015ec648b30353eed1d0070000db0700005501e80300000090010000900100000000000000000001dbe04867c42f4be9c33447ce856b2079b63709c6f59e4423f2c3381e0969b20000e8764817000000000000000000000000e87648170000000000000000000000c45ee678799d3eff024253b90e84927cc6809f37720eb43a44f8657b87cf45bc389738884b3882f81c117568b8a57ed1932e9d035f04b49d95320d9021994c850f25b8e38551030000300000500000aaaa020000001000fbff0000100000000a000000403800005802000000000000000000000000500000c800001e00000000e8764817000000000000000000000000e87648170000000000000000000000e8030000009001001e000000009001000401002000004038000000000000000000001027000080b2e60e80c3c90180969800000000000000000000000000050000000a0000000a0000000100000001050000000006000000580200000300000059000000000000001e00000006000000020000001400000002000000150180000a80c84358a62885a21f919c583c76ffd7784d89441e84b8051e2a6f92d39e3e5ffc80532e85eed045d52d30950254f1a06bbc36c791ca6840be623485146ac19ad396150180001480cd86b870a09d37da7dcb1c95d7a97e2d061dd701b67f4dbc18227928dd07a3a280d58f3ee935d62db9ca52481526d98cafc3cf8b3c76d829f53f19dd18d3b5f3d899018000a480f0391087e1a965846400166e1319c552c5ed50cfd242aee90f409446ab7b94758005ab41999ad6a54ade7dfeb524e16772571b3f43bea2920b2958edabb66a04c580474034aec36a0baa6fb797679270c837ef5ed0f1c7e5673010dd16bf390469a9d48000c0787694c040f5e73d9b7addd6cb603d15d3b0021d9da3ceafbd080d00000401485e4993f016e2d2f8e5f43be7bb259486040015018001018093cd0833a646108627a0abb0c1c74d56dc93f5d37a5c12563c1f3f53661623cf805da29f971106bbbf298300105b9c497204a5bd81131c4e3d59af24182ee76af21501800102803808a1f9a1ed9df2f55624ee5ec1b4f90c5b5d6b1e7f6acdad2011d9b91126e1807f1966fef92b5aa55bc885ae3036e3845bb05551d0496fc667ace455d6ef764c1501800110808cde5ffcfc7f02559aa531e7fa1cac5cc3d4767ebc9e284a22ba5d1a385a7ed180e9865ddc9ead7f13d14df026ca4647f1a2e55d66ce23669df5ceeb554640f4f5150180011080bea88465837cd4f0f401c6bafd4ec367903d8ac6a0c7b52fc85cf30b9d354d9e800ebe650369ce0e3ce85b22816181f598cc6ecf7a3b22314357ab4181b9ccb8671501800404800cf6fb2aa078f9a747836812f8d89638a7fb3e5d0d7227756c8fc301f6d2f5cc803d38bf966c25f2f19b0b8cac0c5f4d25fb8962f20de5a8986d691ed9a2aa5a251501800404803a8ade541f10b1509992cc3fe4cfd267b61b83fdd731b55fbf10acfa7950fe4b800fe6b03639ff2739e42b263a64b97458bbb7b04a7f13a7dd10b77ba5c3d3940e1d0280046480addf2d2206b7757577996c9c37aafe43f52c3ff6860d21346850c182c03d24ef805c23ffebaea922397423cc697f0b769c384908cd8848a07673dbfe1ba706291980da6a91b27486aa7f48d015fff737e269addb169852ce12cb69a46cc350861d50809ce93dcb3a478e4f43cd270d64c97c3b3992919b5f151b4507b02394ab0be49f1501800802802097d23e0d23ace24d5f96fac6f6b62fa51b042a5af9ae4009c64df8d1eb382d80a9433febdd8594144651b2653ba5d64bd6703997436061f6ffc25fc1cf2097dc150180080480c9813c46a054d2966919ef836517e1006a7d9b2a2d424a3cd9b7cd7c8ea76e9a80d48c844a1076aaf08cffc0f6d90b01a98f61555a56e228d8b80dd081403171a5990180080c80d066a00dc79b0a8601ea466d09a4c969c246ed7d8b4f029afcd41944e33ae8b08014cb2a691c6adfa53c9a168a34fec9f34b2e52979ed9cec258a008bc43f7e1c78035310f9020f175e93e118197c2c76b1e445aad0fb35f10a81074b2ff201306cbbc8008405456f5a4efb16ffa83d0070000200000000000000000545628b8ad2696ed532c080000200000000000000000490180085080720475da1eb4b5f077fca4ad1e6add131c2ce61ab829c8ca4ef18029c79bacc048563524f87eaf5eaf000800001404e8030000685628b8ad2696ed532c080000340ce8030000d4070000f2070000990180085080f838d87f0dd4bf1f08671f82ff022192f9f34e61e0d02d036a71e137d373707380aa355c7789c126573837fde95fd60e274cddf219667c77dafaf68056bda3be668007042a0d62233e2c11a767e0231c176e6188e4e92a1a72824fee2a600cd81af31501800a0080db3bb1ed1a103404b67cbaf621d072a75c2e4032abbbd9131f808935b0d4ab7f801e33e7113f701d758488751ebeeba927f1c4868a4cd674ce8faf0c39eca4b16a9901800c20805d5b0e4db857b29690cd80b1a465f922930a2824583a17956bfa3e6bf9b472f380b6a9f43245bbc7b97e90b7a10f9f34a278b00033bfcebaa7e97870925c56055980e08202f643c479533f93eea09586502bdac989d24f518b5020bf99acd938b9f2150180100280e84015ffbd8d05efa8962f8aadae0ba4180ecca50821a4aa744245eefebb1e4580a54b0069bf2d0e88a8e00099dcc800f6e0474a904d3116953cdcdca2940d65a1a10280101780932edc507cca768cad5334f8059779676fbc4d2e2d6178c6fe8e2b0e3eefd44480f938d806a16fbb5367b9f4beca51cdb1389825570ff1201ed332005b0d452880803636a7a20fd9f62f714e4edc65a5888a6a2ed41993ca4da9f3f6b2f6fc998a4e80200d54a30fd69028b041b105165ec1d14f12fa9e2a0b94236403140e6bcdac1e8034484ffd63678ff2fc926492bea8177eb435a6f39e0256a05419a1c572c623329901801060807f443125bb5b140fa4143328f167f581dd45542cefd0fa40426893a68d6d90cc80130d8ca189ca04528b6c768c16d91748f4e8c4098239bf15ed58ceaaecc7ca64800695333da432176eb8189e38de4421658d1497ee0e9b2c4b39ec1543036c277f1501801200804f39a131d9ac6aabf819341c3cdb6865998fb630aa677145a5be9f00c4a71f4680aa21fb1594824bf9d5b9c19d8c1a2df251a0fe59a0945c434091945abc27975ca9038022d580c7f341af5a9b1a28524bb8b74512c3d9fab1b164dc92262c59af7aa77641db3d809c659382ea08c2c1a07c5dd938f72bac83b3f5fa9373875c4bda622e9962c66780de3b20bbcec97367816b77a7914e20ebb5e2324b058ab6766692410904e84dd0804de62c9f80e29431aeb3c0f713701cfa017c07f531c69b2dceb4b7ed3a7b75928077e20e3e8fd18cebde793df6204f3259fb7851cd86b5e29df9245d4bb4d06fee80ad4b1f1bff610d731ae7bab96ee9dbd3c0396f92a720cc9a814202f007dc03fc802e997541464939b382d86000353ce3a3b5e5f36e979ca2126ed611703c42e0872d0480364e8076d70d537f2d895f33fd09e2a3f5c0b198f2ddf6f600fe06060e927c9c278fa080c289a7ca876a070237a7e50624bc4180df99cac4f2864f39a538dda98dfd33ba802ad0c1b1c962b83bf4bcbc8670cefaa403273368f9e6e12a2947c5573d8a145480b9e3a62e0e36feeecf90645d515c1cb87efafae31d85025158131dd78de7a55e800cc15a9d83df18b5f4d9ef46245e9864d3a9314066ecfd4f39e576933c9feea08040d9ea03ca5269ac56b07c924191233ca2c0b3a8d238b5442983033f0e1e9e4380374b2c59ae8fe22deb920ac93589e20a1aca24bf9261afa1c553248fbb1473fd80d81c1d021087b20a853f76219fa76c718e994a7f6123975d1e24adb80316e260b1048038bd802a714a5a2ea4c5b25b3af09bce05f009316149bb7d14bd235e436d99b7bf2d48806a589224d4d534b6331ba0977040bc657cd51bf4c05a58067e3d6ac2be3da8908000912ae6a5c315f81a0e3b509a044e67419272ed02110c41a675c8c91fb5735b80993bbeeedb11f905e32baf1d7b1bd1fadda3acd0e076a83a2535b7aae39a0d3780ee24f3b2903321d446785887676af40410ce03efae00d65ad807f9eb6e44f6bc80ec32b4e081b8ead82fbe9c65cc39a91a97c009616c3f6b01437dc8c2730cbf88808d7aaccd962246b357466fc9d9c06006347edb93665759686056990b1db1c9f480d8ad9b7d7d3cecfbbb642a382f222160fe55dbf7e40e6c22de282a07dc361beb80bbbc5b0ff9f35df1a9b986a72b2442da03f93a45fef20802b94c773f9fe267923505803b7680742868383d04df90032d0698ffc3af78e94ad383d6882a863afe5eff0c6ed3678060bb8cf21be66125eed568f1fb055b56a0fb3def61f28054d42be5c7c724f7c880e3bac8ff7c3819e8351a6f38c6c32b2685a550ce4af50b631025a5d2ba7bdc5e8092d48796bc98e900a130bbffc27af63cf1c5b831c76316751a6679fbc228bb7380ab5806bff3f2e19e0ba562e0c3f27f955159f3641792d2acc8a1efec29dcd7e28036410790d4d330c0e848c3762394a60453ab1681e73d368147fbbcee24ec002e80160b83a164038f80571a0e5c69bf5b662acc68caa10cfa67cf9c50b87afab827803579a8242d681e18153339172c3ddbc356f53e1f46c60f0778959892e874d91e8017242a99d10f54e9f056e07b804693a81de82312f0735cab483b0a5a6cb443e680c6e23d4e232a91069bdb681e7e5a8309c6537da00df3ef481d6bc2859e0085c6a903803ca180670b04a57b97c2b26a9fea56e8925625427d3a3e4bf3952f149c94520d53a82280055461cd38a9cf927d9a89d17e559dcf1aa968924dc6f1941e327184c2ee9bf880fa7fd77d177520ad84723bc1d6635426a6389edf5ed9b55df52f37f644c3fc6d80d43aac2ec8df93850d8fb9e7bea864d228418eec6cb9cd00f7803b98f0b6c35e80abe87bb4984be27ca570bdf988035478b15e9427c5c42955d46e2c901675c74b805885c0f20bb2dddf674fa02d518ae49a306d54e1d4f83999f36fb100cbe0dd528041226c11ce70de8e21f814a8d8db54142882a37b18e183ea7c37f476600141181d0280410980b99d2f06330426d6fd2236079500a33e1655e08090f0962ea588cbd0eac4f7ee80413feba6193feec3f4bc72cde1c7aa72d21681191385e6f1c791614155951bf0805fdb71f3db4adaa5f1baf89d0dea15d9fdb4a1c8ac29a00ed1b8b6492bfdb35080a5544c237847f51820a9aee8563b7c83502755218c2cbf5a8e1810d2fe18de729501805139344607000020aaaa020000001000344607000020aaaa020000001000344607000020aaaa020000001000344607000020aaaa020000001000344607000020aaaa020000001000344607000020aaaa020000001000344607000020aaaa020000001000a9038058c38033fb0b82b4466907d39970dd5e098a14d0b7d085ced766dfbc88288c087bdc8c806301994fa7771d2f7c41d3c22670f0fe3909fb2a44d96c661ed3ded90e06b912804b67a06013fb7b06e5a5bcc576a575b380149037fe60434c6db85c7b4e4b536080befaa9698c856efcf4e587e452b1029cac0dc1e86582920759840687155794bb80107386445b6175c3bc908ebcd6c17d6a82dac1c1fb9bb600f6f73a8d6de4e822807feccf79d85e050f458cdf053a880d157db42d3fc1d382ea37e9e6b49c0a4365807cd2230ec8256dd2dc3189c852608334b9e531f73579143af31c2360ea9af729990180610080084fc0ec034be7d2bf630b685d86cc989a1ebff3c9b71494f747cee62f5d975080b4dd9b8d61fe6edfd32e5946762ab4b388cd03da8386ec5b2d6d85b1ae2d942980af579d5ddc5c697d42bfc014076594e66c7b324cfd3017810c4e93e4f6f0ae9ea903806938803271cfa7df9ee7c69a172b0eaafde90864e22f55abc4cf18c1005b6408e134c380378fe030a51ef64a26c89f3f170eb1c47cb837bc98a179c0ee6a646e323a8ee180f4c6355f089d28cca637f9115300b449a98d2869f0866ba4124cf2044e91ef778027f70c5fd56ffba90b436874f45eb3a6c3ea5ecacf30720af1f6753767a9580e80073b3e21ed410087262fa5f8981cde9fa62eb8b04ba9b56bf26ef03fb81a9870809fb6d7014c854f8ac4ac9eb1c7e014d4347a4781df21559f01afebdf01c92b0a807683b6b7c778d2e5822ad54a415ce6d79e931b7f76d14be1efdf961770c3ab061501808001809974c5f900dc6cf2270218c4084321b780e0f681d76432760971011e879b54a7807e88df503fa7f1c6b444ee4b48ba83f122b2d38cdc2df957ea9a3d1ff4c9e617b10480b07b8034551a19e8e89fc9727f7103cb1db3e468ea827957dc585852c39fa354bd3ffa8013a13f70fbd34f2a2e564450c978048a43317770fb0602e22763e85d4507b6f1805e7928d1195c354973747f2c22165d0df486ebdc226762eb7cc0abe8f4859d2c8059771dfc7bd636b0e438e7c44696986066e7960013c28401407ee85d28403794807216fb3af8f88cd40c131480ae0f690ddac4a73430db31a04ab1e87bef54ced780223476531d8b382ad64d32b640a401d2de1df2660df5bf4339083183a30981ef807c3e1c0f1cfa08ce5d7c2343cfd8df3da11834e0c05c7622d29032e7b56f8814805c96dd70b176e6d0cf7598fbec04e64fa3dfa06dda086083dee5c34e949bd11980fb9aa550a0af9fd0cd47a559bd1471291bf4da049240f88a964fd496c36ffefb3d0680bff280606ad500ff07801bb7b2bb86596bd12a19f3de80716c4da78763b9a4cd33cc2d802626fcd90645454a021e80719aec628af61dc941a03bba4ec0debc4de7e1d00180f5d1bed807825da595b42ef1ac5619b90267c8a050a1a31ac7da9cf6f72b2f1c80fca08370a18a8f3d85475a604ecc4d0984ee9a15fec6c48317ecc94e670757a3804c94605ed78ab21cf9de7122439b6899f5e27c241945ef0613d9bdc9173c299c801fda53452b457f7618c62f1616ed83e2c7fa7cf71f72f13ea34aa18e3d9ad78a808c1da5170fac10ce617b5d6d18c20b99991cce727b793e7e5a743dcc2c8e1e6a8000e50c9a800d3b9982be82539333cd4929760e31ae5dbba7d5a6e85bacf602b9808fac6f5b26a0a091512ba579c768e78cd75b5f91ff02dcd27a011a1b9d3f8ca080e5fb24d0ede34e1264234630abe08cc092f4b599e762eb6b274c1f222f6f2c4380ea31c1b905706f3f7a9557ab6e03ebc7fa459ed493288ab18b7248d56274d9d480b0e82fc048cc11108e543d86e9144d1f6b981230bb0c550a00df138b360b4fa3150180c00080c8f363b02c567228097f4141ff33ac30edbdc57e116dd12b31ca08dd6eedb7da806fd53b6a51e8f50f3bc69bcc298fe24cc7655c7b207660d28d6dcb26aa1b5e0e1d0280c08880cf00071125f3e7e02061c1d5ef78089721cbd3de3dd35818f197d9ce4047393480c53eb1641d7868157a4ea1f8116ed9172735bcb3a86494b8d1b9c3447cfc6fc9808c68f773671aec1db46b2c37b4c827e64aa7290d65e305cdaaa6b175596256f1808c42b10e16c2f8a5bae3bff8e01348d996b61749ced835c72181de8e3c3cf790b10480ce398045143a91a29e44ee933dd173865a746c593d4568da922617b6b25e8d49779a7e80519332e3dfefb7b4b6cb74150e52dd6cd5cacd5e33d707660394b1d3bf951a9680582326eb0161c33b55cfb3879dfbf9834b5278739ad1473b5916527e21afc16c8062baa6d4be8eace5285c5b1af2204784d5fd3990d8db14e01c7ea3d207e7af21807b7ad6eb73914fd07fc55ad89ba771070c1da532a9269475dc64f1995d4d926680b4fe59659ae0a562b4434ee170cc343daba709b4e6ad61be99fb5dd01c0f6efc808eb1dfd2bfd20b53f2a808e7d31c37875d42d6778ada487e75d274b70b8aa27e80bfede393866c60fd8d64d7518acf798aa6f77b8137333c680aed3de3c159e753808e7d06a371709c1c5f99e40a3e85a4c5a4f123b8ae16f5c87db977d17f30927e350580d2f580bcf393a2947a27925c1465aac44bb58da954b3cdebad224080d3930b6233b70880ea454c17403966165424880578a7212f7ecc8d1ec559a736d1a51395d70ee2b680b5d53fa5baccd383187aa9f249ca8d781430a943a42aaa118449a6d4fb5d034b80fa2f95cf5f443da1ce75b758629c2466fc7cb001ff3ea8ebe460fc4479e845ab80aed41dae45995900dccc86c02c2ce77a6b8581ebf9c78fe3f1385824ad87893680209819bb86075acb7874483c54a91cfd2e803479be2828b94c1d17c18a63346280f962ac5e4c61706f3e8e1aabf638e56867e25ac3b87d22801c05b043de364444802e87816751bd98244d1dc3629241dad5a5e56ad2e4ee90f3dfa65d83f70f8aa6805265811d246a9043865dd4d45ea416720c3fe719228c9eb139ecc5f80dbb906a80227968f2460b7fb0c536fcd7c0d6d682249aaa9f651c3e2a9b201a2d26e1c181b90580ddda802654f350f2901df03459efca15248804666c7ce2397bf89b8a61a52a7cb4908580ab36cb981c8e67b6cfb591f032139b2e586b1fe9905d7b324fd71df7e06a91ee802e1953c645c2b0ed27aacb7a47675de1f2fb687cdc7cc67f2b4d9fc329e898ca80f44b1c46a3ae1d8208dfc5295818fee4cd2568b2d286a8f15178448066ae60e580b9ffad52dc65652408d1652e750b2c81f47098139420a34c9a389bc14d7ccff980735ef803cdd397217afdfbbe850b3f5151d915a0e011e24fefe257f00327e91480bdfc1af50c16e33afacbb49999e93688b45101f50f8ef228be0976da0a916eb980045622a5e6f1835b920bbb8a0b3fd8c31f3516b74ddbc93628e6d8e2f830cebf80dc8ba398e0ab0313cff307a9bf29dbf30509bc737d3bcafcfb647f8a0b81290f80c02bd1fef4b8c700febf28d1739305b4a093ccf3fe0476b62c7861e865ad315180723d1fcb4b0bdb05810017af15de0ccd0c2b8f7581d07e82d49a9520ab3836784d0880ffff80d6173d5589210b3a301d4833ab0204b398370586c0c0040594cfc53bf817e0c38084b762ffc6d28885b9dab75a47bb231ee903b7ee035c1b57d7b3fb4825a9c5c48078381a8ce1fea930d598d66350dedc89a28648d42b83148e8ae5b55787c0cf5b806a4e7a611c4080d0234e45c80c6080f35576df9bfbe399a5483ea6a24c32a10980716550542c078f89b87a12ff14a7337b901fb4252843e2d104a50c0f823abe7c802e54fe01f7fbb40cbb796dee788dbc22a9d310710c8609fbf88f7fffc48a210f80ef479a8e3b93e1f101aef14f708332b5e777c7d31a2e7fd6c4fc6dc0c331718680089d8ce6b66ea01d9b02aa65b92f604518e726646998e65ed26abd0c52eff8d6803b9f59d065396d623dda6468a7dfee22b0ecec0bc4d396962021c574abf2c37480a199ba02d76a45392076693831744897e697d1c8edd32b76ac2e9d7b6494af238026c3c20f98dc3cabb3f326af3446634c5b26a1564289bcb81381fb71d18bea52803b29b5852f8737552afd5a9cbae2a8d5745ea706c8de272c5f66711f44460951806ebbdaf6616cb74a2a50e9387c5c3bb685cde4c34da1f597e3ad6606ad0112dd8070528a344a17926d3533e02b3b17d1fed5ff719c005fe77be2fb274ac719b2bb80aa9c8cdaa24f8a404c87ee7a64945cbe9a4923c340c61b641c63d6c803bf886480189db87304a3ab0f81c4eb13a2fd1c23f4d61cb492092c34fd99aceab4c8ccbd19018104090080cb211743348cd1a951cd184fc512dafe5e0b4a415af2118db6e807d851a2cdc780c2638cb1714bf35497ff309a7433d63e4dd90e364e1d1a424b2c51d93b7f4997190181094001806201a048b20d5ac9248d3d6d59705679846a8fcda38d451b781a55215a237efb802e8902539049bd590ac5802df66bde9e4bcaff54dcc7db75da266fef55d5143fbd029d007f03cfdce586301014700e2c2593d1c08078a9420cbcaebf2b45ae305b2d61fe8ef887e9b070e85a935507ec7565bba69b505f0e7b9012096b41c4eb3aaf947f6ea429080100685f0d9ef3b78afddab7f5c7142131132ad4200200000000000000585f02275f64c354954352b71eea39cfaca210020000004c5f0ec2d17a76153ff51817f12d9cfc3c7f04008017697cf415a5fc1cba73b5f96c2875f93d6c3686e9136ae8902bc2702d01dabd69049d0da05ca59913bc38a8630590f2627c07d9806f985f1aafbe565bb8f15e007a1f749d884af5c580ba76cfcd84988f7a0765e880f74409233555db1d3ee2ab18fc556397536bde697e768a8a3ef619c44d1f43a580ae08e04f1cc53d432812523051675ba60f4d07652ff9290995b445d9325c3bc380ec85a49e15777a970df3463a0d93e4e40c8c65fb8a96a2b2f726e1b7aa8bddf0802aad815050372f7147bd756b071bf40b09a185063d353a0cc5e3bf3ac96290e3809ed496cd348041136959e09bdc5d688210fe22809a8adaaf04a22cc77b18fe54803fcd727d8e7b0648c685c10e60b5b8bc2d426ed06bc3666b1256dcac2150c345802183cff80b4c7bb89689fb72b9f125176d92923b9cb4c143d6cf557341af3405c1059e710b30bd2eab0352ddcc26417aa1945fd380bce7e5aade74d2874c4e83e100906f97ffaa555efe5ed31dcf69d7f082752bbf8032a5322997010e2f65fd80990af0edff34712de10c2de5d33e41450add52432280911e79cbfc8b9cccf4f128b0dae62b732e9cdb31e960b64f52ce03a4707b3cdb80b7bc9fec0d5e9a2cba69174c8902604e8f8374f820ff1013a8002ab4adfe0bf280b9aee043e378f8313e68a6030679ccf3880fa1e7ab19b6244b5c262b7a152f004c5f03c716fb8fff3de61a883bb76adb34a20400809ee74aa0ad552ff03b9e9b742bc0f896f90d28eed97092d688392b6028203df78008a0c609ab4888f02c2545c002153297c2641c5a7b4f3d8e25c634e721f80bea80b6617c764df278313c426c46961ccde8ee7a03f9007b74bc8bc6c49d1583cf7d803125d663e61c3ccf1296de13ac02fba7a7aa89cd117b047d14386bc0f7c5e4dd80765c01d5c4c06a84f25a6b160ec74761b004793c0fbc2f848caab5551305ebb809019e7bbb460270642b5bcaf032ea04d56a00853c570f6ccad3d833b8c4110d000004003c57033ab67066cce00ee307000004003c570cbb81823589d77dd607000004007d059eb6f36e027abb2091cfb5110ab5087ff96e685f06155b3cd9a8c9e5e9a23fd5dc13a5ed2091cfea1000000000685f08316cbf8fa0da822a20ac1c55bf1be320801e000000000000505f0e7b9012096b41c4eb3aaf947f6ea429080000808127f92e325e777e31ce64afed9cbf6747185e2deba3b77c7f1fb40a689f70ab80ccd7eea271ab4ba95eed0fbf898a82fb654e2a3232ed1234bcaa4ab30e8e91ec8030c90301fa21909abd6a0ff174a6b455f3f96cfc740df229aea0976643336926800d49fef039517cc312c00412803ca1df50ac6d90c50541f649a9c85b83c0fdd880a33a93b27b85892bc18f025efca8636773ff54e7562558400e4eb7d220f8e043803c9ba584e93befb83760f055d65e0bb40817bb54391c88ca9f7b161eb7079f9680b5889eaf970461a048cfba76c1b3fb6c627d77b818e7e9d9aa935514881ba5a8685f090e2fbf2d792cb324bffa9427fe1f0e204aad1c019fb61c0171019ede3d8a54d27e44a9d5ce189618f22d3008505f0e7b9012096b41c4eb3aaf947f6ea4290809004c5f03b4123b2e186e07fb7bad5dda5f55c0040080d346b6d27624a25b7b2015ba73675ccf5f645768d0976dec8a4a6dc5a3a04c31d5019ef78c98723ddc9073523ef3beefda0c104480de2cdcc7fc6b10feb2adda900ebb9ae378fe711655f1ff2346d308d82faed8a080ae9dfbd24879b8d42c59bbf7e72885d0dd8cadf1a10dcf4eabbde8641f2430b280cf928e710e57bd0c41dd5df6657675dc5a6aba5d3ede20fe149124fd6a179159c9069f012b746dcf32e843354583c9702cc020ebbf809d43ec70c4c51aacf2423b930571c48be74da508f5fb762a3eaad5dea20afd4f80051c3c7b71cafc975cc5c2b6e46eeb7d05bd250985273206afa294347ff32b77805f3c47a03129d01a26c21447e16d61cb840a588a06ec526a33c53037539ef07f6c570f9d7e46afdef838d8070000340cd0070000db070000f8070000800a52ea7adf57aff93d43fe7bd95d3c1e71b533a01c157d8916c13bde099b2e565c5700bd9a93e85e3ce1d20700002408d6070000dc07000080173a411eefb7091e94e8519f0a50f9b8b30dbf2b4de0b8635bc61255b5fc16a6800f1ee02e9ff1d16be98c8af4be8cb4bec5b9b8a9ec379b6cad9ff91950004b1c80c97c91e2e5a027b0420eb33bc7c337502e2ba0961c4f3b67648c500e30a88a3780cea9bc128aee6d6c9113a748f7f04046fd05a553ad30e94780dcb84642cdc4e380c0d74b1d4f6d5ae4ad1685e208872663b69666eddba51d0d05f059527d9e267b80c1a41ba7cb2edae98cb3467e393c4744559c525529ed82aab276258f11798ada80007a2b526bbc5b14ab43d462fbec870cc7ddc93d78a7ae32ce9b2c1ce826aea38d089f06604cff828a6e3f579ca6c59ace013dffff806f3d3a93c0a973bfe9dd3b5374c55d54bfb53853368ccbc3cddffff0cb3fb59f80328bf5f1fcdf834f91542719684a7e941c521df826d6769e10c7a387c21b240680da2c952a23b289de0889cca9aaa5888a11bd031a978975b3722656e00b2ba02c8097974986d5a66937ce2457d5cae4f22b762ca9c5b348fd2f3f88459c415d8fa980721afc2bf227efcdd25c70681d673c50fac798a31805f56e5d9790d8215ae83380983ca1f01ba94b20756250e56543e579e921fa9410d5c79fe54a4708d2d71775800b58bdf8fa8dceb4d82a4c2e51f692921536176062343cd90b07cf58b5aea231808586a6e7dfe4ab2d0df6034ff337a8d252874e0af6be212688f602481c73ab1e8029241b5c67484fb7797a730867a2a79ee6ee4c8c45e32f74d7935a5f1de75c8380d3cec5aa5096a611b04c9f9083bbf0739b911d028fd9cdddeb66a490f7dd1d7280f7477af09e803dc67c0493a5774603c23fcfa475c860775d23ba6968002ea55a80901c4465af974bfe690004bdfa941b38cdb9437f8a21266bcb1498732488c76580fe2902e6f88bc48fd097e36b98a75eed7895555afcfff011246370112b36abbc80806f30f4e79b96408e369531c35a6cde27880d9e3c191a37585f3010f9bbff5d8094934fbb6dcc5432dbde3ebaae96ca62366caa4a71c925b647b272d12b79931d80daa3d13a763e7f11d95ca372c96bd6554077e507a19cd11f250bfcdf421cf3aa2d069f0ad157e461d71fd4c1f936839a5f1f3e6bbf801c145cc01e958be57dbedca18f3fac8a3a94edd9acd6ef14f97e98be00d7e0445857060394315fe95b98f30700002000000000000000005857002b2165312d890ff0070000200000000000000000803244367efa8dc598855a70630c52f6c7ab99e6da40fc84849a3addec3b8a6ffe8066c8c4538dee13fa77c9b9a6916ac016d25911e28b3d4478328941a5f842b19980e0bc9102a86834b5ef2029eb4c635c7d14ec8347288d2d28450594ccf555f2d5801978bc40ea365e72da0597e8d86a4d3479ca2f871bbab8030cfdbe2d3db2b6bc80236e3f56efc7c729153aab82d4d9340784523591b976670a3a82daf2ea1005398058a47f2bb2cc12ee02880a99bb9d5b0dd181550fb022df318c5b9c74d6ba703d80449416449b190c57da732355497e86010089761fade693cdd8a464f87ed4979280d4fe3e57123722d2122f49971109b3658b13ae721b425adf35e278e6ddd4bb8c804793443a067df5e20bbe8022ed078cea4d714ade8d279591ed29d8a9dc7543e2c9069f0d3719f5b0b12c7105c073c507445948ebbf805bf7a3246f314f535cd13a55fa998fac4e0bdc8c5b7e7c4a21cdde39069f1f3580051c3c7b71cafc975cc5c2b6e46eeb7d05bd250985273206afa294347ff32b778066c68d34e6e4d2f9bb541ad53f0262d6ad6a1e3140be41d4ed25a9c0657467946c570f9d7e46afdef838d8070000340cd0070000db070000f8070000800a52ea7adf57aff93d43fe7bd95d3c1e71b533a01c157d8916c13bde099b2e565c5700bd9a93e85e3ce1d20700002408d6070000dc07000080173a411eefb7091e94e8519f0a50f9b8b30dbf2b4de0b8635bc61255b5fc16a6800f1ee02e9ff1d16be98c8af4be8cb4bec5b9b8a9ec379b6cad9ff91950004b1c80c97c91e2e5a027b0420eb33bc7c337502e2ba0961c4f3b67648c500e30a88a37802d477121ddfd03fed7301b1b80e1719cb18cbe12502bd5ca9cfbf69efbec7f3780c0d74b1d4f6d5ae4ad1685e208872663b69666eddba51d0d05f059527d9e267b80c1a41ba7cb2edae98cb3467e393c4744559c525529ed82aab276258f11798ada80007a2b526bbc5b14ab43d462fbec870cc7ddc93d78a7ae32ce9b2c1ce826aea309089f0d7fefc408aac59dbfe80a72ac8e3ce5ffbf80f9f7e3fdb6d2a44499042c0c872ffd157e5f7b99ebe000c8cacd340a86246e0d803f166db79f0e184b7cd30a8e3afd5f7856601df6c23ae947cf298a9b71c650b9807e1ee2c8362cd0745aa0c58780b99fefa1024c1b472d3386a2319e71b68028a680cede792b17ae08708e2484013c18a5ad76db74c964854e03cf0e0754056f0ff38090c332a4d08d465efe0aa60e5ac5f8079cd6aacc9d0357f350d4998c01559f3f80c6c78ab7769ee8a3b518544830e25875a2ff4b29ca19344b90e09d056a5be9eb80230bd1a0336cfdc9d8885f9651418bebd978630ead20faa99a124ae9084011ac80a9e6d29bd1ee69601d4b3bc1f545c816c3d0508772b487f6b2a3d8d3e14fadd7802b8202f42f4f42a9cf05655ee07a836c4829b8d1eeb746fed8c6ad329a0dca3780562088503d26107972931cda7381468daededd36a51baac07a9b8abe0d020ddd80b918df5aca54ab15ece824ccc16ae099547b66f0833d749a33af593965eda3ee80f190a16091b9458b87d836b1185615422ecc58bd13333667fdb0dbf8f2604c5a80afedabf2a2e50b825dcc04154645e29ec25fd1176ef4a4632f56549b170914f6800b8f6c7ad5920a31d451776516aa912a9524aacc198b9dcb7d481c898be49e4b805887454a350907355a2e7a821380eafde4e9d74dcd1766524b6e4dc73cbe290eb103bf0e02656c61795f64697370617463685f71756575655f72656d61696e696e675f63617061636974790fe038470c0d000020aaaa0200000010003847070d000020aaaa0200000010007c800050344608000020aaaa020000001000344608000020aaaa0200000010007c802001344608000020aaaa020000001000344608000020aaaa02000000100080f56581bb3ed11321a39e25730eaa0e99cdb01282d383f8c8d53aab621a9381018079c36a25c7c0116fed11cce5e179b6f65f5c8824c17cb8048fd2ccb007ebc49980c1c5d7cd0aff0514d7affea47aff45e00929fdeffcd7bcd00f573c585559c1e5002ce803000000d407000000d607000000d807000000db07000000dc07000000f007000000f207000000f307000000f5070000003808000000", + ], + "hash": "0x15177d4bdc975077b85261c09503bf40932aae9d3a7a2e948870afe3432976be", + "header": "0x4e945cf0873decede74578298736ecf712d71319f10e4c7b37d7d7f503b43f5fda7a39015beb846ad4364b35d2c07b3e7c25217183431c264f973727ec9bf8f3744cda8f561dce8cf71ded3927204ced6a6e7a545d4eff86a59c94af93e7afd7fa7265eb0c066175726120c867750800000000045250535290fecbb2d1c841cb5ab9b89e315282a71ef3d640ac12de4f266ec2810c465353a866de720405617572610101466ddd00c77882cb47a572a8d8620a6bd3fb1d4b33ebf1e3e6e58d5837316540472f9f682b2562486eb1589999e9d2e0e99a2997fe32c94ea44b9a2a89992883", +} +`; + +exports[`wasm > LightClient works 2`] = ` +[ + [ + [ + "0x45323df7cc47150b3930e2666b0aa3130a6ccf00f78ac824e72d27429ca3b8b0", + "0x00", + ], + [ + "0x45323df7cc47150b3930e2666b0aa313a2bca190d36bd834cc73a38fc213ecbd", + "0x99b71c01", + ], + [ + "0x45323df7cc47150b3930e2666b0aa313c522231880238a0c56021b8744a00743", + "0x0000300000500000aaaa020000001000fbff0000100000000a0000004038000058020000", + ], + ], + [ + [ + "0x26aa394eea5630e07c48ae0c9558cef734abf5cb34d6244378cddbf18e849d96", + "0x0000000007423c7fd10132df0900", + ], + [ + "0x26aa394eea5630e07c48ae0c9558cef74e7b9012096b41c4eb3aaf947f6ea429", + "0x0000", + ], + [ + "0x26aa394eea5630e07c48ae0c9558cef75684a022a34dd8bfa2baaf44f172b710", + "0x01", + ], + [ + "0x26aa394eea5630e07c48ae0c9558cef7f9cce9c888469bb1a0dceaa129672ef8", + "0xb122146163616c61", + ], + ], + [ + [ + "0x45323df7cc47150b3930e2666b0aa3130a6ccf00f78ac824e72d27429ca3b8b0", + "0x00", + ], + [ + "0x45323df7cc47150b3930e2666b0aa313a2bca190d36bd834cc73a38fc213ecbd", + "0x99b71c01", + ], + ], +] +`; + exports[`wasm > decode & create proof works 1`] = ` { "0x06de3d8a54d27e44a9d5ce189618f22d4e7b9012096b41c4eb3aaf947f6ea429": "0x0400", diff --git a/packages/core/src/wasm-executor/browser-wasm-executor.js b/packages/core/src/wasm-executor/browser-wasm-executor.js index 9a57a46e..c84df974 100644 --- a/packages/core/src/wasm-executor/browser-wasm-executor.js +++ b/packages/core/src/wasm-executor/browser-wasm-executor.js @@ -26,6 +26,53 @@ const testing = async (callback, key) => { return pkg.testing(callback, key) } -const wasmExecutor = { runTask, getRuntimeVersion, calculateStateRoot, createProof, decodeProof, testing } +const startNetworkService = async (config, callback) => { + return pkg.start_network_service(config, callback) +} + +const connectionReset = async (connectionId) => { + return pkg.connection_reset(connectionId) +} + +const messageRecieved = async (connectionId, data) => { + return pkg.message_received(connectionId, data) +} + +const connectionWritableBytes = async (connectionId, bytes) => { + return pkg.connection_writable_bytes(connectionId, bytes) +} + +const wakeUp = async (callback) => { + return pkg.wake_up(callback) +} + +const queryChain = async (chainId, requestId, request, retries, callback) => { + return pkg.query_chain(chainId, requestId, request, retries, callback) +} + +const getPeers = async (chainId) => { + return pkg.peers_list(chainId) +} + +const getLatestBlock = async (chainId) => { + return pkg.latest_block(chainId) +} + +const wasmExecutor = { + runTask, + getRuntimeVersion, + calculateStateRoot, + createProof, + decodeProof, + testing, + startNetworkService, + queryChain, + getPeers, + getLatestBlock, + connectionReset, + messageRecieved, + connectionWritableBytes, + wakeUp, +} Comlink.expose(wasmExecutor) diff --git a/packages/core/src/wasm-executor/executor.test.ts b/packages/core/src/wasm-executor/executor.test.ts index 7385d79d..ceec1969 100644 --- a/packages/core/src/wasm-executor/executor.test.ts +++ b/packages/core/src/wasm-executor/executor.test.ts @@ -6,6 +6,7 @@ import { readFileSync } from 'node:fs' import _ from 'lodash' import path from 'node:path' +import { LightClient } from './light-client.js' import { WELL_KNOWN_KEYS, upgradeGoAheadSignal } from '../utils/proof.js' import { calculateStateRoot, @@ -160,4 +161,31 @@ describe('wasm', () => { const slotDuration = await getAuraSlotDuration(getCode()) expect(slotDuration).eq(12000) }) + + it('LightClient works', async () => { + const lightClient = new LightClient({ + genesisBlockHash: '0xfc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c', + bootnodes: [ + '/dns/acala-bootnode-4.aca-api.network/tcp/30334/ws/p2p/12D3KooWBLwm4oKY5fsbkdSdipHzYJJHSHhuoyb1eTrH31cidrnY', + ], + }) + await lightClient.isReady + + const block = await lightClient.queryBlock('0x15177d4bdc975077b85261c09503bf40932aae9d3a7a2e948870afe3432976be') + expect(block).toMatchSnapshot() + + const storage = await Promise.all( + [ + '0x45323df7cc47150b3930e2666b0aa313c522231880238a0c56021b8744a00743', + '0x26aa394eea5630e07c48ae0c9558cef734abf5cb34d6244378cddbf18e849d96', + '0x45323df7cc47150b3930e2666b0aa31362f8058e9dc65b738fce4a22e26fa4f2', + ].map((key) => + lightClient.queryStorage( + [key as HexString], + '0x15177d4bdc975077b85261c09503bf40932aae9d3a7a2e948870afe3432976be', + ), + ), + ) + expect(storage).toMatchSnapshot() + }, 10000) }) diff --git a/packages/core/src/wasm-executor/index.ts b/packages/core/src/wasm-executor/index.ts index 455e16a0..b531590b 100644 --- a/packages/core/src/wasm-executor/index.ts +++ b/packages/core/src/wasm-executor/index.ts @@ -8,8 +8,9 @@ import { Block } from '../blockchain/block.js' import { PREFIX_LENGTH, stripChildPrefix } from '../utils/index.js' import { defaultLogger, truncate } from '../logger.js' -import type { JsCallback } from '@acala-network/chopsticks-executor' -export { JsCallback } +import { LightClientConfig } from './light-client.js' +import type { JsLightClientCallback, JsRuntimeCallback, Request } from '@acala-network/chopsticks-executor' +export { JsRuntimeCallback } export type RuntimeVersion = { specName: string @@ -43,6 +44,13 @@ export type TaskResponse = Error: string } +export type Peer = { + peerId: string + roles: string + bestNumber: number + bestHash: string +} + export interface WasmExecutor { getRuntimeVersion: (code: HexString) => Promise calculateStateRoot: (entries: [HexString, HexString][], trie_version: number) => Promise @@ -56,9 +64,23 @@ export interface WasmExecutor { allowUnresolvedImports: boolean runtimeLogLevel: number }, - callback?: JsCallback, + callback?: JsRuntimeCallback, ) => Promise - testing: (callback: JsCallback, key: any) => Promise + testing: (callback: JsRuntimeCallback, key: any) => Promise + startNetworkService: (config: LightClientConfig, callback: JsLightClientCallback) => Promise + getPeers: (chainId: number) => Promise<[string, string, number, string][]> + getLatestBlock: (chainId: number) => Promise<[number, HexString]> + messageRecieved: (connectionId: number, data: Uint8Array) => Promise + connectionWritableBytes: (connectionId: number, numBytes: number) => Promise + connectionReset: (connectionId: number) => Promise + wakeUp: (callback: JsLightClientCallback) => Promise + queryChain: ( + chainId: number, + requestId: number, + request: Request, + retries: number, + callback: JsLightClientCallback, + ) => Promise } const logger = defaultLogger.child({ name: 'executor' }) @@ -121,7 +143,7 @@ export const runTask = async ( allowUnresolvedImports: boolean runtimeLogLevel: number }, - callback: JsCallback = emptyTaskHandler, + callback: JsRuntimeCallback = emptyTaskHandler, ) => { const worker = await getWorker() logger.trace(truncate(task), 'taskRun') @@ -134,7 +156,7 @@ export const runTask = async ( return response } -export const taskHandler = (block: Block): JsCallback => { +export const taskHandler = (block: Block): JsRuntimeCallback => { return { getStorage: async function (key: HexString) { return block.get(key) @@ -205,6 +227,65 @@ export const getAuraSlotDuration = _.memoize(async (wasm: HexString): Promise { + const worker = await getWorker() + return worker.remote.startNetworkService(config, Comlink.proxy(callback)) +} + +export const queryChain = async ( + chainId: number, + requestId: number, + request: Request, + retries: number, + callback: JsLightClientCallback, +) => { + const worker = await getWorker() + return worker.remote.queryChain(chainId, requestId, request, retries, callback) +} + +export const getPeers = async (chainId: number) => { + const worker = await getWorker() + return worker.remote + .getPeers(chainId) + .then((peers) => { + return peers.map( + ([peerId, roles, bestNumber, bestHash]) => + ({ + peerId, + roles, + bestNumber, + bestHash, + }) satisfies Peer, + ) + }) + .catch(() => []) +} + +export const messageRecieved = async (connectionId: number, data: Uint8Array) => { + const worker = await getWorker() + return worker.remote.messageRecieved(connectionId, data) +} + +export const connectionWritableBytes = async (connectionId: number, numBytes: number) => { + const worker = await getWorker() + return worker.remote.connectionWritableBytes(connectionId, numBytes) +} + +export const connectionReset = async (connectionId: number) => { + const worker = await getWorker() + return worker.remote.connectionReset(connectionId) +} + +export const wakeUp = async (callback: JsLightClientCallback) => { + const worker = await getWorker() + return worker.remote.wakeUp(Comlink.proxy(callback)) +} + +export const getLatestBlock = async (chainId: number) => { + const worker = await getWorker() + return worker.remote.getLatestBlock(chainId) +} + export const destroyWorker = async () => { if (!__executor_worker) return const executor = await __executor_worker diff --git a/packages/core/src/wasm-executor/light-client.ts b/packages/core/src/wasm-executor/light-client.ts new file mode 100644 index 00000000..09fcd0e8 --- /dev/null +++ b/packages/core/src/wasm-executor/light-client.ts @@ -0,0 +1,271 @@ +import { HexString } from '@polkadot/util/types' +import { JsLightClientCallback, Response } from '@acala-network/chopsticks-executor' +import { WebSocket } from 'ws' + +globalThis.WebSocket = typeof globalThis.WebSocket !== 'undefined' ? globalThis.WebSocket : (WebSocket as any) + +import { Deferred, defer } from '../utils/index.js' +import { + connectionReset, + connectionWritableBytes, + getLatestBlock, + getPeers, + messageRecieved, + queryChain, + startNetworkService, + wakeUp, +} from './index.js' +import { defaultLogger } from '../logger.js' + +const logger = defaultLogger.child({ name: 'light-client' }) + +export class Connection { + public destroyed = false + #socket: globalThis.WebSocket + + #onOpen: (event: Event) => void = (event) => { + if (this.onOpen && !this.destroyed) { + this.onOpen(this.#socket, event) + } + } + + #onClose: (event: CloseEvent) => void = (event) => { + if (this.onClose && !this.destroyed) { + this.onClose(this.#socket, event) + } + } + + #onMessage: (event: MessageEvent) => void = (event) => { + if (this.onMessage && !this.destroyed) { + this.onMessage(this.#socket, new Uint8Array(event.data as ArrayBuffer)) + } + } + + #onError: (event: Event) => void = (event) => { + if (this.onError && !this.destroyed) { + this.onError(this.#socket, event) + } + } + + public onOpen?: (ws: globalThis.WebSocket, event: Event) => void + public onClose?: (ws: globalThis.WebSocket, event: CloseEvent) => void + public onMessage?: (ws: globalThis.WebSocket, data: Uint8Array) => void + public onError?: (ws: globalThis.WebSocket, event: Event) => void + + constructor(address: string) { + this.#socket = new globalThis.WebSocket(address) + this.#socket.binaryType = 'arraybuffer' + this.#socket.addEventListener('error', this.#onError) + this.#socket.addEventListener('open', this.#onOpen) + this.#socket.addEventListener('close', this.#onClose) + this.#socket.addEventListener('message', this.#onMessage) + } + + send(data: Uint8Array) { + this.#socket.send(data) + } + + destroy() { + this.destroyed = true + if (this.#socket.readyState === 1) { + this.#socket.close() + } + } +} + +export type LightClientConfig = { + genesisBlockHash: string + bootnodes: string[] +} + +export class LightClient implements JsLightClientCallback { + #requestId = 1 + // blacklist of addresses that we have failed to connect to + #blacklist: string[] = [] + #connections: Map = new Map() + #queryResponse: Map> = new Map() + + #chainId = defer() + + constructor(config: LightClientConfig) { + startNetworkService(config, this) + .then((chainId) => { + this.#chainId.resolve(chainId) + }) + .catch((e) => { + logger.error(e) + this.#chainId.reject(e) + }) + } + + get isReady() { + return this.#chainId.promise.then(() => {}) + } + + connect(connId: number, address: string, _cert: Uint8Array) { + if (this.#blacklist.includes(address)) { + connectionReset(connId) + return + } + + const connection = new Connection(address) + this.#connections.set(connId, connection) + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this + + connection.onError = function (ws, error) { + if (ws.readyState === 1 || ws.readyState === 0) return + + if (!error['code'] || ['EHOSTUNREACH', 'ECONNREFUSED', 'ETIMEDOUT'].includes(error['code'])) { + self.#blacklist.push(address) + logger.debug(`${error['message'] || ''} [blacklisted]`) + } + + if (!connection.destroyed) { + connection.destroyed = true + self.#connections.delete(connId) + connectionReset(connId) + } + } + + connection.onMessage = function (ws, data) { + if (connection.destroyed) return + if (ws.readyState != 1) return + messageRecieved(connId, data) + } + + connection.onClose = function () { + if (!connection.destroyed) { + connection.destroyed = true + self.#connections.delete(connId) + connectionReset(connId) + } + } + + connection.onOpen = function () { + connectionWritableBytes(connId, 1024 * 1024) + } + } + + async queryResponse(requestId: number, response: Response) { + this.#queryResponse.get(requestId)?.resolve(response) + this.#queryResponse.delete(requestId) + } + + messageSend(connectionId: number, data: Uint8Array) { + const connection = this.#connections.get(connectionId) + if (!connection) { + this.resetConnection(connectionId) + } else { + connection.send(data) + } + } + + resetConnection(connectionId: number) { + try { + const connection = this.#connections.get(connectionId) + if (connection && !connection.destroyed) { + connection.destroy() + this.#connections.delete(connectionId) + } + } catch (_e) { + _e + } + } + + startTimer(ms: number) { + ms = Number(ms) + // In both NodeJS and browsers, if `setTimeout` is called with a value larger than + // 2147483647, the delay is for some reason instead set to 1. + // As mentioned in the documentation of `start_timer`, it is acceptable to end the + // timer before the given number of milliseconds has passed. + if (ms > 2147483647) ms = 2147483647 + // In browsers, `setTimeout` works as expected when `ms` equals 0. However, NodeJS + // requires a minimum of 1 millisecond (if `0` is passed, it is automatically replaced + // with `1`) and wants you to use `setImmediate` instead. + if (ms == 0 && typeof setImmediate === 'function') { + setImmediate(() => { + try { + wakeUp(this) + } catch (_e) { + _e + } + }) + } else { + setTimeout(() => { + try { + wakeUp(this) + } catch (_e) { + _e + } + }, ms) + } + } + + async queryStorage(keys: HexString[], at: HexString) { + const chainId = await this.#chainId.promise + const requestId = this.#requestId++ + const deferred = defer() + this.#queryResponse.set(requestId, deferred) + await queryChain( + chainId, + requestId, + { + storage: { + hash: at, + keys, + }, + }, + 10, + this, + ) + const response = await deferred.promise + if ('error' in response) { + throw new Error(response.error) + } + if ('storage' in response) { + return response.storage + } + throw new Error('Invalid response') + } + + async queryBlock(block: HexString | number) { + const chainId = await this.#chainId.promise + const requestId = this.#requestId++ + const deferred = defer() + this.#queryResponse.set(requestId, deferred) + await queryChain( + chainId, + requestId, + { + block: { + number: typeof block === 'number' ? block : null, + hash: typeof block === 'string' ? block : null, + header: true, + body: true, + }, + }, + 10, + this, + ) + const response = await deferred.promise + if ('error' in response) { + throw new Error(response.error) + } + if ('block' in response) { + return response.block + } + throw new Error('Invalid response') + } + + async getPeers() { + const chainId = await this.#chainId.promise + return getPeers(chainId) + } + + async getLatestBlock() { + const chainId = await this.#chainId.promise + return getLatestBlock(chainId) + } +} diff --git a/packages/core/src/wasm-executor/node-wasm-executor.js b/packages/core/src/wasm-executor/node-wasm-executor.js index fd779fe9..b9043ceb 100644 --- a/packages/core/src/wasm-executor/node-wasm-executor.js +++ b/packages/core/src/wasm-executor/node-wasm-executor.js @@ -29,6 +29,53 @@ const testing = async (callback, key) => { return pkg.testing(callback, key) } -const wasmExecutor = { runTask, getRuntimeVersion, calculateStateRoot, createProof, decodeProof, testing } +const startNetworkService = async (config, callback) => { + return pkg.start_network_service(config, callback) +} + +const connectionReset = async (connectionId) => { + return pkg.connection_reset(connectionId) +} + +const messageRecieved = async (connectionId, data) => { + return pkg.message_received(connectionId, data) +} + +const connectionWritableBytes = async (connectionId, bytes) => { + return pkg.connection_writable_bytes(connectionId, bytes) +} + +const wakeUp = async (callback) => { + return pkg.wake_up(callback) +} + +const queryChain = async (chainId, requestId, request, retries, callback) => { + return pkg.query_chain(chainId, requestId, request, retries, callback) +} + +const getPeers = async (chainId) => { + return pkg.peers_list(chainId) +} + +const getLatestBlock = async (chainId) => { + return pkg.latest_block(chainId) +} + +const wasmExecutor = { + runTask, + getRuntimeVersion, + calculateStateRoot, + createProof, + decodeProof, + testing, + startNetworkService, + queryChain, + getPeers, + getLatestBlock, + connectionReset, + messageRecieved, + connectionWritableBytes, + wakeUp, +} Comlink.expose(wasmExecutor, nodeEndpoint(parentPort)) diff --git a/packages/e2e/src/__snapshots__/genesis-provider.test.ts.snap b/packages/e2e/src/__snapshots__/genesis-provider.test.ts.snap index 000e54ce..ab3106b5 100644 --- a/packages/e2e/src/__snapshots__/genesis-provider.test.ts.snap +++ b/packages/e2e/src/__snapshots__/genesis-provider.test.ts.snap @@ -1,8 +1,8 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`genesis provider works Asset Hub Kusama > genesis provider works > build blocks 1`] = `"Kusama Asset Hub Local"`; +exports[`genesis provider works Asset Hub Kusama > genesis provider works > build blocks 1`] = `"Statemine"`; -exports[`genesis provider works Kusama > genesis provider works > build blocks 1`] = `"Kusama Local Testnet"`; +exports[`genesis provider works Kusama > genesis provider works > build blocks 1`] = `"Parity-kusama"`; exports[`genesis provider works Kusama > genesis provider works > handles tx 1`] = ` { @@ -49,7 +49,7 @@ exports[`genesis provider works Kusama > genesis provider works > handles tx 3`] } `; -exports[`genesis provider works Mandala > genesis provider works > build blocks 1`] = `"Acala Mandala TC7"`; +exports[`genesis provider works Mandala > genesis provider works > build blocks 1`] = `"Mandala"`; exports[`genesis provider works Mandala > genesis provider works > handles tx 1`] = ` { diff --git a/packages/e2e/src/chopsticks-provider.test.ts b/packages/e2e/src/chopsticks-provider.test.ts index 38b1b7c8..2ba85048 100644 --- a/packages/e2e/src/chopsticks-provider.test.ts +++ b/packages/e2e/src/chopsticks-provider.test.ts @@ -67,7 +67,7 @@ describe('chopsticks provider works', async () => { it('system rpc', async () => { expect(await api.rpc.system.chain()).toMatch('Acala') - expect(await api.rpc.system.name()).toMatch('Subway') + expect(await api.rpc.system.name()).toMatch(/Subway|Acala/) expect(await api.rpc.system.version()).toBeInstanceOf(String) expect(await api.rpc.system.properties()).not.toBeNull() await check(api.rpc.system.health()).toMatchObject({ diff --git a/packages/e2e/src/helper.ts b/packages/e2e/src/helper.ts index 56383976..524cdc2b 100644 --- a/packages/e2e/src/helper.ts +++ b/packages/e2e/src/helper.ts @@ -6,12 +6,14 @@ import { beforeAll, beforeEach, expect, vi } from 'vitest' import { Api } from '@acala-network/chopsticks' import { + ApiT, Blockchain, BuildBlockMode, GenesisProvider, StorageValues, genesisSetup, } from '@acala-network/chopsticks-core' +import { LightClientConfig, P2P } from '@acala-network/chopsticks-core/p2p.js' import { SqliteDatabase } from '@acala-network/chopsticks-db' import { createServer } from '@acala-network/chopsticks/server.js' import { defer } from '@acala-network/chopsticks-core/utils/index.js' @@ -23,6 +25,7 @@ import { withExpect } from '@acala-network/chopsticks-testing' export { testingPairs, setupContext } from '@acala-network/chopsticks-testing' export type SetupOption = { + p2p?: LightClientConfig endpoint?: string | string[] blockHash?: HexString mockSignatureHost?: boolean @@ -38,6 +41,16 @@ export const env = { endpoint: 'wss://acala-rpc.aca-api.network', // 3,800,000 blockHash: '0x0df086f32a9c3399f7fa158d3d77a1790830bd309134c5853718141c969299c7' as HexString, + p2p: { + genesisBlockHash: '0xfc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c', + bootnodes: [ + '/dns/acala-bootnode-4.aca-api.network/tcp/30334/ws/p2p/12D3KooWBLwm4oKY5fsbkdSdipHzYJJHSHhuoyb1eTrH31cidrnY', + '/dns/acala-bootnode-5.aca-api.network/tcp/443/wss/p2p/12D3KooWN6ZZ2LFSJo2vDci3hqmmcvqMcKJAbREvuYCdvoBvV2D4', + '/dns/acala-bootnode-6.aca-api.network/tcp/80/ws/p2p/12D3KooWEBniruZHpoVj8RUtAFPahaN8UaGP6UtQb5Bdp4MVYbLc', + '/dns/acala-bootnode-6.aca-api.network/tcp/443/wss/p2p/12D3KooWEBniruZHpoVj8RUtAFPahaN8UaGP6UtQb5Bdp4MVYbLc', + '/dns/acala-bootnode-7.aca-api.network/tcp/80/ws/p2p/12D3KooWMq7AtHFx3ZboMT92HQw8BvhZFzJh8UrPCZeMB3yFLe1V', + ], + }, }, rococo: { endpoint: 'wss://rococo-rpc.polkadot.io', @@ -46,6 +59,7 @@ export const env = { } export const setupAll = async ({ + p2p, endpoint, blockHash, mockSignatureHost, @@ -63,9 +77,14 @@ export const setupAll = async ({ } else { provider = new WsProvider(endpoint, 3_000) } - const api = new Api(provider) - await api.isReady + let api: ApiT + if (p2p) { + api = await P2P.create(p2p, new Api(provider)) + } else { + api = new Api(provider) + await api.isReady + } const header = await api.getHeader(blockHash) if (!header) { diff --git a/packages/e2e/src/http.test.ts b/packages/e2e/src/http.test.ts index ac4c0e2a..6f43333e 100644 --- a/packages/e2e/src/http.test.ts +++ b/packages/e2e/src/http.test.ts @@ -33,17 +33,15 @@ describe('http.server', () => { method: 'POST', body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'system_health', params: [] }), }) - expect(await res.json()).toMatchInlineSnapshot(` - { - "id": 1, - "jsonrpc": "2.0", - "result": { - "isSyncing": false, - "peers": 0, - "shouldHavePeers": false, - }, - } - `) + expect(await res.json()).toMatchObject( + expect.objectContaining({ + id: 1, + jsonrpc: '2.0', + result: expect.objectContaining({ + isSyncing: false, + }), + }), + ) } { diff --git a/packages/e2e/src/system.test.ts b/packages/e2e/src/system.test.ts index 87acef70..ffc0e4e2 100644 --- a/packages/e2e/src/system.test.ts +++ b/packages/e2e/src/system.test.ts @@ -9,14 +9,16 @@ describe('system rpc', () => { it('works', async () => { expect(await api.rpc.system.chain()).toMatch('Acala') - expect(await api.rpc.system.name()).toMatch('Subway') + expect(await api.rpc.system.name()).toMatch(/Subway|Acala/) expect(await api.rpc.system.version()).toBeInstanceOf(String) expect(await api.rpc.system.properties()).not.toBeNull() - await check(api.rpc.system.health()).toMatchObject({ - peers: 0, - isSyncing: false, - shouldHavePeers: false, - }) + check(await api.rpc.system.health()) + .toJson() + .toMatchObject( + expect.objectContaining({ + isSyncing: false, + }), + ) }) it('zero is not replaced with null', async () => { diff --git a/packages/web-test/src/App.tsx b/packages/web-test/src/App.tsx index 8a7dbf5d..99464f35 100644 --- a/packages/web-test/src/App.tsx +++ b/packages/web-test/src/App.tsx @@ -107,6 +107,16 @@ function App() { block: config.block, mockSignatureHost: true, db: new IdbDatabase('cache'), + // p2p: { + // genesisBlockHash: '0xfc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c', + // bootnodes: [ + // '/dns/acala-bootnode-4.aca-api.network/tcp/30334/ws/p2p/12D3KooWBLwm4oKY5fsbkdSdipHzYJJHSHhuoyb1eTrH31cidrnY', + // '/dns/acala-bootnode-5.aca-api.network/tcp/443/wss/p2p/12D3KooWN6ZZ2LFSJo2vDci3hqmmcvqMcKJAbREvuYCdvoBvV2D4', + // '/dns/acala-bootnode-6.aca-api.network/tcp/80/ws/p2p/12D3KooWEBniruZHpoVj8RUtAFPahaN8UaGP6UtQb5Bdp4MVYbLc', + // '/dns/acala-bootnode-6.aca-api.network/tcp/443/wss/p2p/12D3KooWEBniruZHpoVj8RUtAFPahaN8UaGP6UtQb5Bdp4MVYbLc', + // '/dns/acala-bootnode-7.aca-api.network/tcp/80/ws/p2p/12D3KooWMq7AtHFx3ZboMT92HQw8BvhZFzJh8UrPCZeMB3yFLe1V', + // ], + // }, }) globalThis.chain = chain diff --git a/vendor/smoldot b/vendor/smoldot index a6c2b5bc..6d6922b1 160000 --- a/vendor/smoldot +++ b/vendor/smoldot @@ -1 +1 @@ -Subproject commit a6c2b5bc8cea736ec6bf985f34e2c587fff63feb +Subproject commit 6d6922b166dee10b3143cfc30c7465df237339ba