diff --git a/bindings/pezkuwi-light/.gitignore b/bindings/pezkuwi-light/.gitignore new file mode 100644 index 00000000..c42713eb --- /dev/null +++ b/bindings/pezkuwi-light/.gitignore @@ -0,0 +1,2 @@ +/build +/rust/target/ diff --git a/bindings/pezkuwi-light/build.gradle b/bindings/pezkuwi-light/build.gradle new file mode 100644 index 00000000..79164f95 --- /dev/null +++ b/bindings/pezkuwi-light/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'org.mozilla.rust-android-gradle.rust-android' +apply from: '../../scripts/bundled-config.gradle' + +android { + namespace 'io.novafoundation.nova.pezkuwi_light' + + ndkVersion "26.1.10909125" + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation kotlinDep + + testImplementation jUnitDep +} + +cargo { + module = "rust/" + libname = "pezkuwi_light" + targets = ["arm", "arm64", "x86", "x86_64"] + profile = "release" + pythonCommand = "python3" +} + +tasks.matching { it.name.matches(/merge.*JniLibFolders/) }.configureEach { + it.inputs.dir(new File(buildDir, "rustJniLibs/android")) + it.dependsOn("cargoBuild") +} + +// Chain specifications the light client starts from: genesis state root, boot nodes and, for the +// relay chain, a checkpoint. A checkpoint is a trust anchor, so it reaches the client only with the +// APK; pezkuwi-wallet-utils' light_client_specs.py writes them and signed/targets.json lists them. +def lightClientSpecs = "https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/light-client" +def isLightClientSpec = { it instanceof Map && it.genesis?.stateRootHash && it.bootNodes } +bundleConfigAsset("bundleZagrosLightSpec", "$lightClientSpecs/zagros.json", "light-client/zagros.json") { + isLightClientSpec(it) && it.lightSyncState +} +bundleConfigAsset("bundleZagrosPeopleLightSpec", "$lightClientSpecs/people-zagros.json", "light-client/people-zagros.json") { + isLightClientSpec(it) && it.relay_chain == "zagros" +} diff --git a/bindings/pezkuwi-light/consumer-rules.pro b/bindings/pezkuwi-light/consumer-rules.pro new file mode 100644 index 00000000..1191b44a --- /dev/null +++ b/bindings/pezkuwi-light/consumer-rules.pro @@ -0,0 +1,11 @@ +# JNI resolves these methods by name. +-keep class io.novafoundation.nova.lightclient.PezkuwiLight { + native ; +} +# Native code calls HostResolver.resolve by name. +-keep interface io.novafoundation.nova.lightclient.HostResolver { + java.lang.String[] resolve(java.lang.String); +} +-keepclassmembers class * implements io.novafoundation.nova.lightclient.HostResolver { + java.lang.String[] resolve(java.lang.String); +} diff --git a/bindings/pezkuwi-light/proguard-rules.pro b/bindings/pezkuwi-light/proguard-rules.pro new file mode 100644 index 00000000..1191b44a --- /dev/null +++ b/bindings/pezkuwi-light/proguard-rules.pro @@ -0,0 +1,11 @@ +# JNI resolves these methods by name. +-keep class io.novafoundation.nova.lightclient.PezkuwiLight { + native ; +} +# Native code calls HostResolver.resolve by name. +-keep interface io.novafoundation.nova.lightclient.HostResolver { + java.lang.String[] resolve(java.lang.String); +} +-keepclassmembers class * implements io.novafoundation.nova.lightclient.HostResolver { + java.lang.String[] resolve(java.lang.String); +} diff --git a/bindings/pezkuwi-light/rust/.cargo/config.toml b/bindings/pezkuwi-light/rust/.cargo/config.toml new file mode 100644 index 00000000..5070d2c9 --- /dev/null +++ b/bindings/pezkuwi-light/rust/.cargo/config.toml @@ -0,0 +1,11 @@ +[target.aarch64-linux-android] +rustflags = ["-C", "link-arg=-z", "-C", "link-arg=max-page-size=16384"] + +[target.armv7-linux-androideabi] +rustflags = ["-C", "link-arg=-z", "-C", "link-arg=max-page-size=16384"] + +[target.i686-linux-android] +rustflags = ["-C", "link-arg=-z", "-C", "link-arg=max-page-size=16384"] + +[target.x86_64-linux-android] +rustflags = ["-C", "link-arg=-z", "-C", "link-arg=max-page-size=16384"] diff --git a/bindings/pezkuwi-light/rust/Cargo.lock b/bindings/pezkuwi-light/rust/Cargo.lock new file mode 100644 index 00000000..6cab369d --- /dev/null +++ b/bindings/pezkuwi-light/rust/Cargo.lock @@ -0,0 +1,2311 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" +dependencies = [ + "android_log-sys", + "env_filter 0.1.4", + "log", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-tungstenite" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c348fb0b6d132c596eca3dcd941df48fb597aafcb07a738ec41c004b087dc99" +dependencies = [ + "atomic-waker", + "futures-core", + "futures-io", + "futures-task", + "futures-util", + "log", + "pin-project-lite", + "tungstenite", +] + +[[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.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec 0.4.12", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375989decec1a0fa424442bed520fa12ede43a48475ce8c7417d2c69b32b5f6b" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "defmt" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "defmt-parser" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-zebra" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775765289f7c6336c18d3d66127527820dd45ffd9eb3b6b8ee4708590e6c20f5" +dependencies = [ + "curve25519-dalek", + "ed25519", + "hashbrown", + "pkcs8", + "rand_core", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "env_filter 2.0.0", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls", + "rustls-pki-types", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom_or_panic" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand_core", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3fef046dca3ca91ee1408a8c1b80ab777e80a4d308d1bf4e7adb3fcb047e08" +dependencies = [ + "arrayvec 0.7.8", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ac703058abac89e5c39b0b879dce2b6b14a4ba36600c7e7e3d0c60a6883ef3c" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee43309eb53bafb8c36d35d6e444769c8f8e6206a9fbf7546f5db9d7d4a0e93" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dc32470c67eb7d0e9ccb74303fa925313dc04cecb1aa048beaa6f391e36a93d" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libsecp256k1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" +dependencies = [ + "arrayref", + "base64", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "multi-stash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685a9ac4b61f4e728e1d2c6a7844609c16527aeb5e6c865915c08e619c16410f" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pezkuwi-light" +version = "0.1.0" +dependencies = [ + "android_logger", + "async-tungstenite", + "env_logger", + "futures-rustls", + "futures-util", + "hex", + "jni", + "log", + "pin-project", + "rand", + "rustls", + "serde_json", + "smol", + "smoldot", + "smoldot-light", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ruzstd" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schnorrkel" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9fcb6c2e176e86ec703e22560d99d65a5ee9056ae45a08e13e84ebf796296f" +dependencies = [ + "aead", + "arrayref", + "arrayvec 0.7.8", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "smoldot" +version = "0.21.0" +source = "git+https://github.com/pezkuwichain/smoldot?rev=182d996a3ae2a9b507a776fa307f2e6fc5d14f7a#182d996a3ae2a9b507a776fa307f2e6fc5d14f7a" +dependencies = [ + "arrayvec 0.7.8", + "async-lock", + "atomic-take", + "base64", + "bip39", + "blake2-rfc", + "bs58", + "chacha20", + "crossbeam-queue", + "derive_more", + "ed25519-zebra", + "either", + "event-listener", + "fnv", + "futures-lite", + "futures-util", + "hashbrown", + "hex", + "hmac 0.12.1", + "itertools", + "libm", + "libsecp256k1", + "merlin", + "nom", + "num-bigint", + "num-rational", + "num-traits", + "pbkdf2", + "pin-project", + "poly1305", + "rand", + "rand_chacha", + "ruzstd", + "schnorrkel", + "serde", + "serde_json", + "sha2 0.10.9", + "sha3", + "siphasher", + "slab", + "smallvec", + "soketto", + "twox-hash", + "wasmi", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "smoldot-light" +version = "0.19.0" +source = "git+https://github.com/pezkuwichain/smoldot?rev=182d996a3ae2a9b507a776fa307f2e6fc5d14f7a#182d996a3ae2a9b507a776fa307f2e6fc5d14f7a" +dependencies = [ + "async-channel", + "async-lock", + "base64", + "blake2-rfc", + "bs58", + "derive_more", + "either", + "event-listener", + "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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "httparse", + "log", + "rand", + "sha1", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "twox-hash" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasmi" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19af97fcb96045dd1d6b4d23e2b4abdbbe81723dbc5c9f016eb52145b320063" +dependencies = [ + "arrayvec 0.7.8", + "multi-stash", + "smallvec", + "spin", + "wasmi_collections", + "wasmi_core", + "wasmi_ir", + "wasmparser", +] + +[[package]] +name = "wasmi_collections" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e80d6b275b1c922021939d561574bf376613493ae2b61c6963b15db0e8813562" + +[[package]] +name = "wasmi_core" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8c51482cc32d31c2c7ff211cd2bedd73c5bd057ba16a2ed0110e7a96097c33" +dependencies = [ + "downcast-rs", + "libm", +] + +[[package]] +name = "wasmi_ir" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e431a14c186db59212a88516788bd68ed51f87aa1e08d1df742522867b5289a" +dependencies = [ + "wasmi_core", +] + +[[package]] +name = "wasmparser" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.11", +] + +[[package]] +name = "webpki-roots" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[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.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/bindings/pezkuwi-light/rust/Cargo.toml b/bindings/pezkuwi-light/rust/Cargo.toml new file mode 100644 index 00000000..720553aa --- /dev/null +++ b/bindings/pezkuwi-light/rust/Cargo.toml @@ -0,0 +1,44 @@ +[package] +authors = ['PezkuwiChain'] +edition = '2021' +license = 'Apache-2.0' +name = "pezkuwi-light" +version = "0.1.0" +publish = false + +[dependencies] +# PezkuwiChain fork of smoldot: upstream smoldot cannot verify PezkuwiChain blocks (sovereign +# sr25519 and BABE VRF signing contexts, `TeyrchainHost` runtime API). +smoldot-light = { git = "https://github.com/pezkuwichain/smoldot", rev = "182d996a3ae2a9b507a776fa307f2e6fc5d14f7a", default-features = false, features = ["std"] } +smoldot = { git = "https://github.com/pezkuwichain/smoldot", rev = "182d996a3ae2a9b507a776fa307f2e6fc5d14f7a", default-features = false, features = ["std"] } +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] } +futures-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12", "logging"] } +webpki-roots = "0.26" +pin-project = "1" +async-tungstenite = { version = "0.28", default-features = false, features = ["handshake", "futures-03-sink"] } +futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } +smol = "2.0" +jni = { version = "0.21", default-features = false } +log = "0.4" +rand = "0.8" +hex = "0.4" +serde_json = "1" + +[target.'cfg(target_os = "android")'.dependencies] +android_logger = { version = "0.14", default-features = false } + +[dev-dependencies] +env_logger = { version = "0.11", default-features = false, features = ["humantime"] } +tungstenite = "0.24" + +# Stripped here, not left to the Android Gradle plugin: it packages a library it cannot strip as +# it is, and with debug info this one is 147 MB per ABI instead of 8.6 MB. +[profile.release] +strip = true +debug = false +lto = true +opt-level = "s" + +[lib] +name = "pezkuwi_light" +crate-type = ["cdylib", "rlib"] diff --git a/bindings/pezkuwi-light/rust/src/engine.rs b/bindings/pezkuwi-light/rust/src/engine.rs new file mode 100644 index 00000000..1a657430 --- /dev/null +++ b/bindings/pezkuwi-light/rust/src/engine.rs @@ -0,0 +1,423 @@ +//! Embedded PezkuwiChain light client behind a loopback JSON-RPC WebSocket server. +//! +//! The wallet talks to its chains through `SocketService` (a JSON-RPC WebSocket client), so the +//! light client is exposed the same way: `ws://127.0.0.1://`. Every +//! accepted connection adds its own smoldot chain instance; smoldot shares the underlying sync +//! between instances of the same chain, so a connection costs a JSON-RPC frontend, not a sync. +//! +//! The port is bound to 127.0.0.1 only and every path carries a random token, so other apps on +//! the device cannot drive the client without first learning the token from this process. + +use async_tungstenite::tungstenite::{ + handshake::server::{ErrorResponse, Request, Response}, + http::StatusCode, + Message, +}; +use futures_util::{SinkExt as _, StreamExt as _}; +use crate::platform::{PezkuwiPlatform, Resolve}; +use smoldot_light::{ + AddChainConfig, AddChainConfigJsonRpc, AddChainSuccess, ChainId, + Client, HandleRpcError, JsonRpcResponses, +}; +use std::{ + collections::HashMap, + fs, io, + net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream}, + num::NonZero, + path::PathBuf, + sync::{Arc, Mutex}, + thread, + time::Duration, +}; + +type Platform = PezkuwiPlatform; + +/// How often the finalized state of each chain is written to disk, so that the next start +/// resumes from it instead of syncing from the chain specification. +const DATABASE_SAVE_INTERVAL: Duration = Duration::from_secs(60); +const FIRST_DATABASE_SAVE: Duration = Duration::from_secs(20); + +/// Request id of the database export on a chain's own JSON-RPC frontend. +const DATABASE_REQUEST_ID: &str = "pezkuwi-light-database"; + +pub struct Engine { + inner: Arc, + platform: PezkuwiPlatform, + port: u16, +} + +struct Inner { + client: Mutex>, + chains: Mutex>, + executor: Arc>, + token: String, + db_dir: PathBuf, +} + +struct Registered { + spec: Arc, + /// Instance added at registration. It keeps the chain synced while no connection is open and + /// serves the database export. + base: ChainId, + /// Base instance of the relay chain, for a teyrchain. + relay: Option, +} + +#[derive(Debug)] +pub enum Error { + UnknownRelayChain(String), + AlreadyRegistered(String), + AddChain(String), + Io(io::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::UnknownRelayChain(key) => write!(f, "relay chain {key} is not registered"), + Error::AlreadyRegistered(key) => write!(f, "chain {key} is already registered"), + Error::AddChain(err) => write!(f, "smoldot rejected the chain: {err}"), + Error::Io(err) => write!(f, "{err}"), + } + } +} + +impl Engine { + /// Starts the light client and its loopback server. `db_dir` holds one finalized-state file + /// per chain key; `resolver` resolves the host names of peers. + pub fn start(db_dir: PathBuf, resolver: Arc) -> Result { + fs::create_dir_all(&db_dir).map_err(Error::Io)?; + let listener = + TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))).map_err(Error::Io)?; + let port = listener.local_addr().map_err(Error::Io)?.port(); + + let mut token = [0u8; 32]; + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token); + + let platform = PezkuwiPlatform::new( + env!("CARGO_PKG_NAME").into(), + env!("CARGO_PKG_VERSION").into(), + resolver, + ); + let executor = Arc::new(smol::Executor::new()); + let inner = Arc::new(Inner { + client: Mutex::new(Client::new(platform.clone())), + chains: Mutex::new(HashMap::new()), + executor: executor.clone(), + token: hex::encode(token), + db_dir, + }); + + let listener = smol::Async::new(listener).map_err(Error::Io)?; + executor.spawn(accept_loop(inner.clone(), listener)).detach(); + thread::Builder::new() + .name("pezkuwi-light-rpc".into()) + .spawn(move || smol::block_on(executor.run(futures_util::future::pending::<()>()))) + .map_err(Error::Io)?; + + Ok(Engine { + inner, + platform, + port, + }) + } + + /// Restricts new peer connections to secure WebSockets (`wss`), which look like any HTTPS + /// WebSocket on the wire. Off by default. + pub fn set_secure_only(&self, secure_only: bool) { + self.platform.set_secure_only(secure_only); + } + + /// `ws://127.0.0.1:/`; a chain is reached at `/`. + pub fn base_url(&self) -> String { + format!("ws://127.0.0.1:{}/{}", self.port, self.inner.token) + } + + /// Registers a chain under `key` and starts syncing it. A teyrchain names the key of its + /// already registered relay chain. + pub fn add_chain(&self, key: &str, spec: &str, relay_key: Option<&str>) -> Result<(), Error> { + let mut chains = self.inner.chains.lock().unwrap(); + if chains.contains_key(key) { + return Err(Error::AlreadyRegistered(key.into())); + } + let relay = match relay_key { + Some(relay_key) => Some( + chains + .get(relay_key) + .ok_or_else(|| Error::UnknownRelayChain(relay_key.into()))? + .base, + ), + None => None, + }; + + let database = fs::read_to_string(self.inner.database_path(key)).unwrap_or_default(); + let AddChainSuccess { + chain_id, + json_rpc_responses, + } = self + .inner + .client + .lock() + .unwrap() + .add_chain(AddChainConfig { + user_data: (), + specification: spec, + database_content: &database, + potential_relay_chains: relay.into_iter(), + json_rpc: AddChainConfigJsonRpc::Enabled { + max_pending_requests: NonZero::new(4).unwrap(), + max_subscriptions: 0, + }, + }) + .map_err(|err| Error::AddChain(err.to_string()))?; + + chains.insert( + key.into(), + Registered { + spec: spec.into(), + base: chain_id, + relay, + }, + ); + self.inner + .executor + .spawn(save_database( + self.inner.clone(), + key.to_owned(), + chain_id, + json_rpc_responses.expect("JSON-RPC is enabled"), + )) + .detach(); + Ok(()) + } +} + +impl Inner { + fn database_path(&self, key: &str) -> PathBuf { + database_path(&self.db_dir, key) + } +} + +fn database_path(db_dir: &std::path::Path, key: &str) -> PathBuf { + // Keys come from the app (genesis hashes); keep them from escaping the directory anyway. + let file: String = key + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) + .collect(); + db_dir.join(format!("{file}.db")) +} + +/// Constant-time comparison, so the token cannot be guessed byte by byte from timing. +fn token_matches(token: &str, candidate: &str) -> bool { + let (a, b) = (token.as_bytes(), candidate.as_bytes()); + a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0 +} + +async fn accept_loop(inner: Arc, listener: smol::Async) { + loop { + match listener.accept().await { + Ok((stream, _)) => inner.executor.spawn(serve(inner.clone(), stream)).detach(), + Err(err) => { + log::warn!("loopback accept failed: {err}"); + smol::Timer::after(Duration::from_millis(100)).await; + } + } + } +} + +/// Maps `//` to a registered chain key, rejecting everything else during the +/// WebSocket handshake. +fn route(token: &str, is_registered: impl Fn(&str) -> bool, path: &str) -> Option { + let (candidate, key) = path.strip_prefix('/')?.split_once('/')?; + (token_matches(token, candidate) && is_registered(key)).then(|| key.to_owned()) +} + +async fn serve(inner: Arc, stream: smol::Async) { + let mut routed = None; + let handshake = async_tungstenite::accept_hdr_async(stream, |request: &Request, response: Response| { + routed = route( + &inner.token, + |key| inner.chains.lock().unwrap().contains_key(key), + request.uri().path(), + ); + if routed.is_some() { + Ok(response) + } else { + let mut rejection = ErrorResponse::new(None); + *rejection.status_mut() = StatusCode::NOT_FOUND; + Err(rejection) + } + }) + .await; + let (Ok(socket), Some(key)) = (handshake, routed) else { + return; + }; + + let (chain_id, mut responses) = { + let chains = inner.chains.lock().unwrap(); + let registered = &chains[&key]; + let added = inner.client.lock().unwrap().add_chain(AddChainConfig { + user_data: (), + specification: ®istered.spec, + // The base instance already holds the chain; smoldot shares it with this one. + database_content: "", + potential_relay_chains: registered.relay.into_iter(), + json_rpc: AddChainConfigJsonRpc::Enabled { + max_pending_requests: NonZero::new(128).unwrap(), + max_subscriptions: 1024, + }, + }); + match added { + Ok(success) => (success.chain_id, success.json_rpc_responses.expect("JSON-RPC is enabled")), + Err(err) => { + log::warn!("{key}: cannot open a connection instance: {err}"); + return; + } + } + }; + + let (mut sink, mut source) = socket.split(); + let (overflow_tx, overflow_rx) = smol::channel::unbounded::(); + + let inbound = async { + while let Some(Ok(message)) = source.next().await { + let request = match message { + Message::Text(text) => text, + Message::Binary(bytes) => match String::from_utf8(bytes) { + Ok(text) => text, + Err(_) => continue, + }, + Message::Close(_) => break, + _ => continue, + }; + let refused = inner.client.lock().unwrap().json_rpc_request(request, chain_id); + if let Err(HandleRpcError::TooManyPendingRequests { json_rpc_request }) = refused { + if let Some(error) = busy_error(&json_rpc_request) { + let _ = overflow_tx.send(error).await; + } + } + } + }; + let outbound = async { + loop { + let response = futures_util::future::select( + Box::pin(responses.next()), + Box::pin(overflow_rx.recv()), + ) + .await; + let text = match response { + futures_util::future::Either::Left((Some(text), _)) => text, + futures_util::future::Either::Right((Ok(text), _)) => text, + _ => break, + }; + if sink.send(Message::Text(text)).await.is_err() { + break; + } + } + }; + futures_util::future::select(Box::pin(inbound), Box::pin(outbound)).await; + + let () = inner.client.lock().unwrap().remove_chain(chain_id); +} + +/// JSON-RPC error for a request smoldot refused because its queue is full, so the caller gets an +/// answer instead of waiting forever. Notifications (no id) get none. +fn busy_error(request: &str) -> Option { + let id = serde_json::from_str::(request).ok()?.get("id")?.clone(); + Some( + serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "error": { "code": -32000, "message": "light client is busy, retry" }, + }) + .to_string(), + ) +} + +/// Periodically writes the chain's finalized state to disk through its base instance. +async fn save_database( + inner: Arc, + key: String, + chain_id: ChainId, + mut responses: JsonRpcResponses, +) { + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": DATABASE_REQUEST_ID, + "method": "chainHead_unstable_finalizedDatabase", + "params": [], + }) + .to_string(); + let path = inner.database_path(&key); + let mut wait = FIRST_DATABASE_SAVE; + loop { + smol::Timer::after(wait).await; + wait = DATABASE_SAVE_INTERVAL; + if inner + .client + .lock() + .unwrap() + .json_rpc_request(request.clone(), chain_id) + .is_err() + { + continue; + } + while let Some(response) = responses.next().await { + let response: serde_json::Value = match serde_json::from_str(&response) { + Ok(value) => value, + Err(_) => continue, + }; + if response.get("id").and_then(|id| id.as_str()) != Some(DATABASE_REQUEST_ID) { + continue; + } + if let Some(database) = response.get("result").and_then(|db| db.as_str()) { + if let Err(err) = write_atomically(&path, database) { + log::warn!("{key}: cannot save the light client database: {err}"); + } + } + break; + } + } +} + +/// Writes through a temporary file and a rename, so a crash never leaves a torn database. +fn write_atomically(path: &std::path::Path, contents: &str) -> io::Result<()> { + let temporary = path.with_extension("db.tmp"); + fs::write(&temporary, contents)?; + fs::rename(&temporary, path) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn route_requires_the_token_and_a_registered_chain() { + let token = "ab".repeat(32); + let route = |path: &str| route(&token, |key| key == "people", path); + assert_eq!(route(&format!("/{token}/people")), Some("people".into())); + assert_eq!(route(&format!("/{token}/relay")), None); + assert_eq!(route(&format!("/{}/people", "ac".repeat(32))), None); + assert_eq!(route(&format!("/{}/people", &token[1..])), None); + assert_eq!(route(&format!("/{token}people")), None); + assert_eq!(route("/people"), None); + assert_eq!(route(&format!("{token}/people")), None); + } + + #[test] + fn database_path_stays_inside_the_directory() { + assert_eq!( + database_path(std::path::Path::new("/data"), "../../etc/passwd"), + PathBuf::from("/data/______etc_passwd.db") + ); + } + + #[test] + fn busy_error_answers_the_request_id() { + let error = busy_error(r#"{"jsonrpc":"2.0","id":7,"method":"state_call","params":[]}"#).unwrap(); + let error: serde_json::Value = serde_json::from_str(&error).unwrap(); + assert_eq!(error["id"], 7); + assert_eq!(error["error"]["code"], -32000); + assert_eq!(busy_error(r#"{"jsonrpc":"2.0","method":"x"}"#), None); + } +} diff --git a/bindings/pezkuwi-light/rust/src/lib.rs b/bindings/pezkuwi-light/rust/src/lib.rs new file mode 100644 index 00000000..3e6d5cd6 --- /dev/null +++ b/bindings/pezkuwi-light/rust/src/lib.rs @@ -0,0 +1,170 @@ +//! JNI surface of the embedded PezkuwiChain light client. See [`engine`] for how it works. + +pub mod engine; +pub mod platform; + +use engine::Engine; +use jni::objects::{GlobalRef, JObject, JObjectArray, JString}; +use jni::sys::{jboolean, jstring, JNI_TRUE}; +use jni::{JNIEnv, JavaVM}; +use platform::Resolve; +use std::{io, net::IpAddr, sync::Arc, sync::OnceLock}; + +/// One light client per process: smoldot shares chains between its instances, and the app +/// reaches it through a single loopback server. +static ENGINE: OnceLock> = OnceLock::new(); + +fn engine() -> Option<&'static Engine> { + ENGINE.get().and_then(|engine| engine.as_ref().ok()) +} + +fn throw(env: &mut JNIEnv, message: &str) { + let _ = env.throw_new("java/lang/IllegalStateException", message); +} + +fn read_string(env: &mut JNIEnv, value: &JString) -> Option { + env.get_string(value).ok().map(Into::into) +} + +/// Resolves peer host names through the app's `HostResolver`, so the light client follows the +/// app's DNS policy (DNS over HTTPS first). +struct AppResolver { + vm: JavaVM, + resolver: GlobalRef, +} + +impl Resolve for AppResolver { + fn resolve(&self, host: &str) -> io::Result> { + // Blocking-pool threads are reused, so they stay attached instead of paying an attach + // per lookup. + let mut env = self + .vm + .attach_current_thread_permanently() + .map_err(|_| lookup_error(host, "cannot attach to the JVM"))?; + // The frame's own result must convert from `jni::errors::Error`; the lookup's outcome + // travels inside it. + env.with_local_frame(8, |env| -> jni::errors::Result<_> { + Ok(lookup(env, &self.resolver, host)) + }) + .unwrap_or_else(|_| Err(lookup_error(host, "JNI frame failed"))) + } +} + +fn lookup_error(host: &str, what: &str) -> io::Error { + io::Error::new(io::ErrorKind::Other, format!("{host}: {what}")) +} + +/// Calls `String[] resolve(String)` on the app's resolver. A thrown exception (an unknown host) +/// is cleared and reported as an error. +fn lookup(env: &mut JNIEnv, resolver: &GlobalRef, host: &str) -> io::Result> { + let failed = |what| lookup_error(host, what); + let name = env.new_string(host).map_err(|_| failed("invalid name"))?; + let answer = env.call_method( + resolver, + "resolve", + "(Ljava/lang/String;)[Ljava/lang/String;", + &[(&name).into()], + ); + if env.exception_check().unwrap_or(false) { + let _ = env.exception_clear(); + return Err(failed("no address")); + } + let answer: JObjectArray = answer + .and_then(|value| value.l()) + .map_err(|_| failed("resolver failed"))? + .into(); + let count = env.get_array_length(&answer).map_err(|_| failed("bad answer"))?; + let mut addresses = Vec::new(); + for index in 0..count { + let element: JString = env + .get_object_array_element(&answer, index) + .map_err(|_| failed("bad answer"))? + .into(); + if let Some(address) = read_string(env, &element).and_then(|text| text.parse().ok()) { + addresses.push(address); + } + } + Ok(addresses) +} + +/// Starts the light client (once per process) and returns its loopback base URL. +#[no_mangle] +pub extern "system" fn Java_io_novafoundation_nova_lightclient_PezkuwiLight_start<'local>( + mut env: JNIEnv<'local>, + _this: JObject<'local>, + database_dir: JString<'local>, + resolver: JObject<'local>, +) -> jstring { + let Some(database_dir) = read_string(&mut env, &database_dir) else { + throw(&mut env, "invalid database directory"); + return std::ptr::null_mut(); + }; + let resolver = match (env.get_java_vm(), env.new_global_ref(&resolver)) { + (Ok(vm), Ok(resolver)) => AppResolver { vm, resolver }, + _ => { + throw(&mut env, "invalid resolver"); + return std::ptr::null_mut(); + } + }; + let started = ENGINE.get_or_init(|| { + #[cfg(target_os = "android")] + android_logger::init_once( + android_logger::Config::default() + .with_max_level(log::LevelFilter::Info) + .with_tag("PezkuwiLight"), + ); + Engine::start(database_dir.into(), Arc::new(resolver)).map_err(|err| err.to_string()) + }); + match started { + Ok(engine) => match env.new_string(engine.base_url()) { + Ok(url) => url.into_raw(), + Err(_) => std::ptr::null_mut(), + }, + Err(err) => { + throw(&mut env, err); + std::ptr::null_mut() + } + } +} + +/// Registers a chain under `key`; a teyrchain passes its relay chain's key. +#[no_mangle] +pub extern "system" fn Java_io_novafoundation_nova_lightclient_PezkuwiLight_addChain<'local>( + mut env: JNIEnv<'local>, + _this: JObject<'local>, + key: JString<'local>, + chain_spec: JString<'local>, + relay_key: JString<'local>, +) { + let Some(engine) = engine() else { + throw(&mut env, "light client is not started"); + return; + }; + let (Some(key), Some(chain_spec)) = + (read_string(&mut env, &key), read_string(&mut env, &chain_spec)) + else { + throw(&mut env, "invalid chain key or specification"); + return; + }; + let relay_key = if relay_key.is_null() { + None + } else { + read_string(&mut env, &relay_key) + }; + if let Err(err) = engine.add_chain(&key, &chain_spec, relay_key.as_deref()) { + throw(&mut env, &err.to_string()); + } +} + +/// Restricts new peer connections to secure WebSockets. +#[no_mangle] +pub extern "system" fn Java_io_novafoundation_nova_lightclient_PezkuwiLight_setSecureOnly<'local>( + mut env: JNIEnv<'local>, + _this: JObject<'local>, + secure_only: jboolean, +) { + match engine() { + Some(engine) => engine.set_secure_only(secure_only == JNI_TRUE), + None => throw(&mut env, "light client is not started"), + } +} diff --git a/bindings/pezkuwi-light/rust/src/platform.rs b/bindings/pezkuwi-light/rust/src/platform.rs new file mode 100644 index 00000000..5280ba9f --- /dev/null +++ b/bindings/pezkuwi-light/rust/src/platform.rs @@ -0,0 +1,423 @@ +//! smoldot platform for the wallet: secure WebSockets and an app-supplied name resolver. +//! +//! smoldot's own `DefaultPlatform` cannot open `wss` connections and resolves names through the +//! system resolver. Where our domains are blocked, both fail: a poisoned DNS answer decides where +//! the client connects, and plain libp2p TCP is recognisable on the wire. This platform: +//! - opens `/dns//tcp//wss` over TLS (rustls, Mozilla roots), so a connection looks +//! like any HTTPS WebSocket; +//! - resolves every name through [`Resolve`], which the app backs with its DNS-over-HTTPS +//! resolver, so the light client and the rest of the app follow one DNS policy; +//! - can be restricted to secure WebSockets only ([`PezkuwiPlatform::set_secure_only`]). +//! +//! The peer's identity is still checked end to end by libp2p Noise, so neither the TLS terminator +//! in front of a node nor a CDN can alter what the client verifies. + +use futures_util::{future, AsyncRead, AsyncWrite, FutureExt as _}; +use smoldot_light::platform::{ + with_buffers, Address, ConnectionType, LogLevel, MultiStreamAddress, + MultiStreamWebRtcConnection, PlatformRef, SubstreamDirection, +}; +use std::{ + borrow::Cow, + future::Future, + fmt::{self, Write as _}, + io, + net::{self, SocketAddr}, + panic, + pin::Pin, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + thread, + time::{Duration, Instant, UNIX_EPOCH}, +}; + +/// Resolves a host name to addresses. Called on a blocking thread. +pub trait Resolve: Send + Sync + 'static { + fn resolve(&self, host: &str) -> io::Result>; +} + +/// The operating system's resolver, for tests and hosts without an app resolver. +pub struct SystemResolver; + +impl Resolve for SystemResolver { + fn resolve(&self, host: &str) -> io::Result> { + use std::net::ToSocketAddrs as _; + Ok((host, 0).to_socket_addrs()?.map(|addr| addr.ip()).collect()) + } +} + +/// Bound on each TCP connection attempt, so an address that silently drops packets does not hold +/// a connection slot. +const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); + +#[derive(Clone)] +pub struct PezkuwiPlatform(Arc); + +struct Shared { + client_name: String, + client_version: String, + executor: Arc>, + resolver: Arc, + tls: futures_rustls::TlsConnector, + secure_only: AtomicBool, +} + +// smoldot requires `UnwindSafe`. A panic inside a task is caught at the task boundary +// (`spawn_task`), and nothing here is left half-updated across it: the resolver is called +// through `&self`, and the TLS configuration is immutable. +impl panic::UnwindSafe for PezkuwiPlatform {} +impl panic::RefUnwindSafe for PezkuwiPlatform {} + +impl PezkuwiPlatform { + pub fn new(client_name: String, client_version: String, resolver: Arc) -> Self { + let executor = Arc::new(smol::Executor::new()); + let threads = thread::available_parallelism().map_or(2, |n| n.get().min(4)); + for n in 0..threads { + let executor = executor.clone(); + thread::Builder::new() + .name(format!("pezkuwi-light-{n}")) + .spawn(move || smol::block_on(executor.run(future::pending::<()>()))) + .expect("cannot spawn a light client thread"); + } + + let roots = rustls::RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), + }; + let tls = rustls::ClientConfig::builder_with_provider(Arc::new( + rustls::crypto::ring::default_provider(), + )) + .with_safe_default_protocol_versions() + .expect("ring supports the default protocol versions") + .with_root_certificates(roots) + .with_no_client_auth(); + + PezkuwiPlatform(Arc::new(Shared { + client_name, + client_version, + executor, + resolver, + tls: futures_rustls::TlsConnector::from(Arc::new(tls)), + secure_only: AtomicBool::new(false), + })) + } + + /// Restricts new connections to secure WebSockets. Open connections are kept. + pub fn set_secure_only(&self, secure_only: bool) { + self.0.secure_only.store(secure_only, Ordering::Relaxed); + } +} + +/// A connected byte stream: plain TCP, WebSocket, or WebSocket over TLS. +pub trait Socket: AsyncRead + AsyncWrite + Send {} +impl Socket for T {} + +type BoxedSocket = Pin>; +type Connecting = future::BoxFuture<'static, io::Result>; + +#[pin_project::pin_project] +pub struct Stream(#[pin] with_buffers::WithBuffers); + +/// Where to connect and what to speak once connected. +struct Target { + host: Host, + port: u16, + websocket: Option, +} + +enum Host { + Ip(net::IpAddr), + Name(String), +} + +struct WebSocket { + /// `Host` header value. + authority: String, + /// TLS server name, for `wss`. + tls_name: Option, +} + +fn target(address: Address) -> Target { + match address { + Address::TcpIp { ip, port } => Target { + host: Host::Ip(ip), + port, + websocket: None, + }, + Address::TcpDns { hostname, port } => Target { + host: Host::Name(hostname.into()), + port, + websocket: None, + }, + Address::WebSocketIp { ip, port } => { + Target { + host: Host::Ip(ip), + port, + websocket: Some(WebSocket { + authority: SocketAddr::from((ip, port)).to_string(), + tls_name: None, + }), + } + } + Address::WebSocketDns { + hostname, + port, + secure, + } => Target { + host: Host::Name(hostname.into()), + port, + websocket: Some(WebSocket { + authority: if (secure && port == 443) || (!secure && port == 80) { + hostname.into() + } else { + format!("{hostname}:{port}") + }, + tls_name: secure.then(|| hostname.into()), + }), + }, + } +} + +/// IPv4 first: a resolver may return IPv6 addresses on a network without an IPv6 route, and a +/// dead route would otherwise cost a full connect timeout before every connection. +fn ordered(mut addresses: Vec) -> Vec { + let mut seen = std::collections::HashSet::new(); + addresses.retain(|address| seen.insert(*address)); + addresses.sort_by_key(|address| address.is_ipv6()); + addresses +} + +async fn connect_tcp(shared: Arc, host: Host, port: u16) -> io::Result { + let addresses = match host { + Host::Ip(address) => vec![address], + Host::Name(name) => { + let resolver = shared.resolver.clone(); + ordered(smol::unblock(move || resolver.resolve(&name)).await?) + } + }; + let mut last_error = io::Error::new(io::ErrorKind::NotFound, "the name has no address"); + for address in addresses { + let attempt = smol::net::TcpStream::connect(SocketAddr::from((address, port))); + let timeout = async { + smol::Timer::after(CONNECT_TIMEOUT).await; + Err(io::Error::new(io::ErrorKind::TimedOut, "connect timed out")) + }; + match future::select(Box::pin(attempt), Box::pin(timeout)).await.factor_first().0 { + Ok(socket) => { + let _ = socket.set_nodelay(true); + return Ok(socket); + } + Err(err) => last_error = err, + } + } + Err(last_error) +} + +async fn connect(shared: Arc, target: Target) -> io::Result { + let tcp = connect_tcp(shared.clone(), target.host, target.port).await?; + let Some(websocket) = target.websocket else { + return Ok(Box::pin(tcp)); + }; + match &websocket.tls_name { + None => websocket_handshake(tcp, &websocket.authority).await, + Some(name) => { + let name = rustls::pki_types::ServerName::try_from(name.clone()) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + let tls = shared.tls.connect(name, tcp).await?; + websocket_handshake(tls, &websocket.authority).await + } + } +} + +async fn websocket_handshake(socket: T, authority: &str) -> io::Result +where + T: AsyncRead + AsyncWrite + Send + Unpin + 'static, +{ + let connection = smoldot::libp2p::websocket::websocket_client_handshake( + smoldot::libp2p::websocket::Config { + tcp_socket: socket, + host: authority, + url: "/", + }, + ) + .await?; + Ok(Box::pin(connection)) +} + +impl PlatformRef for PezkuwiPlatform { + type Delay = future::Map ()>; + type Instant = Instant; + type MultiStream = std::convert::Infallible; + type Stream = Stream; + type StreamConnectFuture = future::Ready; + type MultiStreamConnectFuture = future::Pending>; + type ReadWriteAccess<'a> = with_buffers::ReadWriteAccess<'a, Instant>; + type StreamUpdateFuture<'a> = future::BoxFuture<'a, ()>; + type StreamErrorRef<'a> = &'a io::Error; + type NextSubstreamFuture<'a> = future::Pending>; + + fn now_from_unix_epoch(&self) -> Duration { + UNIX_EPOCH.elapsed().expect("the clock is set after 1970") + } + + fn now(&self) -> Instant { + Instant::now() + } + + fn fill_random_bytes(&self, buffer: &mut [u8]) { + rand::RngCore::fill_bytes(&mut rand::thread_rng(), buffer); + } + + fn sleep(&self, duration: Duration) -> Self::Delay { + smol::Timer::after(duration).map(drop as fn(Instant)) + } + + fn sleep_until(&self, when: Instant) -> Self::Delay { + smol::Timer::at(when).map(drop as fn(Instant)) + } + + fn spawn_task(&self, _task_name: Cow, task: impl Future + Send + 'static) { + self.0 + .executor + .spawn(panic::AssertUnwindSafe(task).catch_unwind().map(drop)) + .detach(); + } + + fn log<'a>( + &self, + log_level: LogLevel, + log_target: &'a str, + message: &'a str, + key_values: impl Iterator, + ) { + let level = match log_level { + LogLevel::Error => log::Level::Error, + LogLevel::Warn => log::Level::Warn, + LogLevel::Info => log::Level::Info, + LogLevel::Debug => log::Level::Debug, + LogLevel::Trace => log::Level::Trace, + }; + if !log::log_enabled!(target: log_target, level) { + return; + } + let mut line = String::from(message); + for (n, (key, value)) in key_values.enumerate() { + let _ = write!(line, "{}{key}={value}", if n == 0 { "; " } else { ", " }); + } + log::log!(target: log_target, level, "{line}"); + } + + fn client_name(&self) -> Cow<'_, str> { + Cow::Borrowed(&self.0.client_name) + } + + fn client_version(&self) -> Cow<'_, str> { + Cow::Borrowed(&self.0.client_version) + } + + fn supports_connection_type(&self, connection_type: ConnectionType) -> bool { + match connection_type { + ConnectionType::WebSocketDns { secure: true, .. } => true, + ConnectionType::TcpIpv4 + | ConnectionType::TcpIpv6 + | ConnectionType::TcpDns + | ConnectionType::WebSocketIpv4 { .. } + | ConnectionType::WebSocketIpv6 { .. } + | ConnectionType::WebSocketDns { secure: false, .. } => { + !self.0.secure_only.load(Ordering::Relaxed) + } + // WebRTC needs a multi-stream implementation, which a native client has no use for. + _ => false, + } + } + + fn connect_stream(&self, address: Address) -> Self::StreamConnectFuture { + let connecting: Connecting = Box::pin(connect(self.0.clone(), target(address))); + future::ready(Stream(with_buffers::WithBuffers::new(connecting))) + } + + fn connect_multistream(&self, _address: MultiStreamAddress) -> Self::MultiStreamConnectFuture { + unreachable!("no multi-stream connection type is supported") + } + + fn open_out_substream(&self, connection: &mut Self::MultiStream) { + match *connection {} + } + + fn next_substream<'a>( + &self, + connection: &'a mut Self::MultiStream, + ) -> Self::NextSubstreamFuture<'a> { + match *connection {} + } + + fn read_write_access<'a>( + &self, + stream: Pin<&'a mut Self::Stream>, + ) -> Result, &'a io::Error> { + stream.project().0.read_write_access(Instant::now()) + } + + fn wait_read_write_again<'a>(&self, stream: Pin<&'a mut Self::Stream>) -> Self::StreamUpdateFuture<'a> { + Box::pin( + stream + .project() + .0 + .wait_read_write_again(|when| smol::Timer::at(when).map(drop)), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn platform() -> PezkuwiPlatform { + PezkuwiPlatform::new("test".into(), "0".into(), Arc::new(SystemResolver)) + } + + #[test] + fn secure_only_admits_nothing_but_secure_websockets() { + let platform = platform(); + let plain = [ + ConnectionType::TcpIpv4, + ConnectionType::TcpIpv6, + ConnectionType::TcpDns, + ConnectionType::WebSocketIpv4 { remote_is_localhost: false }, + ConnectionType::WebSocketIpv6 { remote_is_localhost: false }, + ConnectionType::WebSocketDns { secure: false, remote_is_localhost: false }, + ]; + let secure = ConnectionType::WebSocketDns { secure: true, remote_is_localhost: false }; + assert!(platform.supports_connection_type(secure)); + assert!(plain.iter().all(|kind| platform.supports_connection_type(*kind))); + + platform.set_secure_only(true); + assert!(platform.supports_connection_type(secure)); + assert!(plain.iter().all(|kind| !platform.supports_connection_type(*kind))); + } + + #[test] + fn websocket_targets_carry_the_right_host_header_and_tls_name() { + let wss = target(Address::WebSocketDns { hostname: "node.example", port: 443, secure: true }); + let ws = wss.websocket.unwrap(); + assert_eq!((ws.authority.as_str(), ws.tls_name.as_deref()), ("node.example", Some("node.example"))); + + let wss = target(Address::WebSocketDns { hostname: "node.example", port: 8443, secure: true }); + assert_eq!(wss.websocket.unwrap().authority, "node.example:8443"); + + let ws = target(Address::WebSocketDns { hostname: "node.example", port: 30335, secure: false }); + let ws = ws.websocket.unwrap(); + assert_eq!((ws.authority.as_str(), ws.tls_name), ("node.example:30335", None)); + + assert!(target(Address::TcpDns { hostname: "node.example", port: 30333 }).websocket.is_none()); + } + + #[test] + fn ipv4_is_tried_before_ipv6() { + let v6: net::IpAddr = "2001:db8::1".parse().unwrap(); + let v4: net::IpAddr = "192.0.2.1".parse().unwrap(); + let other: net::IpAddr = "192.0.2.2".parse().unwrap(); + assert_eq!(ordered(vec![v6, v4, other, v4]), vec![v4, other, v6]); + } +} diff --git a/bindings/pezkuwi-light/rust/tests/loopback.rs b/bindings/pezkuwi-light/rust/tests/loopback.rs new file mode 100644 index 00000000..ba488b50 --- /dev/null +++ b/bindings/pezkuwi-light/rust/tests/loopback.rs @@ -0,0 +1,233 @@ +//! End-to-end tests of the loopback server. +//! +//! The Zagros test syncs the live testnet, so it is ignored by default. Run it with the chain +//! specifications of the relay chain and People: +//! +//! ```sh +//! PEZKUWI_LIGHT_RELAY_SPEC=zagros.json PEZKUWI_LIGHT_PEOPLE_SPEC=people-zagros.json \ +//! cargo test --release -- --ignored --nocapture +//! ``` + +use pezkuwi_light::{engine::Engine, platform::SystemResolver}; +use std::{ + net::TcpStream, + sync::Arc, + time::{Duration, Instant}, +}; +use tungstenite::{stream::MaybeTlsStream, Message, WebSocket}; + +type Socket = WebSocket>; + +fn temporary_dir(name: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!("pezkuwi-light-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + dir +} + +fn start(db_dir: std::path::PathBuf) -> Engine { + // RUST_LOG=... shows smoldot's own timeline when a measurement needs explaining. + let _ = env_logger::builder().format_timestamp_millis().is_test(true).try_init(); + Engine::start(db_dir, Arc::new(SystemResolver)).unwrap() +} + +/// Connects to the loopback server; a read that waits longer than a sync can take fails the test +/// instead of hanging it. +fn connect(url: String) -> Socket { + let (socket, _) = tungstenite::connect(url).unwrap(); + if let MaybeTlsStream::Plain(stream) = socket.get_ref() { + stream.set_read_timeout(Some(Duration::from_secs(180))).unwrap(); + } + socket +} + +fn rpc(socket: &mut Socket, id: u64, method: &str, params: serde_json::Value) { + let request = serde_json::json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params}); + socket.send(Message::Text(request.to_string())).unwrap(); +} + +fn next_json(socket: &mut Socket) -> serde_json::Value { + loop { + if let Message::Text(text) = socket.read().unwrap() { + return serde_json::from_str(&text).unwrap(); + } + } +} + +fn answer(socket: &mut Socket, id: u64) -> serde_json::Value { + loop { + let message = next_json(socket); + if message["id"] == id { + return message; + } + } +} + +/// `Vec` SCALE encoding: compact length, then the bytes. +fn scale_bytes(bytes: &[u8]) -> String { + assert!(bytes.len() < 64, "single-byte compact length only"); + format!("0x{:02x}{}", (bytes.len() as u8) << 2, hex::encode(bytes)) +} + +#[test] +fn a_path_without_the_token_is_refused() { + let engine = start(temporary_dir("refused")); + let base = engine.base_url(); + let (_, token) = base.rsplit_once('/').unwrap(); + let wrong = format!("{}{}", &token[..token.len() - 1], if token.ends_with('0') { '1' } else { '0' }); + let port = base.split(':').nth(2).unwrap().split('/').next().unwrap(); + + for url in [ + format!("ws://127.0.0.1:{port}/{wrong}/relay"), + format!("ws://127.0.0.1:{port}/relay"), + // The token is right but no chain is registered under the key. + format!("{base}/relay"), + ] { + match tungstenite::connect(&url) { + Err(tungstenite::Error::Http(response)) => assert_eq!(response.status(), 404, "{url}"), + other => panic!("{url} was not refused: {:?}", other.map(|_| ())), + } + } +} + +#[test] +#[ignore = "syncs the live Zagros testnet; needs PEZKUWI_LIGHT_RELAY_SPEC and PEZKUWI_LIGHT_PEOPLE_SPEC"] +fn resolves_kurd_names_on_zagros_people() { + let relay = std::fs::read_to_string(std::env::var("PEZKUWI_LIGHT_RELAY_SPEC").unwrap()).unwrap(); + let people = std::fs::read_to_string(std::env::var("PEZKUWI_LIGHT_PEOPLE_SPEC").unwrap()).unwrap(); + let db_dir = temporary_dir("zagros"); + let started = Instant::now(); + + let engine = start(db_dir.clone()); + engine.add_chain("relay", &relay, None).unwrap(); + engine.add_chain("people", &people, Some("relay")).unwrap(); + assert!(engine.add_chain("people", &people, Some("relay")).is_err()); + + let mut socket = connect(format!("{}/people", engine.base_url())); + rpc(&mut socket, 1, "chain_subscribeFinalizedHeads", serde_json::json!([])); + loop { + if next_json(&mut socket).pointer("/params/result/number").is_some() { + break; + } + } + println!("first verified People head after {:.1}s", started.elapsed().as_secs_f64()); + let synced_at = std::time::SystemTime::now(); + + // Registered on Zagros People by the Meclis grant of 2026-10-31: Governance-owned, active, + // official. The full node answers the same bytes. + rpc(&mut socket, 2, "state_call", serde_json::json!(["NavnisanApi_resolve", scale_bytes(b"dks.kurd")])); + let resolved = answer(&mut socket, 2)["result"].as_str().unwrap().to_owned(); + assert!( + resolved.starts_with("0x0195c0de27b9a054a3a68cf2bb26abf99071ad46c2485295bb4120185a811a042f"), + "{resolved}" + ); + + rpc(&mut socket, 3, "state_call", serde_json::json!(["NavnisanApi_resolve", scale_bytes(b"yok-boyle-bir-ad.kurd")])); + assert_eq!(answer(&mut socket, 3)["result"], "0x00"); + + // A second connection to the same chain shares the sync and answers at once. + let mut second = connect(format!("{}/people", engine.base_url())); + let asked = Instant::now(); + rpc(&mut second, 4, "state_call", serde_json::json!(["NavnisanApi_resolve", scale_bytes(b"dks.kurd")])); + assert_eq!(answer(&mut second, 4)["result"].as_str().unwrap(), resolved); + println!("second connection answered in {:.2}s", asked.elapsed().as_secs_f64()); + + // The finalized state is saved for the next start: a save made after the first verified + // head, from which a new client resumes. + let deadline = Instant::now() + Duration::from_secs(150); + for file in ["relay.db", "people.db"] { + let path = db_dir.join(file); + while std::fs::metadata(&path).and_then(|m| m.modified()).map_or(true, |at| at < synced_at) { + assert!(Instant::now() < deadline, "{file} was not saved after the sync"); + std::thread::sleep(Duration::from_millis(500)); + } + println!("{file}: {} bytes", std::fs::metadata(&path).unwrap().len()); + } + + let restarted = Instant::now(); + let warm = start(db_dir); + warm.add_chain("relay", &relay, None).unwrap(); + warm.add_chain("people", &people, Some("relay")).unwrap(); + let mut socket = connect(format!("{}/people", warm.base_url())); + rpc(&mut socket, 5, "state_call", serde_json::json!(["NavnisanApi_resolve", scale_bytes(b"dks.kurd")])); + assert_eq!(answer(&mut socket, 5)["result"].as_str().unwrap(), resolved); + println!("warm start resolved after {:.1}s", restarted.elapsed().as_secs_f64()); +} + +/// Replaces a specification's boot nodes. +fn with_boot_nodes(spec: &str, boot_nodes: &[&str]) -> String { + let mut spec: serde_json::Value = serde_json::from_str(spec).unwrap(); + spec["bootNodes"] = serde_json::json!(boot_nodes); + spec.to_string() +} + +#[test] +#[ignore = "syncs the live Zagros testnet; needs PEZKUWI_LIGHT_RELAY_SPEC and PEZKUWI_LIGHT_PEOPLE_SPEC"] +fn syncs_zagros_over_secure_websockets_only() { + // The `/wss` boot nodes of 2026-10-31: nginx on 443 in front of each node's WebSocket port. + let relay = with_boot_nodes( + &std::fs::read_to_string(std::env::var("PEZKUWI_LIGHT_RELAY_SPEC").unwrap()).unwrap(), + &[ + "/dns/zagros-bootnode-0.pezkuwichain.io/tcp/443/wss/p2p/12D3KooWHKMr39ubvsigZXdVJzr7nnMp5YTzMywKuTzPDo3Xf1tZ", + "/dns/zagros-bootnode-1.pezkuwichain.io/tcp/443/wss/p2p/12D3KooWH6tzPuoFeyf8n6gNmus9Q3CdEGEVUvbduUnPTqJcLRLC", + ], + ); + let people = with_boot_nodes( + &std::fs::read_to_string(std::env::var("PEZKUWI_LIGHT_PEOPLE_SPEC").unwrap()).unwrap(), + &["/dns/zagros-people-bootnode.pezkuwichain.io/tcp/443/wss/p2p/12D3KooWLnDS5p5bvMzfco2BR4arKRbtpqoZwRvLKgCdzgVpuEQG"], + ); + let started = Instant::now(); + let engine = start(temporary_dir("wss")); + // Peers found through the DHT advertise plain TCP too; none of them may be used. + engine.set_secure_only(true); + engine.add_chain("relay", &relay, None).unwrap(); + engine.add_chain("people", &people, Some("relay")).unwrap(); + + let mut socket = connect(format!("{}/people", engine.base_url())); + rpc(&mut socket, 1, "state_call", serde_json::json!(["NavnisanApi_resolve", scale_bytes(b"dks.kurd")])); + let resolved = answer(&mut socket, 1)["result"].as_str().unwrap().to_owned(); + assert!( + resolved.starts_with("0x0195c0de27b9a054a3a68cf2bb26abf99071ad46c2485295bb4120185a811a042f"), + "{resolved}" + ); + println!("resolved over wss only after {:.1}s", started.elapsed().as_secs_f64()); +} + +/// Measures a start from a saved database in a process of its own, as an app launch is. The first +/// run with an empty `PEZKUWI_LIGHT_DB_DIR` syncs and waits for a save; later runs resume from it. +#[test] +#[ignore = "syncs the live Zagros testnet; needs PEZKUWI_LIGHT_RELAY_SPEC, PEZKUWI_LIGHT_PEOPLE_SPEC and PEZKUWI_LIGHT_DB_DIR"] +fn resolves_after_a_start_from_the_saved_database() { + let relay = std::fs::read_to_string(std::env::var("PEZKUWI_LIGHT_RELAY_SPEC").unwrap()).unwrap(); + let people = std::fs::read_to_string(std::env::var("PEZKUWI_LIGHT_PEOPLE_SPEC").unwrap()).unwrap(); + let db_dir = std::path::PathBuf::from(std::env::var("PEZKUWI_LIGHT_DB_DIR").unwrap()); + let warm = db_dir.join("people.db").exists(); + let started = Instant::now(); + + let engine = start(db_dir.clone()); + engine.add_chain("relay", &relay, None).unwrap(); + engine.add_chain("people", &people, Some("relay")).unwrap(); + let mut socket = connect(format!("{}/people", engine.base_url())); + rpc(&mut socket, 1, "state_call", serde_json::json!(["NavnisanApi_resolve", scale_bytes(b"dks.kurd")])); + let resolved = answer(&mut socket, 1)["result"].as_str().unwrap().to_owned(); + assert!( + resolved.starts_with("0x0195c0de27b9a054a3a68cf2bb26abf99071ad46c2485295bb4120185a811a042f"), + "{resolved}" + ); + println!( + "{} start resolved after {:.1}s", + if warm { "warm" } else { "cold" }, + started.elapsed().as_secs_f64() + ); + + if !warm { + let synced_at = std::time::SystemTime::now(); + let deadline = Instant::now() + Duration::from_secs(150); + for file in ["relay.db", "people.db"] { + let path = db_dir.join(file); + while std::fs::metadata(&path).and_then(|m| m.modified()).map_or(true, |at| at < synced_at) { + assert!(Instant::now() < deadline, "{file} was not saved after the sync"); + std::thread::sleep(Duration::from_millis(500)); + } + } + } +} diff --git a/bindings/pezkuwi-light/src/main/AndroidManifest.xml b/bindings/pezkuwi-light/src/main/AndroidManifest.xml new file mode 100644 index 00000000..cc947c56 --- /dev/null +++ b/bindings/pezkuwi-light/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/bindings/pezkuwi-light/src/main/java/io/novafoundation/nova/lightclient/HostResolver.kt b/bindings/pezkuwi-light/src/main/java/io/novafoundation/nova/lightclient/HostResolver.kt new file mode 100644 index 00000000..8c701e18 --- /dev/null +++ b/bindings/pezkuwi-light/src/main/java/io/novafoundation/nova/lightclient/HostResolver.kt @@ -0,0 +1,13 @@ +package io.novafoundation.nova.lightclient + +/** + * Resolves the host names of light client peers, so they follow the app's DNS policy. + * + * Called from native threads, never the main thread. Throw (for example + * [java.net.UnknownHostException]) when the name has no address. + */ +fun interface HostResolver { + + /** Addresses of [host] as IP literals, IPv4 or IPv6. */ + fun resolve(host: String): Array +} diff --git a/bindings/pezkuwi-light/src/main/java/io/novafoundation/nova/lightclient/PezkuwiLight.kt b/bindings/pezkuwi-light/src/main/java/io/novafoundation/nova/lightclient/PezkuwiLight.kt new file mode 100644 index 00000000..434799f5 --- /dev/null +++ b/bindings/pezkuwi-light/src/main/java/io/novafoundation/nova/lightclient/PezkuwiLight.kt @@ -0,0 +1,43 @@ +package io.novafoundation.nova.lightclient + +import java.io.File + +/** + * Embedded PezkuwiChain light client (smoldot fork `pezkuwichain/smoldot`). + * + * Every answer is verified against finalized block headers, so it does not depend on trusting + * an RPC node. The client is reached like any JSON-RPC node, over a WebSocket bound to + * 127.0.0.1 whose path carries a random per-process token: `chainUrl(start(dir), key)`. + */ +object PezkuwiLight { + + init { + System.loadLibrary("pezkuwi_light") + } + + /** + * Starts the light client once per process and returns its loopback base URL. Later calls + * return the same URL. [databaseDir] keeps each chain's finalized state between starts; + * [resolver] resolves the host names of peers. + */ + fun start(databaseDir: File, resolver: HostResolver): String = start(databaseDir.absolutePath, resolver) + + /** + * Registers [chainSpec] under [key] and starts syncing it. A teyrchain names the [key] its + * relay chain was registered under, which must come first. + * + * @throws IllegalStateException if the client is not started, the key is taken, or smoldot + * rejects the specification + */ + external fun addChain(key: String, chainSpec: String, relayChainKey: String?) + + /** + * Restricts new peer connections to secure WebSockets (`wss`, normally on port 443), which + * look like any HTTPS WebSocket on the wire. Open connections are kept. + */ + external fun setSecureOnly(secureOnly: Boolean) + + fun chainUrl(baseUrl: String, key: String): String = "$baseUrl/$key" + + private external fun start(databaseDir: String, resolver: HostResolver): String +} diff --git a/common/src/main/java/io/novafoundation/nova/common/di/CommonApi.kt b/common/src/main/java/io/novafoundation/nova/common/di/CommonApi.kt index d7b54e4a..bb58b1aa 100644 --- a/common/src/main/java/io/novafoundation/nova/common/di/CommonApi.kt +++ b/common/src/main/java/io/novafoundation/nova/common/di/CommonApi.kt @@ -15,6 +15,7 @@ import io.novafoundation.nova.common.data.network.AppLinksProvider import io.novafoundation.nova.common.data.network.HttpExceptionHandler import io.novafoundation.nova.common.data.network.NetworkApiCreator import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser +import io.novafoundation.nova.common.data.network.dns.EncryptedFirstDns import io.novafoundation.nova.common.data.network.rpc.SocketSingleRequestExecutor import io.novafoundation.nova.common.data.providers.deviceid.DeviceIdProvider import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository @@ -167,6 +168,8 @@ interface CommonApi { fun provideEncryptedPreferences(): EncryptedPreferences + fun encryptedFirstDns(): EncryptedFirstDns + fun provideIconGenerator(): IconGenerator fun provideClipboardManager(): ClipboardManager diff --git a/runtime/build.gradle b/runtime/build.gradle index ca53ccd1..5893b734 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -57,6 +57,7 @@ dependencies { implementation project(":bindings:metadata_shortener") implementation project(":bindings:sr25519-bizinikiwi") + implementation project(":bindings:pezkuwi-light") implementation gsonDep implementation substrateSdkDep diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/di/ChainRegistryModule.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/di/ChainRegistryModule.kt index 29ff4187..ceec116a 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/di/ChainRegistryModule.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/di/ChainRegistryModule.kt @@ -6,6 +6,7 @@ import dagger.Module import dagger.Provides import io.novafoundation.nova.common.BuildConfig import io.novafoundation.nova.common.data.network.NetworkApiCreator +import io.novafoundation.nova.common.data.network.dns.EncryptedFirstDns import io.novafoundation.nova.common.data.network.rpc.BulkRetriever import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.di.scope.ApplicationScope @@ -25,6 +26,7 @@ import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionPool import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionSecrets import io.novafoundation.nova.runtime.multiNetwork.connection.Web3ApiPool import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.NodeAutobalancer +import io.novafoundation.nova.runtime.multiNetwork.connection.light.LightClients import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.strategy.NodeSelectionStrategyProvider import io.novafoundation.nova.runtime.multiNetwork.connection.node.healthState.NodeHealthStateTesterFactory import io.novafoundation.nova.runtime.multiNetwork.runtime.AsyncChainSyncDispatcher @@ -179,16 +181,25 @@ class ChainRegistryModule { .build() ) + @Provides + @ApplicationScope + fun provideLightClients( + context: Context, + encryptedFirstDns: EncryptedFirstDns, + ) = LightClients(context, encryptedFirstDns) + @Provides @ApplicationScope fun provideChainConnectionFactory( socketProvider: Provider, externalRequirementsFlow: MutableStateFlow, nodeAutobalancer: NodeAutobalancer, + lightClients: LightClients, ) = ChainConnectionFactory( externalRequirementsFlow, nodeAutobalancer, socketProvider, + lightClients, ) @Provides diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeDependencies.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeDependencies.kt index f56a081e..732ea32e 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeDependencies.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/di/RuntimeDependencies.kt @@ -3,6 +3,7 @@ package io.novafoundation.nova.runtime.di import android.content.Context import com.google.gson.Gson import io.novafoundation.nova.common.data.network.NetworkApiCreator +import io.novafoundation.nova.common.data.network.dns.EncryptedFirstDns import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository import io.novafoundation.nova.common.data.storage.Preferences import io.novafoundation.nova.common.interfaces.FileProvider @@ -32,4 +33,6 @@ interface RuntimeDependencies { fun chainAssetDao(): ChainAssetDao fun assetsIconModeService(): AssetsIconModeRepository + + fun encryptedFirstDns(): EncryptedFirstDns } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/ChainConnection.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/ChainConnection.kt index 0e73a1c6..2696724a 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/ChainConnection.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/ChainConnection.kt @@ -5,6 +5,8 @@ import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.common.utils.share import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.connection.autobalance.NodeAutobalancer +import io.novafoundation.nova.runtime.multiNetwork.connection.light.LightClients +import io.novafoundation.nova.runtime.multiNetwork.connection.light.LightFallbackPolicy import io.novasama.substrate_sdk_android.wsrpc.SocketService import io.novasama.substrate_sdk_android.wsrpc.interceptor.WebSocketResponseInterceptor import io.novasama.substrate_sdk_android.wsrpc.interceptor.WebSocketResponseInterceptor.ResponseDelivery @@ -14,6 +16,7 @@ import io.novasama.substrate_sdk_android.wsrpc.state.SocketStateMachine.State import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -21,6 +24,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull @@ -28,12 +32,14 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transform import javax.inject.Provider class ChainConnectionFactory( private val externalRequirementFlow: Flow, private val nodeAutobalancer: NodeAutobalancer, private val socketServiceProvider: Provider, + private val lightClients: LightClients, ) { suspend fun create(chain: Chain): ChainConnection { @@ -41,7 +47,8 @@ class ChainConnectionFactory( socketService = socketServiceProvider.get(), externalRequirementFlow = externalRequirementFlow, nodeAutobalancer = nodeAutobalancer, - initialChain = chain + initialChain = chain, + lightClients = lightClients.takeIf { it.supports(chain.id) }, ) connection.setup() @@ -58,6 +65,11 @@ private const val BLUST_RATE_LIMIT_ERROR_CODE = -32097 private const val ON_FINALITY_RATE_LIMIT_ERROR_CODE = -32029 +/** How often a connection on the light client checks whether to retry RPC. */ +private const val LIGHT_FALLBACK_TICK_MILLIS = 15_000L + +private const val LIGHT_FALLBACK_TAG = "LightFallback" + private val RATE_LIMIT_ERROR_CODES = listOf( INFURA_ERROR_CODE, ALCHEMY_ERROR_CODE, @@ -71,6 +83,7 @@ class ChainConnection internal constructor( private val externalRequirementFlow: Flow, private val nodeAutobalancer: NodeAutobalancer, initialChain: Chain, + private val lightClients: LightClients? = null, ) : CoroutineScope by CoroutineScope(Dispatchers.Default), WebSocketResponseInterceptor { @@ -94,11 +107,14 @@ class ChainConnection internal constructor( .distinctUntilChanged() .share(SharingStarted.Eagerly) - val currentUrl = nodeAutobalancer.connectionUrlFlow( + private val rpcUrl = nodeAutobalancer.connectionUrlFlow( chainId = initialChain.id, changeConnectionEventFlow = nodeChangeSignal, availableNodesFlow = availableNodes, - ).share(SharingStarted.Eagerly) + ) + + val currentUrl = (lightClients?.let { rpcUrl.withLightFallback(it) } ?: rpcUrl) + .share(SharingStarted.Eagerly) suspend fun setup() { socketService.setInterceptor(this) @@ -138,15 +154,7 @@ class ChainConnection internal constructor( socketService.stop() } - private suspend fun actualUrl(): String? { - return when (val stateSnapshot = state.first()) { - is State.WaitingForReconnect -> stateSnapshot.url - is State.Connecting -> stateSnapshot.url - is State.Connected -> stateSnapshot.url - State.Disconnected -> null - is State.Paused -> stateSnapshot.url - } - } + private suspend fun actualUrl(): String? = state.first().urlOrNull() private fun Flow.nodeChangeEvents(): Flow { return mapNotNull { stateValue -> @@ -156,6 +164,86 @@ class ChainConnection internal constructor( private fun State.needsAutobalance() = this is State.WaitingForReconnect && attempt > 1 + private sealed interface FallbackEvent { + class Rpc(val node: NodeWithSaturatedUrl?) : FallbackEvent + class Socket(val state: State) : FallbackEvent + object Tick : FallbackEvent + } + + /** + * Moves the connection to the light client once every RPC node has failed in a row, and back + * to RPC when a retry connects (see [LightFallbackPolicy]). Events are handled one at a time, + * in the order they arrive. + */ + private fun Flow.withLightFallback(lightClients: LightClients): Flow { + Log.d(LIGHT_FALLBACK_TAG, "${chain.value.name}: light client fallback armed") + val policy = LightFallbackPolicy() + var latestRpc: NodeWithSaturatedUrl? = null + var light: NodeWithSaturatedUrl? = null + val ticks = flow { + while (true) { + delay(LIGHT_FALLBACK_TICK_MILLIS) + emit(FallbackEvent.Tick) + } + } + val rpcNodeCount = { chain.value.nodes.nodes.count { it.connectionType == Chain.Node.ConnectionType.WSS } } + + return merge(map(FallbackEvent::Rpc), state.map(FallbackEvent::Socket), ticks).transform { event -> + when (event) { + is FallbackEvent.Rpc -> { + latestRpc = event.node + if (policy.source == LightFallbackPolicy.Source.RPC) emit(event.node) + } + + is FallbackEvent.Socket -> { + val onLight = light != null && event.state.urlOrNull() == light?.saturatedUrl + if (event.state is State.Connected && !onLight) policy.onRpcConnected() + + if (event.state.needsAutobalance()) { + Log.d(LIGHT_FALLBACK_TAG, "${chain.value.name}: ${event.state} failed, source ${policy.source}, on light $onLight") + if (onLight) { + policy.onLightUnavailable(rpcNodeCount()) + emit(latestRpc) + } else if (policy.onRpcFailure(rpcNodeCount(), System.currentTimeMillis())) { + val url = lightClients.url(chain.value.id) + Log.d(LIGHT_FALLBACK_TAG, "${chain.value.name}: light client url $url") + if (url == null) { + policy.onLightUnavailable(rpcNodeCount()) + } else { + Log.d(LIGHT_FALLBACK_TAG, "Every RPC node of ${chain.value.name} failed, using the light client") + light = lightClientNode(url).also { emit(it) } + } + } + } + } + + FallbackEvent.Tick -> if (policy.onTick(rpcNodeCount(), System.currentTimeMillis())) { + Log.d(LIGHT_FALLBACK_TAG, "${chain.value.name}: retrying RPC") + emit(latestRpc) + } + } + } + } + + private fun lightClientNode(url: String) = NodeWithSaturatedUrl( + node = Chain.Node( + chainId = chain.value.id, + unformattedUrl = url, + name = "Light client", + orderId = Int.MAX_VALUE, + isCustom = false, + ), + saturatedUrl = url, + ) + + private fun State.urlOrNull(): String? = when (this) { + is State.WaitingForReconnect -> url + is State.Connecting -> url + is State.Connected -> url + State.Disconnected -> null + is State.Paused -> url + } + override fun onRpcResponseReceived(rpcResponse: RpcResponse): ResponseDelivery { val error = rpcResponse.error diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/light/LightClients.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/light/LightClients.kt new file mode 100644 index 00000000..262250bf --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/light/LightClients.kt @@ -0,0 +1,76 @@ +package io.novafoundation.nova.runtime.multiNetwork.connection.light + +import android.content.Context +import android.util.Log +import io.novafoundation.nova.common.utils.LOG_TAG +import io.novafoundation.nova.lightclient.HostResolver +import io.novafoundation.nova.lightclient.PezkuwiLight +import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import okhttp3.Dns +import java.io.File + +/** + * The embedded light client (`bindings/pezkuwi-light`) for the chains it can verify. + * + * Started on first use, so a wallet whose RPC nodes answer never runs it. Chain specifications + * come from the APK (`light-client/` assets, built from pezkuwi-wallet-utils): the genesis state + * root, boot nodes and, for a relay chain, a checkpoint. Peer host names resolve through [dns], + * the app's DNS-over-HTTPS-first resolver. + */ +class LightClients( + private val context: Context, + private val dns: Dns, +) { + + private class LightChain(val asset: String, val relay: ChainId?) + + private val chains = mapOf( + ZAGROS to LightChain("light-client/zagros.json", relay = null), + ZAGROS_PEOPLE to LightChain("light-client/people-zagros.json", relay = ZAGROS), + ) + + private val mutex = Mutex() + private var baseUrl: String? = null + private val registered = mutableSetOf() + + fun supports(chainId: ChainId): Boolean = chainId in chains + + /** Loopback JSON-RPC URL of [chainId], or null when the light client cannot serve it. */ + suspend fun url(chainId: ChainId): String? = mutex.withLock { + withContext(Dispatchers.IO) { + runCatching { + if (!supports(chainId)) return@runCatching null + val base = baseUrl ?: PezkuwiLight.start(File(context.filesDir, DATABASE_DIR), resolver()).also { baseUrl = it } + register(chainId) + PezkuwiLight.chainUrl(base, chainId) + }.onFailure { + Log.w(LOG_TAG, "Light client unavailable for $chainId", it) + }.getOrNull() + } + } + + private fun register(chainId: ChainId) { + if (chainId in registered) return + val chain = chains.getValue(chainId) + chain.relay?.let(::register) + + val spec = context.assets.open(chain.asset).bufferedReader().use { it.readText() } + PezkuwiLight.addChain(chainId, spec, chain.relay) + registered += chainId + } + + private fun resolver() = HostResolver { host -> + dns.lookup(host).mapNotNull { it.hostAddress }.toTypedArray() + } + + private companion object { + const val ZAGROS: ChainId = "1b0b4727bbb5e44ed587d1056b4ad537ffb330034509a58866a088d77e792cd5" + const val ZAGROS_PEOPLE: ChainId = "c72b38f36affaeb2661018b201c2be02e8c09fc699640525d39200a10fd77878" + + const val DATABASE_DIR = "light-client" + } +} diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/light/LightFallbackPolicy.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/light/LightFallbackPolicy.kt new file mode 100644 index 00000000..f146a2ae --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/light/LightFallbackPolicy.kt @@ -0,0 +1,69 @@ +package io.novafoundation.nova.runtime.multiNetwork.connection.light + +/** + * When a chain's connection moves from its RPC nodes to the embedded light client, and back. + * + * RPC comes first: a light client puts load on the peer-to-peer nodes that RPC does not, and + * those nodes have a fixed number of light client slots. Once every RPC node has failed in a row, + * the light client takes over. RPC is retried after [initialBackoffMillis], doubling up to + * [maxBackoffMillis] while it keeps failing, and takes back the connection once it connects. + * + * Not thread-safe: one connection feeds it its events in order. + */ +class LightFallbackPolicy( + private val initialBackoffMillis: Long = 60_000, + private val maxBackoffMillis: Long = 15 * 60_000, +) { + + enum class Source { RPC, LIGHT } + + var source = Source.RPC + private set + + private var consecutiveRpcFailures = 0 + private var backoffMillis = initialBackoffMillis + private var rpcRetryAt: Long? = null + + /** An RPC node failed. True when the light client should take over now. */ + fun onRpcFailure(rpcNodeCount: Int, nowMillis: Long): Boolean { + if (source != Source.RPC) return false + + consecutiveRpcFailures++ + if (consecutiveRpcFailures < rpcNodeCount.coerceAtLeast(1)) return false + + source = Source.LIGHT + rpcRetryAt = nowMillis + backoffMillis + backoffMillis = (backoffMillis * 2).coerceAtMost(maxBackoffMillis) + return true + } + + fun onRpcConnected() { + source = Source.RPC + consecutiveRpcFailures = 0 + backoffMillis = initialBackoffMillis + rpcRetryAt = null + } + + /** + * True when RPC should be tried again now. One more RPC failure then hands the connection + * straight back to the light client, without walking every node again. + */ + fun onTick(rpcNodeCount: Int, nowMillis: Long): Boolean { + val retryAt = rpcRetryAt ?: return false + if (source != Source.LIGHT || nowMillis < retryAt) return false + + returnToRpc(rpcNodeCount) + return true + } + + /** The light client could not start or its connection failed: RPC is all there is. */ + fun onLightUnavailable(rpcNodeCount: Int) { + if (source == Source.LIGHT) returnToRpc(rpcNodeCount) + } + + private fun returnToRpc(rpcNodeCount: Int) { + source = Source.RPC + consecutiveRpcFailures = (rpcNodeCount - 1).coerceAtLeast(0) + rpcRetryAt = null + } +} diff --git a/runtime/src/test/java/io/novafoundation/nova/runtime/multiNetwork/connection/light/LightFallbackPolicyTest.kt b/runtime/src/test/java/io/novafoundation/nova/runtime/multiNetwork/connection/light/LightFallbackPolicyTest.kt new file mode 100644 index 00000000..1444397b --- /dev/null +++ b/runtime/src/test/java/io/novafoundation/nova/runtime/multiNetwork/connection/light/LightFallbackPolicyTest.kt @@ -0,0 +1,94 @@ +package io.novafoundation.nova.runtime.multiNetwork.connection.light + +import io.novafoundation.nova.runtime.multiNetwork.connection.light.LightFallbackPolicy.Source +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class LightFallbackPolicyTest { + + private val minute = 60_000L + private val policy = LightFallbackPolicy(initialBackoffMillis = minute, maxBackoffMillis = 4 * minute) + + @Test + fun `light client takes over only after every RPC node failed in a row`() { + assertFalse(policy.onRpcFailure(rpcNodeCount = 3, nowMillis = 0)) + assertFalse(policy.onRpcFailure(rpcNodeCount = 3, nowMillis = 0)) + assertEquals(Source.RPC, policy.source) + + assertTrue(policy.onRpcFailure(rpcNodeCount = 3, nowMillis = 0)) + assertEquals(Source.LIGHT, policy.source) + } + + @Test + fun `a connected RPC node resets the count`() { + policy.onRpcFailure(rpcNodeCount = 2, nowMillis = 0) + policy.onRpcConnected() + + assertFalse(policy.onRpcFailure(rpcNodeCount = 2, nowMillis = 0)) + assertTrue(policy.onRpcFailure(rpcNodeCount = 2, nowMillis = 0)) + } + + @Test + fun `a single RPC node hands over on its first failure`() { + assertTrue(policy.onRpcFailure(rpcNodeCount = 1, nowMillis = 0)) + } + + @Test + fun `RPC is retried after the backoff and one more failure returns to the light client`() { + policy.onRpcFailure(rpcNodeCount = 3, nowMillis = 0) + policy.onRpcFailure(rpcNodeCount = 3, nowMillis = 0) + policy.onRpcFailure(rpcNodeCount = 3, nowMillis = 0) + + assertFalse(policy.onTick(rpcNodeCount = 3, nowMillis = minute - 1)) + assertTrue(policy.onTick(rpcNodeCount = 3, nowMillis = minute)) + assertEquals(Source.RPC, policy.source) + + assertTrue(policy.onRpcFailure(rpcNodeCount = 3, nowMillis = minute)) + assertEquals(Source.LIGHT, policy.source) + } + + @Test + fun `the backoff doubles while RPC keeps failing, up to the maximum`() { + var now = 0L + policy.onRpcFailure(rpcNodeCount = 1, nowMillis = now) + + for (expectedWait in listOf(minute, 2 * minute, 4 * minute, 4 * minute)) { + assertFalse(policy.onTick(rpcNodeCount = 1, nowMillis = now + expectedWait - 1)) + now += expectedWait + assertTrue(policy.onTick(rpcNodeCount = 1, nowMillis = now)) + assertTrue(policy.onRpcFailure(rpcNodeCount = 1, nowMillis = now)) + } + } + + @Test + fun `a connected RPC retry resets the backoff`() { + policy.onRpcFailure(rpcNodeCount = 1, nowMillis = 0) + policy.onTick(rpcNodeCount = 1, nowMillis = minute) + policy.onRpcFailure(rpcNodeCount = 1, nowMillis = minute) + policy.onTick(rpcNodeCount = 1, nowMillis = 3 * minute) + policy.onRpcConnected() + + assertTrue(policy.onRpcFailure(rpcNodeCount = 1, nowMillis = 10 * minute)) + assertFalse(policy.onTick(rpcNodeCount = 1, nowMillis = 11 * minute - 1)) + assertTrue(policy.onTick(rpcNodeCount = 1, nowMillis = 11 * minute)) + } + + @Test + fun `an unavailable light client leaves the connection on RPC`() { + policy.onRpcFailure(rpcNodeCount = 2, nowMillis = 0) + policy.onRpcFailure(rpcNodeCount = 2, nowMillis = 0) + policy.onLightUnavailable(rpcNodeCount = 2) + + assertEquals(Source.RPC, policy.source) + assertFalse(policy.onTick(rpcNodeCount = 2, nowMillis = 10 * minute)) + } + + @Test + fun `no failure is counted while on the light client`() { + policy.onRpcFailure(rpcNodeCount = 1, nowMillis = 0) + assertFalse(policy.onRpcFailure(rpcNodeCount = 1, nowMillis = 0)) + assertEquals(Source.LIGHT, policy.source) + } +} diff --git a/settings.gradle b/settings.gradle index 50dba306..4c81b7ed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -38,6 +38,7 @@ include ':feature-buy-api' include ':feature-buy-impl' include ':bindings:hydra-dx-math' include ':bindings:sr25519-bizinikiwi' +include ':bindings:pezkuwi-light' include ':feature-proxy-impl' include ':feature-proxy-api' include ':feature-push-notifications'