diff --git a/Cargo.lock b/Cargo.lock index 9d167d8b7..f32dbc248 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -831,7 +831,7 @@ dependencies = [ "ark-serialize 0.3.0", "ark-std 0.3.0", "derivative", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "paste", "rustc_version 0.3.3", @@ -851,7 +851,7 @@ dependencies = [ "derivative", "digest 0.10.7", "itertools 0.10.5", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "paste", "rustc_version 0.4.1", @@ -884,7 +884,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" dependencies = [ - "num-bigint", + "num-bigint 0.4.6", "num-traits", "quote", "syn 1.0.109", @@ -896,7 +896,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" dependencies = [ - "num-bigint", + "num-bigint 0.4.6", "num-traits", "proc-macro2", "quote", @@ -921,7 +921,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-std 0.4.0", "digest 0.10.7", - "num-bigint", + "num-bigint 0.4.6", ] [[package]] @@ -968,6 +968,47 @@ dependencies = [ "term", ] +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn 2.0.104", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + [[package]] name = "asn1-rs" version = "0.7.1" @@ -1707,12 +1748,30 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bimap" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.69.5" @@ -1839,6 +1898,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bls48581" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30ea323232ae8657491c6944bba23425867c9110dd46e80cbefaf5f5d90aec" +dependencies = [ + "hex", + "rand 0.8.5", + "serde_json", + "sha2", + "uniffi", +] + [[package]] name = "blst" version = "0.3.15" @@ -1874,6 +1946,29 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "bs58" version = "0.5.1" @@ -1979,6 +2074,38 @@ dependencies = [ "crossbeam-channel", ] +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.26", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "cbor4ii" version = "0.3.3" @@ -2567,7 +2694,7 @@ dependencies = [ "asn1-rs", "displaydoc", "nom", - "num-bigint", + "num-bigint 0.4.6", "num-traits", "rusticata-macros", ] @@ -2858,6 +2985,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed448-goldilocks-plus" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3689f7a47d1a75cd7483b8df8c4a0d5bea3bfc2ec41ef6e02e75123ef980bdf6" +dependencies = [ + "crypto-bigint 0.5.5", + "elliptic-curve 0.13.8", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "serdect 0.3.0", + "sha3", + "signature 2.2.0", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -2891,6 +3035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct 0.2.0", + "base64ct", "crypto-bigint 0.5.5", "digest 0.10.7", "ff 0.13.1", @@ -2900,7 +3045,10 @@ dependencies = [ "pkcs8 0.10.2", "rand_core 0.6.4", "sec1 0.7.3", + "serde_json", + "serdect 0.2.0", "subtle", + "tap", "zeroize", ] @@ -3076,10 +3224,37 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ + "bitvec", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "ff_ce" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a682c12d0cc98a32ab7540401a5ea1ed21d11571eea11d5829cd721f85ff0" +dependencies = [ + "byteorder", + "ff_derive_ce", + "hex", + "rand 0.4.6", +] + +[[package]] +name = "ff_derive_ce" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c052fa6d4c2f12305ec364bfb8ef884836f3f61ea015b202372ff996d1ac4b" +dependencies = [ + "num-bigint 0.2.6", + "num-integer", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -3137,6 +3312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "libz-sys", "miniz_oxide", ] @@ -3413,13 +3589,12 @@ dependencies = [ ] [[package]] -name = "fs4" -version = "0.13.1" +name = "fs-err" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" dependencies = [ - "rustix 1.0.8", - "windows-sys 0.59.0", + "autocfg", ] [[package]] @@ -3428,6 +3603,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "funty" version = "2.0.0" @@ -3669,6 +3850,17 @@ dependencies = [ "regex-syntax 0.8.5", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "governor" version = "0.10.0" @@ -5349,6 +5541,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15413ef615ad868d4d65dce091cb233b229419c7c0c4bcaa746c0901c49ff39c" +dependencies = [ + "zlib-rs", +] + [[package]] name = "libz-sys" version = "1.1.22" @@ -5511,6 +5712,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -5774,6 +5985,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -5826,7 +6048,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "num-bigint", + "num-bigint 0.4.6", "num-integer", "num-traits", ] @@ -5989,6 +6211,46 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "oxilangtag" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f3f87617a86af77fa3691e6350483e7154c2ead9f1261b75130e21ca0f8acb" +dependencies = [ + "serde", +] + +[[package]] +name = "oxiri" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4ed3a7192fa19f5f48f99871f2755047fabefd7f222f12a1df1773796a102" + +[[package]] +name = "oxrdf" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a04761319ef84de1f59782f189d072cbfc3a9a40c4e8bded8667202fbd35b02a" +dependencies = [ + "oxilangtag", + "oxiri", + "rand 0.8.5", + "thiserror 2.0.12", +] + +[[package]] +name = "oxttl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d385f1776d7cace455ef6b7c54407838eff902ca897303d06eb12a26f4cf8a0" +dependencies = [ + "memchr", + "oxilangtag", + "oxiri", + "oxrdf", + "thiserror 2.0.12", +] + [[package]] name = "p256" version = "0.11.1" @@ -6205,7 +6467,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher", + "siphasher 1.0.1", ] [[package]] @@ -6266,6 +6528,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "polling" version = "3.9.0" @@ -6309,6 +6577,17 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "poseidon-rs" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7bd44e953add4c619a75a970cc4335e3175dfecc61ab64ee6caa8fffde95c95" +dependencies = [ + "ff_ce", + "rand 0.4.6", + "serde_json", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -6616,6 +6895,50 @@ dependencies = [ "unsigned-varint 0.8.0", ] +[[package]] +name = "quilibrium-names-sdk" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c774848a8c34480b1ea5f17aedf2297cda7bb8ec58f4c7ed479e129dcf2b4496" +dependencies = [ + "anyhow", + "base64 0.22.1", + "borsh", + "bs58", + "ff_ce", + "hex", + "num-bigint 0.4.6", + "poseidon-rs", + "quilibrium-verkle", + "serde", + "serde_json", + "sha2", + "sha3", + "thiserror 2.0.12", + "ureq 2.12.1", +] + +[[package]] +name = "quilibrium-verkle" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbbad7ede869ee34d96a117b4d1d843795c9bd26ef4bc726493785d6e5c6793d" +dependencies = [ + "anyhow", + "bls48581", + "borsh", + "ff_ce", + "hex", + "num-bigint 0.4.6", + "oxrdf", + "oxttl", + "poseidon-rs", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.12", +] + [[package]] name = "quinn" version = "0.11.8" @@ -6709,6 +7032,19 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -6751,6 +7087,21 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.6.4" @@ -6820,6 +7171,15 @@ dependencies = [ "yasna", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "recvmsg" version = "1.0.0" @@ -7065,7 +7425,7 @@ dependencies = [ "bytes", "fastrlp 0.3.1", "fastrlp 0.4.0", - "num-bigint", + "num-bigint 0.4.6", "num-integer", "num-traits", "parity-scale-codec", @@ -7359,6 +7719,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "sct" version = "0.7.1" @@ -7405,6 +7785,7 @@ dependencies = [ "der 0.7.10", "generic-array", "pkcs8 0.10.2", + "serdect 0.2.0", "subtle", "zeroize", ] @@ -7480,18 +7861,28 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -7563,6 +7954,26 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct 0.2.0", + "serde", +] + +[[package]] +name = "serdect" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" +dependencies = [ + "base16ct 0.2.0", + "serde", +] + [[package]] name = "serial_test" version = "3.2.0" @@ -7692,12 +8103,18 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ - "num-bigint", + "num-bigint 0.4.6", "num-traits", "thiserror 2.0.12", "time", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "siphasher" version = "1.0.1" @@ -7719,6 +8136,12 @@ dependencies = [ "serde", ] +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "snapchain" version = "0.11.0" @@ -7746,6 +8169,7 @@ dependencies = [ "chrono", "clap", "ed25519-dalek", + "ed448-goldilocks-plus", "eth-signature-verifier", "fancy-regex", "figment", @@ -7782,6 +8206,7 @@ dependencies = [ "parking_lot", "pre-commit", "prost 0.13.5", + "quilibrium-names-sdk", "ractor", "rand 0.8.5", "reqwest", @@ -7799,6 +8224,7 @@ dependencies = [ "solar-macros", "strum 0.27.2", "strum_macros 0.27.2", + "svm-rs-builds", "tar", "tempfile", "thiserror 1.0.69", @@ -7813,6 +8239,7 @@ dependencies = [ "tracing", "tracing-subscriber", "tracing-test", + "ureq 3.1.4", "url", "walkdir", ] @@ -7867,7 +8294,7 @@ dependencies = [ "alloy-primitives", "bumpalo", "either", - "num-bigint", + "num-bigint 0.4.6", "num-rational", "semver 1.0.26", "solar-data-structures", @@ -7951,7 +8378,7 @@ dependencies = [ "bumpalo", "itertools 0.14.0", "memchr", - "num-bigint", + "num-bigint 0.4.6", "num-rational", "num-traits", "smallvec", @@ -8080,13 +8507,12 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "svm-rs" -version = "0.5.16" +version = "0.5.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d304f1b54e9c83ec8f0537c9dd40d46344bd9142cc528d5242c4b6fe11ced0" +checksum = "909e8ff825120cd2b34ceb236ab72e2a7f74b1d3a86c247936c8ff7a80c5d408" dependencies = [ "const-hex", "dirs 6.0.0", - "fs4", "reqwest", "semver 1.0.26", "serde", @@ -8100,9 +8526,9 @@ dependencies = [ [[package]] name = "svm-rs-builds" -version = "0.5.16" +version = "0.5.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ed5035d2abae3cd98c201b116ff22a82861589c060f3e4b687ce951cf381a6e" +checksum = "c1ebe77b200f965e8dbec3ef1d8337e974179ca1ecaa9fc28f67288d6b438159" dependencies = [ "const-hex", "semver 1.0.26", @@ -8242,6 +8668,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -8504,6 +8939,15 @@ dependencies = [ "rustc-serialize", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.23" @@ -8866,6 +9310,136 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "uniffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb08c58c7ed7033150132febe696bef553f891b1ede57424b40d87a89e3c170" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_build", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cade167af943e189a55020eda2c314681e223f1e42aca7c4e52614c2b627698f" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck 0.5.0", + "once_cell", + "paste", + "serde", + "textwrap", + "toml 0.5.11", + "uniffi_meta", + "uniffi_udl", +] + +[[package]] +name = "uniffi_build" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7cf32576e08104b7dc2a6a5d815f37616e66c6866c2a639fe16e6d2286b75b" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a" +dependencies = [ + "quote", + "syn 2.0.104", +] + +[[package]] +name = "uniffi_core" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f" +dependencies = [ + "anyhow", + "bytes", + "log", + "once_cell", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c65a5b12ec544ef136693af8759fb9d11aefce740fb76916721e876639033b" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.104", + "toml 0.5.11", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a74ed96c26882dac1ca9b93ca23c827e284bacbd7ec23c6f0b0372f747d59e4" +dependencies = [ + "anyhow", + "bytes", + "siphasher 0.3.11", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6f984f0781f892cc864a62c3a5c60361b1ccbd68e538e6c9fbced5d82268ac" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037820a4cfc4422db1eaa82f291a3863c92c7d1789dc513489c36223f9b4cdfc" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + [[package]] name = "universal-hash" version = "0.5.1" @@ -8894,6 +9468,53 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls 0.23.29", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "ureq" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "percent-encoding", + "rustls 0.23.29", + "rustls-pki-types", + "ureq-proto", + "utf-8", + "webpki-roots 1.0.2", +] + +[[package]] +name = "ureq-proto" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +dependencies = [ + "base64 0.22.1", + "http 1.3.1", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.4" @@ -9143,6 +9764,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + [[package]] name = "which" version = "4.4.2" @@ -9864,21 +10494,24 @@ dependencies = [ [[package]] name = "zip" -version = "2.4.2" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" dependencies = [ "arbitrary", "crc32fast", - "crossbeam-utils", - "displaydoc", "flate2", "indexmap 2.10.0", "memchr", - "thiserror 2.0.12", "zopfli", ] +[[package]] +name = "zlib-rs" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f936044d677be1a1168fae1d03b583a285a5dd9d8cbf7b24c23aa1fc775235" + [[package]] name = "zopfli" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 40ed8f22e..2a6421d9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,10 @@ solar-data-structures = "=0.1.1" solar-macros = "=0.1.1" solar-config = "=0.1.1" nix = { version = "0.29", features = ["resource"] } +ureq = "3.1.4" +quilibrium-names-sdk = { version = "2.1.0", default-features = false, features = ["blocking"] } +svm-rs-builds = "0.5.22" +ed448-goldilocks-plus = "0.16.0" [build-dependencies] tonic-build = "0.9.2" diff --git a/config/sample.toml b/config/sample.toml index 177c17869..b9370368a 100644 --- a/config/sample.toml +++ b/config/sample.toml @@ -9,9 +9,12 @@ stop_at = 200 start_from = 0 url = "https://fnames.farcaster.xyz/transfers" +solana_rpc_url = "" +quilibrium_rpc_url = "" + [consensus] num_shards = 1 shard_ids = [1] [mempool] -queue_size = 500 \ No newline at end of file +queue_size = 500 diff --git a/src/cfg.rs b/src/cfg.rs index 6a5636ba9..8ebdde6a7 100644 --- a/src/cfg.rs +++ b/src/cfg.rs @@ -98,6 +98,8 @@ pub struct Config { pub clear_db: bool, pub statsd: StatsdConfig, pub l1_rpc_url: String, + pub solana_rpc_url: String, + pub quilibrium_rpc_url: String, pub fc_network: FarcasterNetwork, pub read_node: bool, pub pruning: PruningConfig, @@ -124,6 +126,8 @@ impl Default for Config { clear_db: false, statsd: StatsdConfig::default(), l1_rpc_url: "".to_string(), + solana_rpc_url: "".to_string(), + quilibrium_rpc_url: "".to_string(), fc_network: FarcasterNetwork::Devnet, snapshot: storage::db::snapshot::Config::default(), read_node: false, diff --git a/src/connectors/onchain_events/mod.rs b/src/connectors/onchain_events/mod.rs index 2683a51d0..4bb9c1d4d 100644 --- a/src/connectors/onchain_events/mod.rs +++ b/src/connectors/onchain_events/mod.rs @@ -8,16 +8,23 @@ use alloy_rpc_types::{Filter, Log}; use alloy_sol_types::{sol, SolEvent}; use alloy_transport_http::{Client, Http}; use async_trait::async_trait; +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use base64::Engine; use foundry_common::ens::EnsResolver::EnsResolverInstance; use foundry_common::ens::{namehash, EnsError, EnsRegistry}; use futures_util::stream::StreamExt; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::str::FromStr; +use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use thiserror::Error; -use tokio::sync::{broadcast, mpsc}; -use tracing::{error, info, warn}; +use tokio::{ + sync::{broadcast, mpsc}, + task::spawn_blocking, +}; +use tracing::{debug, error, info, warn}; use crate::core::error::HubError; use crate::mempool::mempool::{MempoolRequest, MempoolSource}; @@ -78,6 +85,132 @@ const RENT_EXPIRY_IN_SECONDS: u64 = 365 * 24 * 60 * 60; // One year const RETRY_TIMEOUT_SECONDS: u64 = 10; const BASE_BLOCK_PAGE_SIZE: u64 = 8000; // Alchemy max is 10K +const NAME_SERVICE_PROGRAM_ID: &str = "namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX"; +const SOL_TLD_LABEL: &str = "sol"; + +// Solana Name Service derivation constants +const HASH_PREFIX: &str = "SPL Name Service"; +const ROOT_DOMAIN_ACCOUNT: &str = "58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx"; + +mod derivation { + use super::*; + use ed25519_dalek::VerifyingKey; + + /// Derive the domain key for a Solana Name Service domain. + /// This is a minimal implementation based on the Bonfida SNS SDK and SPL Name Service. + pub fn get_domain_key(domain: &str) -> Result<[u8; 32], String> { + // Hash the domain name with the SNS prefix + let hashed_name = hash_domain_name(domain); + + // Decode the root domain account + let root_domain = bs58::decode(ROOT_DOMAIN_ACCOUNT) + .into_vec() + .map_err(|e| format!("Failed to decode root domain: {}", e))?; + + if root_domain.len() != 32 { + return Err("Invalid root domain length".to_string()); + } + let mut root_array = [0u8; 32]; + root_array.copy_from_slice(&root_domain); + + // Decode the name service program ID + let program_id = bs58::decode(NAME_SERVICE_PROGRAM_ID) + .into_vec() + .map_err(|e| format!("Failed to decode program ID: {}", e))?; + + if program_id.len() != 32 { + return Err("Invalid program ID length".to_string()); + } + let mut program_array = [0u8; 32]; + program_array.copy_from_slice(&program_id); + + // Derive the PDA (Program Derived Address) using the SPL Name Service algorithm + find_program_address(&program_array, &hashed_name, None, Some(&root_array)) + } + + fn hash_domain_name(domain: &str) -> Vec { + let prefixed = format!("{}{}", HASH_PREFIX, domain); + Sha256::digest(prefixed.as_bytes()).to_vec() + } + + /// Find a program-derived address for the given seeds and program ID. + /// Based on Solana's find_program_address implementation. + fn find_program_address( + program_id: &[u8; 32], + hashed_name: &[u8], + name_class: Option<&[u8; 32]>, + parent: Option<&[u8; 32]>, + ) -> Result<[u8; 32], String> { + // Build the seeds vector according to SPL Name Service algorithm + let mut seeds_vec = Vec::new(); + seeds_vec.extend_from_slice(hashed_name); + + // Add name_class (or zeros if None) + if let Some(class) = name_class { + seeds_vec.extend_from_slice(class); + } else { + seeds_vec.extend_from_slice(&[0u8; 32]); + } + + // Add parent (or zeros if None) + if let Some(parent_addr) = parent { + seeds_vec.extend_from_slice(parent_addr); + } else { + seeds_vec.extend_from_slice(&[0u8; 32]); + } + + // Split into 32-byte chunks as required by Solana PDA derivation + let seed_chunks: Vec<&[u8]> = seeds_vec.chunks(32).collect(); + + // Try bumps from 255 down to 0 + for bump in (0..=255u8).rev() { + if let Ok(pda) = create_program_address(&seed_chunks, bump, program_id) { + return Ok(pda); + } + } + + Err("Could not find valid PDA".to_string()) + } + + /// Create a program address from seeds and program ID. + /// Based on Solana's create_program_address implementation. + fn create_program_address( + seeds: &[&[u8]], + bump: u8, + program_id: &[u8; 32], + ) -> Result<[u8; 32], String> { + // Hash: seeds || [bump] || program_id || "ProgramDerivedAddress" + let mut hasher = Sha256::new(); + for seed in seeds { + hasher.update(seed); + } + hasher.update(&[bump]); + hasher.update(program_id); + hasher.update(b"ProgramDerivedAddress"); + let hash = hasher.finalize(); + + // Check if the resulting address is NOT on the Ed25519 curve + if is_on_curve(&hash) { + return Err("Address is on curve".to_string()); + } + + let mut result = [0u8; 32]; + result.copy_from_slice(&hash); + Ok(result) + } + + fn is_on_curve(pubkey: &[u8]) -> bool { + if pubkey.len() != 32 { + return false; + } + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(pubkey); + + // Try to create a verifying key from the bytes + // If it succeeds, the point is on the curve + VerifyingKey::from_bytes(&bytes).is_ok() + } +} #[derive(Debug, Serialize, Deserialize)] pub struct Config { @@ -166,6 +299,470 @@ pub enum Chain { OptimismMainnet, } +#[derive(Error, Debug)] +pub enum SolanaNameServiceError { + #[error("solana name must end with .sol")] + InvalidSuffix, + #[error("solana name missing label")] + MissingLabel, + #[error("solana account owner mismatch")] + InvalidAccountOwner, + #[error("solana rpc error: {0}")] + Rpc(String), + #[error("solana account not found")] + AccountNotFound, + #[error("solana account data invalid: {0}")] + AccountData(String), + #[error("unable to decode solana account data: {0}")] + Base64(String), + #[error("failed to derive solana name address: {0}")] + AddressDerivation(String), +} + +#[async_trait] +pub trait SolanaNameResolver: Send + Sync { + async fn resolve(&self, name: String) -> Result, SolanaNameServiceError>; +} + +pub struct SolanaNameService { + agent: Arc, + rpc_url: String, +} + +impl SolanaNameService { + pub fn new(rpc_url: String) -> Result { + let agent = Arc::new(ureq::Agent::new_with_defaults()); + Ok(Self { agent, rpc_url }) + } + + async fn fetch_account_owner( + &self, + account_pubkey: &[u8; 32], + ) -> Result, SolanaNameServiceError> { + let account_key = bs58::encode(account_pubkey).into_string(); + let payload = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "getAccountInfo", + "params": [account_key, { "encoding": "base64" }] + }); + let agent = Arc::clone(&self.agent); + let rpc_url = self.rpc_url.clone(); + let payload_string = payload.to_string(); + let rpc_response: RpcResponse = spawn_blocking( + move || -> Result, SolanaNameServiceError> { + let mut response = agent + .post(&rpc_url) + .header("Content-Type", "application/json") + .send(payload_string.into_bytes()) + .map_err(|err| SolanaNameServiceError::Rpc(err.to_string()))?; + let body = response + .body_mut() + .read_to_string() + .map_err(|err| SolanaNameServiceError::Rpc(err.to_string()))?; + serde_json::from_str::>(&body) + .map_err(|err| SolanaNameServiceError::Rpc(err.to_string())) + }, + ) + .await + .map_err(|err| SolanaNameServiceError::Rpc(err.to_string()))??; + + if let Some(error) = rpc_response.error { + return Err(SolanaNameServiceError::Rpc(error.message)); + } + + let account = rpc_response + .result + .and_then(|result| result.value) + .ok_or(SolanaNameServiceError::AccountNotFound)?; + + if account.owner != NAME_SERVICE_PROGRAM_ID { + return Err(SolanaNameServiceError::InvalidAccountOwner); + } + + let data = BASE64_STANDARD + .decode(account.data.0) + .map_err(|err| SolanaNameServiceError::Base64(err.to_string()))?; + + if data.len() < 64 { + return Err(SolanaNameServiceError::AccountData( + "account data too short".to_string(), + )); + } + + Ok(data[32..64].to_vec()) + } + + async fn resolve_inner(&self, name: String) -> Result, SolanaNameServiceError> { + let normalized = name.trim().to_lowercase(); + if !normalized.ends_with(".sol") { + return Err(SolanaNameServiceError::InvalidSuffix); + } + let label = normalized.trim_end_matches(".sol"); + if label.is_empty() { + return Err(SolanaNameServiceError::MissingLabel); + } + + let name_account = Self::derive_domain_account(label)?; + self.fetch_account_owner(&name_account).await + } + + fn derive_domain_account(label: &str) -> Result<[u8; 32], SolanaNameServiceError> { + derivation::get_domain_key(label) + .map_err(|err| SolanaNameServiceError::AddressDerivation(err)) + } +} + +#[derive(Deserialize)] +struct RpcResponse { + result: Option>, + error: Option, +} + +#[derive(Deserialize)] +struct RpcResult { + value: Option, +} + +#[derive(Deserialize)] +struct RpcError { + message: String, +} + +#[derive(Deserialize)] +struct AccountInfo { + data: (String, String), + owner: String, +} + +#[async_trait] +impl SolanaNameResolver for SolanaNameService { + async fn resolve(&self, name: String) -> Result, SolanaNameServiceError> { + self.resolve_inner(name).await + } +} + +// Quilibrium Name Service (QNS) support + +#[derive(Error, Debug)] +pub enum QnsError { + #[error("qns name must end with .q")] + InvalidSuffix, + #[error("qns name missing label")] + MissingLabel, + #[error("qns name not found: {0}")] + NameNotFound(String), + #[error("qns error: {0}")] + Sdk(String), + #[error("qns client not configured")] + NotConfigured, + #[error("qns invalid signature: {0}")] + InvalidSignature(String), +} + +/// Verify an ed448 signature for a QNS username proof. +/// Message format: "FARCASTER_QNS:" + name + ":" + big_endian_u64(fid) +pub fn verify_qns_signature( + name: &str, + fid: u64, + public_key: &[u8], + signature: &[u8], +) -> Result<(), QnsError> { + use ed448_goldilocks_plus::{Signature, VerifyingKey}; + + // Construct the message: "FARCASTER_QNS:" + name + ":" + big_endian_u64(fid) + let mut message = Vec::new(); + message.extend_from_slice(b"FARCASTER_QNS:"); + message.extend_from_slice(name.as_bytes()); + message.extend_from_slice(b":"); + message.extend_from_slice(&fid.to_be_bytes()); + + // Parse the public key (ed448 public keys are 57 bytes) + let pubkey_bytes: [u8; 57] = public_key + .try_into() + .map_err(|_| QnsError::InvalidSignature("public key must be 57 bytes".to_string()))?; + let pubkey = VerifyingKey::from_bytes(&pubkey_bytes) + .map_err(|e| QnsError::InvalidSignature(format!("invalid public key: {:?}", e)))?; + + // Parse the signature (ed448 signatures are 114 bytes) + let sig_bytes: [u8; 114] = signature + .try_into() + .map_err(|_| QnsError::InvalidSignature("signature must be 114 bytes".to_string()))?; + let sig = Signature::from_bytes(&sig_bytes) + .map_err(|e| QnsError::InvalidSignature(format!("invalid signature: {:?}", e)))?; + + // Verify the signature using Ed448 "pure" mode (no context) + pubkey + .verify_raw(&sig, &message) + .map_err(|e| QnsError::InvalidSignature(format!("signature verification failed: {:?}", e))) +} + +#[async_trait] +pub trait QnsResolver: Send + Sync { + async fn resolve(&self, name: String) -> Result, QnsError>; +} + +pub struct QuilibriumNameService { + client: quilibrium_names_sdk::blocking::QnsClient, +} + +impl QuilibriumNameService { + pub fn new(rpc_url: String) -> Result { + let client = if rpc_url.is_empty() { + quilibrium_names_sdk::blocking::QnsClient::default() + } else { + quilibrium_names_sdk::blocking::QnsClient::with_timeout( + &rpc_url, + std::time::Duration::from_secs(30), + ) + }; + Ok(Self { client }) + } + + fn resolve_inner(&self, name: String) -> Result, QnsError> { + let normalized = name.trim().to_lowercase(); + if !normalized.ends_with(".q") { + return Err(QnsError::InvalidSuffix); + } + let label = normalized.trim_end_matches(".q"); + if label.is_empty() { + return Err(QnsError::MissingLabel); + } + + let record = self.client.resolve(&normalized).map_err(|e| match e { + quilibrium_names_sdk::QnsError::NameNotFound(n) => QnsError::NameNotFound(n), + other => QnsError::Sdk(other.to_string()), + })?; + + // The authority_key is the owner's ed448 public key (hex string) + // Convert it to bytes + let owner_hex = record.header.authority_key.trim_start_matches("0x"); + let owner_bytes = hex::decode(owner_hex) + .map_err(|e| QnsError::Sdk(format!("invalid owner hex: {}", e)))?; + + Ok(owner_bytes) + } +} + +#[async_trait] +impl QnsResolver for QuilibriumNameService { + async fn resolve(&self, name: String) -> Result, QnsError> { + // The SDK uses blocking calls, so we use resolve_inner which does sync I/O + // This is acceptable because the SDK is designed for blocking use + self.resolve_inner(name) + } +} + +#[cfg(test)] +mod sol_resolver_tests { + use super::*; + + #[test] + fn bonfida_sol_derives_expected_account() { + let derived = SolanaNameService::derive_domain_account("bonfida").expect("derive bonfida"); + let expected_bytes = bs58::decode("Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb") + .into_vec() + .expect("decode bonfida.sol"); + let expected: [u8; 32] = expected_bytes.try_into().expect("pubkey length"); + assert_eq!(derived, expected); + } + + #[test] + fn derivation_module_derives_bonfida_correctly() { + // Test the derivation module directly + let derived = derivation::get_domain_key("bonfida").expect("derive bonfida"); + let expected_bytes = bs58::decode("Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb") + .into_vec() + .expect("decode bonfida.sol"); + let expected: [u8; 32] = expected_bytes.try_into().expect("pubkey length"); + + assert_eq!( + derived, expected, + "Derived address for 'bonfida' should match expected Solana Name Service address" + ); + + // Verify the derived address as base58 + let derived_base58 = bs58::encode(&derived).into_string(); + assert_eq!( + derived_base58, "Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb", + "Derived address should encode to expected base58 string" + ); + } + + #[test] + fn derivation_produces_off_curve_addresses() { + // Test that derivation produces valid PDAs (off-curve addresses) + use ed25519_dalek::VerifyingKey; + + let derived = derivation::get_domain_key("bonfida").expect("derive bonfida"); + + // A valid PDA should NOT be a valid Ed25519 point + assert!( + VerifyingKey::from_bytes(&derived).is_err(), + "Derived PDA should not be on the Ed25519 curve" + ); + } + + #[test] + fn derivation_is_deterministic() { + // Test that derivation produces the same result every time + let derived1 = derivation::get_domain_key("bonfida").expect("derive bonfida 1"); + let derived2 = derivation::get_domain_key("bonfida").expect("derive bonfida 2"); + let derived3 = derivation::get_domain_key("bonfida").expect("derive bonfida 3"); + + assert_eq!(derived1, derived2, "Derivation should be deterministic"); + assert_eq!(derived2, derived3, "Derivation should be deterministic"); + } + + #[test] + fn different_domains_produce_different_addresses() { + // Test that different domain names produce different addresses + let bonfida = derivation::get_domain_key("bonfida").expect("derive bonfida"); + let solana = derivation::get_domain_key("solana").expect("derive solana"); + + assert_ne!( + bonfida, solana, + "Different domain names should produce different addresses" + ); + } +} + +#[cfg(test)] +mod qns_signature_tests { + use super::*; + + #[test] + fn test_verify_qns_signature_rejects_invalid_pubkey() { + let name = "test.q"; + let fid: u64 = 12345; + let invalid_pubkey = vec![0u8; 10]; // Too short + let signature = vec![0u8; 114]; + + let result = verify_qns_signature(name, fid, &invalid_pubkey, &signature); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("public key"), + "Expected error about public key, got: {}", + err_msg + ); + } + + #[test] + fn test_verify_qns_signature_rejects_invalid_signature() { + let name = "test.q"; + let fid: u64 = 12345; + // Valid length pubkey but random bytes + let pubkey = vec![0u8; 57]; + let invalid_signature = vec![0u8; 10]; // Too short + + let result = verify_qns_signature(name, fid, &pubkey, &invalid_signature); + assert!(result.is_err()); + } + + #[test] + fn test_verify_qns_signature_constructs_correct_message() { + // This test verifies the message format is correct by checking that + // a valid keypair can sign and verify + use ed448_goldilocks_plus::SigningKey; + + let name = "cassie.q"; + let fid: u64 = 12345; + + // Generate a test keypair + let signing_key = SigningKey::generate(&mut rand::thread_rng()); + let verifying_key = signing_key.verifying_key(); + + // Construct the expected message + let mut message = Vec::new(); + message.extend_from_slice(b"FARCASTER_QNS:"); + message.extend_from_slice(name.as_bytes()); + message.extend_from_slice(b":"); + message.extend_from_slice(&fid.to_be_bytes()); + + // Sign the message using raw mode (no context) + let signature = signing_key.sign_raw(&message); + + // Verify using our function + let pubkey_bytes = verifying_key.to_bytes(); + let sig_bytes = signature.to_bytes(); + let result = verify_qns_signature(name, fid, &pubkey_bytes, &sig_bytes); + assert!( + result.is_ok(), + "Valid signature should verify: {:?}", + result + ); + } + + #[test] + fn test_verify_qns_signature_rejects_wrong_fid() { + use ed448_goldilocks_plus::SigningKey; + + let name = "cassie.q"; + let fid: u64 = 12345; + let wrong_fid: u64 = 99999; + + // Generate a test keypair + let signing_key = SigningKey::generate(&mut rand::thread_rng()); + let verifying_key = signing_key.verifying_key(); + + // Construct message with correct fid + let mut message = Vec::new(); + message.extend_from_slice(b"FARCASTER_QNS:"); + message.extend_from_slice(name.as_bytes()); + message.extend_from_slice(b":"); + message.extend_from_slice(&fid.to_be_bytes()); + + // Sign the message + let signature = signing_key.sign_raw(&message); + + // Get pubkey bytes + let pubkey_bytes = verifying_key.to_bytes(); + let sig_bytes = signature.to_bytes(); + + // Verify with wrong fid should fail + let result = verify_qns_signature(name, wrong_fid, &pubkey_bytes, &sig_bytes); + assert!( + result.is_err(), + "Signature with wrong fid should not verify" + ); + } + + #[test] + fn test_verify_qns_signature_rejects_wrong_name() { + use ed448_goldilocks_plus::SigningKey; + + let name = "cassie.q"; + let wrong_name = "alice.q"; + let fid: u64 = 12345; + + // Generate a test keypair + let signing_key = SigningKey::generate(&mut rand::thread_rng()); + let verifying_key = signing_key.verifying_key(); + + // Construct message with correct name + let mut message = Vec::new(); + message.extend_from_slice(b"FARCASTER_QNS:"); + message.extend_from_slice(name.as_bytes()); + message.extend_from_slice(b":"); + message.extend_from_slice(&fid.to_be_bytes()); + + // Sign the message + let signature = signing_key.sign_raw(&message); + + // Get pubkey bytes + let pubkey_bytes = verifying_key.to_bytes(); + let sig_bytes = signature.to_bytes(); + + // Verify with wrong name should fail + let result = verify_qns_signature(wrong_name, fid, &pubkey_bytes, &sig_bytes); + assert!( + result.is_err(), + "Signature with wrong name should not verify" + ); + } +} + impl Chain { pub fn from_chain_id(chain_id: u32) -> Option { match chain_id { @@ -179,6 +776,8 @@ impl Chain { pub struct ChainClients { pub chain_api_map: HashMap>, + pub solana_name_service: Option>, + pub qns_service: Option>, } impl ChainClients { @@ -208,7 +807,33 @@ impl ChainClients { chain_api_map.insert(Chain::OptimismMainnet, client); } - ChainClients { chain_api_map } + let solana_name_service: Option> = + if !app_config.solana_rpc_url.is_empty() { + match SolanaNameService::new(app_config.solana_rpc_url.clone()) { + Ok(client) => Some(Box::new(client)), + Err(err) => { + warn!("Failed to initialize Solana name service: {}", err); + None + } + } + } else { + None + }; + + let qns_service: Option> = + match QuilibriumNameService::new(app_config.quilibrium_rpc_url.clone()) { + Ok(client) => Some(Box::new(client)), + Err(err) => { + warn!("Failed to initialize Quilibrium name service: {}", err); + None + } + }; + + ChainClients { + chain_api_map, + solana_name_service, + qns_service, + } } pub fn for_chain(&self, chain: Chain) -> Result<&Box, HubError> { @@ -219,6 +844,30 @@ impl ChainClients { )), } } + + pub async fn resolve_sol_name(&self, name: String) -> Result, HubError> { + match &self.solana_name_service { + Some(client) => client + .resolve(name) + .await + .map_err(|err| HubError::validation_failure(&err.to_string())), + None => Err(HubError::invalid_internal_state( + "Solana RPC client not configured", + )), + } + } + + pub async fn resolve_qns_name(&self, name: String) -> Result, HubError> { + match &self.qns_service { + Some(client) => client + .resolve(name) + .await + .map_err(|err| HubError::validation_failure(&err.to_string())), + None => Err(HubError::invalid_internal_state( + "Quilibrium name service client not configured", + )), + } + } } pub struct RealL1Client { diff --git a/src/core/validations/error.rs b/src/core/validations/error.rs index c1c23883f..bdb1ed675 100644 --- a/src/core/validations/error.rs +++ b/src/core/validations/error.rs @@ -42,6 +42,18 @@ pub enum ValidationError { EnsNameUnsupportedSubdomain(String), #[error("ensName \"{0}\" is not a valid ENS name")] EnsNameNotValid(String), + #[error("solName \"{0}\" doesn't match {1}")] + SolNameDoesntMatch(String, String), + #[error("solName \"{0}\" > 20 characters")] + SolNameExceedsLength(String), + #[error("solName \"{0}\" doesn't end with {1}")] + SolNameDoesntEndWith(String, String), + #[error("qnsName \"{0}\" doesn't match {1}")] + QnsNameDoesntMatch(String, String), + #[error("qnsName \"{0}\" > 20 characters")] + QnsNameExceedsLength(String), + #[error("qnsName \"{0}\" doesn't end with {1}")] + QnsNameDoesntEndWith(String, String), #[error("text > 1024 bytes for long cast")] TextTooLongForLongCast, #[error("text too short for long cast")] diff --git a/src/core/validations/message.rs b/src/core/validations/message.rs index bd5c14e8e..40c2e3950 100644 --- a/src/core/validations/message.rs +++ b/src/core/validations/message.rs @@ -181,6 +181,26 @@ pub fn validate_message( return Err(ValidationError::UnsupportedFeature); } } + Ok(UserNameType::UsernameTypeSolana) => { + if version.is_enabled(ProtocolFeature::SolanaNamesValidation) { + let name = &std::str::from_utf8(&proof.name) + .map_err(|_| ValidationError::InvalidData)? + .to_string(); + validate_sol_name(name)?; + } else { + return Err(ValidationError::UnsupportedFeature); + } + } + Ok(UserNameType::UsernameTypeQns) => { + if version.is_enabled(ProtocolFeature::QnsNamesValidation) { + let name = &std::str::from_utf8(&proof.name) + .map_err(|_| ValidationError::InvalidData)? + .to_string(); + validate_q_name(name)?; + } else { + return Err(ValidationError::UnsupportedFeature); + } + } _ => return Err(ValidationError::InvalidUsernameType), } } @@ -376,6 +396,68 @@ pub fn validate_base_name(input: &String) -> Result<(), ValidationError> { Ok(()) } +pub fn validate_sol_name(input: &String) -> Result<(), ValidationError> { + if !input.ends_with(".sol") { + return Err(ValidationError::SolNameDoesntEndWith( + input.clone(), + ".sol".to_string(), + )); + } + + let name_parts: Vec<&str> = input.split('.').collect(); + if name_parts.len() != 2 || name_parts[0].is_empty() { + return Err(ValidationError::InvalidData); + } + + if input.len() > 20 { + return Err(ValidationError::SolNameExceedsLength(input.clone())); + } + + if !Regex::new(FNAME_REGEX) + .unwrap() + .is_match(name_parts[0]) + .map_err(|_| ValidationError::InvalidData)? + { + return Err(ValidationError::SolNameDoesntMatch( + input.clone(), + FNAME_REGEX.to_string(), + )); + } + + Ok(()) +} + +pub fn validate_q_name(input: &String) -> Result<(), ValidationError> { + if !input.ends_with(".q") { + return Err(ValidationError::QnsNameDoesntEndWith( + input.clone(), + ".q".to_string(), + )); + } + + let name_parts: Vec<&str> = input.split('.').collect(); + if name_parts.len() != 2 || name_parts[0].is_empty() { + return Err(ValidationError::InvalidData); + } + + if input.len() > 20 { + return Err(ValidationError::QnsNameExceedsLength(input.clone())); + } + + if !Regex::new(FNAME_REGEX) + .unwrap() + .is_match(name_parts[0]) + .map_err(|_| ValidationError::InvalidData)? + { + return Err(ValidationError::QnsNameDoesntMatch( + input.clone(), + FNAME_REGEX.to_string(), + )); + } + + Ok(()) +} + pub fn validate_twitter_username(input: &String) -> Result<(), ValidationError> { if input.len() > 15 { return Err(ValidationError::UsernameExceedsLength(input.clone(), 15)); @@ -596,6 +678,10 @@ pub fn validate_user_data_add_body( validate_base_name(&body.value)?; } else if body.value.ends_with(".eth") { validate_ens_name(&body.value)?; + } else if body.value.ends_with(".sol") { + validate_sol_name(&body.value)?; + } else if body.value.ends_with(".q") { + validate_q_name(&body.value)?; } else { validate_fname(&body.value)?; }; diff --git a/src/core/validations/message_test.rs b/src/core/validations/message_test.rs index a1ab8b71a..417193199 100644 --- a/src/core/validations/message_test.rs +++ b/src/core/validations/message_test.rs @@ -306,6 +306,7 @@ mod tests { "valid-basename.base.eth", "valid-user123", "validusername123", + "validsol.sol", ]; let invalid_names = vec![ @@ -323,6 +324,10 @@ mod tests { "invalid_username!", ValidationError::FnameExceedsLength("invalid_username!".to_string()), ), // Contains special character + ( + "too_long_for_a_sol_name.sol", + ValidationError::SolNameExceedsLength("too_long_for_a_sol_name.sol".to_string()), + ), ]; for name in valid_names { let msg = create_user_data_add( diff --git a/src/core/validations/validations_test.rs b/src/core/validations/validations_test.rs index 91ef6c844..832c125c9 100644 --- a/src/core/validations/validations_test.rs +++ b/src/core/validations/validations_test.rs @@ -1,8 +1,8 @@ mod tests { use crate::core::validations::error::ValidationError; use crate::core::validations::message::{ - validate_user_data_add_body, validate_user_data_primary_address_ethereum, - validate_user_data_primary_address_solana, + validate_q_name, validate_sol_name, validate_user_data_add_body, + validate_user_data_primary_address_ethereum, validate_user_data_primary_address_solana, }; use crate::core::validations::verification::{validate_add_address, validate_fname_transfer}; use crate::proto; @@ -329,4 +329,154 @@ mod tests { let result = validate_user_data_add_body(&user_data_body_sol, false, EngineVersion::V5); assert!(result.is_ok()); } + + // Tests for validate_sol_name + + #[test] + fn test_validate_sol_name_valid() { + let name = "bonfida.sol".to_string(); + let result = validate_sol_name(&name); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_sol_name_valid_short() { + let name = "a.sol".to_string(); + let result = validate_sol_name(&name); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_sol_name_missing_suffix() { + let name = "bonfida".to_string(); + let result = validate_sol_name(&name); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ValidationError::SolNameDoesntEndWith(_, _) + )); + } + + #[test] + fn test_validate_sol_name_wrong_suffix() { + let name = "bonfida.eth".to_string(); + let result = validate_sol_name(&name); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ValidationError::SolNameDoesntEndWith(_, _) + )); + } + + #[test] + fn test_validate_sol_name_too_long() { + let name = "averylongsolananamethatexceeds.sol".to_string(); + let result = validate_sol_name(&name); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ValidationError::SolNameExceedsLength(_) + )); + } + + #[test] + fn test_validate_sol_name_empty_label() { + let name = ".sol".to_string(); + let result = validate_sol_name(&name); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ValidationError::InvalidData)); + } + + #[test] + fn test_validate_sol_name_invalid_chars() { + let name = "invalid@name.sol".to_string(); + let result = validate_sol_name(&name); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ValidationError::SolNameDoesntMatch(_, _) + )); + } + + // Tests for validate_q_name + + #[test] + fn test_validate_q_name_valid() { + let name = "cassie.q".to_string(); + let result = validate_q_name(&name); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_q_name_valid_short() { + let name = "a.q".to_string(); + let result = validate_q_name(&name); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_q_name_missing_suffix() { + let name = "cassie".to_string(); + let result = validate_q_name(&name); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ValidationError::QnsNameDoesntEndWith(_, _) + )); + } + + #[test] + fn test_validate_q_name_wrong_suffix() { + let name = "cassie.eth".to_string(); + let result = validate_q_name(&name); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ValidationError::QnsNameDoesntEndWith(_, _) + )); + } + + #[test] + fn test_validate_q_name_too_long() { + let name = "averylongqnsnamethatis.q".to_string(); + let result = validate_q_name(&name); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ValidationError::QnsNameExceedsLength(_) + )); + } + + #[test] + fn test_validate_q_name_empty_label() { + let name = ".q".to_string(); + let result = validate_q_name(&name); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ValidationError::InvalidData)); + } + + #[test] + fn test_validate_q_name_invalid_chars() { + let name = "invalid@name.q".to_string(); + let result = validate_q_name(&name); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + ValidationError::QnsNameDoesntMatch(_, _) + )); + } + + #[test] + fn test_validate_q_name_with_numbers() { + let name = "test123.q".to_string(); + let result = validate_q_name(&name); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_q_name_with_hyphen() { + let name = "test-name.q".to_string(); + let result = validate_q_name(&name); + assert!(result.is_ok()); + } } diff --git a/src/network/server.rs b/src/network/server.rs index bc766bbfd..ae681e67f 100644 --- a/src/network/server.rs +++ b/src/network/server.rs @@ -156,15 +156,17 @@ impl MyHubService { if let Some(message_data) = &message.data { match &message_data.body { Some(proto::message_data::Body::UserDataBody(user_data)) => { - if user_data.r#type() == proto::UserDataType::Username { - if user_data.value.ends_with(".eth") { - self.validate_ens_username(fid, user_data.value.to_string()) - .await?; - } + if user_data.r#type() == proto::UserDataType::Username + && (user_data.value.ends_with(".eth") + || user_data.value.ends_with(".sol") + || user_data.value.ends_with(".q")) + { + self.validate_external_username(fid, user_data.value.to_string()) + .await?; }; } Some(proto::message_data::Body::UsernameProofBody(proof)) => { - self.validate_ens_username_proof(fid, &proof).await?; + self.validate_external_username_proof(fid, &proof).await?; } Some(proto::message_data::Body::VerificationAddAddressBody(body)) => { if body.verification_type == 1 { @@ -390,18 +392,33 @@ impl MyHubService { }) } - pub async fn validate_ens_username_proof( + pub async fn validate_external_username_proof( &self, fid: u64, proof: &UserNameProof, ) -> Result<(), HubError> { - let resolved_ens_address = self.resolve_ens_address(proof).await?; - if resolved_ens_address != proof.owner { + let resolved_owner = self.resolve_username_owner(proof).await?; + if resolved_owner != proof.owner { return Err(HubError::validation_failure( "invalid ens name, resolved address doesn't match proof owner address", )); } + // QNS names are self-evidentiary - ownership is proven by verifying the ed448 signature + // over the message "FARCASTER_QNS:" + name + ":" + big_endian_u64(fid) + if UserNameType::try_from(proof.r#type) == Ok(UserNameType::UsernameTypeQns) { + let name = std::str::from_utf8(&proof.name) + .map_err(|_| HubError::validation_failure("QNS name is not valid utf8"))?; + crate::connectors::onchain_events::verify_qns_signature( + name, + fid, + &proof.owner, + &proof.signature, + ) + .map_err(|e| HubError::validation_failure(&e.to_string()))?; + return Ok(()); + } + let stores = self .get_stores_for(fid) .map_err(|_| HubError::internal_db_error("stores not found for fid"))?; @@ -417,11 +434,11 @@ impl MyHubService { match id_register.body { Some(Body::IdRegisterEventBody(id_register)) => { // Check verified addresses if the resolved address doesn't match the custody address - if id_register.to != resolved_ens_address { + if id_register.to != resolved_owner { let verification = VerificationStore::get_verification_add( &stores.verification_store, fid, - &resolved_ens_address, + &resolved_owner, None, )?; @@ -439,7 +456,7 @@ impl MyHubService { } } - async fn resolve_ens_address(&self, proof: &UserNameProof) -> Result, HubError> { + async fn resolve_username_owner(&self, proof: &UserNameProof) -> Result, HubError> { let name = std::str::from_utf8(&proof.name) .map_err(|_| HubError::validation_failure("ENS name is not utf8"))?; @@ -460,6 +477,22 @@ impl MyHubService { } self.chain_clients.for_chain(Chain::BaseMainnet)? } + Ok(UserNameType::UsernameTypeSolana) => { + if !name.ends_with(".sol") { + return Err(HubError::validation_failure( + "Solana name does not end with .sol", + )); + } + return self.chain_clients.resolve_sol_name(name.to_string()).await; + } + Ok(UserNameType::UsernameTypeQns) => { + if !name.ends_with(".q") { + return Err(HubError::validation_failure( + "Quilibrium name does not end with .q", + )); + } + return self.chain_clients.resolve_qns_name(name.to_string()).await; + } _ => { return Err(HubError::validation_failure( format!( @@ -484,7 +517,7 @@ impl MyHubService { Ok(resolved_ens_address) } - async fn validate_ens_username(&self, fid: u64, name: String) -> Result<(), HubError> { + async fn validate_external_username(&self, fid: u64, name: String) -> Result<(), HubError> { let stores = self .get_stores_for(fid) .map_err(|_| HubError::invalid_parameter("stores not found for fid"))?; @@ -499,7 +532,7 @@ impl MyHubService { Some(message_data) => match message_data.body { Some(body) => match body { proto::message_data::Body::UsernameProofBody(proof) => { - self.validate_ens_username_proof(fid, &proof).await + self.validate_external_username_proof(fid, &proof).await } _ => Err(HubError::validation_failure( "username proof has wrong type", @@ -1791,9 +1824,9 @@ impl HubService for MyHubService { let req = request.into_inner(); let name_str = std::str::from_utf8(&req.name).unwrap_or(""); - // Check if this is an .eth name (look in username_proof_store) or fname (look in user_data_store) - if name_str.ends_with(".eth") { - // Look for ENS username proofs in the username_proof_store + // Check if this is an .eth/.sol/.q name (look in username_proof_store) or fname (look in user_data_store) + if name_str.ends_with(".eth") || name_str.ends_with(".sol") || name_str.ends_with(".q") { + // Look for external username proofs in the username_proof_store let proof_opt = self.shard_stores.iter().find_map(|(_shard_entry, stores)| { match UsernameProofStore::get_username_proof( &stores.username_proof_store, @@ -1817,7 +1850,7 @@ impl HubService for MyHubService { Ok(Response::new(proof_message)) } else { Err(Status::not_found( - "ENS username proof not found".to_string(), + "External username proof not found".to_string(), )) } } else { diff --git a/src/network/server_tests.rs b/src/network/server_tests.rs index fe9239ee6..1208f8e7d 100644 --- a/src/network/server_tests.rs +++ b/src/network/server_tests.rs @@ -9,7 +9,9 @@ mod tests { use std::time::{Duration, Instant}; use tokio::time::{sleep, timeout}; - use crate::connectors::onchain_events::{Chain, ChainAPI, ChainClients}; + use crate::connectors::onchain_events::{ + Chain, ChainAPI, ChainClients, SolanaNameResolver, SolanaNameServiceError, + }; use crate::core::validations::{self, verification::VerificationAddressClaim}; use crate::mempool::mempool::{self, Mempool}; use crate::mempool::routing; @@ -83,6 +85,21 @@ mod tests { } } + struct MockSolanaResolver { + resolved: Vec, + } + + #[async_trait] + impl SolanaNameResolver for MockSolanaResolver { + async fn resolve(&self, name: String) -> Result, SolanaNameServiceError> { + if name == "username.sol" { + Ok(self.resolved.clone()) + } else { + Err(SolanaNameServiceError::InvalidSuffix) + } + } + } + async fn subscribe_and_listen( service: &MyHubService, shard_id: u32, @@ -192,6 +209,21 @@ mod tests { MyHubService, broadcast::Sender, broadcast::Sender, + ) { + make_server_with_resolver(rpc_auth, None).await + } + + async fn make_server_with_resolver( + rpc_auth: Option, + solana_resolver: Option>, + ) -> ( + HashMap, + HashMap, + [ShardEngine; 2], + BlockEngine, + MyHubService, + broadcast::Sender, + broadcast::Sender, ) { let (msgs_request_tx, msgs_request_rx) = mpsc::channel(100); @@ -272,6 +304,8 @@ mod tests { let mut chain_clients = ChainClients { chain_api_map: HashMap::new(), + solana_name_service: solana_resolver, + qns_service: None, }; chain_clients.chain_api_map.insert( Chain::EthMainnet, @@ -1004,7 +1038,7 @@ mod tests { }; let result = service - .validate_ens_username_proof(fid, &username_proof) + .validate_external_username_proof(fid, &username_proof) .await; assert!(result.is_ok()); @@ -1037,7 +1071,7 @@ mod tests { }; let result = service - .validate_ens_username_proof(fid, &username_proof) + .validate_external_username_proof(fid, &username_proof) .await; assert!(result.is_ok()); @@ -1063,6 +1097,45 @@ mod tests { assert_eq!(result.unwrap().into_inner(), user_data_add); } + #[tokio::test] + async fn test_solana_proof() { + let owner = hex::decode("91031dcfdea024b4d51e775486111d2b2a715871").unwrap(); + let ( + _stores, + _senders, + [mut engine1, mut _engine2], + _block_engine, + service, + _shard_decision_tx, + _block_decision_tx, + ) = make_server_with_resolver( + None, + Some(Box::new(MockSolanaResolver { + resolved: owner.clone(), + })), + ) + .await; + let signer = test_helper::default_signer(); + let fid = SHARD1_FID; + + test_helper::register_user(fid, signer.clone(), owner.clone(), &mut engine1).await; + + let username_proof = UserNameProof { + timestamp: messages_factory::farcaster_time() as u64, + name: b"username.sol".to_vec(), + owner, + signature: "signature".to_string().encode_to_vec(), + fid, + r#type: UserNameType::UsernameTypeSolana as i32, + }; + + let result = service + .validate_external_username_proof(fid, &username_proof) + .await; + + assert!(result.is_ok()); + } + #[tokio::test] async fn test_ens_proof_with_bad_owner() { let ( @@ -1091,7 +1164,7 @@ mod tests { // Proof owner does not match owner of ens name let result = service - .validate_ens_username_proof(fid, &username_proof) + .validate_external_username_proof(fid, &username_proof) .await; assert!(result.is_err()); } @@ -1129,7 +1202,7 @@ mod tests { }; let result = service - .validate_ens_username_proof(fid, &username_proof) + .validate_external_username_proof(fid, &username_proof) .await; assert!(result.is_err()); @@ -1175,7 +1248,7 @@ mod tests { }; let result = service - .validate_ens_username_proof(fid, &username_proof) + .validate_external_username_proof(fid, &username_proof) .await; assert!(result.is_ok()); diff --git a/src/proto/username_proof.proto b/src/proto/username_proof.proto index f67ea20c9..2a9b03f1a 100644 --- a/src/proto/username_proof.proto +++ b/src/proto/username_proof.proto @@ -5,6 +5,8 @@ enum UserNameType { USERNAME_TYPE_FNAME = 1; USERNAME_TYPE_ENS_L1 = 2; USERNAME_TYPE_BASENAME = 3; + USERNAME_TYPE_SOLANA = 4; + USERNAME_TYPE_QNS = 5; } message UserNameProof { @@ -14,4 +16,4 @@ message UserNameProof { bytes signature = 4; uint64 fid = 5; UserNameType type = 6; -} \ No newline at end of file +} diff --git a/src/storage/store/engine.rs b/src/storage/store/engine.rs index 84534d7ab..4883b9fb8 100644 --- a/src/storage/store/engine.rs +++ b/src/storage/store/engine.rs @@ -1399,7 +1399,7 @@ impl ShardEngine { txn: &RocksDbTransactionBatch, ) -> Result, MessageValidationError> { // TODO(aditi): The fnames proofs should live in the username proof store. - if name.ends_with(".eth") { + if name.ends_with(".eth") || name.ends_with(".sol") { let version = EngineVersion::current(self.network); let batch_txn = if version.is_enabled(ProtocolFeature::DependentMessagesInBulkSubmit) { txn diff --git a/src/version/version.rs b/src/version/version.rs index 545c201f7..befa5a1b3 100644 --- a/src/version/version.rs +++ b/src/version/version.rs @@ -3,7 +3,7 @@ use crate::proto::FarcasterNetwork; use strum::IntoEnumIterator; use strum_macros::EnumIter; -const LATEST_PROTOCOL_VERSION: u32 = 10; +const LATEST_PROTOCOL_VERSION: u32 = 11; #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, EnumIter)] pub enum EngineVersion { @@ -23,6 +23,7 @@ pub enum EngineVersion { V13 = 13, V14 = 14, V15 = 15, + V16 = 16, } pub enum ProtocolFeature { @@ -44,6 +45,8 @@ pub enum ProtocolFeature { StorageLendingLimitFix, StopRevokingExistingMessages, IncreaseUsernameProofSizeLimit, + SolanaNamesValidation, + QnsNamesValidation, } pub struct VersionSchedule { @@ -173,7 +176,7 @@ const ENGINE_VERSION_SCHEDULE_TESTNET: &[VersionSchedule] = [ const ENGINE_VERSION_SCHEDULE_DEVNET: &[VersionSchedule] = [VersionSchedule { active_at: 0, - version: EngineVersion::V15, + version: EngineVersion::V16, }] .as_slice(); @@ -225,6 +228,9 @@ impl EngineVersion { ProtocolFeature::StorageLendingLimitFix => self >= &EngineVersion::V13, ProtocolFeature::StopRevokingExistingMessages => self >= &EngineVersion::V14, ProtocolFeature::IncreaseUsernameProofSizeLimit => self >= &EngineVersion::V15, + ProtocolFeature::SolanaNamesValidation | ProtocolFeature::QnsNamesValidation => { + self >= &EngineVersion::V16 + } } } @@ -243,7 +249,8 @@ impl EngineVersion { EngineVersion::V10 => 7, EngineVersion::V11 | EngineVersion::V12 | EngineVersion::V13 => 8, EngineVersion::V14 => 9, - EngineVersion::V15 => LATEST_PROTOCOL_VERSION, + EngineVersion::V15 => 10, + EngineVersion::V16 => LATEST_PROTOCOL_VERSION, } } @@ -413,7 +420,7 @@ mod version_test { #[test] fn test_latest() { - assert_eq!(EngineVersion::latest(), EngineVersion::V15); + assert_eq!(EngineVersion::latest(), EngineVersion::V16); assert_eq!( EngineVersion::version_for(&FarcasterTime::current(), FarcasterNetwork::Devnet), EngineVersion::latest() diff --git a/tests/consensus_test.rs b/tests/consensus_test.rs index d632a163f..2dd6092ce 100644 --- a/tests/consensus_test.rs +++ b/tests/consensus_test.rs @@ -441,6 +441,8 @@ impl NodeForTest { gossip_tx.clone(), ChainClients { chain_api_map: Default::default(), + solana_name_service: None, + qns_service: None, }, "".to_string(), "".to_string(), diff --git a/tests/quilibrium_resolver_test.rs b/tests/quilibrium_resolver_test.rs new file mode 100644 index 000000000..199ca08f6 --- /dev/null +++ b/tests/quilibrium_resolver_test.rs @@ -0,0 +1,76 @@ +use snapchain::connectors::onchain_events::QnsResolver; +use snapchain::connectors::onchain_events::QuilibriumNameService; + +/// Integration test that can be run manually against a live Quilibrium RPC endpoint. +/// +/// Set the following env vars before running to enable the test: +/// - `QNS_TEST_RPC_URL`: full RPC URL (or leave empty to use the default public RPC) +/// - `QNS_TEST_NAME`: the `.q` name to resolve (e.g. `cassie.q`) +/// - `QNS_TEST_EXPECTED_OWNER`: expected owner as a hex string (with or without 0x) +#[tokio::test] +async fn test_qns_resolver_with_public_rpc() { + let rpc_url = std::env::var("QNS_TEST_RPC_URL").unwrap_or_default(); + + let (name, expected_owner_hex) = match ( + std::env::var("QNS_TEST_NAME"), + std::env::var("QNS_TEST_EXPECTED_OWNER"), + ) { + (Ok(name), Ok(owner)) if !name.is_empty() && !owner.is_empty() => (name, owner), + _ => return, + }; + + let mut owner_bytes = expected_owner_hex.trim_start_matches("0x").to_string(); + if owner_bytes.len() % 2 == 1 { + owner_bytes = format!("0{}", owner_bytes); + } + let expected_owner = + hex::decode(owner_bytes).expect("QNS_TEST_EXPECTED_OWNER must be valid hex bytes"); + + let resolver: QuilibriumNameService = + QuilibriumNameService::new(rpc_url).expect("invalid quilibrium rpc url"); + let resolved_owner: Vec = resolver + .resolve(name.clone()) + .await + .expect("failed to resolve quilibrium name"); + + assert_eq!( + resolved_owner, expected_owner, + "resolved owner mismatch for {name}" + ); +} + +/// Test that the resolver rejects names without the .q suffix +#[tokio::test] +async fn test_qns_resolver_rejects_invalid_suffix() { + let rpc_url = std::env::var("QNS_TEST_RPC_URL").unwrap_or_default(); + + let resolver: QuilibriumNameService = + QuilibriumNameService::new(rpc_url).expect("invalid quilibrium rpc url"); + + let result = resolver.resolve("test.eth".to_string()).await; + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.to_string().contains("must end with .q"), + "expected invalid suffix error, got: {}", + err + ); +} + +/// Test that the resolver rejects names with empty labels +#[tokio::test] +async fn test_qns_resolver_rejects_empty_label() { + let rpc_url = std::env::var("QNS_TEST_RPC_URL").unwrap_or_default(); + + let resolver: QuilibriumNameService = + QuilibriumNameService::new(rpc_url).expect("invalid quilibrium rpc url"); + + let result = resolver.resolve(".q".to_string()).await; + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.to_string().contains("missing label"), + "expected missing label error, got: {}", + err + ); +} diff --git a/tests/solana_derivation_test.rs b/tests/solana_derivation_test.rs new file mode 100644 index 000000000..72506d195 --- /dev/null +++ b/tests/solana_derivation_test.rs @@ -0,0 +1,134 @@ +// Integration test for Solana Name Service derivation +// This test can be run independently: cargo test --test test_solana_derivation + +use sha2::{Digest, Sha256}; + +const HASH_PREFIX: &str = "SPL Name Service"; +const ROOT_DOMAIN_ACCOUNT: &str = "58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx"; +const NAME_SERVICE_PROGRAM_ID: &str = "namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX"; + +fn hash_domain_name(domain: &str) -> Vec { + let prefixed = format!("{}{}", HASH_PREFIX, domain); + Sha256::digest(prefixed.as_bytes()).to_vec() +} + +fn create_program_address( + seeds: &[&[u8]], + bump: u8, + program_id: &[u8; 32], +) -> Result<[u8; 32], String> { + use ed25519_dalek::VerifyingKey; + + let mut hasher = Sha256::new(); + for seed in seeds { + hasher.update(seed); + } + hasher.update(&[bump]); + hasher.update(program_id); + hasher.update(b"ProgramDerivedAddress"); + let hash = hasher.finalize(); + + // Check if on curve + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(&hash); + + if VerifyingKey::from_bytes(&bytes).is_ok() { + return Err("Address is on curve".to_string()); + } + + Ok(bytes) +} + +fn find_program_address( + program_id: &[u8; 32], + hashed_name: &[u8], + name_class: Option<&[u8; 32]>, + parent: Option<&[u8; 32]>, +) -> Result<[u8; 32], String> { + let mut seeds_vec = Vec::new(); + seeds_vec.extend_from_slice(hashed_name); + + if let Some(class) = name_class { + seeds_vec.extend_from_slice(class); + } else { + seeds_vec.extend_from_slice(&[0u8; 32]); + } + + if let Some(parent_addr) = parent { + seeds_vec.extend_from_slice(parent_addr); + } else { + seeds_vec.extend_from_slice(&[0u8; 32]); + } + + let seed_chunks: Vec<&[u8]> = seeds_vec.chunks(32).collect(); + + for bump in (0..=255u8).rev() { + if let Ok(pda) = create_program_address(&seed_chunks, bump, program_id) { + return Ok(pda); + } + } + + Err("Could not find valid PDA".to_string()) +} + +fn get_domain_key(domain: &str) -> Result<[u8; 32], String> { + let hashed_name = hash_domain_name(domain); + + let root_domain = bs58::decode(ROOT_DOMAIN_ACCOUNT) + .into_vec() + .map_err(|e| format!("Failed to decode root domain: {}", e))?; + if root_domain.len() != 32 { + return Err("Invalid root domain length".to_string()); + } + let mut root_array = [0u8; 32]; + root_array.copy_from_slice(&root_domain); + + let program_id = bs58::decode(NAME_SERVICE_PROGRAM_ID) + .into_vec() + .map_err(|e| format!("Failed to decode program ID: {}", e))?; + if program_id.len() != 32 { + return Err("Invalid program ID length".to_string()); + } + let mut program_array = [0u8; 32]; + program_array.copy_from_slice(&program_id); + + find_program_address(&program_array, &hashed_name, None, Some(&root_array)) +} + +#[test] +fn test_bonfida_derivation() { + let derived = get_domain_key("bonfida").expect("derive bonfida"); + let expected_bytes = bs58::decode("Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb") + .into_vec() + .expect("decode expected"); + let expected: [u8; 32] = expected_bytes.try_into().expect("convert to array"); + + assert_eq!( + derived, expected, + "bonfida.sol should derive to Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb" + ); + + // Also verify as base58 + let derived_base58 = bs58::encode(&derived).into_string(); + assert_eq!( + derived_base58, + "Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb" + ); +} + +#[test] +fn test_derivation_is_deterministic() { + let derived1 = get_domain_key("bonfida").expect("derive 1"); + let derived2 = get_domain_key("bonfida").expect("derive 2"); + assert_eq!(derived1, derived2, "Derivation should be deterministic"); +} + +#[test] +fn test_different_domains_differ() { + let bonfida = get_domain_key("bonfida").expect("derive bonfida"); + let solana = get_domain_key("solana").expect("derive solana"); + assert_ne!( + bonfida, solana, + "Different domains should have different addresses" + ); +} diff --git a/tests/solana_resolver_test.rs b/tests/solana_resolver_test.rs new file mode 100644 index 000000000..89ffd4ec1 --- /dev/null +++ b/tests/solana_resolver_test.rs @@ -0,0 +1,43 @@ +use snapchain::connectors::onchain_events::SolanaNameResolver; +use snapchain::connectors::onchain_events::SolanaNameService; + +/// Integration test that can be run manually against a live Solana RPC endpoint. +/// +/// Set the following env vars before running to enable the test: +/// - `SOL_TEST_RPC_URL`: full RPC URL (e.g. https://api.mainnet-beta.solana.com) +/// - `SOL_TEST_NAME`: the `.sol` name to resolve (e.g. `bonfida.sol`) +/// - `SOL_TEST_EXPECTED_OWNER`: expected owner as a hex string (with or without 0x) +#[tokio::test] +async fn test_sol_resolver_with_public_rpc() { + let rpc_url = match std::env::var("SOL_TEST_RPC_URL") { + Ok(url) if !url.is_empty() => url, + _ => return, + }; + + let (name, expected_owner_hex) = match ( + std::env::var("SOL_TEST_NAME"), + std::env::var("SOL_TEST_EXPECTED_OWNER"), + ) { + (Ok(name), Ok(owner)) if !name.is_empty() && !owner.is_empty() => (name, owner), + _ => return, + }; + + let mut owner_bytes = expected_owner_hex.trim_start_matches("0x").to_string(); + if owner_bytes.len() % 2 == 1 { + owner_bytes = format!("0{}", owner_bytes); + } + let expected_owner = + hex::decode(owner_bytes).expect("SOL_TEST_EXPECTED_OWNER must be valid hex bytes"); + + let resolver: SolanaNameService = + SolanaNameService::new(rpc_url).expect("invalid solana rpc url"); + let resolved_owner: Vec = resolver + .resolve(name.clone()) + .await + .expect("failed to resolve solana name"); + + assert_eq!( + resolved_owner, expected_owner, + "resolved owner mismatch for {name}" + ); +}