diff --git a/Cargo.lock b/Cargo.lock index 435d4fe2..f8cf7570 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "addr2line" @@ -81,9 +81,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -96,9 +96,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -242,13 +242,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -286,9 +286,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-fips-sys" -version = "0.13.16" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b00953a69b2cfb471d13d72538d2e66930832340b0f31deadd404b48c573c5" +checksum = "118303cd75f63d1933a90c2ceb7e697281ac6acbdbcc490b46419f25a527ab90" dependencies = [ "bindgen", "cc", @@ -301,9 +301,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.17.3" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-fips-sys", "aws-lc-sys", @@ -313,9 +313,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ "bindgen", "cc", @@ -516,7 +516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ "bitcoin_hashes", - "rand 0.8.7", + "rand 0.8.8", "rand_core 0.6.4", "serde", "unicode-normalization", @@ -597,9 +597,9 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +checksum = "c20659f9bbee16cbbd2f7393e40ab6309f5a98f76a2eb57a995ec508b72387fe" dependencies = [ "cc", "glob", @@ -630,9 +630,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.3.0" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -680,12 +680,12 @@ dependencies = [ [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if 1.0.4", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "rand_core 0.10.1", ] @@ -932,7 +932,7 @@ dependencies = [ "log", "openssl", "openssl-sys", - "rand 0.8.7", + "rand 0.8.8", "reqwest", "serde", "serde_json", @@ -1106,7 +1106,7 @@ dependencies = [ "hex-literal", "indexmap", "num-bigint", - "rand 0.8.7", + "rand 0.8.8", "rand_chacha 0.3.1", "thiserror 2.0.20", ] @@ -1207,7 +1207,7 @@ dependencies = [ "hex", "indexmap", "prettytable-rs", - "rand 0.8.7", + "rand 0.8.8", "rand_chacha 0.3.1", "serde", "serde_json", @@ -1337,7 +1337,7 @@ dependencies = [ "bech32", "chia-protocol 0.26.0", "indexmap", - "rand 0.8.7", + "rand 0.8.8", "rand_chacha 0.3.1", "thiserror 2.0.20", ] @@ -1476,7 +1476,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51af8606c8376d730d6607cb69fe5edd87f5d0d106e5620065f58a31e7f348a4" dependencies = [ - "rand 0.8.7", + "rand 0.8.8", "rcgen", "rsa 0.9.10", "thiserror 1.0.69", @@ -1715,9 +1715,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" dependencies = [ "glob", "libc", @@ -1726,9 +1726,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.2" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -1736,9 +1736,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -1748,14 +1748,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -1898,7 +1898,7 @@ dependencies = [ "num-integer", "num-traits", "p256", - "rand 0.8.7", + "rand 0.8.8", "sha1", "sha3", ] @@ -1921,7 +1921,7 @@ dependencies = [ "num-integer", "num-traits", "p256", - "rand 0.8.7", + "rand 0.8.8", "sha1", "sha3", "thiserror 1.0.69", @@ -1966,15 +1966,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[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" @@ -2069,9 +2060,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -2239,9 +2230,9 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if 1.0.4", ] @@ -2426,9 +2417,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "datalayer-driver" @@ -2453,7 +2444,7 @@ dependencies = [ "num-bigint", "openssl", "openssl-sys", - "rand 0.8.7", + "rand 0.8.8", "thiserror 1.0.69", "tokio", ] @@ -2678,7 +2669,7 @@ dependencies = [ "async-trait", "dig-ip", "dig-nat", - "rand 0.8.7", + "rand 0.8.8", "serde", "serde_json", "sha2 0.10.9", @@ -2754,7 +2745,7 @@ dependencies = [ "futures-util", "hex", "lru", - "rand 0.8.7", + "rand 0.8.8", "ring", "rustls", "rustls-pemfile", @@ -3016,9 +3007,9 @@ dependencies = [ [[package]] name = "dig-node-control-interface" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc89557fb776432bd92b99831adfca70d9a39b7da53a26c04f41abef787b0d7" +checksum = "7668b2b45f3fa813efddf24cbf872d05851ee5203248a9d18ff8755bc1e3b9b8" dependencies = [ "async-trait", "semver", @@ -3090,7 +3081,7 @@ dependencies = [ [[package]] name = "dig-node-service" -version = "0.170.1" +version = "0.171.0" dependencies = [ "async-trait", "axum", @@ -3221,7 +3212,7 @@ dependencies = [ "dig-dht", "dig-nat", "dig-peer", - "rand 0.8.7", + "rand 0.8.8", "rand_pcg 0.3.1", "serde", ] @@ -3232,7 +3223,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7058e5d32c8f335e6d3494fbceb261328367df76bb2913509beaeab12386e815" dependencies = [ - "rand 0.8.7", + "rand 0.8.8", "serde", "serde_json", "tokio", @@ -3618,7 +3609,7 @@ dependencies = [ "futures", "hex", "object_store", - "rand 0.8.7", + "rand 0.8.8", "rand_chacha 0.3.1", "reqwest", "thiserror 1.0.69", @@ -3785,13 +3776,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -3828,9 +3819,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" dependencies = [ "serde", ] @@ -3922,11 +3913,10 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -3971,19 +3961,20 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "libz-sys", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -4073,9 +4064,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -4088,9 +4079,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -4098,15 +4089,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -4126,32 +4117,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-timer" @@ -4161,9 +4152,9 @@ checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -4250,9 +4241,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "group" @@ -4267,9 +4258,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" dependencies = [ "atomic-waker", "bytes", @@ -4414,9 +4405,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -4434,9 +4425,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -4474,9 +4465,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -4578,9 +4569,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -4592,9 +4583,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -4605,9 +4596,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -4619,16 +4610,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -4639,15 +4631,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -4732,9 +4724,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -4762,9 +4754,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is-terminal" @@ -4819,9 +4811,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if 1.0.4", "futures-util", @@ -4874,9 +4866,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libloading" @@ -4896,9 +4888,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" dependencies = [ "libc", ] @@ -4947,9 +4939,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -4962,9 +4954,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru" @@ -5050,9 +5042,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -5147,7 +5139,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.7", + "rand 0.8.8", "smallvec", "zeroize", ] @@ -5180,9 +5172,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -5258,7 +5250,7 @@ dependencies = [ "parking_lot", "percent-encoding", "quick-xml 0.37.5", - "rand 0.8.7", + "rand 0.8.8", "reqwest", "ring", "serde", @@ -5517,9 +5509,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plist" @@ -5571,9 +5563,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -5728,9 +5720,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.16" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "bytes", "getrandom 0.4.3", @@ -5791,9 +5783,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -5817,7 +5809,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20 0.10.1", + "chacha20 0.10.2", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -5953,9 +5945,9 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" dependencies = [ "allocator-api2", "bumpalo", @@ -5980,9 +5972,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -6166,12 +6158,12 @@ dependencies = [ [[package]] name = "rtoolbox" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6514,16 +6506,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.13", + "rustls-webpki 0.103.15", "subtle", "zeroize", ] @@ -6551,9 +6543,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -6572,9 +6564,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -6701,7 +6693,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -6737,7 +6729,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -6835,7 +6827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if 1.0.4", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "digest 0.11.3", ] @@ -7149,9 +7141,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.2" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -7287,7 +7279,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -7310,9 +7302,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "num-conv", @@ -7330,9 +7322,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -7340,9 +7332,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -7365,9 +7357,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -7382,13 +7374,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -7413,9 +7405,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -7443,15 +7435,16 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "futures-util", + "libc", "pin-project-lite", "tokio", ] @@ -7739,7 +7732,7 @@ dependencies = [ "httparse", "log", "native-tls", - "rand 0.8.7", + "rand 0.8.8", "rustls", "rustls-pki-types", "sha1", @@ -7864,9 +7857,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -7933,9 +7926,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -7948,9 +7941,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -7958,9 +7951,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7968,9 +7961,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -7981,9 +7974,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] @@ -8010,12 +8003,12 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.253.0" +version = "0.258.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59972d6cd272259de647b7c1f1912e45e289c75ffd4be04e10695507cd7e1b59" +checksum = "e974fe6821a8cf64575d51ea2194e2c8f77e7b66e9afe7419ce8a97f9ee0d251" dependencies = [ "leb128fmt", - "wasmparser 0.253.0", + "wasmparser 0.258.0", ] [[package]] @@ -8059,9 +8052,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.253.0" +version = "0.258.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19db11f87d2486580e1e8b6f494c54df7e0566b87d0b599db843c24019667339" +checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" dependencies = [ "bitflags", "indexmap", @@ -8256,31 +8249,31 @@ dependencies = [ [[package]] name = "wast" -version = "253.0.0" +version = "258.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3264542f8965c5d84fb1085d924bfba9a6314bb228eff13a2de14d7627664d0" +checksum = "97f7defc7ecca8b19ac7f824598eadd0c53985ee00c74060d65051e9da5b58a1" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width 0.2.2", - "wasm-encoder 0.253.0", + "wasm-encoder 0.258.0", ] [[package]] name = "wat" -version = "1.253.0" +version = "1.258.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfc5ce906144200c972ec617470aa35bd847472e170b26dde3e80541c674055" +checksum = "7555c008cca87f2ac58d9f83ccda7e7b44611093ce28eb28f052e7c78024b9bf" dependencies = [ "wast", ] [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -8687,9 +8680,9 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "wyz" @@ -8720,9 +8713,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.28" +version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" [[package]] name = "xmltree" @@ -8805,18 +8798,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -8866,9 +8859,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -8877,9 +8870,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -8888,13 +8881,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -8915,6 +8908,12 @@ dependencies = [ "zopfli", ] +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index f1a95eff..b5ce4228 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,90 +1,90 @@ -[workspace] -resolver = "2" -# The canonical dig-node repo is a small workspace of the node ENGINE + its two -# DIG-Browser host shells: -# * dig-node-core — the NODE engine library (crate `dig_node_core`): RPC dispatch, -# serve/fetch/redirect, chain-watch, subscriptions, gap-fill, -# cache, P2P. The single node implementation shared by BOTH host -# shells below. (Renamed from `dig-node` so the engine library and -# the produced `dig-node` binary no longer share a name, #216.) -# * dig-node-service — the OS-service binary (`dig-node`): axum transport + control -# plane + CLI + service install. Depends on the engine library. -# * dig-runtime — the DIG Browser's in-process node: a cdylib (`dig_runtime.dll`) -# exposing the `dig_rpc`/`dig_wallet_rpc` C-ABI the browser links. -# * dig-wallet — the DIG Browser's built-in Chia wallet host (loopback UI + BLS -# signing), brought up by dig-runtime beside the node. -# For the `.dig` STORE FORMAT the node depends on digstore's store-format LIBRARY crates -# (digstore-core/-crypto/-chain/-host/-remote/-stage) as GIT dependencies — dig-node-core -# -> store-lib, never the reverse. digstore is only ever an RPC client of a node. -members = [ - "crates/dig-node-core", - "crates/dig-chat-protocol", - "crates/dig-node-service", - "crates/dig-runtime", - "crates/dig-wallet", -] - -[workspace.package] -edition = "2021" -# The RELEASE version of the repo's shipped artifact — the `dig-node` binary -# (`dig-node-service`, which inherits this via `version.workspace = true`). This is -# the version the nightly-release.yml stable channel + version-increment CI reads from -# the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a -# release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet) -# keep their own independent versions — only the released binary tracks the workspace version. -version = "0.170.1" - -# Release hardening, matching digstore: keep integer-overflow checks ON in release. -# The node parses untrusted serialized input and does offset/length arithmetic over -# it, so silent wrapping in release would turn a length bug into a memory/logic hazard. -[profile.release] -overflow-checks = true - -# -- Retired: the dig-gossip vendored-fork patches (L7 peer network) ---------------------------------- -# -# This workspace used to re-declare `[patch.crates-io]` entries redirecting `chia-protocol` and -# `chia-sdk-client` to the ADDITIVE forks dig-gossip vendored, because a git dependency's own patches -# do not apply transitively (cargo honours patches only from the ROOT manifest being built). -# -# dig-gossip 0.23.0 DELETED both forks (dig_ecosystem#2228). The DIG introducer opcodes -# (`RegisterPeer=218` / `RegisterAck=219`), `send_protocol_message` and the DIG rate-limit rows now -# live in the crates.io crate `dig-peer-protocol` 0.6, a superset of plain upstream `chia-protocol`, -# which dig-gossip depends on directly. There is nothing left at that rev to patch to, so the patch -# section — and the #1529 three-rev lockstep it forced — is retired rather than re-pointed. - -# The dig-nat unification patch is RETIRED (#1280 crates.io cascade): the ENTIRE peer stack takes -# dig-nat from crates.io, so cargo resolves ONE dig-nat instance without any git redirect. -# dig-constants is likewise a plain crates.io dep everywhere now. -# -# The RESOLVED line today is dig-nat **0.18.0** (see `crates/dig-node-core/Cargo.toml`, which owns the -# declaration and the reasoning). Do not read a version out of this comment: an earlier revision of it -# asserted "dig-nat 0.7 ... resolves ONE dig-nat 0.7 instance", which was true of the #1280 cascade and -# was eleven minors stale by the time anyone read it. `tests/dependency_tree.rs` asserts the -# single-instance invariant against the resolved LOCK, which is the only claim that cannot go stale. -# -# THE PEER STACK IS NOW ON THE 0.36.1 LINE (dig_ecosystem#3161, 2026-08-26). -# -# The wall this comment used to describe was chia-bls: every published stack member required -# `dig-nat ^0.20` -> `dig-tls ^0.4`, which is the chia-bls 0.36.1 uplift, while dig-gossip was pinned -# at a rev reaching chia-protocol 0.26 through `dig-peer-protocol 0.6.0`. Adopting any of them split -# chia-bls across two lines inside one crate. -# -# The predicted unblock is exactly what happened, and it happened UPSTREAM: dig-gossip `main` -# (v0.30.0) moved to `dig-peer-protocol 0.7` and chia 0.36.1. Repointing the git rev in -# `crates/dig-node-core/Cargo.toml` is what let the stack move; nothing was shimmed, and nothing in -# this file needed to change to allow it. -# -# The resolved stack is dig-nat 0.20, dig-dht 0.12.1, dig-download 0.20.4, dig-peer 0.12.0, -# dig-peer-selector 0.9.2 -- the ^0.20 TIER, deliberately not the newest. -# -# WHY THE OLDER TIER IS THE CORRECT CHOICE, and the distinction a future lane will get wrong: the -# step from dig-nat 0.20 to 0.21 carries ZERO chia delta (both take `dig-tls ^0.4`). It is still not -# takeable, for a reason one level up -- dig-gossip v0.30.0 declares `dig-nat ^0.20`, while -# dig-dht 0.13 / dig-peer 0.13 / dig-download 0.21 / dig-peer-selector 0.10 all require `^0.21`. -# Taking the newer tier resolves TWO dig-nat lines while cargo prints success, MEASURED here, and -# dig-nat values cross from this crate into dig-download/dig-gossip/dig-peer-selector. One line per -# family beats the highest version numbers (CLAUDE.md §2.4b). -# -# So "0.21 is chia-blocked" is FALSE and "dig-node cannot reach 0.21" is still TRUE. The remaining -# unblock is a dig-gossip release against `dig-nat ^0.21`, upstream, never an edit in this file. Do -# not bridge two lines with a shim -- that is the §4.1 byte-drift class. +[workspace] +resolver = "2" +# The canonical dig-node repo is a small workspace of the node ENGINE + its two +# DIG-Browser host shells: +# * dig-node-core — the NODE engine library (crate `dig_node_core`): RPC dispatch, +# serve/fetch/redirect, chain-watch, subscriptions, gap-fill, +# cache, P2P. The single node implementation shared by BOTH host +# shells below. (Renamed from `dig-node` so the engine library and +# the produced `dig-node` binary no longer share a name, #216.) +# * dig-node-service — the OS-service binary (`dig-node`): axum transport + control +# plane + CLI + service install. Depends on the engine library. +# * dig-runtime — the DIG Browser's in-process node: a cdylib (`dig_runtime.dll`) +# exposing the `dig_rpc`/`dig_wallet_rpc` C-ABI the browser links. +# * dig-wallet — the DIG Browser's built-in Chia wallet host (loopback UI + BLS +# signing), brought up by dig-runtime beside the node. +# For the `.dig` STORE FORMAT the node depends on digstore's store-format LIBRARY crates +# (digstore-core/-crypto/-chain/-host/-remote/-stage) as GIT dependencies — dig-node-core +# -> store-lib, never the reverse. digstore is only ever an RPC client of a node. +members = [ + "crates/dig-node-core", + "crates/dig-chat-protocol", + "crates/dig-node-service", + "crates/dig-runtime", + "crates/dig-wallet", +] + +[workspace.package] +edition = "2021" +# The RELEASE version of the repo's shipped artifact — the `dig-node` binary +# (`dig-node-service`, which inherits this via `version.workspace = true`). This is +# the version the nightly-release.yml stable channel + version-increment CI reads from +# the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a +# release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet) +# keep their own independent versions — only the released binary tracks the workspace version. +version = "0.171.0" + +# Release hardening, matching digstore: keep integer-overflow checks ON in release. +# The node parses untrusted serialized input and does offset/length arithmetic over +# it, so silent wrapping in release would turn a length bug into a memory/logic hazard. +[profile.release] +overflow-checks = true + +# -- Retired: the dig-gossip vendored-fork patches (L7 peer network) ---------------------------------- +# +# This workspace used to re-declare `[patch.crates-io]` entries redirecting `chia-protocol` and +# `chia-sdk-client` to the ADDITIVE forks dig-gossip vendored, because a git dependency's own patches +# do not apply transitively (cargo honours patches only from the ROOT manifest being built). +# +# dig-gossip 0.23.0 DELETED both forks (dig_ecosystem#2228). The DIG introducer opcodes +# (`RegisterPeer=218` / `RegisterAck=219`), `send_protocol_message` and the DIG rate-limit rows now +# live in the crates.io crate `dig-peer-protocol` 0.6, a superset of plain upstream `chia-protocol`, +# which dig-gossip depends on directly. There is nothing left at that rev to patch to, so the patch +# section — and the #1529 three-rev lockstep it forced — is retired rather than re-pointed. + +# The dig-nat unification patch is RETIRED (#1280 crates.io cascade): the ENTIRE peer stack takes +# dig-nat from crates.io, so cargo resolves ONE dig-nat instance without any git redirect. +# dig-constants is likewise a plain crates.io dep everywhere now. +# +# The RESOLVED line today is dig-nat **0.18.0** (see `crates/dig-node-core/Cargo.toml`, which owns the +# declaration and the reasoning). Do not read a version out of this comment: an earlier revision of it +# asserted "dig-nat 0.7 ... resolves ONE dig-nat 0.7 instance", which was true of the #1280 cascade and +# was eleven minors stale by the time anyone read it. `tests/dependency_tree.rs` asserts the +# single-instance invariant against the resolved LOCK, which is the only claim that cannot go stale. +# +# THE PEER STACK IS NOW ON THE 0.36.1 LINE (dig_ecosystem#3161, 2026-08-26). +# +# The wall this comment used to describe was chia-bls: every published stack member required +# `dig-nat ^0.20` -> `dig-tls ^0.4`, which is the chia-bls 0.36.1 uplift, while dig-gossip was pinned +# at a rev reaching chia-protocol 0.26 through `dig-peer-protocol 0.6.0`. Adopting any of them split +# chia-bls across two lines inside one crate. +# +# The predicted unblock is exactly what happened, and it happened UPSTREAM: dig-gossip `main` +# (v0.30.0) moved to `dig-peer-protocol 0.7` and chia 0.36.1. Repointing the git rev in +# `crates/dig-node-core/Cargo.toml` is what let the stack move; nothing was shimmed, and nothing in +# this file needed to change to allow it. +# +# The resolved stack is dig-nat 0.20, dig-dht 0.12.1, dig-download 0.20.4, dig-peer 0.12.0, +# dig-peer-selector 0.9.2 -- the ^0.20 TIER, deliberately not the newest. +# +# WHY THE OLDER TIER IS THE CORRECT CHOICE, and the distinction a future lane will get wrong: the +# step from dig-nat 0.20 to 0.21 carries ZERO chia delta (both take `dig-tls ^0.4`). It is still not +# takeable, for a reason one level up -- dig-gossip v0.30.0 declares `dig-nat ^0.20`, while +# dig-dht 0.13 / dig-peer 0.13 / dig-download 0.21 / dig-peer-selector 0.10 all require `^0.21`. +# Taking the newer tier resolves TWO dig-nat lines while cargo prints success, MEASURED here, and +# dig-nat values cross from this crate into dig-download/dig-gossip/dig-peer-selector. One line per +# family beats the highest version numbers (CLAUDE.md §2.4b). +# +# So "0.21 is chia-blocked" is FALSE and "dig-node cannot reach 0.21" is still TRUE. The remaining +# unblock is a dig-gossip release against `dig-nat ^0.21`, upstream, never an edit in this file. Do +# not bridge two lines with a shim -- that is the §4.1 byte-drift class. diff --git a/SPEC.md b/SPEC.md index 63043426..0163cdbc 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1606,7 +1606,7 @@ lowercase 64-hex; a capsule reference is `storeId:rootHash`. Malformed refs yiel | `control.sync.status` | — | `available` (always `true` — the chunked capsule download needs no identity), `method: "chunked-capsule-download-with-section-21-clone-fallback"`, `identity_loaded`, `pinned_total`, `pinned_synced`, `whole_store_trigger_supported` (`true` — a store id alone is enough) | | `control.sync.trigger` | `store` = `storeId[:rootHash]`, or `store_id` [+ `root`] — the root is OPTIONAL; without one the node resolves the store's CHAIN-ANCHORED tip and syncs that generation | `status: "synced"`, `root`, `size_bytes`, `served_root` | | `control.wallet.balance` | `address` (bech32m string), `asset` (`"xch"` \| `"dig"` \| `{"cat":"<64-hex asset id>"}`, default `"xch"`) | `balance` (confirmed, spendable — JSON NUMBER, u64 base units), `pending` (unspent + unconfirmed — JSON NUMBER, u64 base units), `source` (`"db"` \| `"fallback"` — which tier produced the figure, §18.7b), `synced` (bool), `peak_height` (`u32` or `null`). Matches `dig-node-control-interface` 0.3.0's `WalletBalanceResult { balance: u64, pending: u64, .. }` and dig-app's `BalanceResponse { balance: u64 }` — a Rust-to-Rust numeric contract, never a decimal string. The wallet backend tracks the base-unit total as `u128` (headroom for summed intermediate math); the wire boundary saturating-casts to `u64` (a single address's balance can never exceed `u64::MAX` mojos, ~18.4M XCH). READ-ONLY chain read of a PUBLIC address (no seed/signing key). Reuses the B.6 sync-state routing: the local DB when the address is the wallet's own and the DB is synced, else the coinset fallback. Per §18.7b, `source`/`synced`/`peak_height` describe the TIER that answered: a `"db"` answer reports the node's own peak and reports `synced: true` only while the replica is FOLLOWING the chain, so a behind-but-once-synced replica answers `synced: false` WITH its real `peak_height` rather than presenting a stale figure as current; a tier with NO observable peer height also answers `synced: false`, because nothing corroborated the figure, and so does a replica with NO peak of its OWN — `synced: true` beside `peak_height: null` would claim a reading is current while refusing to say what it is a reading of (§18.7b); a `"fallback"` answer reports `synced: false` and `peak_height: null`. This is an OPEN read (`is_open_control_read`, no token); the cheap local-DB fast path is unbounded, but the EXPENSIVE coinset-fallback leg is subject to a GLOBAL token-bucket rate bound (defense-in-depth against an open-read amplification/oracle sweep — #1957): a burst of arbitrary-address fallback reads beyond the bound is refused with `WALLET_RATE_LIMITED` (§10), while any single honest read (DB fast path or one fallback) always succeeds. A CAT scopes by the asset id the REQUEST named -- any CAT, not only `$DIG` -- and BOTH tiers MUST scope to that id. `"dig"` is the canonical id `digstore_chain::dig::DIG_ASSET_ID` spelled as a token, and `{"cat":""}` MUST mean the same asset. Every scoping hash a tier derives MUST be derived FROM the requested id: a filter keyed to a fixed asset answers every other CAT an EMPTY list, which is indistinguishable from holding none of it -- a silent wrong answer with nothing to observe. An `asset` that is PRESENT and does not parse is `INVALID_PARAMS`; it MUST NOT default to `"xch"`, because a mistyped asset id would then read as a balance for the wrong token. An OMITTED `asset` is the documented `"xch"` default. A hint is not an asset: the fallback tier finds CAT coins with `get_coin_records_by_hints`, which takes no asset id and answers with EVERY coin hinted to the address -- any CAT of any TAIL, and any plain XCH coin whose spend carried a hint memo -- so a `"fallback"` answer MUST keep only the coins sitting at that asset's CAT puzzle hash (`digstore_chain::cat::cat_puzzle_hash(owner_p2_hash, asset_id)`, the canonical curry), the exact equivalent of the DB tier's `hint IN (...) AND asset_id = ?`. Summing the raw hint answer reports a holding the address does not have, at the asked-for asset's scale rather than each coin's own: one hinted XCH coin of 10^8 mojos (`0.0001 XCH`) totals as `100000` at `$DIG`'s 3 decimals. Over-filtering is the same lie mirrored -- a real `$DIG` holder answered zero -- so the filter MUST key on that puzzle hash and nothing heuristic. A synced empty address is a SUCCESS `{balance:0, synced:true}`, never an error (and `synced` there means MEASURED-current, never merely eligible); the read-failure shapes are DISTINCT errors `WALLET_NO_CHAIN_SOURCE`/`WALLET_NOT_SYNCED`/`WALLET_READ_FAILED`/`WALLET_RATE_LIMITED` (§10), never a fabricated `0`. `INVALID_PARAMS` on a missing/malformed `address` or a bad `asset`. | -| `control.wallet.coins` | `address` (bech32m string), `asset` (`"xch"` \| `"dig"` \| `{"cat":"<64-hex asset id>"}`, default `"xch"`) | `coins` (array of `{coin_id, asset, amount, parent_coin_info, puzzle_hash, created_height, spent_height}`; all hashes lowercase 64-hex unprefixed, `amount` a JSON NUMBER in base units), `source`, `synced`, `peak_height` — the tier fields carrying exactly their `control.wallet.balance` meanings (§18.7b). The UNSPENT coins at the address for the asset, i.e. the read a caller building a spend needs; a balance is this read reduced to a sum, which is why the two take identical params. It scopes to the asset by the SAME tier-agnostic rule, for the sharper reason: a coin list is spend INPUTS, so a hinted XCH or foreign-CAT coin served as a `$DIG` coin is a spend built on inputs of the wrong asset. Coins seen only in the mempool are INCLUDED with `created_height: null`, so the caller decides what is spendable for its purpose rather than the node hiding one. `coins: []` MUST mean a chain WAS consulted and the address holds nothing; every way of failing to consult one is a DISTINCT error (`WALLET_NO_CHAIN_SOURCE`/`WALLET_NOT_SYNCED`/`WALLET_READ_FAILED`/`WALLET_RATE_LIMITED`, §10), NEVER an empty list — an empty list would tell a holder of funds that they hold none, and a spend built on it refuses with an untrue shortfall. OPEN read, same global fallback rate bound as the balance. `INVALID_PARAMS` on a missing/malformed `address` or a bad `asset`. | +| `control.wallet.coins` | `address` (bech32m string), `asset` (`"xch"` \| `"dig"` \| `{"cat":"<64-hex asset id>"}`, default `"xch"`), `after_coin_id` (OPTIONAL, 64 lowercase-hex, an `0x` prefix TOLERATED and normalized away), `limit` (OPTIONAL, `1..=1000`, default `100`) | `coins` (array of `{coin_id, asset, amount, parent_coin_info, puzzle_hash, created_height, spent_height}`; all hashes lowercase 64-hex unprefixed, `amount` a JSON NUMBER in base units), `complete` (bool), `cursor` (string \| `null`), `source`, `synced`, `peak_height` — the tier fields carrying exactly their `control.wallet.balance` meanings (§18.7b). ONE PAGE of the UNSPENT coins at the address for the asset, i.e. the read a caller building a spend needs; a balance is this read reduced to a sum, which is why the two take identical params. It scopes to the asset by the SAME tier-agnostic rule, for the sharper reason: a coin list is spend INPUTS, so a hinted XCH or foreign-CAT coin served as a `$DIG` coin is a spend built on inputs of the wrong asset. Coins seen only in the mempool are INCLUDED with `created_height: null`, so the caller decides what is spendable for its purpose rather than the node hiding one. `coins: []` MUST mean a chain WAS consulted and the address holds nothing; every way of failing to consult one is a DISTINCT error (`WALLET_NO_CHAIN_SOURCE`/`WALLET_NOT_SYNCED`/`WALLET_READ_FAILED`/`WALLET_RATE_LIMITED`, §10), NEVER an empty list — an empty list would tell a holder of funds that they hold none, and a spend built on it refuses with an untrue shortfall. The read is PAGED, because an address's unspent-coin count is unbounded and every spend's change coin adds one — the same exposure `control.wallet.coinsByParent` carries, on a control plane with no request rate limiting. A node MUST return coins ASCENDING by `coin_id`, MUST keep that order stable across the pages of one walk, and MUST NOT page by OFFSET: an address's unspent set SHRINKS as coins are spent, so under an offset every row after a departed coin moves one position earlier and the next page begins one row late — a coin the caller never sees, on the read whose purpose is coin selection. A node MUST derive `complete` from whether rows remain BEYOND the page, never from the page LENGTH: a coin count that is an exact multiple of the page size makes the final full page indistinguishable from a truncated one, and a caller stopping there builds a spend from half an address's coins and refuses with an untrue shortfall. The scope, asset, unspent predicate and page bound MUST be applied at the SAME level: paginating a broader read and filtering afterwards cuts the page before the filter, so pages arrive short and `complete` is computed from a count that no longer describes what remains. `cursor` is the `coin_id` of the LAST record actually returned, or `null` for an empty page, and is what a caller passes back as `after_coin_id`. An out-of-range `limit` is REFUSED as `INVALID_PARAMS`, never clamped — a silently shrunk page hands back a cursor for a position the caller did not ask about. Both page params are OPTIONAL and a request naming neither is byte-identical to the pre-paging request. OPEN read, same global fallback rate bound as the balance. `INVALID_PARAMS` on a missing/malformed `address`, a bad `asset`, a malformed `after_coin_id`, or a `limit` outside `1..=1000`. | | `control.wallet.coinById` | `coin_id` (64 lowercase-hex, an optional `0x` prefix TOLERATED and normalized away) | `coin` (`{coin_id, asset, amount, parent_coin_info, puzzle_hash, created_height, spent_height}` or `null`), `source`, `synced`, `peak_height` — the tier fields carrying exactly their `control.wallet.balance` meanings (§18.7b); the tier fields MUST describe WHAT ANSWERED THIS READ. Where the local replica HOLDS the named coin and is authoritative for the set it follows (the same `control.wallet.balance` eligibility test, §18.7b), the node MUST answer from the replica: `source: "db"`, `peak_height` the replica's own peak, and `synced` MEASURED against the peers' announced peak rather than assumed — a replica that completed a catch-up and then fell behind still serves the coin, with its real peak, labelled stale. A replica MISS MUST fall through to the chain tier and be reported as such (`source: "fallback"`, `synced: false`, `peak_height: null`); it MUST NEVER be served as an absence, because the replica is populated only from this node's own subscriptions, so a miss means "this node does not watch that coin", which is NOT absence. A node MUST NOT report `source: "fallback"`, `synced: false` for a coin it holds: a warrant no read can ever carry turns every consumer-side freshness guard into an unconditional refusal, which ends a mint watch in "the chain could not be reached" on a healthy node. ONE coin by its own id, SPENT OR UNSPENT — the read a caller polling a spend needs and `control.wallet.coins` structurally cannot give: a created DID coin sits at nobody's wallet address, and a spent funding coin is gone from every unspent list. `asset` in the record is ALWAYS `null`: a coin id alone does not reveal whether a coin is XCH, a CAT or a singleton — that needs the puzzle, which this read never inspects — so naming one would assert a classification the node did not verify. A returned record MUST be bound to the id asked for: a coin id is self-certifying (`SHA256(parent ‖ puzzle_hash ‖ amount)`), so a source that answers with a DIFFERENT coin is a `WALLET_READ_FAILED` (§10) — never that coin's record, and never `coin: null`. `coin: null` MUST mean a chain source ANSWERED and reported no such coin; every way of failing to get an answer is a DISTINCT error (`WALLET_NO_CHAIN_SOURCE`/`WALLET_READ_FAILED`/`WALLET_RATE_LIMITED`, §10), NEVER a `null` — a `null` for an outage would tell a caller polling a mint that its coin does not exist, so a pending mint reads as awaiting forever. A caller MUST treat `null` as "not seen yet" and keep polling, not as "never happened". OPEN read (no token), same global fallback rate bound as the balance. `INVALID_PARAMS` on a missing/malformed `coin_id`, refused BEFORE any network call — an unanswerable question and a chain that answered "no" must never wear the same shape; the well-formedness rule is `dig-node-control-interface`'s own `WalletCoinByIdParams::validated()`, consumed rather than restated. | | `control.wallet.coinSpend` | `coin_id` (64 lowercase-hex, an optional `0x` prefix TOLERATED and normalized away) | `spend` (`{coin, puzzle_reveal, solution}` or `null`), `source`, `synced`, `peak_height` -- the tier fields carrying exactly their `control.wallet.coinById` meanings, and here always `"fallback"` / `false` / `null`: the local replica stores coin records, not spends, so it can never produce this answer. THE SPEND THAT SPENT ONE COIN, named by that coin's own id (a spend has no id of its own on chain). A coin record carries a puzzle HASH and says only that a coin is gone; the puzzle REVEAL and the solution exist only here, and they are what a caller reconstructing a lineage -- following a dig-profile's DID singleton forward -- needs. `coin` is the full record shape `control.wallet.coinById` returns, with `asset` ALWAYS `null` (this read classifies nothing) and `spent_height` ALWAYS non-null (a spend of a coin nothing calls spent is a contradiction; the node MUST fail closed rather than emit one). The node MUST verify that `puzzle_reveal` tree-hashes to `coin.puzzle_hash` and MUST refuse -- `WALLET_READ_FAILED` (§10) -- when it does not or will not parse: the reveal comes from an unauthenticated peer, a puzzle hash IS the reveal's CLVM tree hash, so the lie is locally detectable and a caller would otherwise curry a forged program into the spend it signs. The returned spend MUST be bound to the id asked for, by the same self-certifying coin-id recomputation `control.wallet.coinById` requires. `spend: null` MUST mean a chain source ANSWERED and holds no spend of that coin -- it is UNSPENT, or unknown; distinguishing those two is `control.wallet.coinById`'s job. Every way of failing to get an answer is a DISTINCT error, NEVER `null`: a caller walking a lineage reads "no spend" as *this is the tip* and stops, so a failure disguised as absence yields a spend built against a superseded singleton, and a mint poll reads it as "my funding coin is still there" and funds the same mint twice. OPEN read (no token), same global fallback rate bound. `INVALID_PARAMS` on a missing/malformed `coin_id`, refused BEFORE any network call; the rule is `dig-node-control-interface`'s own `WalletCoinSpendParams::validated()`, consumed rather than restated. | | `control.wallet.coinsByParent` | `parent_coin_id` (64 lowercase-hex, `0x` TOLERATED), optional `after_coin_id` (same rule), optional `limit` (1..=1000, default 100) | `coins` (array of the `control.wallet.coinById` record shape), `complete`, `cursor`, `source`, `synced`, `peak_height`. ONE PAGE of the DIRECT children created by spending the named parent. ONE HOP, never a walk: the node MUST NOT recurse -- a transitive walk over caller-supplied input is unbounded work the caller cannot bound, and a partial walk returned as complete is a lineage with a silent hole in it. A caller composes hops itself, pairing this with `control.wallet.coinSpend`. Children MUST be returned in ASCENDING `coin_id` order and that order MUST be stable across the pages of one walk, because `after_coin_id` means *strictly after this id in that order* and without a fixed order a cursor names no position (a walk would repeat some children and skip others). `complete` states whether the page is the WHOLE child set and MUST be derived from whether further children EXIST -- never from whether the page filled: the two differ exactly when the child count is an integer multiple of `limit`, where the second declares a truncated page whole and ends a lineage walk one hop early while looking finished. `cursor` is the LAST child in the page (the id the caller was handed), or `null` for an empty page; a node MUST NOT emit `complete: false` with `cursor: null`, which leaves a caller with no way to make progress. An out-of-range `limit` is REFUSED as `INVALID_PARAMS`, never clamped: the page boundary is what the caller resumes from, so a silently shrunk page hands back a cursor for a position the caller never asked about. Every record MUST report `asset: null` (naming a coin by its parent classifies nothing). Every child MUST name the requested parent; a source that returns one that does not fails the WHOLE read (`WALLET_READ_FAILED`, §10) rather than having the row filtered out. `coins: []` MUST mean a chain ANSWERED and the parent created no children it knows of -- typically it is unspent; every way of failing to consult a chain is a DISTINCT error, never an empty page, because an empty page reads as *that spend created nothing*. OPEN read (no token), same global fallback rate bound. `INVALID_PARAMS` on a missing/malformed id or an illegal `limit`, refused BEFORE any network call; the rules are `dig-node-control-interface`'s own `WalletCoinsByParentParams::validated()`. | @@ -2428,7 +2428,7 @@ on-disk master token = local-machine control), never an unauthenticated backdoor - `capsule fetch ` → `control.capsule.fetch`. - `sync [status]` → `control.sync.status`; `sync trigger ` → `control.sync.trigger`. - `wallet balance
[--asset xch|dig|<64-hex asset id>]` → `control.wallet.balance`; - `wallet coins
[--asset xch|dig|<64-hex asset id>]` → `control.wallet.coins`; + `wallet coins
[--asset xch|dig|<64-hex asset id>] [--after-coin-id ] [--limit ]` → `control.wallet.coins`; `wallet coin-by-id ` → `control.wallet.coinById`; `wallet coin-spend ` → `control.wallet.coinSpend`; `wallet coins-by-parent [--after-coin-id ] [--limit ]` → diff --git a/crates/dig-node-service/Cargo.toml b/crates/dig-node-service/Cargo.toml index ffe448d5..db7bba99 100644 --- a/crates/dig-node-service/Cargo.toml +++ b/crates/dig-node-service/Cargo.toml @@ -89,7 +89,7 @@ dig-node-core = { path = "../dig-node-core" } # "0.6.0" the suite went green over a `control.wallet.coinById` the contract had never heard of -- # it checked nothing about the very method the change added. A caret range keeps the pin moving # with the published catalog instead of silently narrowing what CI can see. -dig-node-control-interface = "0.24" +dig-node-control-interface = "0.25" # The deterministic mirror-coin collateral model: the per-epoch requirement, the controller # multiplier, the small-network handicap, the floor clamp, and the safety-margin arithmetic. diff --git a/crates/dig-node-service/src/control.rs b/crates/dig-node-service/src/control.rs index 2091e0b1..ef2b67c0 100644 --- a/crates/dig-node-service/src/control.rs +++ b/crates/dig-node-service/src/control.rs @@ -60,7 +60,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use dig_node_control_interface::params::{ - WalletCoinByIdParams, WalletCoinSpendParams, WalletCoinsByParentParams, + WalletCoinByIdParams, WalletCoinSpendParams, WalletCoinsByParentParams, WalletCoinsParams, WalletReservationsReserveParams, }; use dig_node_control_interface::results::{ @@ -1670,33 +1670,39 @@ fn wallet_coin_spend_param(id: &Value, params: &Value) -> std::result::Result std::result::Result { - const METHOD: &str = "control.wallet.coinsByParent"; +) -> std::result::Result<(Option, Option), Value> { let invalid = |detail: String| { - Err(control_error( + control_error( id.clone(), ErrorCode::InvalidParams, - format!("{METHOD}: {detail}"), - )) - }; - let Some(parent_coin_id) = params.get("parent_coin_id").and_then(|v| v.as_str()) else { - return invalid( - "params.parent_coin_id must be a 64-character lowercase-hex coin id string".to_string(), - ); + format!("{method}: {detail}"), + ) }; - // A present-but-wrong-typed field is refused rather than silently ignored: a caller that sent - // `limit: "50"` asked for a page size, and serving it the default while reporting success would - // make the page boundary — the thing it resumes from — different from the one it believes in. let after_coin_id = match params.get("after_coin_id") { None | Some(Value::Null) => None, Some(Value::String(s)) => Some(s.clone()), Some(other) => { - return invalid(format!( + return Err(invalid(format!( "params.after_coin_id must be a coin id string or omitted, got {other}" - )) + ))) } }; let limit = match params.get("limit") { @@ -1704,12 +1710,78 @@ fn wallet_coins_by_parent_params( Some(v) => match v.as_u64().and_then(|n| u32::try_from(n).ok()) { Some(n) => Some(n), None => { - return invalid(format!( + return Err(invalid(format!( "params.limit must be a positive whole number or omitted, got {v}" - )) + ))) } }, }; + Ok((after_coin_id, limit)) +} + +/// Parse + validate the whole `control.wallet.coins` request: the address, the asset, and the +/// optional page cursor + size (dig-node#381). +/// +/// The paging rules are the CONTRACT's `WalletCoinsParams::validated`, consumed rather than +/// restated — an out-of-range `limit` is refused rather than clamped, and a cursor is held to the +/// same lowercase-64-hex rule every by-coin read uses. A second copy of a published rule agrees +/// until it does not, and here the divergence would surface as a page boundary this node and its +/// caller disagree about, which is a coin the caller never sees. +fn wallet_coins_params( + id: &Value, + params: &Value, +) -> std::result::Result< + ( + String, + dig_wallet::sage::rpc::BalanceAsset, + Option, + u32, + ), + Value, +> { + const METHOD: &str = "control.wallet.coins"; + let (address, asset) = wallet_address_params(METHOD, id, params)?; + let (after_coin_id, limit) = page_fields(id, METHOD, params)?; + let validated = WalletCoinsParams { + // The contract does not validate the address (only a bech32m reader can), and this node + // decodes it downstream; passing it through keeps the type honest rather than empty. + address: address.clone(), + // The asset has already been parsed into this node's own enum, and re-encoding it into the + // contract's `Asset` only to discard it would be a conversion with no reader. + asset: ControlAsset::from(asset), + after_coin_id, + limit, + } + .validated() + .map_err(|e| { + control_error( + id.clone(), + ErrorCode::InvalidParams, + format!("{METHOD}: {}", e.message), + ) + })?; + let limit = validated.effective_limit(); + Ok((address, asset, validated.after_coin_id, limit)) +} + +fn wallet_coins_by_parent_params( + id: &Value, + params: &Value, +) -> std::result::Result { + const METHOD: &str = "control.wallet.coinsByParent"; + let invalid = |detail: String| { + Err(control_error( + id.clone(), + ErrorCode::InvalidParams, + format!("{METHOD}: {detail}"), + )) + }; + let Some(parent_coin_id) = params.get("parent_coin_id").and_then(|v| v.as_str()) else { + return invalid( + "params.parent_coin_id must be a 64-character lowercase-hex coin id string".to_string(), + ); + }; + let (after_coin_id, limit) = page_fields(id, METHOD, params)?; WalletCoinsByParentParams { parent_coin_id: parent_coin_id.to_string(), after_coin_id, @@ -1795,11 +1867,15 @@ fn wallet_read_error(method: &str, id: Value, address: &str, e: BalanceError) -> /// holder of funds that they hold none, and a spend built on that refuses with an untrue shortfall. async fn wallet_coins(ctx: &ControlCtx, id: Value, params: &Value) -> Value { const METHOD: &str = "control.wallet.coins"; - let (address, asset) = match wallet_address_params(METHOD, &id, params) { + let (address, asset, after_coin_id, limit) = match wallet_coins_params(&id, params) { Ok(parsed) => parsed, Err(response) => return response, }; - match ctx.wallet.coins_for_address(&address, asset).await { + match ctx + .wallet + .coins_for_address(&address, asset, after_coin_id.as_deref(), limit) + .await + { Ok(r) => control_ok(id, coins_wire(&r, asset)), Err(e) => wallet_read_error(METHOD, id, &address, e), } @@ -2829,6 +2905,10 @@ fn coins_wire(r: &dig_wallet::sage::rpc::WalletCoinsResult, asset: BalanceAsset) // read's filtering ever changes. "spent_height": c.spent_height, })).collect::>(), + // Always a concrete boolean. The contract's `null` means "a node too old to page", and + // emitting it from a node that DOES page would tell a caller its cursor is meaningless. + "complete": r.complete, + "cursor": r.cursor, "source": r.source.as_wire(), "synced": r.synced, "peak_height": r.peak_height, @@ -3691,6 +3771,68 @@ mod tests { assert_eq!(parsed.len(), 2); } + /// **Proves:** the coin read's page bound is the CONTRACT's, refused rather than clamped, and + /// pinned from BOTH sides. + /// + /// A bound tested only from below can only confirm itself, so the at-maximum value MUST be + /// accepted and the one-over MUST be refused. Clamping instead of refusing would hand back a + /// cursor for a position the caller never asked about, and the caller would mis-size every + /// subsequent request against a boundary it does not share with the node. + /// + /// The default is asserted against the CONTRACT's constant rather than a literal `100`: a node + /// and a client resolving an omitted `limit` to two different numbers page to two different + /// boundaries, which is where a coin goes missing. + #[test] + fn the_coin_page_bound_is_the_contracts_and_is_refused_rather_than_clamped() { + use dig_node_control_interface::params::{COINS_DEFAULT_LIMIT, COINS_MAX_LIMIT}; + + let ask = |extra: Value| { + let mut params = json!({ "address": "xch1abc", "asset": "dig" }); + if let Value::Object(map) = extra { + for (k, v) in map { + params[k] = v; + } + } + wallet_coins_params(&json!(1), ¶ms) + }; + + assert_eq!( + ask(json!({})).expect("an omitted limit is legal").3, + COINS_DEFAULT_LIMIT, + "an omitted limit must resolve to the CONTRACT's default, not to this node's guess" + ); + assert_eq!( + ask(json!({ "limit": COINS_MAX_LIMIT })) + .expect("the documented maximum must be ACCEPTED, or it is not the real bound") + .3, + COINS_MAX_LIMIT + ); + + for over in [COINS_MAX_LIMIT + 1, 0] { + let refused = ask(json!({ "limit": over })).expect_err("must be refused, not clamped"); + assert_eq!( + refused["error"]["code"], -32602, + "an out-of-range page size is INVALID_PARAMS: {refused}" + ); + } + + // The cursor is held to the same lowercase-64-hex rule every by-coin read uses, and the + // `0x` prefix is normalized away rather than passed through to the query. + assert!(ask(json!({ "after_coin_id": "AB".repeat(32) })).is_err()); + assert_eq!( + ask(json!({ "after_coin_id": format!("0x{}", "ab".repeat(32)) })) + .expect("an 0x-prefixed cursor is tolerated") + .2 + .as_deref(), + Some(&*"ab".repeat(32)) + ); + + // A present-but-wrong-typed page field is refused rather than ignored: serving the default + // while reporting success would make the boundary differ from the one the caller believes. + assert!(ask(json!({ "limit": "50" })).is_err()); + assert!(ask(json!({ "after_coin_id": 7 })).is_err()); + } + /// `coins_wire` emits the contract shape: the requested asset echoed onto every coin, an /// explicitly-null `spent_height` (every coin here is unspent), and the tier fields. /// @@ -3721,6 +3863,8 @@ mod tests { spent_height: None, }, ], + complete: true, + cursor: Some("dd".repeat(32)), source: Source::Db, synced: true, peak_height: Some(5_000_000), @@ -3743,6 +3887,7 @@ mod tests { "created_height": null, "spent_height": null } ], + "complete": true, "cursor": "dd".repeat(32), "source": "db", "synced": true, "peak_height": 5_000_000 }) ); @@ -3781,6 +3926,8 @@ mod tests { created_height: Some(1), spent_height: None, }], + complete: true, + cursor: Some("aa".repeat(32)), source: Source::Fallback, synced: false, peak_height: None, @@ -3841,6 +3988,8 @@ mod tests { created_height: Some(5_000_000), spent_height: Some(5_000_042), }], + complete: true, + cursor: Some("aa".repeat(32)), source: Source::Fallback, synced: false, peak_height: None, diff --git a/crates/dig-node-service/src/control_cli.rs b/crates/dig-node-service/src/control_cli.rs index 96c5a660..2a4f532e 100644 --- a/crates/dig-node-service/src/control_cli.rs +++ b/crates/dig-node-service/src/control_cli.rs @@ -61,8 +61,14 @@ pub enum ControlAction { SyncTrigger { store: String }, /// `control.wallet.balance` — the READ-ONLY balance of a public address (XCH or $DIG). WalletBalance { address: String, asset: String }, - /// `control.wallet.coins` — the READ-ONLY unspent coins of a public address (XCH or $DIG). - WalletCoins { address: String, asset: String }, + /// `control.wallet.coins` — ONE PAGE of the READ-ONLY unspent coins of a public address (XCH + /// or $DIG). `after_coin_id` resumes from the `cursor` of a previous page. + WalletCoins { + address: String, + asset: String, + after_coin_id: Option, + limit: Option, + }, /// `control.wallet.coinById` — the READ-ONLY lookup of ONE coin by coin id (spent or not). WalletCoinById { coin_id: String }, /// `control.wallet.coinSpend` — the READ-ONLY spend that spent one coin (reveal + solution). @@ -277,10 +283,28 @@ impl ControlAction { // generation; folding it into the `store` arm above would send one joined field the // node refuses as a missing `root`. ControlAction::CapsuleFetch { store, root } => json!({ "store": store, "root": root }), - ControlAction::WalletBalance { address, asset } - | ControlAction::WalletCoins { address, asset } => { + ControlAction::WalletBalance { address, asset } => { json!({ "address": address, "asset": asset_to_wire(asset) }) } + // Split from the balance arm because this read is PAGED. The two page fields are + // OMITTED when unset rather than sent as null, so the node applies the CONTRACT's + // default page size -- sending a number this CLI invented would make `dign wallet + // coins` page differently from every other client for no reason a user asked for. + ControlAction::WalletCoins { + address, + asset, + after_coin_id, + limit, + } => { + let mut params = json!({ "address": address, "asset": asset_to_wire(asset) }); + if let Some(after) = after_coin_id { + params["after_coin_id"] = json!(after); + } + if let Some(limit) = limit { + params["limit"] = json!(limit); + } + params + } ControlAction::WalletCoinById { coin_id } | ControlAction::WalletCoinSpend { coin_id } => json!({ "coin_id": coin_id }), // A DIFFERENT field name, on purpose: the contract names the subject @@ -399,6 +423,8 @@ pub fn cli_covered_control_methods() -> Vec<&'static str> { ControlAction::WalletCoins { address: String::new(), asset: String::new(), + after_coin_id: None, + limit: None, } .method(), ControlAction::WalletCoinById { @@ -704,14 +730,19 @@ fn summarize(method: &str, result: &Value) -> String { // Says explicitly whether the page is the whole child set, and how to get the rest. A // summary that printed only a count would let a truncated page read as a finished hop -- // the exact misreading `complete` exists to prevent. + // Says explicitly whether the page is the whole unspent set, and how to get the rest. A + // summary that printed only a count would let a truncated page read as an address's whole + // holdings -- which is a person deciding they cannot afford something they can. + "control.wallet.coins" => { + let coins = result["coins"].as_array().map(Vec::len).unwrap_or(0); + format!("{coins} unspent coin(s){}", page_suffix(result)) + } "control.wallet.coinsByParent" => { let coins = result["coins"].as_array().map(Vec::len).unwrap_or(0); - let more = match (result["complete"].as_bool(), result["cursor"].as_str()) { - (Some(true), _) => " · complete".to_string(), - (_, Some(cursor)) => format!(" · MORE remain — resume after {cursor}"), - _ => " · completeness unknown (a node too old to say)".to_string(), - }; - format!("{coins} direct child coin(s) — one hop, not a lineage{more}") + format!( + "{coins} direct child coin(s) — one hop, not a lineage{}", + page_suffix(result) + ) } "control.collateral.requirement" => summarize_collateral_requirement(result), "control.collateral.buffer" => summarize_collateral_buffer(result), @@ -725,6 +756,23 @@ fn summarize(method: &str, result: &Value) -> String { } } +/// Whether a paged answer is the whole set, and how to resume when it is not. +/// +/// Shared by both paged coin reads. The dangerous rendering is the one this exists to prevent: a +/// summary that prints only a count lets a TRUNCATED page read as the whole answer, and the two +/// pages are indistinguishable by length whenever the total is a multiple of the page size. +/// +/// The third arm is not dead. `complete: null` is the contract's "a node too old to page", and +/// saying so is better than printing nothing — a caller shown a bare count from such a node cannot +/// tell it apart from a node that measured and found the set complete. +fn page_suffix(result: &Value) -> String { + match (result["complete"].as_bool(), result["cursor"].as_str()) { + (Some(true), _) => " · complete".to_string(), + (_, Some(cursor)) => format!(" · MORE remain — resume after {cursor}"), + _ => " · completeness unknown (a node too old to say)".to_string(), + } +} + /// `dign collateral buffer` — how much $DIG to hold, and whether this operator is short. /// /// # Why the served-pair count is an OPERAND and not a lookup diff --git a/crates/dig-node-service/src/entrypoint.rs b/crates/dig-node-service/src/entrypoint.rs index fa649fd0..effe98ac 100644 --- a/crates/dig-node-service/src/entrypoint.rs +++ b/crates/dig-node-service/src/entrypoint.rs @@ -301,13 +301,24 @@ enum WalletCommand { #[arg(long, default_value = "xch")] asset: String, }, - /// List the unspent coins at a public address (READ-ONLY; needs no seed or pairing). + /// List ONE PAGE of the unspent coins at a public address (READ-ONLY; needs no seed or + /// pairing). + /// + /// A funded address accumulates coins without limit, so the answer is PAGED. Pass the `cursor` + /// from one page as `--after-coin-id` to get the next; `complete` says whether there is one. Coins { /// The bech32m address to read (`xch1…`). address: String, /// The asset to list: `xch` (default) or `dig`. #[arg(long, default_value = "xch")] asset: String, + /// Resume STRICTLY AFTER this coin — pass the `cursor` from the previous page. Omit to + /// start at the first coin. + #[arg(long)] + after_coin_id: Option, + /// The page size (1..=1000). Omit to let the node use the contract's default of 100. + #[arg(long)] + limit: Option, }, /// Look up ONE coin by its coin id, spent or unspent (READ-ONLY; needs no seed or pairing). /// @@ -982,7 +993,17 @@ fn wallet_action(cmd: WalletCommand) -> Option { WalletCommand::Balance { address, asset } => { ControlAction::WalletBalance { address, asset } } - WalletCommand::Coins { address, asset } => ControlAction::WalletCoins { address, asset }, + WalletCommand::Coins { + address, + asset, + after_coin_id, + limit, + } => ControlAction::WalletCoins { + address, + asset, + after_coin_id, + limit, + }, WalletCommand::CoinById { coin_id } => ControlAction::WalletCoinById { coin_id }, WalletCommand::CoinSpend { coin_id } => ControlAction::WalletCoinSpend { coin_id }, WalletCommand::CoinsByParent { diff --git a/crates/dig-wallet/Cargo.toml b/crates/dig-wallet/Cargo.toml index 6b35675b..0ede2f70 100644 --- a/crates/dig-wallet/Cargo.toml +++ b/crates/dig-wallet/Cargo.toml @@ -64,7 +64,7 @@ axum = "0.7" # The PUBLISHED control-plane contract. `BalanceAsset` is re-exported from it rather than # restated here: the wire spelling of an asset and the asset a read scopes to must be the same # value, and two enums naming the same tokens agree only until one of them gains a variant. -dig-node-control-interface = "0.24" +dig-node-control-interface = "0.25" # The ecosystem's single definition of a Chia offer id (dig_ecosystem#3140, #283): # `sha256(spend_bundle.to_bytes())`, the value Chia's `Offer.name()`, Sage and dexie use. This # crate once derived the id itself from the offered coin set alone, which collided across offers diff --git a/crates/dig-wallet/src/sage/db.rs b/crates/dig-wallet/src/sage/db.rs index 474fbca5..b227f81d 100644 --- a/crates/dig-wallet/src/sage/db.rs +++ b/crates/dig-wallet/src/sage/db.rs @@ -2820,6 +2820,68 @@ impl WalletDb { Ok(row.is_some()) } + /// ONE PAGE of the UNSPENT coins for `asset_id` scoped to `puzzle_hashes`, ASCENDING by + /// `coin_id` (dig-node#381). + /// + /// # Every filter is applied HERE, in SQL, and that is the point + /// + /// The scope, the asset, the unspent predicate and the page bound are one query. Paginating a + /// broader query and filtering the rows afterwards would cut the page BEFORE the filter, so a + /// page of `limit` rows could shrink to any smaller number on the way out — short pages, and a + /// `complete` computed from a row count that no longer describes what remains. On a coin read + /// the caller spends what it is handed, so a page that lost rows is funds it cannot see. + /// + /// # It returns up to `limit + 1` rows on purpose + /// + /// The caller drops the last one and reads its EXISTENCE as "more remain". Deriving + /// completeness from whether the page filled instead agrees on every input except a coin count + /// that is an exact multiple of the page size — where it declares a truncated page whole. + /// + /// Mempool coins (`created_height IS NULL`) are INCLUDED, matching + /// [`Self::coins_scoped`] and the contract: the caller decides whether an unconfirmed coin is + /// spendable for its purpose, and the node never decides that by hiding one. + pub async fn unspent_coins_page( + &self, + asset_id: Option<&str>, + puzzle_hashes: &[String], + after_coin_id: Option<&str>, + limit: u32, + ) -> sqlx::Result> { + if puzzle_hashes.is_empty() { + return Ok(vec![]); + } + let ph = Self::placeholders(puzzle_hashes.len()); + // The SAME scope/asset pairing `coins_scoped` uses: a CAT coin sits at an unrelated puzzle + // hash and is HINTED to the address, so an asset-scoped read matches on the hint. + let (scope_col, asset_clause) = match asset_id { + Some(_) => ("hint", "AND asset_id = ?"), + None => ("puzzle_hash", "AND asset_id IS NULL"), + }; + let cursor_clause = if after_coin_id.is_some() { + "AND coin_id > ?" + } else { + "" + }; + let sql = format!( + "SELECT * FROM coins WHERE {scope_col} IN ({ph}) {asset_clause} \ + AND spent_height IS NULL {cursor_clause} ORDER BY coin_id ASC LIMIT ?" + ); + let mut q = sqlx::query(&sql); + for p in puzzle_hashes { + q = q.bind(p.to_ascii_lowercase()); + } + if let Some(a) = asset_id { + q = q.bind(a.to_ascii_lowercase()); + } + if let Some(cursor) = after_coin_id { + q = q.bind(cursor.to_ascii_lowercase()); + } + // One row beyond the page, for the caller to read as "more remain" and then drop. + q = q.bind(i64::from(limit) + 1); + let rows = q.fetch_all(&self.pool).await?; + Ok(rows.iter().map(Self::coin_from_row).collect()) + } + /// All coins (any spent state) for `asset_id` scoped to `puzzle_hashes`. Used by /// `get_coins`, which applies its own spent/filter modes over the returned set. pub async fn coins_scoped( diff --git a/crates/dig-wallet/src/sage/rpc.rs b/crates/dig-wallet/src/sage/rpc.rs index 4866df46..cbf1dcd1 100644 --- a/crates/dig-wallet/src/sage/rpc.rs +++ b/crates/dig-wallet/src/sage/rpc.rs @@ -269,8 +269,18 @@ pub struct WalletCoin { /// [`WalletBackend::coins_for_address`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct WalletCoinsResult { - /// The unspent coins, newest-first is NOT guaranteed; order is the source's. + /// ONE PAGE of the unspent coins, ASCENDING by `coin_id` — a total, stable order, because a + /// cursor names no position without one. pub coins: Vec, + /// Whether [`Self::coins`] is the WHOLE unspent set at this address for this asset. + /// + /// Derived from whether rows remain BEYOND the page, never from whether the page filled. The + /// two differ exactly when the coin count is a multiple of the page size, and getting it wrong + /// there hands a spend builder a partial coin set that looks complete — it then refuses with a + /// shortfall that is not true, while the funds sit in the coins that were withheld. + pub complete: bool, + /// The last coin in this page — what a caller resumes from — or `None` for an empty page. + pub cursor: Option, /// Which tier produced these coins. pub source: Source, /// Whether THIS answer is CURRENT — see [`WalletBalanceResult::synced`], of which this is the @@ -1471,6 +1481,8 @@ impl WalletBackend { &self, address: &str, asset: BalanceAsset, + after_coin_id: Option<&str>, + limit: u32, ) -> std::result::Result { let puzzle_hash = normalize_ph(&decode_address(address).ok_or(BalanceError::InvalidAddress)?); @@ -1505,9 +1517,13 @@ impl WalletBackend { match source { Source::Db => { let scope = [puzzle_hash]; + // Scope, asset, unspent and the page bound are ONE query. Paging a broader read + // and filtering afterwards would cut the page before the filter, so a page would + // arrive short and `complete` would be computed from a count that no longer + // describes what remains. let rows = self .db - .coins_scoped(asset_id.as_deref(), &scope) + .unspent_coins_page(asset_id.as_deref(), &scope, after_coin_id, limit) .await .map_err(|e| read_err(e.into()))?; let peak_height = self @@ -1516,13 +1532,19 @@ impl WalletBackend { .await .map_err(|e| read_err(e.into()))? .peak_height; + let page_size = limit as usize; + // The query returned up to one row PAST the page; its existence is what says more + // remain, and it is dropped rather than served. + let complete = rows.len() <= page_size; let coins = rows .iter() - .filter(|r| r.spent_height.is_none()) + .take(page_size) .map(coin_from_row) .collect::, _>>() .map_err(read_err)?; Ok(WalletCoinsResult { + cursor: coins.last().map(|c| c.coin_id.clone()), + complete, coins, source, // The same measurement as the balance read's: this is that answer unreduced, @@ -1546,16 +1568,35 @@ impl WalletBackend { return Err(BalanceError::RateLimited); } let phs = [puzzle_hash]; - let coins = self + let mut unspent: Vec = self .asset_scoped_fallback_coins(asset, &phs) .await - .map_err(read_err)?; - Ok(WalletCoinsResult { - coins: coins - .iter() - .filter(|c| c.spent_height.is_none()) - .map(coin_from_fallback) + .map_err(read_err)? + .iter() + .filter(|c| c.spent_height.is_none()) + .map(coin_from_fallback) + .collect(); + // Sorted HERE rather than trusted from the source, for the reason + // `coins_by_parent` records: the tier underneath merges peer and coinset answers + // and promises no order at all, and an order that varies between pages loses rows. + unspent.sort_by(|a, b| a.coin_id.cmp(&b.coin_id)); + let remaining: Vec = match after_coin_id { + Some(cursor) => unspent + .into_iter() + .filter(|c| c.coin_id.as_str() > cursor) .collect(), + None => unspent, + }; + let page_size = limit as usize; + // This tier answers with the whole set in one call and has no cursor to push the + // page down into, so the bound is on the RESPONSE. The upstream call count is one + // per request either way. + let complete = remaining.len() <= page_size; + let coins: Vec = remaining.into_iter().take(page_size).collect(); + Ok(WalletCoinsResult { + cursor: coins.last().map(|c| c.coin_id.clone()), + complete, + coins, source, // The replica neither produced these coins nor bounds their freshness (#2233). synced: false, @@ -5032,6 +5073,13 @@ mod tests { db } + /// The page size every coin-read test that is NOT about paging asks for. + /// + /// Large enough that those tests see the whole fixture, so the paging change cannot silently + /// truncate what they assert about — and named rather than a bare literal, so a reader can tell + /// "this test does not care about the page" from "this test chose 100 for a reason". + const TEST_PAGE: u32 = 100; + fn coin_at_ph( id: &str, ph: &str, @@ -5312,7 +5360,7 @@ mod tests { let be = backend_over(fb).await; let r = be - .coins_for_address(&address, BalanceAsset::DIG) + .coins_for_address(&address, BalanceAsset::DIG, None, TEST_PAGE) .await .unwrap(); let mut ids: Vec<&str> = r.coins.iter().map(|c| c.coin_id.as_str()).collect(); @@ -5350,7 +5398,12 @@ mod tests { let be = backend_over(fb).await; let r = be - .coins_for_address(&address, BalanceAsset::Cat(foreign_asset_id())) + .coins_for_address( + &address, + BalanceAsset::Cat(foreign_asset_id()), + None, + TEST_PAGE, + ) .await .unwrap(); let ids: Vec<&str> = r.coins.iter().map(|c| c.coin_id.as_str()).collect(); @@ -5376,7 +5429,12 @@ mod tests { let be = backend_over(fb).await; let r = be - .coins_for_address(&address, BalanceAsset::Cat(Bytes32::from([0x77u8; 32]))) + .coins_for_address( + &address, + BalanceAsset::Cat(Bytes32::from([0x77u8; 32])), + None, + TEST_PAGE, + ) .await .unwrap(); assert!( @@ -6746,7 +6804,7 @@ mod tests { .with_chain_peer_tier_for_tests(peers_level_at(500)); let r = be - .coins_for_address(&owned_address(), BalanceAsset::Xch) + .coins_for_address(&owned_address(), BalanceAsset::Xch, None, TEST_PAGE) .await .unwrap(); @@ -6778,7 +6836,7 @@ mod tests { let arbitrary = encode_address(&"33".repeat(32), "xch").unwrap(); let r = be - .coins_for_address(&arbitrary, BalanceAsset::Xch) + .coins_for_address(&arbitrary, BalanceAsset::Xch, None, TEST_PAGE) .await .unwrap(); @@ -6809,7 +6867,8 @@ mod tests { let be = WalletBackend::new(db, Arc::new(EmptyFallback), WalletConfig::default()); let arbitrary = encode_address(&"33".repeat(32), "xch").unwrap(); assert_eq!( - be.coins_for_address(&arbitrary, BalanceAsset::Xch).await, + be.coins_for_address(&arbitrary, BalanceAsset::Xch, None, TEST_PAGE) + .await, Err(BalanceError::NoChainSource) ); @@ -6817,7 +6876,7 @@ mod tests { let db = db_with_owned_derivation(false, None).await; let be = WalletBackend::new(db, Arc::new(EmptyFallback), WalletConfig::default()); assert_eq!( - be.coins_for_address(&owned_address(), BalanceAsset::Xch) + be.coins_for_address(&owned_address(), BalanceAsset::Xch, None, TEST_PAGE) .await, Err(BalanceError::NotSynced) ); @@ -6827,7 +6886,8 @@ mod tests { db.set_initial_sync_complete(true).await.unwrap(); let be = WalletBackend::new(db, Arc::new(ErringFallback), WalletConfig::default()); assert!(matches!( - be.coins_for_address(&arbitrary, BalanceAsset::Xch).await, + be.coins_for_address(&arbitrary, BalanceAsset::Xch, None, TEST_PAGE) + .await, Err(BalanceError::ReadFailed(_)) )); @@ -6835,7 +6895,7 @@ mod tests { let db = WalletDb::open_in_memory().await.unwrap(); let be = WalletBackend::new(db, Arc::new(EmptyFallback), WalletConfig::default()); assert_eq!( - be.coins_for_address("not-an-address", BalanceAsset::Xch) + be.coins_for_address("not-an-address", BalanceAsset::Xch, None, TEST_PAGE) .await, Err(BalanceError::InvalidAddress) ); @@ -7115,7 +7175,7 @@ mod tests { .await .unwrap(); let coins = be - .coins_for_address(&owned_address(), BalanceAsset::Xch) + .coins_for_address(&owned_address(), BalanceAsset::Xch, None, TEST_PAGE) .await .unwrap(); let peak = be.chain_peak().await.unwrap(); @@ -7157,7 +7217,7 @@ mod tests { .with_chain_peer_tier_for_tests(peers_ahead_of_the_replica()); let coins = be - .coins_for_address(&owned_address(), BalanceAsset::Xch) + .coins_for_address(&owned_address(), BalanceAsset::Xch, None, TEST_PAGE) .await .unwrap(); assert_eq!(coins.source, Source::Db); @@ -7169,6 +7229,178 @@ mod tests { ); } + /// The four unspent coins a paging fixture needs, ASCENDING by coin id. + /// + /// FOUR, read TWO at a time, so the truncated page and the final page carry the SAME row count + /// and only one of them is the last. Every length-based inference gives the same answer for + /// both, which is what makes `complete` load-bearing here rather than decorative. + const PAGE_FIXTURE: [&str; 4] = ["aa11", "bb22", "cc33", "dd44"]; + + async fn db_with_four_unspent_coins() -> WalletDb { + let db = db_with_owned_derivation(true, Some(REPLICA_PEAK)).await; + for (i, id) in PAGE_FIXTURE.iter().enumerate() { + db.upsert_coin(&coin_at_ph(id, &owned_ph(), 10 + i as u64, Some(1), None)) + .await + .unwrap(); + } + db + } + + /// **Proves:** the page boundary survives a coin being SPENT between two pages — no coin is + /// skipped and none is repeated. + /// + /// This is the fixture an OFFSET implementation cannot pass, and the reason the read pages by + /// cursor. Coin `aa11` is spent after page one is served, so the unspent set shrinks from four + /// rows to three. An offset of 2 into the shrunken set lands on `dd44` and SKIPS `cc33` + /// entirely; the cursor `bb22` still names a position, so page two is `cc33` then `dd44`. + /// + /// The skipped coin is the whole cost: a caller building a spend never sees it, and refuses + /// with a shortfall that is not true while the money sits in the coin it could not see. The + /// assertion is therefore on the coin IDENTITIES and not on the row count — a count-only + /// assertion would pass against an implementation that returned the wrong two coins. + /// + /// Exactly ONE actor varies (the first coin is spent); the other three stay honest, so the + /// test can still see what a correct implementation returns. + #[tokio::test] + async fn a_coin_spent_between_pages_shifts_no_boundary_and_loses_no_coin() { + let db = db_with_four_unspent_coins().await; + let be = WalletBackend::new(db.clone(), Arc::new(EmptyFallback), WalletConfig::default()) + .with_chain_peer_tier_for_tests(peers_level_at(REPLICA_PEAK)); + + let first = be + .coins_for_address(&owned_address(), BalanceAsset::Xch, None, 2) + .await + .unwrap(); + assert_eq!( + first.source, + Source::Db, + "the fixture must take the DB tier" + ); + assert_eq!( + first + .coins + .iter() + .map(|c| c.coin_id.as_str()) + .collect::>(), + vec!["aa11", "bb22"], + "the first page must be the first two coins in ascending coin-id order" + ); + assert_eq!(first.cursor.as_deref(), Some("bb22")); + + // The event the cursor exists for: a coin BEFORE the boundary leaves the unspent set. + db.upsert_coin(&coin_at_ph("aa11", &owned_ph(), 10, Some(1), Some(2))) + .await + .unwrap(); + + let second = be + .coins_for_address( + &owned_address(), + BalanceAsset::Xch, + first.cursor.as_deref(), + 2, + ) + .await + .unwrap(); + assert_eq!( + second + .coins + .iter() + .map(|c| c.coin_id.as_str()) + .collect::>(), + vec!["cc33", "dd44"], + "a coin was skipped or repeated across the boundary -- an offset-paged read lands on \ + `dd44` alone here, and `cc33` becomes money the caller cannot see" + ); + } + + /// **Proves:** an exactly-full FINAL page reports `complete: true`, and the truncated page + /// before it reports `false` while carrying the same number of rows. + /// + /// Four coins at a page size of two: both pages hold exactly two. `complete = coins.len() < + /// limit` calls BOTH of them incomplete, and "a full page means more" calls both truncated, so + /// this fixture is the one that separates a completeness derived from what REMAINS from one + /// inferred from length. A spurious `complete: false` on the last page costs one wasted + /// request; a spurious `true` on the first costs a spend built on half an address's coins. + #[tokio::test] + async fn an_exactly_full_final_page_is_complete_and_the_one_before_it_is_not() { + let db = db_with_four_unspent_coins().await; + let be = WalletBackend::new(db, Arc::new(EmptyFallback), WalletConfig::default()) + .with_chain_peer_tier_for_tests(peers_level_at(REPLICA_PEAK)); + + let first = be + .coins_for_address(&owned_address(), BalanceAsset::Xch, None, 2) + .await + .unwrap(); + assert!( + !first.complete, + "two of four coins were withheld -- a `true` here presents half an address's holdings \ + as all of them" + ); + + let second = be + .coins_for_address( + &owned_address(), + BalanceAsset::Xch, + first.cursor.as_deref(), + 2, + ) + .await + .unwrap(); + assert_eq!( + second.coins.len(), + first.coins.len(), + "both pages carry the same row count -- which is exactly why length cannot decide it" + ); + assert!( + second.complete, + "the last two coins fit exactly, so this page IS the end of the set" + ); + assert_eq!( + second.cursor.as_deref(), + Some("dd44"), + "the cursor is the last coin HANDED over, even on a complete page" + ); + } + + /// **Proves:** walking the pages end to end yields every coin exactly once, in order. + /// + /// The composition test the two above cannot replace: each of them looks at one boundary, and + /// a duplicate or a gap can hide in the seam between three of them. The page size of ONE makes + /// every coin its own boundary, so every seam is exercised — and it also pins the loop's + /// termination, since a read that never sets `complete` would spin here rather than pass. + #[tokio::test] + async fn a_cursor_walk_visits_every_coin_exactly_once() { + let db = db_with_four_unspent_coins().await; + let be = WalletBackend::new(db, Arc::new(EmptyFallback), WalletConfig::default()) + .with_chain_peer_tier_for_tests(peers_level_at(REPLICA_PEAK)); + + let mut walked: Vec = Vec::new(); + let mut cursor: Option = None; + for _ in 0..PAGE_FIXTURE.len() + 2 { + let page = be + .coins_for_address(&owned_address(), BalanceAsset::Xch, cursor.as_deref(), 1) + .await + .unwrap(); + walked.extend(page.coins.iter().map(|c| c.coin_id.clone())); + if page.complete { + break; + } + cursor = page.cursor.clone(); + assert!( + cursor.is_some(), + "an incomplete page with no cursor is a walk that cannot continue" + ); + } + assert_eq!( + walked, + PAGE_FIXTURE + .iter() + .map(|s| s.to_string()) + .collect::>(), + "the walk must visit every coin exactly once, in ascending order" + ); + } + /// **Proves:** a fallback answer is unchanged — it never borrows the replica's freshness or /// its height, however caught-up the replica happens to be. #[tokio::test] @@ -7297,13 +7529,14 @@ mod tests { ) .with_fallback_rate_limit(1.0, 0.0); assert!( - be.coins_for_address(&arbitrary, BalanceAsset::Xch) + be.coins_for_address(&arbitrary, BalanceAsset::Xch, None, TEST_PAGE) .await .is_ok(), "the first read spends the only token" ); assert_eq!( - be.coins_for_address(&arbitrary, BalanceAsset::Xch).await, + be.coins_for_address(&arbitrary, BalanceAsset::Xch, None, TEST_PAGE) + .await, Err(BalanceError::RateLimited), "a second coin read must be refused, not amplified onto the third party" );