diff --git a/Cargo.lock b/Cargo.lock index aaae0f8..b3f72f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -719,9 +719,9 @@ dependencies = [ [[package]] name = "borsh_utils" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9716bbbca7bd5eb81c46151ef82ce10ebcf428f63397371284b2b705c28cb9" +checksum = "afe6e88b898c9baea16d8f01a67b948c3816e48e96f779afd4a7932b03d1cac1" dependencies = [ "bitcoin", "borsh", @@ -2036,9 +2036,9 @@ dependencies = [ [[package]] name = "libveritas" -version = "0.3.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a640a4f8a32bfc381e67d80ae0ac8e6e7b139073dba53556474d18ab81fe33b8" +checksum = "1860beafc30c88b2ad698483376e6cc6f437de34ec4536b6db6d185e604154bc" dependencies = [ "base64 0.22.1", "borsh", @@ -2055,9 +2055,9 @@ dependencies = [ [[package]] name = "libveritas_testutil" -version = "0.3.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677cf31a735595a5fe6f4d17df083778b5baabc9411ebb4894de519b9dd9d085" +checksum = "e02c3cb2fb0e9ec4cdd022fd225d5e3b6f66276cb788976089d2e59c9d5337c9" dependencies = [ "bitcoin", "borsh", @@ -3416,9 +3416,9 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "sip7" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0ed9b8980298850d71acd2c96f55bef13a4e4d28d2976c503a86c69c791fd34" +checksum = "b1c60c754d3958120c272c963e2333f4ea2f142f9832e5ffdd4cc04caa9ab946" dependencies = [ "base64 0.22.1", "hex", @@ -3476,9 +3476,9 @@ dependencies = [ [[package]] name = "spacedb" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a48cda82e951391df9d0a54c96f8b04e117ff39abad5359b181cb71c80798d77" +checksum = "cf14a3d14c3dcc6b87d837e246d9de73ef24b0d83796a294ceaa82e8acf4e8c9" dependencies = [ "bincode", "borsh", @@ -3490,9 +3490,9 @@ dependencies = [ [[package]] name = "spaces_checkpoint" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611add09bc1b52a4739a71bb15bcbdb162449c668bf0c74f8c4a48835c4e5c60" +checksum = "c1ab25dcb0d99385d0f6ca9213c6ed0ec182625d794ed2051fa1568b81270700" dependencies = [ "anyhow", "clap", @@ -3510,9 +3510,9 @@ dependencies = [ [[package]] name = "spaces_client" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27ec4e8bc75f0f2e87beb778702149efd341a0b10efe42493b3de32a670d64c" +checksum = "534384e03c6d0bf1196466d9a7e20cebb1c5b425b926834a673439b214ce0815" dependencies = [ "anyhow", "base64 0.22.1", @@ -3545,9 +3545,9 @@ dependencies = [ [[package]] name = "spaces_nums" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b842f5b10f200e3c9548489657da4e889897183c72d35673f27d20c2ecb3be" +checksum = "7995b967a9e479b52f11e4b6f165f3b0c07b58f3aa109234332a2a9fdbab4716" dependencies = [ "bech32", "bitcoin", @@ -3561,9 +3561,9 @@ dependencies = [ [[package]] name = "spaces_protocol" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f412ceb3e0a1ce5ca00a8cc7bb875cd614ba7ac809e4085502ec397e354a4a" +checksum = "592c235dc5872d4d28564fa59e0c4035255042a60d0c77f3732df527f4485100" dependencies = [ "bitcoin", "borsh", @@ -3573,9 +3573,9 @@ dependencies = [ [[package]] name = "spaces_wallet" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ba9c85e7685c47229c8430cb9496c67799af42b7c9bc64f12e15245a601aa2" +checksum = "5d340b0780e3246915db18f4a4b7f6a7aa96af429d75fb4bc622b31b7cb5e878" dependencies = [ "anyhow", "bdk_wallet_backport", diff --git a/Cargo.toml b/Cargo.toml index 939460d..ce1637f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,11 @@ authors = ["Buffrr "] [workspace.dependencies] fabric-resolver = { path = "fabric", version = "0.2.7" } -libveritas = { version = "0.3.3" } -libveritas_testutil = { version = "0.3.3" } +libveritas = { version = "0.4.2" } +libveritas_testutil = { version = "0.4.2" } -spaces_client = { version = "0.2.1" } -spaces_protocol = { version = "0.2.1" } -spaces_nums = { version = "0.2.1" } -spaces_checkpoint = { version = "0.2.1" } +spaces_client = { version = "0.3.0" } +spaces_protocol = { version = "0.3.0" } +spaces_nums = { version = "0.3.0" } +spaces_checkpoint = { version = "0.3.0" } spacedb = { version = "0.1", features = ["hash-idx"] } diff --git a/fabric/go/go.mod b/fabric/go/go.mod index 0ae03e2..c6e6afd 100644 --- a/fabric/go/go.mod +++ b/fabric/go/go.mod @@ -4,7 +4,7 @@ go 1.22 require ( github.com/btcsuite/btcd/btcec/v2 v2.3.6 - github.com/spacesprotocol/libveritas-go v0.3.3 + github.com/spacesprotocol/libveritas-go v0.4.2 ) require ( diff --git a/fabric/go/go.sum b/fabric/go/go.sum index 0baf8c7..b78766f 100644 --- a/fabric/go/go.sum +++ b/fabric/go/go.sum @@ -8,5 +8,5 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/spacesprotocol/libveritas-go v0.3.3 h1:kLiYBLp9vdsoIEYZHRZ+Eo9uKd02Vi02raYFhFZ97lw= -github.com/spacesprotocol/libveritas-go v0.3.3/go.mod h1:HXnX2FNL43ueJuedsQwX9GF5jRHYZLqXYfFLWz900H8= +github.com/spacesprotocol/libveritas-go v0.4.2 h1:7ZFFLWPiCBPiLmHOdsUtvAen6wPF7T1UJMDkFfe9IpA= +github.com/spacesprotocol/libveritas-go v0.4.2/go.mod h1:HXnX2FNL43ueJuedsQwX9GF5jRHYZLqXYfFLWz900H8= diff --git a/fabric/js/fabric-core/src/fabric.ts b/fabric/js/fabric-core/src/fabric.ts index c44e020..f4ba2cd 100644 --- a/fabric/js/fabric-core/src/fabric.ts +++ b/fabric/js/fabric-core/src/fabric.ts @@ -29,6 +29,19 @@ export interface ResolveOptions { noCache?: boolean; } +/** A resolved handle plus its exportable cert chain and parent-space context. */ +export interface ResolvedWithCerts { + /** The resolved leaf zone. */ + zone: FabricZone; + /** + * Parent-space zones ordered `[parent, grandparent, …, top-level space]`. + * Each carries its own commitment/sovereignty (read via `toJson()`). + */ + parents: FabricZone[]; + /** The full certificate chain in `.spacecert` format. */ + certs: Uint8Array; +} + export interface PeerInfo { source_ip: string; url: string; @@ -613,6 +626,49 @@ export class Fabric { return this.provider.createCertificateChain(handle, allCertBytes); } + /** + * Resolve a handle and export its cert chain in one pass, also surfacing each + * parent space's zone (commitment/finality). Returns null if not found. + */ + async resolveWithCerts( + handle: string, + opts?: ResolveOptions, + ): Promise { + const lookup = this.provider.createLookup([handle]); + const allZones: FabricZone[] = []; + const allCertBytes: Uint8Array[] = []; + + let prevBatch: string[] = []; + let batch = lookup.start(); + while (batch.length > 0) { + if (arraysEqual(batch, prevBatch)) break; + // hints=false so the exported certs carry their receipts. + const verified = await this.resolveFlat(batch, false, opts); + allCertBytes.push(...verified.certificates()); + const zones = verified.zones(); + prevBatch = batch; + batch = lookup.advance(zones); + allZones.push(...zones); + } + + const expanded = lookup.expandZones(allZones); + const leaf = expanded.find((z) => z.handle === handle); + if (!leaf) return null; + + // Space roots (canonical starts with @ or #) other than the leaf, resolved + // root→leaf; reverse for parent-first ordering. + const leafCanonical = leaf.toJson().canonical; + const parents = expanded + .filter((z) => { + const c = z.toJson().canonical; + return (c.startsWith("@") || c.startsWith("#")) && c !== leafCanonical; + }) + .reverse(); + + const certs = this.provider.createCertificateChain(handle, allCertBytes); + return { zone: leaf, parents, certs }; + } + /** Resolve a flat list of non-dotted handles in a single relay query. */ private async resolveFlat( handles: string[], diff --git a/fabric/js/fabric-core/src/index.ts b/fabric/js/fabric-core/src/index.ts index 641aa22..f93f2fd 100644 --- a/fabric/js/fabric-core/src/index.ts +++ b/fabric/js/fabric-core/src/index.ts @@ -2,6 +2,7 @@ export { Fabric, FabricError, parseScanUri } from "./fabric.js"; export type { FabricOptions, ResolveOptions, + ResolvedWithCerts, PeerInfo, VerificationBadge, ScanParams, diff --git a/fabric/js/fabric-react-native/package.json b/fabric/js/fabric-react-native/package.json index e2036fa..0cb515f 100644 --- a/fabric/js/fabric-react-native/package.json +++ b/fabric/js/fabric-react-native/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@spacesprotocol/fabric-core": "*", - "@spacesprotocol/react-native-libveritas": "^0.3.3", + "@spacesprotocol/react-native-libveritas": "^0.4.2", "uniffi-bindgen-react-native": "0.29.3-1" }, "devDependencies": { diff --git a/fabric/js/fabric-react-native/src/index.ts b/fabric/js/fabric-react-native/src/index.ts index 7f7f820..43afe68 100644 --- a/fabric/js/fabric-react-native/src/index.ts +++ b/fabric/js/fabric-react-native/src/index.ts @@ -37,6 +37,7 @@ export { export type { FabricZone, ResolveOptions, + ResolvedWithCerts, PeerInfo, HintsResponse, SpaceHint, diff --git a/fabric/js/fabric-web/package.json b/fabric/js/fabric-web/package.json index d9afaff..f1b24c9 100644 --- a/fabric/js/fabric-web/package.json +++ b/fabric/js/fabric-web/package.json @@ -27,7 +27,7 @@ "dependencies": { "@noble/curves": "^1.8.0", "@spacesprotocol/fabric-core": "*", - "@spacesprotocol/libveritas": "^0.3.3" + "@spacesprotocol/libveritas": "^0.4.2" }, "devDependencies": { "@types/node": "^25.5.0", diff --git a/fabric/js/fabric-web/src/index.ts b/fabric/js/fabric-web/src/index.ts index eeee37d..915d1b7 100644 --- a/fabric/js/fabric-web/src/index.ts +++ b/fabric/js/fabric-web/src/index.ts @@ -60,6 +60,7 @@ export { export type { FabricZone, ResolveOptions, + ResolvedWithCerts, PeerInfo, HintsResponse, SpaceHint, diff --git a/fabric/js/package-lock.json b/fabric/js/package-lock.json index 8e41d2a..bb85f37 100644 --- a/fabric/js/package-lock.json +++ b/fabric/js/package-lock.json @@ -28,7 +28,7 @@ "license": "MIT", "dependencies": { "@spacesprotocol/fabric-core": "*", - "@spacesprotocol/react-native-libveritas": "^0.3.3", + "@spacesprotocol/react-native-libveritas": "^0.4.2", "uniffi-bindgen-react-native": "0.29.3-1" }, "devDependencies": { @@ -42,7 +42,7 @@ "dependencies": { "@noble/curves": "^1.8.0", "@spacesprotocol/fabric-core": "*", - "@spacesprotocol/libveritas": "^0.3.3" + "@spacesprotocol/libveritas": "^0.4.2" }, "bin": { "fabric": "dist/cli.js" @@ -972,14 +972,14 @@ "link": true }, "node_modules/@spacesprotocol/libveritas": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@spacesprotocol/libveritas/-/libveritas-0.3.3.tgz", - "integrity": "sha512-Tl9P77c+js0g5e/5Gg7ODGtxDBB1XPYIjf+P9mWkCT4mx6IqO17Sm/kwYJUdqc+dyQiOQmTlpyQCj0no6lGokA==" + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@spacesprotocol/libveritas/-/libveritas-0.4.2.tgz", + "integrity": "sha512-4hzwgMGrsyPOS/mlZm/dnEOz4e9HLfG3qzurN33m5QhA2bsvWP2bJAgMHfoYgX9LFWrEh7UFnyPTbpBfdO6zUw==" }, "node_modules/@spacesprotocol/react-native-libveritas": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@spacesprotocol/react-native-libveritas/-/react-native-libveritas-0.3.3.tgz", - "integrity": "sha512-T/yDJKFHX4fYGF9j2Z67yJiIrEffyOLZWaTYV1WZx9gz5TzdCGiOefJL5M5EI87U/tpML5G9eNl4oC3h+Iy61Q==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@spacesprotocol/react-native-libveritas/-/react-native-libveritas-0.4.2.tgz", + "integrity": "sha512-bf21wsSxAil+RDqjclqqW5HVdz/mlDL4wfF7+GEJDLBt0efhPVkgrWfhzpZn0xU/orRc5aoQBZNCY04/sBA6Ow==", "license": "MIT", "peerDependencies": { "react": "*", diff --git a/fabric/kotlin/build.gradle.kts b/fabric/kotlin/build.gradle.kts index ac67aa0..4372e73 100644 --- a/fabric/kotlin/build.gradle.kts +++ b/fabric/kotlin/build.gradle.kts @@ -19,9 +19,9 @@ dependencies { // compileOnly — consumers provide the right variant at runtime: // Android: org.spacesprotocol:libveritas (AAR) // JVM: org.spacesprotocol:libveritas-jvm (JAR) - compileOnly("org.spacesprotocol:libveritas-jvm:0.3.3") + compileOnly("org.spacesprotocol:libveritas-jvm:0.4.2") // CLI needs it at runtime - runtimeOnly("org.spacesprotocol:libveritas-jvm:0.3.3") + runtimeOnly("org.spacesprotocol:libveritas-jvm:0.4.2") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") compileOnly("fr.acinq.secp256k1:secp256k1-kmp:0.17.3") diff --git a/fabric/python/pyproject.toml b/fabric/python/pyproject.toml index b1046d5..fea2754 100644 --- a/fabric/python/pyproject.toml +++ b/fabric/python/pyproject.toml @@ -3,7 +3,7 @@ name = "fabric-resolver" version = "0.1.0" description = "Python client for the certrelay fabric network" requires-python = ">=3.10" -dependencies = ["libveritas>=0.3.3"] +dependencies = ["libveritas>=0.4.2"] [project.optional-dependencies] signing = ["coincurve"] diff --git a/fabric/rust/src/anchor.rs b/fabric/rust/src/anchor.rs index cbe2fd2..040740d 100644 --- a/fabric/rust/src/anchor.rs +++ b/fabric/rust/src/anchor.rs @@ -47,8 +47,7 @@ mod tests { // Newest-first: entries[0].height = 6480 (tip), entries[59] = 4356 (oldest). const REAL: &str = include_str!("testdata/anchors_newest_first.json"); // The X-Anchor-Root header the same relay served with this body. - const SERVED_ROOT: &str = - "7f20135af5fea06ac6a38303ea887428a614186979a41abde64e03ea6780375e"; + const SERVED_ROOT: &str = "7f20135af5fea06ac6a38303ea887428a614186979a41abde64e03ea6780375e"; #[test] fn tip_is_first_not_last_and_order_is_canonical() { diff --git a/fabric/rust/src/client.rs b/fabric/rust/src/client.rs index 641f107..a7d8607 100644 --- a/fabric/rust/src/client.rs +++ b/fabric/rust/src/client.rs @@ -2,7 +2,7 @@ use crate::seeds::SEEDS; pub use crate::{ AnchorSet, EpochHint, HintsResponse, Message, PeerInfo, Query, QueryRequest, TrustId, }; -use libveritas::cert::CertificateChain; +use libveritas::cert::{Certificate, CertificateChain}; use libveritas::msg::QueryContext; use libveritas::spaces_protocol::sname::{NameLike, SName}; use libveritas::{ @@ -73,6 +73,32 @@ pub struct ResolveOpts { pub no_cache: bool, } +/// A resolved handle together with its exportable certificate chain and the +/// commitment context of each parent space. +#[derive(Clone, Serialize)] +pub struct ResolvedWithCerts { + /// The resolved leaf zone. + pub zone: Zone, + /// Parent-space zones ordered `[parent, grandparent, …, top-level space]`. + /// Each carries its own `sovereignty` and `commitment` (height/roots), so a + /// finalized parent under a still-pending grandparent is fully represented. + pub parents: Vec, + /// The full certificate chain in `.spacecert` format. + pub certs: Vec, +} + +/// Parent-space zones for a resolved `leaf`, ordered `[parent, grandparent, …]`. +/// Parents are the single-label (space-root) zones in the resolution set, which +/// come back root→leaf — so reverse to put the immediate parent first. +fn parent_zones(zones: &[Zone], leaf: &Zone) -> Vec { + zones + .iter() + .filter(|z| z.canonical.is_single_label() && z.canonical != leaf.canonical) + .rev() + .cloned() + .collect() +} + pub struct Fabric { http: reqwest::Client, pool: RelayPool, @@ -512,11 +538,7 @@ impl Fabric { } /// Like [`resolve`](Self::resolve) but with per-call options (e.g. `no_cache`). - pub async fn resolve_with_opts( - &self, - handle: &str, - opts: ResolveOpts, - ) -> Result> { + pub async fn resolve_with_opts(&self, handle: &str, opts: ResolveOpts) -> Result> { let zones = self.resolve_all_with_opts(&[handle], opts).await?; Ok(zones.into_iter().find(|z| z.handle.to_string() == handle)) } @@ -645,18 +667,18 @@ impl Fabric { } /// Like [`resolve_all`](Self::resolve_all) but with per-call options. - pub async fn resolve_all_with_opts( + /// One-pass recursive-name lookup → verified zones (plus the cert chain when + /// `want_certs`). `hints=false` preserves receipts so the certs stay exportable. + async fn resolve_lookup( &self, - handles: &[&str], - opts: ResolveOpts, - ) -> Result> { - let snames: Vec = handles - .iter() - .filter_map(|h| SName::try_from(*h).ok()) - .collect(); - + snames: Vec, + hints: bool, + no_cache: bool, + want_certs: bool, + ) -> Result<(Vec, Vec)> { let lookup = libveritas::names::Lookup::new(snames); - let mut all_zones: Vec = Vec::new(); + let mut zones: Vec = Vec::new(); + let mut certs: Vec = Vec::new(); let mut prev_batch: Vec = Vec::new(); let mut batch: Vec = lookup.start(); @@ -666,14 +688,32 @@ impl Fabric { } let strs: Vec = batch.iter().map(|s| s.to_string()).collect(); let refs: Vec<&str> = strs.iter().map(|s| s.as_str()).collect(); - let (verified, _relay_url) = self.resolve_flat(&refs, true, opts.no_cache).await?; + let (verified, _relay_url) = self.resolve_flat(&refs, hints, no_cache).await?; + if want_certs { + certs.extend(verified.certificates()); + } prev_batch = batch; batch = lookup.advance(&verified.zones); - all_zones.extend(verified.zones); + zones.extend(verified.zones); } - lookup.expand_zones(&mut all_zones); - Ok(all_zones) + lookup.expand_zones(&mut zones); + Ok((zones, certs)) + } + + pub async fn resolve_all_with_opts( + &self, + handles: &[&str], + opts: ResolveOpts, + ) -> Result> { + let snames: Vec = handles + .iter() + .filter_map(|h| SName::try_from(*h).ok()) + .collect(); + let (zones, _) = self + .resolve_lookup(snames, true, opts.no_cache, false) + .await?; + Ok(zones) } /// Export a certificate chain for a handle in `.spacecert` format. @@ -681,30 +721,37 @@ impl Fabric { let sname = SName::try_from(handle) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()))?; - let lookup = libveritas::names::Lookup::new(vec![sname.clone()]); - let mut all_verified: Vec = Vec::new(); - - let mut prev_batch: Vec = Vec::new(); - let mut batch: Vec = lookup.start(); - while !batch.is_empty() { - if batch == prev_batch { - break; - } - let strs: Vec = batch.iter().map(|s| s.to_string()).collect(); - let refs: Vec<&str> = strs.iter().map(|s| s.as_str()).collect(); - let (verified, _relay_url) = self.resolve_flat(&refs, false, false).await?; - prev_batch = batch; - batch = lookup.advance(&verified.zones); - all_verified.push(verified); - } + let (_, certs) = self + .resolve_lookup(vec![sname.clone()], false, false, true) + .await?; + Ok(CertificateChain::new(sname, certs).to_bytes()) + } - let mut certs = Vec::new(); - for msg in &all_verified { - certs.extend(msg.certificates()); - } + /// Resolve a handle and export its certificate chain in one pass, also + /// surfacing each parent space's zone (commitment/finality) for provenance. + /// Returns `None` if the handle doesn't resolve. + pub async fn resolve_with_certs(&self, handle: &str) -> Result> { + let sname = SName::try_from(handle) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string()))?; + // hints=false so the exported certs carry their receipts. + let (zones, certs) = self + .resolve_lookup(vec![sname.clone()], false, false, true) + .await?; - let chain = CertificateChain::new(sname, certs); - Ok(chain.to_bytes()) + let Some(leaf) = zones + .iter() + .find(|z| z.handle.to_string() == handle) + .cloned() + else { + return Ok(None); + }; + let parents = parent_zones(&zones, &leaf); + let certs = CertificateChain::new(sname, certs).to_bytes(); + Ok(Some(ResolvedWithCerts { + zone: leaf, + parents, + certs, + })) } /// Resolve a flat list of non-dotted handles in a single relay query. @@ -1381,3 +1428,38 @@ async fn fetch_anchor_set( Err(last_err.unwrap_or(Error::NoPeers)) } + +#[cfg(test)] +mod parent_zone_tests { + use super::*; + + // Real zones captured from resolving `across-remove-super.genesis@key` on + // mainnet: leaf `across-remove-super#944203-189-0`, its immediate parent + // (alice's numeric space) `#944203-189-0`, and the top-level space `@key`. + const LEAF: &str = include_str!("testdata/zone_leaf.json"); + const PARENT: &str = include_str!("testdata/zone_parent.json"); + const GRANDPARENT: &str = include_str!("testdata/zone_grandparent.json"); + + #[test] + fn parents_are_immediate_first_across_nesting() { + let leaf: Zone = serde_json::from_str(LEAF).unwrap(); + let parent: Zone = serde_json::from_str(PARENT).unwrap(); + let grandparent: Zone = serde_json::from_str(GRANDPARENT).unwrap(); + + // Resolution yields the zones root -> leaf. + let zones = vec![grandparent, parent, leaf.clone()]; + let canon: Vec = parent_zones(&zones, &leaf) + .iter() + .map(|z| z.canonical.to_string()) + .collect(); + + // Immediate parent first, top-level space last — not grandparent-first. + assert_eq!(canon, ["#944203-189-0", "@key"]); + } + + #[test] + fn a_space_root_has_no_parents() { + let space: Zone = serde_json::from_str(GRANDPARENT).unwrap(); + assert!(parent_zones(std::slice::from_ref(&space), &space).is_empty()); + } +} diff --git a/fabric/rust/src/testdata/zone_grandparent.json b/fabric/rust/src/testdata/zone_grandparent.json new file mode 100644 index 0000000..ea1ec3e --- /dev/null +++ b/fabric/rust/src/testdata/zone_grandparent.json @@ -0,0 +1 @@ +{"anchor":961884,"anchor_hash":"12ff97b6079e49ae7c96754eeb5e3e58c4ea5cf482f708034a3e183a11cbef32","sovereignty":"sovereign","handle":"@key","canonical":"@key","alias":null,"script_pubkey":"5120a000d7982133035dc0965d64a1642ea87c454378e1ee66198a8aeda0071b7601","records":[],"fallback_records":[],"delegate":{"status":"exists","value":{"script_pubkey":"5120a000d7982133035dc0965d64a1642ea87c454378e1ee66198a8aeda0071b7601","records":[],"fallback_records":[]}},"commitment":{"status":"exists","value":{"onchain":{"state_root":"d637dc528aed5c8efd5222bfe52a32b04be2e33898fa983113683044be3539d2","prev_root":null,"rolling_hash":"d637dc528aed5c8efd5222bfe52a32b04be2e33898fa983113683044be3539d2","block_height":944205},"receipt_hash":null}},"num_id":null} \ No newline at end of file diff --git a/fabric/rust/src/testdata/zone_leaf.json b/fabric/rust/src/testdata/zone_leaf.json new file mode 100644 index 0000000..57eeacc --- /dev/null +++ b/fabric/rust/src/testdata/zone_leaf.json @@ -0,0 +1 @@ +{"anchor":961884,"anchor_hash":"12ff97b6079e49ae7c96754eeb5e3e58c4ea5cf482f708034a3e183a11cbef32","sovereignty":"sovereign","handle":"across-remove-super.genesis@key","canonical":"across-remove-super#944203-189-0","alias":null,"script_pubkey":"51202a2380b5102472d19d575d11f9a48aff5f5ae5000fff6cae994a7009ce8ded14","records":[{"type":"seq","version":0},{"type":"addr","key":"nostr","value":["npub1yz5y9qfx7xdg66alwt69sthppztuvyxty4lyr42e5jdlzqzg0ajsewn5nj"]},{"type":"sig","canonical":"across-remove-super#944203-189-0","handle":"across-remove-super.genesis@key","sig":"b8bdd09e02576e9dab1cba0d3e2e3ebc5a03b3c9c1c2e8ddde8d21027bd26a95dc5c19e4d6e7b6f769da79cf7395db485008b60ede4dfa9a6843d337923eb739","flags":1}],"fallback_records":[],"delegate":{"status":"unknown"},"commitment":{"status":"unknown"},"num_id":"num14jcf7eg0ncfs5fv74hlshd82878c8h54pcfgqg9s57xquds3hqaq80pw7d"} \ No newline at end of file diff --git a/fabric/rust/src/testdata/zone_parent.json b/fabric/rust/src/testdata/zone_parent.json new file mode 100644 index 0000000..90f0102 --- /dev/null +++ b/fabric/rust/src/testdata/zone_parent.json @@ -0,0 +1 @@ +{"anchor":961884,"anchor_hash":"12ff97b6079e49ae7c96754eeb5e3e58c4ea5cf482f708034a3e183a11cbef32","sovereignty":"sovereign","handle":"#944203-189-0","canonical":"#944203-189-0","alias":null,"script_pubkey":"51203eca93c95522a3dcbd4fbbefb4a8d79d2bff2c3035635bb948a211422957a9a8","records":[],"fallback_records":[],"delegate":{"status":"exists","value":{"script_pubkey":"51203eca93c95522a3dcbd4fbbefb4a8d79d2bff2c3035635bb948a211422957a9a8","records":[],"fallback_records":[]}},"commitment":{"status":"exists","value":{"onchain":{"state_root":"f5a3c42aece87c277531e211fcd431e2dc47d465422502b163c2fb48419f9987","prev_root":null,"rolling_hash":"f5a3c42aece87c277531e211fcd431e2dc47d465422502b163c2fb48419f9987","block_height":944205},"receipt_hash":null}},"num_id":"num17vq32rwwke4wx7375g8g54kl8nv73jrzzgh3vwhgsu86vexlvq0qxyfh9l"} \ No newline at end of file diff --git a/fabric/swift/Package.swift b/fabric/swift/Package.swift index 5ae621c..8bdb5a9 100644 --- a/fabric/swift/Package.swift +++ b/fabric/swift/Package.swift @@ -9,7 +9,7 @@ let package = Package( .executable(name: "fabric", targets: ["FabricCLI"]), ], dependencies: [ - .package(url: "https://github.com/spacesprotocol/libveritas-swift.git", exact: "0.3.3"), + .package(url: "https://github.com/spacesprotocol/libveritas-swift.git", exact: "0.4.2"), .package(url: "https://github.com/21-DOT-DEV/swift-secp256k1.git", exact: "0.17.0"), ], targets: [ diff --git a/relay/src/http.rs b/relay/src/http.rs index 0817adc..a10bc88 100644 --- a/relay/src/http.rs +++ b/relay/src/http.rs @@ -575,7 +575,10 @@ fn etag_matches(if_none_match: &str, etag: &str) -> bool { if_none_match == "*" || if_none_match.split(',').any(|t| { let t = t.trim(); - t == etag || t.strip_prefix("W/").map(|w| w.trim() == etag).unwrap_or(false) + t == etag + || t.strip_prefix("W/") + .map(|w| w.trim() == etag) + .unwrap_or(false) }) } diff --git a/relay/src/store.rs b/relay/src/store.rs index d8c698e..1dca244 100644 --- a/relay/src/store.rs +++ b/relay/src/store.rs @@ -401,13 +401,11 @@ impl SqliteStore { // finality; the owner's records are preserved by the merge // below. Incoming certs are already verified against current // anchors, so a higher anchor is genuine fresher chain truth. - let fresher_proof = update.zone.script_pubkey - == existing.script_pubkey + let fresher_proof = update.zone.script_pubkey == existing.script_pubkey && update.zone.anchor > existing.anchor && sovereignty_rank(update.zone.sovereignty) >= sovereignty_rank(existing.sovereignty); - if !fresher_proof - && !update.zone.is_better_than(existing).unwrap_or(false) + if !fresher_proof && !update.zone.is_better_than(existing).unwrap_or(false) { return None; // stored zone is as good or better } diff --git a/relay/tests/integration_tests.rs b/relay/tests/integration_tests.rs index 579c65a..48bce73 100644 --- a/relay/tests/integration_tests.rs +++ b/relay/tests/integration_tests.rs @@ -378,7 +378,10 @@ fn test_finalize_without_commitment_upgrade_replaces_temp() { .get_handle("alice@sovereign") .unwrap() .unwrap(); - assert!(!alice.zone.records.is_empty(), "precondition: alice has records"); + assert!( + !alice.zone.records.is_empty(), + "precondition: alice has records" + ); let alice_seq = alice.zone.records.seq(); // Finalize WITHOUT upgrading the commitment — only a fresher anchor + empty