From 8d2a6ba2c2b57fffd8a843e48c541a14c5c0c6c4 Mon Sep 17 00:00:00 2001 From: Buffrr Date: Thu, 23 Apr 2026 23:57:40 +0200 Subject: [PATCH 1/3] feat: badge fix, ResolvedBatch.get(), and state persistence - badge_for() returns Unverified when no trust pools are populated instead of None (fixes stale badge after app restart) - Add ResolvedBatch.get(handle) across all 6 clients for easy handle lookup from batch results - Add save_state()/from_state() to Rust and saveState()/loadState() to JS for persisting peers, anchors, trust sets, and zone cache across app restarts --- fabric/go/fabric.go | 17 +++ fabric/js/fabric-core/src/fabric.ts | 62 +++++++++++ fabric/js/fabric-core/src/index.ts | 2 +- fabric/js/fabric-web/src/index.ts | 1 + .../org/spacesprotocol/fabric/Fabric.kt | 10 +- fabric/python/fabric/client.py | 9 ++ fabric/rust/src/client.rs | 104 ++++++++++++++++++ fabric/swift/Sources/Fabric/Fabric.swift | 11 ++ 8 files changed, 214 insertions(+), 2 deletions(-) diff --git a/fabric/go/fabric.go b/fabric/go/fabric.go index cd89912..63fd742 100644 --- a/fabric/go/fabric.go +++ b/fabric/go/fabric.go @@ -110,6 +110,16 @@ type ResolvedBatch struct { Roots []string // hex-encoded root IDs } +// Get looks up a specific handle from the batch. +func (b ResolvedBatch) Get(handle string) *Resolved { + for _, z := range b.Zones { + if z.Handle == handle { + return &Resolved{Zone: z, Roots: b.Roots} + } + } + return nil +} + type anchorPool struct { trusted string // raw entries JSON semiTrusted string // raw entries JSON @@ -254,6 +264,13 @@ func (f *Fabric) Badge(resolved Resolved) VerificationBadge { // BadgeFor returns the verification badge given sovereignty and root IDs. func (f *Fabric) BadgeFor(sovereignty string, roots []string) VerificationBadge { + f.mu.Lock() + hasAny := f.trusted != nil || f.observed != nil || f.semiTrusted != nil + f.mu.Unlock() + if !hasAny { + return BadgeUnverified + } + isTrusted := f.areRootsTrusted(roots) isObserved := isTrusted || f.areRootsObserved(roots) isSemiTrusted := isTrusted || f.areRootsSemiTrusted(roots) diff --git a/fabric/js/fabric-core/src/fabric.ts b/fabric/js/fabric-core/src/fabric.ts index 871fd5b..837e9f9 100644 --- a/fabric/js/fabric-core/src/fabric.ts +++ b/fabric/js/fabric-core/src/fabric.ts @@ -22,6 +22,13 @@ export interface ResolvedBatch { roots: string[]; // hex-encoded } +/** Look up a specific handle from a batch. */ +export function batchGet(batch: ResolvedBatch, handle: string): Resolved | undefined { + const zone = batch.zones.find(z => z.handle === handle); + if (!zone) return undefined; + return { zone, roots: batch.roots }; +} + export interface FabricOptions { provider: VeritasProvider; seeds?: string[]; @@ -167,6 +174,57 @@ export class Fabric { return this.veritas; } + // ── State persistence ── + + /** Export the current state as a JSON string for persistence. */ + saveState(): string { + const zoneCacheObj: Record = {}; + for (const [key, entry] of this.zoneCache) { + zoneCacheObj[key] = entry.zone.toJson(); + } + return JSON.stringify({ + version: 1, + relays: this.pool.urls, + anchors: { + trusted: this.anchorEntries.trusted ?? [], + semi_trusted: this.anchorEntries.semiTrusted ?? [], + observed: this.anchorEntries.observed ?? [], + }, + zone_cache: zoneCacheObj, + }); + } + + /** Restore state from a previously saved JSON string. */ + loadState(json: string): void { + const state = JSON.parse(json); + if (state.relays?.length) { + this.pool.refresh(state.relays); + } + if (state.anchors) { + const a = state.anchors; + if (a.trusted?.length) this.anchorEntries.trusted = a.trusted; + if (a.semi_trusted?.length) this.anchorEntries.semiTrusted = a.semi_trusted; + if (a.observed?.length) this.anchorEntries.observed = a.observed; + this.rebuildVeritas(); + + // Recompute trust sets from anchors + if (this.anchorEntries.trusted && this.veritas) { + const anchors = this.provider.createAnchors(this.anchorEntries.trusted); + this._trusted = anchors.computeTrustSet(); + } + if (this.anchorEntries.semiTrusted && this.veritas) { + const anchors = this.provider.createAnchors(this.anchorEntries.semiTrusted); + this._semiTrusted = anchors.computeTrustSet(); + } + if (this.anchorEntries.observed && this.veritas) { + const anchors = this.provider.createAnchors(this.anchorEntries.observed); + this._observed = anchors.computeTrustSet(); + } + } + // Zone cache restoration would require re-parsing zones from JSON + // which needs the provider. For now, zone cache is rebuilt on first resolve. + } + // ── Trust ── /** Trust a specific trust ID. Fetches anchors matching the given ID. */ @@ -226,6 +284,10 @@ export class Fabric { /** Compute a verification badge given sovereignty type and roots. */ badgeFor(sovereignty: string, roots: string[]): VerificationBadge { + if (!this._trusted && !this._observed && !this._semiTrusted) { + return "unverified"; + } + const isTrusted = this.areRootsTrusted(roots); const isObserved = isTrusted || this.areRootsObserved(roots); const isSemiTrusted = isTrusted || this.areRootsSemiTrusted(roots); diff --git a/fabric/js/fabric-core/src/index.ts b/fabric/js/fabric-core/src/index.ts index b513931..d025fb3 100644 --- a/fabric/js/fabric-core/src/index.ts +++ b/fabric/js/fabric-core/src/index.ts @@ -1,4 +1,4 @@ -export { Fabric, FabricError, parseScanUri } from "./fabric.js"; +export { Fabric, FabricError, parseScanUri, batchGet } from "./fabric.js"; export type { FabricOptions, PeerInfo, diff --git a/fabric/js/fabric-web/src/index.ts b/fabric/js/fabric-web/src/index.ts index 37c783d..0f8d41b 100644 --- a/fabric/js/fabric-web/src/index.ts +++ b/fabric/js/fabric-web/src/index.ts @@ -53,6 +53,7 @@ export class Fabric extends FabricCore { export { FabricError, RelayPool, + batchGet, compareHints, DEFAULT_SEEDS, } from "@spacesprotocol/fabric-core"; diff --git a/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/Fabric.kt b/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/Fabric.kt index 9f6d726..98cc316 100644 --- a/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/Fabric.kt +++ b/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/Fabric.kt @@ -41,7 +41,12 @@ data class PeerInfo( enum class VerificationBadge { Orange, Unverified, None } data class Resolved(val zone: Zone, val roots: List) -data class ResolvedBatch(val zones: List, val roots: List) +data class ResolvedBatch(val zones: List, val roots: List) { + fun get(handle: String): Resolved? { + val zone = zones.find { it.handle == handle } ?: return null + return Resolved(zone, roots) + } +} private val json = Json { ignoreUnknownKeys = true } @@ -129,6 +134,9 @@ class Fabric( badgeFor(resolved.zone.sovereignty, resolved.roots) fun badgeFor(sovereignty: String, roots: List): VerificationBadge { + val hasAny = synchronized(lock) { trusted != null || observed != null || semiTrusted != null } + if (!hasAny) return VerificationBadge.Unverified + val isTrusted = areRootsTrusted(roots) val isObserved = isTrusted || areRootsObserved(roots) val isSemiTrusted = isTrusted || areRootsSemiTrusted(roots) diff --git a/fabric/python/fabric/client.py b/fabric/python/fabric/client.py index de460f9..200a2e5 100644 --- a/fabric/python/fabric/client.py +++ b/fabric/python/fabric/client.py @@ -46,6 +46,13 @@ class ResolvedBatch: zones: list[lv.Zone] roots: list[str] # hex-encoded root IDs + def get(self, handle: str) -> Resolved | None: + """Look up a specific handle from the batch.""" + zone = next((z for z in self.zones if z.handle == handle), None) + if zone is None: + return None + return Resolved(zone=zone, roots=self.roots) + @dataclass class _EpochHint: @@ -210,6 +217,8 @@ def badge(self, resolved: Resolved) -> str: def badge_for(self, sovereignty: str, roots: list[str]) -> str: """Return the verification badge given sovereignty and root IDs.""" + if self._trusted is None and self._observed is None and self._semi_trusted is None: + return BADGE_UNVERIFIED is_trusted = self._are_roots_trusted(roots) is_observed = is_trusted or self._are_roots_observed(roots) is_semi_trusted = is_trusted or self._are_roots_semi_trusted(roots) diff --git a/fabric/rust/src/client.rs b/fabric/rust/src/client.rs index e4da536..507347d 100644 --- a/fabric/rust/src/client.rs +++ b/fabric/rust/src/client.rs @@ -104,6 +104,27 @@ impl AnchorPool { } } +/// Serializable snapshot of Fabric state for persistence. +#[derive(Serialize, Deserialize)] +pub struct FabricState { + pub version: u32, + pub relays: Vec, + pub anchors: AnchorPoolState, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub zone_cache: HashMap, +} + +/// Anchor entries per trust source. +#[derive(Serialize, Deserialize, Default)] +pub struct AnchorPoolState { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub trusted: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub semi_trusted: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub observed: Vec, +} + pub struct RelayPool { inner: Mutex>, } @@ -150,6 +171,20 @@ pub struct ResolvedBatch { pub relays: Vec, } +impl ResolvedBatch { + /// Look up a specific handle from the batch. + pub fn get(&self, handle: &str) -> Option { + self.zones + .iter() + .find(|z| z.handle.to_string() == handle) + .map(|z| Resolved { + zone: z.clone(), + roots: self.roots.clone(), + relays: self.relays.clone(), + }) + } +} + #[derive(Clone, Serialize, Deserialize)] pub struct Resolved { pub zone: Zone, @@ -191,6 +226,68 @@ impl Fabric { self } + /// Export the current state for persistence. + pub fn save_state(&self) -> FabricState { + let pool = self.anchor_pool.lock().unwrap(); + let zone_cache: HashMap = self + .root_cache + .iter() + .map(|entry| (entry.key().clone(), entry.value().clone())) + .collect(); + + FabricState { + version: 1, + relays: self.pool.urls(), + anchors: AnchorPoolState { + trusted: pool.trusted.clone(), + semi_trusted: pool.semi_trusted.clone(), + observed: pool.observed.clone(), + }, + zone_cache, + } + } + + /// Create a Fabric instance from previously saved state. + /// Rebuilds trust sets and Veritas from the persisted anchors. + pub fn from_state(state: FabricState) -> Result { + let fabric = Self::new(); + + if !state.relays.is_empty() { + fabric.pool.refresh(state.relays); + } + + { + let mut pool = fabric.anchor_pool.lock().unwrap(); + pool.trusted = state.anchors.trusted; + pool.semi_trusted = state.anchors.semi_trusted; + pool.observed = state.anchors.observed; + + if !pool.trusted.is_empty() { + *fabric.trusted.lock().unwrap() = Some(compute_trust_set(&pool.trusted)); + } + if !pool.semi_trusted.is_empty() { + *fabric.semi_trusted.lock().unwrap() = Some(compute_trust_set(&pool.semi_trusted)); + } + if !pool.observed.is_empty() { + *fabric.observed.lock().unwrap() = Some(compute_trust_set(&pool.observed)); + } + + let merged = pool.merged(); + if !merged.is_empty() { + let v = Veritas::new().with_anchors(merged).map_err(|e| { + std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{e:?}")) + })?; + *fabric.veritas.lock().unwrap() = v; + } + } + + for (key, zone) in state.zone_cache { + fabric.root_cache.insert(key, zone); + } + + Ok(fabric) + } + fn are_roots_trusted(&self, roots: &[TrustId]) -> bool { let set = self.trusted.lock().unwrap(); let Some(trusted_set) = set.as_ref() else { @@ -235,6 +332,13 @@ impl Fabric { } pub fn badge_for(&self, sov: SovereigntyState, roots: &[TrustId]) -> Badge { + let has_any_pool = self.trusted.lock().unwrap().is_some() + || self.observed.lock().unwrap().is_some() + || self.semi_trusted.lock().unwrap().is_some(); + if !has_any_pool { + return Badge::Unverified; + } + let is_trusted = self.are_roots_trusted(roots); let is_observed = is_trusted || self.are_roots_observed(roots); let is_semi_trusted = is_trusted || self.are_roots_semi_trusted(roots); diff --git a/fabric/swift/Sources/Fabric/Fabric.swift b/fabric/swift/Sources/Fabric/Fabric.swift index 73caca6..efa9a75 100644 --- a/fabric/swift/Sources/Fabric/Fabric.swift +++ b/fabric/swift/Sources/Fabric/Fabric.swift @@ -65,6 +65,12 @@ public struct Resolved { public struct ResolvedBatch { public let zones: [Zone] public let roots: [String] // hex-encoded root IDs + + /// Look up a specific handle from the batch. + public func get(_ handle: String) -> Resolved? { + guard let zone = zones.first(where: { $0.handle == handle }) else { return nil } + return Resolved(zone: zone, roots: roots) + } } // MARK: - Trust kind @@ -263,6 +269,11 @@ public final class Fabric: @unchecked Sendable { /// Badge given sovereignty and roots. public func badgeFor(sovereignty: String, roots: [String]) -> VerificationBadge { + lock.lock() + let hasAny = trusted != nil || observed != nil || semiTrusted != nil + lock.unlock() + if !hasAny { return .unverified } + let isTrusted = areRootsTrusted(roots) let isObserved = isTrusted || areRootsObserved(roots) let isSemiTrusted = isTrusted || areRootsSemiTrusted(roots) From fcdbe0582167e05ab903077ed0e45980819ecd4b Mon Sep 17 00:00:00 2001 From: Buffrr Date: Fri, 24 Apr 2026 03:04:19 +0200 Subject: [PATCH 2/3] feat!: return Zone directly from resolve, drop Resolved/ResolvedBatch Breaking change: resolve() returns Zone, resolveAll() returns Vec. Zone now carries anchor_hash from libveritas 0.2.0, making the Resolved wrapper unnecessary. - badge() takes &Zone directly, reads anchor_hash - badgeFor() takes single anchor_hash string instead of roots array - Remove Resolved, ResolvedBatch, batchGet across all 6 clients - Update all examples - Bump libveritas to 0.2.0 --- Cargo.lock | 8 +- Cargo.toml | 4 +- fabric/examples/go/main.go | 36 ++--- fabric/examples/js/index.mjs | 34 ++--- fabric/examples/kotlin/Example.kt | 34 ++--- fabric/examples/python/example.py | 34 ++--- fabric/examples/rust/src/main.rs | 37 +++-- fabric/examples/swift/Example.swift | 30 ++--- fabric/go/fabric.go | 127 ++++++------------ fabric/go/go.mod | 2 +- fabric/go/go.sum | 4 +- fabric/js/fabric-core/src/fabric.ts | 93 +++++-------- fabric/js/fabric-core/src/index.ts | 4 +- fabric/js/fabric-react-native/src/index.ts | 2 - fabric/js/fabric-web/package.json | 2 +- fabric/js/fabric-web/src/cli.ts | 4 +- fabric/js/fabric-web/src/index.ts | 3 - fabric/kotlin/build.gradle.kts | 4 +- .../org/spacesprotocol/fabric/Fabric.kt | 74 ++++------ fabric/python/fabric/__init__.py | 4 - fabric/python/fabric/client.py | 92 +++++-------- fabric/python/pyproject.toml | 2 +- fabric/rust/src/client.rs | 110 ++++----------- fabric/rust/src/main.rs | 4 +- fabric/swift/Package.swift | 2 +- fabric/swift/Sources/Fabric/Fabric.swift | 94 +++++-------- 26 files changed, 313 insertions(+), 531 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3b86ec..f0b95da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2035,9 +2035,9 @@ dependencies = [ [[package]] name = "libveritas" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d82373509126c4da380014710f2c2605d4fa78797877d8f1eed8632093f636" +checksum = "a9b646e5e793b4a806c5f17646e7b6a1eb9f1ab89b5539f3249e99f02c175e95" dependencies = [ "base64 0.22.1", "borsh", @@ -2054,9 +2054,9 @@ dependencies = [ [[package]] name = "libveritas_testutil" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff1f4c3819010a26f5f254c01a6488c572bc6dbb5a273ebe87eff52e3c8c72c" +checksum = "f4a6fca4cc7c1d13baf31337c776a2bc0b18bdcce80a9c365c8cbc3f8102e740" dependencies = [ "bitcoin", "borsh", diff --git a/Cargo.toml b/Cargo.toml index 1d0a3a8..5c86d9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,8 @@ authors = ["Buffrr "] [workspace.dependencies] fabric-resolver = { path = "fabric", version = "0.1.4" } -libveritas = { version = "0.1" } -libveritas_testutil = { version = "0.1" } +libveritas = { version = "0.2" } +libveritas_testutil = { version = "0.2" } spaces_client = { version = "0.1" } spaces_protocol = { version = "0.1" } diff --git a/fabric/examples/go/main.go b/fabric/examples/go/main.go index 57f7b87..f83fe13 100644 --- a/fabric/examples/go/main.go +++ b/fabric/examples/go/main.go @@ -16,12 +16,12 @@ import ( func exampleResolveIntro() error { // f := fabric.New() - resolved, err := f.Resolve("alice@bitcoin") + zone, err := f.Resolve("alice@bitcoin") // if err != nil { return err } - _ = resolved + _ = zone return nil } @@ -29,16 +29,16 @@ func exampleResolveIntro() error { func exampleResolve() error { // f := fabric.New() - resolved, err := f.Resolve("alice@bitcoin") + zone, err := f.Resolve("alice@bitcoin") if err != nil { return err } - if resolved == nil { + if zone == nil { fmt.Println("handle not found") return nil } - fmt.Printf("Handle found: %s\n", resolved.Zone.Handle) + fmt.Printf("Handle found: %s\n", zone.Handle) // return nil @@ -51,12 +51,12 @@ func exampleTrustAndVerification() error { // // Before pinning a trust id: resolve uses observed (peer) state // badge() returns Unverified - resolved, err := f.Resolve("alice@bitcoin") + zone, err := f.Resolve("alice@bitcoin") if err != nil { return err } - f.Badge(resolved) // Unverified + f.Badge(*zone) // Unverified // Pin trust from a QR scan qr := "veritas://scan?id=14ef902621df01bdeee0b23fedf67458563a20df600af8979a4748dcd9d1b9f9" @@ -67,8 +67,8 @@ func exampleTrustAndVerification() error { } // Does not require re-resolving, badge now checks - // whether resolved was against a trusted root - f.Badge(resolved) // Orange if handle is sovereign (final certificate) + // whether zone was against a trusted root + f.Badge(*zone) // Orange if handle is sovereign (final certificate) // Or from a semi-trusted source (e.g. an explorer you trust with qr scanned over HTTPS) // .Badge() will not show Orange for roots in this trust pool, @@ -94,13 +94,13 @@ func exampleTrustAndVerification() error { /// Unpack records from a resolved handle func exampleUnpackRecords() error { f := fabric.New() - resolved, err := f.Resolve("alice@bitcoin") + zone, err := f.Resolve("alice@bitcoin") if err != nil { return err } // - records, err := resolved.Zone.Records.Unpack() + records, err := zone.Records.Unpack() if err != nil { return err } @@ -123,12 +123,12 @@ func exampleResolveAll() error { f := fabric.New() // - batch, err := f.ResolveAll([]string{"alice@bitcoin", "bob@bitcoin"}) + zones, err := f.ResolveAll([]string{"alice@bitcoin", "bob@bitcoin"}) if err != nil { return err } - for _, zone := range batch.Zones { + for _, zone := range zones { fmt.Printf("%s: %s\n", zone.Handle, zone.Sovereignty) } // @@ -195,16 +195,16 @@ func exampleResolveById() error { f := fabric.New() // - resolved, err := f.ResolveById("num1qx8dtlzq...") + zone, err := f.ResolveById("num1qx8dtlzq...") if err != nil { return err } - if resolved == nil { + if zone == nil { fmt.Println("handle not found") return nil } - fmt.Printf("Handle found: %s\n", resolved.Zone.Handle) + fmt.Printf("Handle found: %s\n", zone.Handle) // return nil @@ -215,12 +215,12 @@ func exampleSearchAddr() error { f := fabric.New() // - batch, err := f.SearchAddr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") + zones, err := f.SearchAddr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") if err != nil { return err } - for _, zone := range batch.Zones { + for _, zone := range zones { fmt.Printf("%s: %s\n", zone.Handle, zone.Sovereignty) } // diff --git a/fabric/examples/js/index.mjs b/fabric/examples/js/index.mjs index aac89ba..19f5eec 100644 --- a/fabric/examples/js/index.mjs +++ b/fabric/examples/js/index.mjs @@ -8,7 +8,7 @@ import { signSchnorr } from "@spacesprotocol/fabric-web/signing"; async function exampleResolveIntro() { // const fabric = new Fabric(); - const resolved = await fabric.resolve("alice@bitcoin"); + const zone = await fabric.resolve("alice@bitcoin"); // } @@ -16,13 +16,13 @@ async function exampleResolveIntro() { async function exampleResolve() { // const fabric = new Fabric(); - const resolved = await fabric.resolve("alice@bitcoin"); - if (!resolved) { + const zone = await fabric.resolve("alice@bitcoin"); + if (!zone) { console.log("handle not found"); return; } - console.log(`Handle found: ${resolved.zone.handle}`); + console.log(`Handle found: ${zone.handle}`); // } @@ -33,9 +33,9 @@ async function exampleTrustAndVerification() { // // Before pinning a trust id: resolve uses observed (peer) state // badge() returns "unverified" - const resolved = await fabric.resolve("alice@bitcoin"); + const zone = await fabric.resolve("alice@bitcoin"); - fabric.badge(resolved); // "unverified" + fabric.badge(zone); // "unverified" // Pin trust from a QR scan const qr = "veritas://scan?id=14ef902621df01bdeee0b23fedf67458563a20df600af8979a4748dcd9d1b9f9"; @@ -44,8 +44,8 @@ async function exampleTrustAndVerification() { await fabric.trustFromQr(qr); // Does not require re-resolving, badge now checks - // whether resolved was against a trusted root - fabric.badge(resolved); // "orange" if handle is sovereign (final certificate) + // whether the zone's anchor_hash is in the trusted set + fabric.badge(zone); // "orange" if handle is sovereign (final certificate) // Or from a semi-trusted source (e.g. an explorer you trust with qr scanned over HTTPS) // .badge() will not show "orange" for roots in this trust pool, @@ -65,10 +65,10 @@ async function exampleTrustAndVerification() { /// Unpack records from a resolved handle async function exampleUnpackRecords() { const fabric = new Fabric(); - const resolved = await fabric.resolve("alice@bitcoin"); + const zone = await fabric.resolve("alice@bitcoin"); // - const json = resolved.zone.toJson(); + const json = zone.toJson(); for (const record of json.records) { if (record.type === "txt") { @@ -85,9 +85,9 @@ async function exampleResolveAll() { const fabric = new Fabric(); // - const batch = await fabric.resolveAll(["alice@bitcoin", "bob@bitcoin"]); + const zones = await fabric.resolveAll(["alice@bitcoin", "bob@bitcoin"]); - for (const zone of batch.zones) { + for (const zone of zones) { console.log(`${zone.handle}`); } // @@ -134,13 +134,13 @@ async function exampleResolveById() { const fabric = new Fabric(); // - const resolved = await fabric.resolveById("num1qx8dtlzq..."); - if (!resolved) { + const zone = await fabric.resolveById("num1qx8dtlzq..."); + if (!zone) { console.log("handle not found"); return; } - console.log(`Handle found: ${resolved.zone.handle}`); + console.log(`Handle found: ${zone.handle}`); // } @@ -149,9 +149,9 @@ async function exampleSearchAddr() { const fabric = new Fabric(); // - const batch = await fabric.searchAddr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"); + const zones = await fabric.searchAddr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"); - for (const zone of batch.zones) { + for (const zone of zones) { console.log(`${zone.handle}`); } // diff --git a/fabric/examples/kotlin/Example.kt b/fabric/examples/kotlin/Example.kt index b0dd707..b40bcfd 100644 --- a/fabric/examples/kotlin/Example.kt +++ b/fabric/examples/kotlin/Example.kt @@ -20,7 +20,7 @@ import org.spacesprotocol.fabric.SIG_PRIMARY_ZONE suspend fun exampleResolveIntro() { // val fabric = Fabric() - val resolved = fabric.resolve("alice@bitcoin") + val zone = fabric.resolve("alice@bitcoin") // } @@ -28,13 +28,13 @@ suspend fun exampleResolveIntro() { suspend fun exampleResolve() { // val fabric = Fabric() - val resolved = fabric.resolve("alice@bitcoin") - if (resolved == null) { + val zone = fabric.resolve("alice@bitcoin") + if (zone == null) { println("handle not found") return } - println("Handle found: ${resolved.zone.handle}") + println("Handle found: ${zone.handle}") // } @@ -45,10 +45,10 @@ suspend fun exampleTrustAndVerification() { // // Before pinning a trust id: resolve uses observed (peer) state // badge() returns Unverified - val resolved = fabric.resolve("alice@bitcoin") + val zone = fabric.resolve("alice@bitcoin") ?: throw IllegalStateException("handle exists") - fabric.badge(resolved) // Unverified + fabric.badge(zone) // Unverified // Pin trust from a QR scan val qr = "veritas://scan?id=14ef902621df01bdeee0b23fedf67458563a20df600af8979a4748dcd9d1b9f9" @@ -57,8 +57,8 @@ suspend fun exampleTrustAndVerification() { fabric.trustFromQr(qr) // Does not require re-resolving, badge now checks - // whether resolved was against a trusted root - fabric.badge(resolved) // Orange if handle is sovereign (final certificate) + // whether zone was against a trusted root + fabric.badge(zone) // Orange if handle is sovereign (final certificate) // Or from a semi-trusted source (e.g. an explorer you trust with qr scanned over HTTPS) // .badge() will not show Orange for roots in this trust pool, @@ -80,11 +80,11 @@ suspend fun exampleTrustAndVerification() { /// Unpack records from a resolved handle suspend fun exampleUnpackRecords() { val fabric = Fabric() - val resolved = fabric.resolve("alice@bitcoin") + val zone = fabric.resolve("alice@bitcoin") ?: throw IllegalStateException("handle exists") // - val records = resolved.zone.records.unpack() + val records = zone.records.unpack() for (record in records) { when (record) { @@ -101,9 +101,9 @@ suspend fun exampleResolveAll() { val fabric = Fabric() // - val batch = fabric.resolveAll(listOf("alice@bitcoin", "bob@bitcoin")) + val zones = fabric.resolveAll(listOf("alice@bitcoin", "bob@bitcoin")) - for (zone in batch.zones) { + for (zone in zones) { println("${zone.handle}: ${zone.sovereignty}") } // @@ -147,13 +147,13 @@ suspend fun exampleResolveById() { val fabric = Fabric() // - val resolved = fabric.resolveById("num1qx8dtlzq...") - if (resolved == null) { + val zone = fabric.resolveById("num1qx8dtlzq...") + if (zone == null) { println("handle not found") return } - println("Handle found: ${resolved.zone.handle}") + println("Handle found: ${zone.handle}") // } @@ -162,9 +162,9 @@ suspend fun exampleSearchAddr() { val fabric = Fabric() // - val batch = fabric.searchAddr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") + val zones = fabric.searchAddr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") - for (zone in batch.zones) { + for (zone in zones) { println("${zone.handle}: ${zone.sovereignty}") } // diff --git a/fabric/examples/python/example.py b/fabric/examples/python/example.py index 3e08f97..d2a1abb 100644 --- a/fabric/examples/python/example.py +++ b/fabric/examples/python/example.py @@ -11,7 +11,7 @@ async def example_resolve_intro(): # fabric = Fabric() - resolved = await fabric.resolve("alice@bitcoin") + zone = await fabric.resolve("alice@bitcoin") # @@ -19,12 +19,12 @@ async def example_resolve(): """Resolve a single handle""" # fabric = Fabric() - resolved = await fabric.resolve("alice@bitcoin") - if resolved is None: + zone = await fabric.resolve("alice@bitcoin") + if zone is None: print("handle not found") return - print(f"Handle found: {resolved.zone.handle}") + print(f"Handle found: {zone.handle}") # @@ -35,9 +35,9 @@ async def example_trust_and_verification(): # # Before pinning a trust id: resolve uses observed (peer) state # badge() returns Unverified - resolved = await fabric.resolve("alice@bitcoin") + zone = await fabric.resolve("alice@bitcoin") - fabric.badge(resolved) # Unverified + fabric.badge(zone) # Unverified # Pin trust from a QR scan qr = "veritas://scan?id=14ef902621df01bdeee0b23fedf67458563a20df600af8979a4748dcd9d1b9f9" @@ -46,8 +46,8 @@ async def example_trust_and_verification(): await fabric.trust_from_qr(qr) # Does not require re-resolving, badge now checks - # whether resolved was against a trusted root - fabric.badge(resolved) # Orange if handle is sovereign (final certificate) + # whether zone was against a trusted root + fabric.badge(zone) # Orange if handle is sovereign (final certificate) # Or from a semi-trusted source (e.g. an explorer you trust with qr scanned over HTTPS) # .badge() will not show Orange for roots in this trust pool, @@ -69,10 +69,10 @@ async def example_trust_and_verification(): async def example_unpack_records(): """Unpack records from a resolved handle""" fabric = Fabric() - resolved = await fabric.resolve("alice@bitcoin") + zone = await fabric.resolve("alice@bitcoin") # - records = resolved.zone.records.unpack() + records = zone.records.unpack() for record in records: if record.type == "txt": @@ -87,9 +87,9 @@ async def example_resolve_all(): fabric = Fabric() # - batch = await fabric.resolve_all(["alice@bitcoin", "bob@bitcoin"]) + zones = await fabric.resolve_all(["alice@bitcoin", "bob@bitcoin"]) - for zone in batch.zones: + for zone in zones: print(f"{zone.handle}: {zone.sovereignty}") # @@ -133,12 +133,12 @@ async def example_resolve_by_id(): fabric = Fabric() # - resolved = await fabric.resolve_by_id("num1qx8dtlzq...") - if resolved is None: + zone = await fabric.resolve_by_id("num1qx8dtlzq...") + if zone is None: print("handle not found") return - print(f"Handle found: {resolved.zone.handle}") + print(f"Handle found: {zone.handle}") # @@ -147,9 +147,9 @@ async def example_search_addr(): fabric = Fabric() # - batch = await fabric.search_addr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") + zones = await fabric.search_addr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") - for zone in batch.zones: + for zone in zones: print(f"{zone.handle}: {zone.sovereignty}") # diff --git a/fabric/examples/rust/src/main.rs b/fabric/examples/rust/src/main.rs index ddd44d6..1bb205d 100644 --- a/fabric/examples/rust/src/main.rs +++ b/fabric/examples/rust/src/main.rs @@ -2,7 +2,7 @@ // cargo add fabric-resolver // -use fabric::client::{Fabric}; +use fabric::client::Fabric; use fabric::libveritas::builder::MessageBuilder; use fabric::libveritas::cert::CertificateChain; use fabric::libveritas::msg::ChainProof; @@ -12,23 +12,22 @@ use fabric::signing::sign_schnorr; async fn example_resolve_intro() -> anyhow::Result<()> { // let fabric = Fabric::new(); - let resolved = fabric.resolve("alice@bitcoin").await?; + let zone = fabric.resolve("alice@bitcoin").await?; // - let _ = resolved; + let _ = zone; Ok(()) } - /// Resolve a single handle async fn example_resolve() -> anyhow::Result<()> { // let fabric = Fabric::new(); - let Some(resolved) = fabric.resolve("alice@bitcoin").await? else { + let Some(zone) = fabric.resolve("alice@bitcoin").await? else { println!("handle not found"); return Ok(()); }; - println!("Handle found: {}", resolved.zone.handle); + println!("Handle found: {}", zone.handle); // Ok(()) @@ -41,9 +40,9 @@ async fn example_trust_and_verification() -> anyhow::Result<()> { // // Before pinning a trust id: resolve uses observed (peer) state // badge() returns Unverified - let resolved = fabric.resolve("alice@bitcoin").await? + let zone = fabric.resolve("alice@bitcoin").await? .expect("handle exists"); - fabric.badge(&resolved); // Unverified + fabric.badge(&zone); // Unverified // Pin trust from a QR scan let qr = "veritas://scan?id=14ef902621df01bdeee0b23fedf67458563a20df600af8979a4748dcd9d1b9f9"; @@ -52,8 +51,8 @@ async fn example_trust_and_verification() -> anyhow::Result<()> { fabric.trust_from_qr(qr).await?; // Does not require re-resolving, badge now checks - // whether resolved was against a trusted root - fabric.badge(&resolved); // Orange if handle is sovereign (final certificate) + // whether zone was verified against a trusted root + fabric.badge(&zone); // Orange if handle is sovereign (final certificate) // Or from a semi-trusted source (e.g. an explorer you trust with qr scanned over HTTPS) // .badge() will not show Orange for roots in this trust pool, @@ -77,10 +76,10 @@ async fn example_trust_and_verification() -> anyhow::Result<()> { /// Unpack records from a resolved handle async fn example_unpack_records() -> anyhow::Result<()> { let fabric = Fabric::new(); - let resolved = fabric.resolve("alice@bitcoin").await?.expect("handle exists"); + let zone = fabric.resolve("alice@bitcoin").await?.expect("handle exists"); // - for record in resolved.zone.records.iter()? { + for record in zone.records.iter()? { match record { ParsedRecord::Txt { key, value } => { println!("txt {}={}", key, value.to_vec().join(", ")) @@ -101,11 +100,11 @@ async fn example_resolve_all() -> anyhow::Result<()> { let fabric = Fabric::new(); // - let batch = fabric + let zones = fabric .resolve_all(&["alice@bitcoin", "bob@bitcoin"]) .await?; - for zone in &batch.zones { + for zone in &zones { println!("{}: {:?}", zone.handle, zone.sovereignty); } // @@ -159,12 +158,12 @@ async fn example_resolve_by_id() -> anyhow::Result<()> { let fabric = Fabric::new(); // - let Some(resolved) = fabric.resolve_by_id("num1qx8dtlzq...").await? else { + let Some(zone) = fabric.resolve_by_id("num1qx8dtlzq...").await? else { println!("handle not found"); return Ok(()); }; - println!("Handle found: {}", resolved.zone.handle); + println!("Handle found: {}", zone.handle); // Ok(()) @@ -175,11 +174,11 @@ async fn example_search_addr() -> anyhow::Result<()> { let fabric = Fabric::new(); // - let batch = fabric + let zones = fabric .search_addr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") .await?; - for zone in &batch.zones { + for zone in &zones { println!("{}: {:?}", zone.handle, zone.sovereignty); } // @@ -239,4 +238,4 @@ async fn main() -> anyhow::Result<()> { println!("Done!"); Ok(()) -} \ No newline at end of file +} diff --git a/fabric/examples/swift/Example.swift b/fabric/examples/swift/Example.swift index 6eedc55..8bb1745 100644 --- a/fabric/examples/swift/Example.swift +++ b/fabric/examples/swift/Example.swift @@ -7,7 +7,7 @@ import Fabric func exampleResolveIntro() async throws { // let fabric = Fabric() - let resolved = try await fabric.resolve("alice@bitcoin") + let zone = try await fabric.resolve("alice@bitcoin") // } @@ -15,12 +15,12 @@ func exampleResolveIntro() async throws { func exampleResolve() async throws { // let fabric = Fabric() - guard let resolved = try await fabric.resolve("alice@bitcoin") else { + guard let zone = try await fabric.resolve("alice@bitcoin") else { print("handle not found") return } - print("Handle found: \(resolved.zone.handle)") + print("Handle found: \(zone.handle)") // } @@ -31,9 +31,9 @@ func exampleTrustAndVerification() async throws { // // Before pinning a trust id: resolve uses observed (peer) state // badge() returns Unverified - let resolved = try await fabric.resolve("alice@bitcoin")! + let zone = try await fabric.resolve("alice@bitcoin")! - fabric.badge(resolved) // Unverified + fabric.badge(zone) // Unverified // Pin trust from a QR scan let qr = "veritas://scan?id=14ef902621df01bdeee0b23fedf67458563a20df600af8979a4748dcd9d1b9f9" @@ -42,8 +42,8 @@ func exampleTrustAndVerification() async throws { try await fabric.trustFromQr(qr) // Does not require re-resolving, badge now checks - // whether resolved was against a trusted root - fabric.badge(resolved) // Orange if handle is sovereign (final certificate) + // whether zone was against a trusted root + fabric.badge(zone) // Orange if handle is sovereign (final certificate) // Or from a semi-trusted source (e.g. an explorer you trust with qr scanned over HTTPS) // .badge() will not show Orange for roots in this trust pool, @@ -65,10 +65,10 @@ func exampleTrustAndVerification() async throws { /// Unpack records from a resolved handle func exampleUnpackRecords() async throws { let fabric = Fabric() - let resolved = try await fabric.resolve("alice@bitcoin")! + let zone = try await fabric.resolve("alice@bitcoin")! // - let records = try resolved.zone.records.unpack() + let records = try zone.records.unpack() for record in records { switch record { @@ -88,9 +88,9 @@ func exampleResolveAll() async throws { let fabric = Fabric() // - let batch = try await fabric.resolveAll(["alice@bitcoin", "bob@bitcoin"]) + let zones = try await fabric.resolveAll(["alice@bitcoin", "bob@bitcoin"]) - for zone in batch.zones { + for zone in zones { print("\(zone.handle): \(zone.sovereignty)") } // @@ -137,12 +137,12 @@ func exampleResolveById() async throws { let fabric = Fabric() // - guard let resolved = try await fabric.resolveById("num1qx8dtlzq...") else { + guard let zone = try await fabric.resolveById("num1qx8dtlzq...") else { print("handle not found") return } - print("Handle found: \(resolved.zone.handle)") + print("Handle found: \(zone.handle)") // } @@ -151,9 +151,9 @@ func exampleSearchAddr() async throws { let fabric = Fabric() // - let batch = try await fabric.searchAddr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") + let zones = try await fabric.searchAddr("nostr", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") - for zone in batch.zones { + for zone in zones { print("\(zone.handle): \(zone.sovereignty)") } // diff --git a/fabric/go/fabric.go b/fabric/go/fabric.go index 63fd742..f6bf840 100644 --- a/fabric/go/fabric.go +++ b/fabric/go/fabric.go @@ -98,27 +98,6 @@ type ReverseRecord struct { Name string `json:"name"` } -// Resolved wraps a single zone with its verification roots. -type Resolved struct { - Zone libveritas.Zone - Roots []string // hex-encoded root IDs -} - -// ResolvedBatch wraps multiple zones with shared verification roots. -type ResolvedBatch struct { - Zones []libveritas.Zone - Roots []string // hex-encoded root IDs -} - -// Get looks up a specific handle from the batch. -func (b ResolvedBatch) Get(handle string) *Resolved { - for _, z := range b.Zones { - if z.Handle == handle { - return &Resolved{Zone: z, Roots: b.Roots} - } - } - return nil -} type anchorPool struct { trusted string // raw entries JSON @@ -257,13 +236,13 @@ func (f *Fabric) ClearTrusted() { f.mu.Unlock() } -// Badge returns the verification badge for a Resolved handle. -func (f *Fabric) Badge(resolved Resolved) VerificationBadge { - return f.BadgeFor(resolved.Zone.Sovereignty, resolved.Roots) +// Badge returns the verification badge for a Zone. +func (f *Fabric) Badge(zone libveritas.Zone) VerificationBadge { + return f.BadgeFor(zone.Sovereignty, zone.AnchorHash) } -// BadgeFor returns the verification badge given sovereignty and root IDs. -func (f *Fabric) BadgeFor(sovereignty string, roots []string) VerificationBadge { +// BadgeFor returns the verification badge given sovereignty and an anchor hash. +func (f *Fabric) BadgeFor(sovereignty string, anchorHash string) VerificationBadge { f.mu.Lock() hasAny := f.trusted != nil || f.observed != nil || f.semiTrusted != nil f.mu.Unlock() @@ -271,9 +250,9 @@ func (f *Fabric) BadgeFor(sovereignty string, roots []string) VerificationBadge return BadgeUnverified } - isTrusted := f.areRootsTrusted(roots) - isObserved := isTrusted || f.areRootsObserved(roots) - isSemiTrusted := isTrusted || f.areRootsSemiTrusted(roots) + isTrusted := f.isRootTrusted(anchorHash) + isObserved := isTrusted || f.isRootObserved(anchorHash) + isSemiTrusted := isTrusted || f.isRootSemiTrusted(anchorHash) if isTrusted && sovereignty == "sovereign" { return BadgeOrange @@ -284,58 +263,43 @@ func (f *Fabric) BadgeFor(sovereignty string, roots []string) VerificationBadge return BadgeNone } -func (f *Fabric) areRootsTrusted(roots []string) bool { +func (f *Fabric) isRootTrusted(anchorHash string) bool { f.mu.Lock() defer f.mu.Unlock() if f.trusted == nil { return false } - for _, root := range roots { - rootBytes, err := hex.DecodeString(root) - if err != nil { - return false - } - if !containsRoot(f.trusted.Roots, rootBytes) { - return false - } + rootBytes, err := hex.DecodeString(anchorHash) + if err != nil { + return false } - return true + return containsRoot(f.trusted.Roots, rootBytes) } -func (f *Fabric) areRootsObserved(roots []string) bool { +func (f *Fabric) isRootObserved(anchorHash string) bool { f.mu.Lock() defer f.mu.Unlock() if f.observed == nil { return false } - for _, root := range roots { - rootBytes, err := hex.DecodeString(root) - if err != nil { - return false - } - if !containsRoot(f.observed.Roots, rootBytes) { - return false - } + rootBytes, err := hex.DecodeString(anchorHash) + if err != nil { + return false } - return true + return containsRoot(f.observed.Roots, rootBytes) } -func (f *Fabric) areRootsSemiTrusted(roots []string) bool { +func (f *Fabric) isRootSemiTrusted(anchorHash string) bool { f.mu.Lock() defer f.mu.Unlock() if f.semiTrusted == nil { return false } - for _, root := range roots { - rootBytes, err := hex.DecodeString(root) - if err != nil { - return false - } - if !containsRoot(f.semiTrusted.Roots, rootBytes) { - return false - } + rootBytes, err := hex.DecodeString(anchorHash) + if err != nil { + return false } - return true + return containsRoot(f.semiTrusted.Roots, rootBytes) } func containsRoot(roots [][]byte, target []byte) bool { @@ -446,14 +410,14 @@ func (f *Fabric) updateAnchors(trustID string, kind trustKind) error { } // Resolve a single handle. Returns nil if not found. Supports dotted names like "hello.alice@bitcoin". -func (f *Fabric) Resolve(handle string) (*Resolved, error) { - batch, err := f.ResolveAll([]string{handle}) +func (f *Fabric) Resolve(handle string) (*libveritas.Zone, error) { + zones, err := f.ResolveAll([]string{handle}) if err != nil { return nil, err } - for _, z := range batch.Zones { + for _, z := range zones { if z.Handle == handle { - return &Resolved{Zone: z, Roots: batch.Roots}, nil + return &z, nil } } return nil, nil @@ -462,7 +426,7 @@ func (f *Fabric) Resolve(handle string) (*Resolved, error) { // ResolveById resolves a numeric ID to a handle by querying relays // for the reverse mapping, then verifying via forward resolution. // Returns nil if not found. -func (f *Fabric) ResolveById(numId string) (*Resolved, error) { +func (f *Fabric) ResolveById(numId string) (*libveritas.Zone, error) { if err := f.Bootstrap(); err != nil { return nil, err } @@ -499,29 +463,29 @@ func (f *Fabric) ResolveById(numId string) (*Resolved, error) { continue } - resolved, err := f.Resolve(name) + zone, err := f.Resolve(name) if err != nil { lastErr = err continue } - if resolved == nil { + if zone == nil { continue } - if resolved.Zone.NumId == nil || *resolved.Zone.NumId != numId { + if zone.NumId == nil || *zone.NumId != numId { lastErr = &FabricError{Code: "verify", Message: fmt.Sprintf("reverse mismatch: expected %s", numId)} continue } - return resolved, nil + return zone, nil } return nil, lastErr } // SearchAddr searches for handles by address record, verifies via forward resolution. -func (f *Fabric) SearchAddr(name, addr string) (ResolvedBatch, error) { +func (f *Fabric) SearchAddr(name, addr string) ([]libveritas.Zone, error) { if err := f.Bootstrap(); err != nil { - return ResolvedBatch{}, err + return nil, err } urls := f.pool.ShuffledURLs(4) var lastErr error = &FabricError{Code: "no_peers", Message: "address search failed"} @@ -557,7 +521,7 @@ func (f *Fabric) SearchAddr(name, addr string) (ResolvedBatch, error) { revNames[i] = h.Rev } - batch, err := f.ResolveAll(revNames) + zones, err := f.ResolveAll(revNames) if err != nil { lastErr = err continue @@ -565,7 +529,7 @@ func (f *Fabric) SearchAddr(name, addr string) (ResolvedBatch, error) { // Filter to zones that actually contain the matching addr record var matching []libveritas.Zone - for _, z := range batch.Zones { + for _, z := range zones { if z.Records != nil { rs := libveritas.NewRecordSet(*z.Records) records, err := rs.Unpack() @@ -585,22 +549,20 @@ func (f *Fabric) SearchAddr(name, addr string) (ResolvedBatch, error) { if len(matching) == 0 { continue } - batch.Zones = matching - return batch, nil + return matching, nil } - return ResolvedBatch{}, lastErr + return nil, lastErr } // ResolveAll resolves multiple handles including dotted names. -func (f *Fabric) ResolveAll(handles []string) (ResolvedBatch, error) { +func (f *Fabric) ResolveAll(handles []string) ([]libveritas.Zone, error) { lookup, err := libveritas.NewLookup(handles) if err != nil { - return ResolvedBatch{}, fmt.Errorf("creating lookup: %w", err) + return nil, fmt.Errorf("creating lookup: %w", err) } defer lookup.Destroy() var allZones []libveritas.Zone - var roots []string var prevBatch []string batch := lookup.Start() for len(batch) > 0 { @@ -609,26 +571,25 @@ func (f *Fabric) ResolveAll(handles []string) (ResolvedBatch, error) { } verified, err := f.resolveFlat(batch, true) if err != nil { - return ResolvedBatch{}, err + return nil, err } zones := verified.Zones() prevBatch = batch var next []string next, err = lookup.Advance(zones) if err != nil { - return ResolvedBatch{}, fmt.Errorf("lookup advance: %w", err) + return nil, fmt.Errorf("lookup advance: %w", err) } allZones = append(allZones, zones...) - roots = append(roots, hex.EncodeToString(verified.RootId())) batch = next } expanded, err := lookup.ExpandZones(allZones) if err != nil { - return ResolvedBatch{}, fmt.Errorf("expand zones: %w", err) + return nil, fmt.Errorf("expand zones: %w", err) } - return ResolvedBatch{Zones: expanded, Roots: roots}, nil + return expanded, nil } // Export resolves a handle and returns the raw certificate chain bytes. diff --git a/fabric/go/go.mod b/fabric/go/go.mod index e840f93..39e1fcb 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.1.4 + github.com/spacesprotocol/libveritas-go v0.2.0 ) require ( diff --git a/fabric/go/go.sum b/fabric/go/go.sum index 5223a91..0a1668a 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.1.4 h1:FuTQO25UgTocr3HMZm/qQnR4SihDVqWZ33On7+AtFD8= -github.com/spacesprotocol/libveritas-go v0.1.4/go.mod h1:HXnX2FNL43ueJuedsQwX9GF5jRHYZLqXYfFLWz900H8= +github.com/spacesprotocol/libveritas-go v0.2.0 h1:QJ/QYb3ixZu+PGQB1MgXaqVwY8NvcqjDE1dtqF2HFVQ= +github.com/spacesprotocol/libveritas-go v0.2.0/go.mod h1:HXnX2FNL43ueJuedsQwX9GF5jRHYZLqXYfFLWz900H8= diff --git a/fabric/js/fabric-core/src/fabric.ts b/fabric/js/fabric-core/src/fabric.ts index 837e9f9..4dabc92 100644 --- a/fabric/js/fabric-core/src/fabric.ts +++ b/fabric/js/fabric-core/src/fabric.ts @@ -12,23 +12,6 @@ import type { export type VerificationBadge = "orange" | "unverified" | "none"; -export interface Resolved { - zone: FabricZone; - roots: string[]; // hex-encoded -} - -export interface ResolvedBatch { - zones: FabricZone[]; - roots: string[]; // hex-encoded -} - -/** Look up a specific handle from a batch. */ -export function batchGet(batch: ResolvedBatch, handle: string): Resolved | undefined { - const zone = batch.zones.find(z => z.handle === handle); - if (!zone) return undefined; - return { zone, roots: batch.roots }; -} - export interface FabricOptions { provider: VeritasProvider; seeds?: string[]; @@ -275,22 +258,24 @@ export class Fabric { this._trusted = null; } - /** Compute a verification badge for a resolved handle. */ - badge(resolved: Resolved): VerificationBadge { - const json = resolved.zone.toJson(); + /** Compute a verification badge for a zone. */ + badge(zone: FabricZone): VerificationBadge { + const json = zone.toJson(); const sovereignty: string = json?.sovereignty ?? "delegated"; - return this.badgeFor(sovereignty, resolved.roots); + const anchorHash: string | undefined = json?.anchor_hash; + if (!anchorHash) return "unverified"; + return this.badgeFor(sovereignty, anchorHash); } - /** Compute a verification badge given sovereignty type and roots. */ - badgeFor(sovereignty: string, roots: string[]): VerificationBadge { + /** Compute a verification badge given sovereignty type and an anchor hash. */ + badgeFor(sovereignty: string, anchorHash: string): VerificationBadge { if (!this._trusted && !this._observed && !this._semiTrusted) { return "unverified"; } - const isTrusted = this.areRootsTrusted(roots); - const isObserved = isTrusted || this.areRootsObserved(roots); - const isSemiTrusted = isTrusted || this.areRootsSemiTrusted(roots); + const isTrusted = this.isRootTrusted(anchorHash); + const isObserved = isTrusted || this.isRootObserved(anchorHash); + const isSemiTrusted = isTrusted || this.isRootSemiTrusted(anchorHash); if (isTrusted && sovereignty === "sovereign") { return "orange"; @@ -301,22 +286,19 @@ export class Fabric { return "none"; } - private areRootsTrusted(roots: string[]): boolean { + private isRootTrusted(anchorHash: string): boolean { if (!this._trusted) return false; - const trustedSet = new Set(this._trusted.roots.map(r => toHex(r))); - return roots.every(root => trustedSet.has(root)); + return this._trusted.roots.some(r => toHex(r) === anchorHash); } - private areRootsObserved(roots: string[]): boolean { + private isRootObserved(anchorHash: string): boolean { if (!this._observed) return false; - const observedSet = new Set(this._observed.roots.map(r => toHex(r))); - return roots.every(root => observedSet.has(root)); + return this._observed.roots.some(r => toHex(r) === anchorHash); } - private areRootsSemiTrusted(roots: string[]): boolean { + private isRootSemiTrusted(anchorHash: string): boolean { if (!this._semiTrusted) return false; - const semiSet = new Set(this._semiTrusted.roots.map(r => toHex(r))); - return roots.every(root => semiSet.has(root)); + return this._semiTrusted.roots.some(r => toHex(r) === anchorHash); } // ── Publish ── @@ -490,17 +472,13 @@ export class Fabric { // ── Resolution ── /** Resolve a single handle. Returns null if not found. Supports nested names like `hello.alice@bitcoin`. */ - async resolve(handle: string): Promise { - const batch = await this.resolveAll([handle]); - const zone = batch.zones.find((z) => z.handle === handle); - if (!zone) { - return null; - } - return { zone, roots: batch.roots }; + async resolve(handle: string): Promise { + const zones = await this.resolveAll([handle]); + return zones.find((z) => z.handle === handle) ?? null; } /** Resolve a numeric ID to a verified handle. */ - async resolveById(numId: string): Promise { + async resolveById(numId: string): Promise { await this.bootstrap(); const relays = this.pool.shuffledUrls(4); let lastErr: Error = new FabricError("reverse resolution failed", "no_peers"); @@ -513,22 +491,22 @@ export class Fabric { const entry = records.find(r => r.id === numId); if (!entry) continue; - let resolved: Resolved | null; + let zone: FabricZone | null; try { - resolved = await this.resolve(entry.name); + zone = await this.resolve(entry.name); } catch (e) { lastErr = e instanceof Error ? e : new FabricError(String(e), "decode"); continue; } - if (!resolved) continue; + if (!zone) continue; - const json = resolved.zone.toJson(); + const json = zone.toJson(); if (json?.num_id !== numId) { lastErr = new FabricError(`reverse mismatch: expected ${numId}`, "verify"); continue; } - return resolved; + return zone; } catch (e) { lastErr = e instanceof FabricError ? e : new FabricError(`reverse failed: ${e}`, "http"); } @@ -538,7 +516,7 @@ export class Fabric { } /** Search for handles by address record. Verifies results via forward resolution. */ - async searchAddr(name: string, addr: string): Promise { + async searchAddr(name: string, addr: string): Promise { await this.bootstrap(); const relays = this.pool.shuffledUrls(4); let lastErr: Error = new FabricError("address search failed", "no_peers"); @@ -553,16 +531,16 @@ export class Fabric { if (!result.handles || result.handles.length === 0) continue; const revNames = result.handles.map(h => h.rev); - let batch: ResolvedBatch; + let zones: FabricZone[]; try { - batch = await this.resolveAll(revNames); + zones = await this.resolveAll(revNames); } catch (e) { lastErr = e instanceof Error ? e : new FabricError(String(e), "decode"); continue; } // Filter to zones that actually have the matching addr record - const matching = batch.zones.filter(zone => { + const matching = zones.filter(zone => { const json = zone.toJson(); const records = json?.records; if (!Array.isArray(records)) return false; @@ -577,7 +555,7 @@ export class Fabric { continue; } - return { zones: matching, roots: batch.roots }; + return matching; } catch (e) { lastErr = e instanceof FabricError ? e : new FabricError(`addr search failed: ${e}`, "http"); } @@ -587,10 +565,9 @@ export class Fabric { } /** Resolve multiple handles, including nested names like `hello.alice@bitcoin`. */ - async resolveAll(handles: string[]): Promise { + async resolveAll(handles: string[]): Promise { const lookup = this.provider.createLookup(handles); const allZones: FabricZone[] = []; - const roots: string[] = []; let prevBatch: string[] = []; let batch = lookup.start(); @@ -601,13 +578,9 @@ export class Fabric { prevBatch = batch; batch = lookup.advance(zones); allZones.push(...zones); - roots.push(toHex(verified.rootId())); } - return { - zones: lookup.expandZones(allZones), - roots, - }; + return lookup.expandZones(allZones); } /** Export a certificate chain for a handle. */ diff --git a/fabric/js/fabric-core/src/index.ts b/fabric/js/fabric-core/src/index.ts index d025fb3..4c108eb 100644 --- a/fabric/js/fabric-core/src/index.ts +++ b/fabric/js/fabric-core/src/index.ts @@ -1,10 +1,8 @@ -export { Fabric, FabricError, parseScanUri, batchGet } from "./fabric.js"; +export { Fabric, FabricError, parseScanUri } from "./fabric.js"; export type { FabricOptions, PeerInfo, VerificationBadge, - Resolved, - ResolvedBatch, ScanParams, SignSchnorrFn, } from "./fabric.js"; diff --git a/fabric/js/fabric-react-native/src/index.ts b/fabric/js/fabric-react-native/src/index.ts index d966a86..b13aca0 100644 --- a/fabric/js/fabric-react-native/src/index.ts +++ b/fabric/js/fabric-react-native/src/index.ts @@ -42,8 +42,6 @@ export type { EpochResult, HandleHint, VerificationBadge, - Resolved, - ResolvedBatch, } from "@spacesprotocol/fabric-core"; // Re-export libveritas types so consumers don't need a separate import diff --git a/fabric/js/fabric-web/package.json b/fabric/js/fabric-web/package.json index dfbc4f3..c97b88c 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.1.4" + "@spacesprotocol/libveritas": "^0.2.0" }, "devDependencies": { "@types/node": "^25.5.0", diff --git a/fabric/js/fabric-web/src/cli.ts b/fabric/js/fabric-web/src/cli.ts index cbed588..7cf1594 100644 --- a/fabric/js/fabric-web/src/cli.ts +++ b/fabric/js/fabric-web/src/cli.ts @@ -45,10 +45,10 @@ async function main() { await fabric.trust(trustId); } - const batch = await fabric.resolveAll(handles); + const zones = await fabric.resolveAll(handles); for (const handle of handles) { - const zone = batch.zones.find((z: any) => z.handle === handle); + const zone = zones.find((z: any) => z.handle === handle); if (!zone) { process.stderr.write(`${handle}: not found\n`); continue; diff --git a/fabric/js/fabric-web/src/index.ts b/fabric/js/fabric-web/src/index.ts index 0f8d41b..60c64f3 100644 --- a/fabric/js/fabric-web/src/index.ts +++ b/fabric/js/fabric-web/src/index.ts @@ -53,7 +53,6 @@ export class Fabric extends FabricCore { export { FabricError, RelayPool, - batchGet, compareHints, DEFAULT_SEEDS, } from "@spacesprotocol/fabric-core"; @@ -66,8 +65,6 @@ export type { EpochResult, HandleHint, VerificationBadge, - Resolved, - ResolvedBatch, } from "@spacesprotocol/fabric-core"; // Re-export libveritas types so consumers don't need a separate import diff --git a/fabric/kotlin/build.gradle.kts b/fabric/kotlin/build.gradle.kts index bc3e06a..518cb52 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.1.4") + compileOnly("org.spacesprotocol:libveritas-jvm:0.2.0") // CLI needs it at runtime - runtimeOnly("org.spacesprotocol:libveritas-jvm:0.1.4") + runtimeOnly("org.spacesprotocol:libveritas-jvm:0.2.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") compileOnly("fr.acinq.secp256k1:secp256k1-kmp:0.17.3") diff --git a/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/Fabric.kt b/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/Fabric.kt index 98cc316..45a0ffb 100644 --- a/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/Fabric.kt +++ b/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/Fabric.kt @@ -40,13 +40,6 @@ data class PeerInfo( enum class VerificationBadge { Orange, Unverified, None } -data class Resolved(val zone: Zone, val roots: List) -data class ResolvedBatch(val zones: List, val roots: List) { - fun get(handle: String): Resolved? { - val zone = zones.find { it.handle == handle } ?: return null - return Resolved(zone, roots) - } -} private val json = Json { ignoreUnknownKeys = true } @@ -130,16 +123,16 @@ class Fabric( fun clearTrusted() { trusted = null } - fun badge(resolved: Resolved): VerificationBadge = - badgeFor(resolved.zone.sovereignty, resolved.roots) + fun badge(zone: Zone): VerificationBadge = + badgeFor(zone.sovereignty, zone.anchorHash) - fun badgeFor(sovereignty: String, roots: List): VerificationBadge { + fun badgeFor(sovereignty: String, anchorHash: String): VerificationBadge { val hasAny = synchronized(lock) { trusted != null || observed != null || semiTrusted != null } if (!hasAny) return VerificationBadge.Unverified - val isTrusted = areRootsTrusted(roots) - val isObserved = isTrusted || areRootsObserved(roots) - val isSemiTrusted = isTrusted || areRootsSemiTrusted(roots) + val isTrusted = isRootTrusted(anchorHash) + val isObserved = isTrusted || isRootObserved(anchorHash) + val isSemiTrusted = isTrusted || isRootSemiTrusted(anchorHash) return when { isTrusted && sovereignty == "sovereign" -> VerificationBadge.Orange isObserved && !isTrusted && !isSemiTrusted -> VerificationBadge.Unverified @@ -207,13 +200,12 @@ class Fabric( // -- Resolution -- - fun resolve(handle: String): Resolved? { - val batch = resolveAll(listOf(handle)) - val zone = batch.zones.find { it.handle == handle } ?: return null - return Resolved(zone, batch.roots) + fun resolve(handle: String): Zone? { + val zones = resolveAll(listOf(handle)) + return zones.find { it.handle == handle } } - fun resolveById(numId: String): Resolved? { + fun resolveById(numId: String): Zone? { bootstrap() val urls = pool.shuffledUrls(4) var lastErr: Exception = FabricError("no_peers", "reverse resolution failed") @@ -238,20 +230,20 @@ class Fabric( val entry = entries.find { it.id == numId } ?: continue - val resolved = resolve(entry.name) ?: continue + val zone = resolve(entry.name) ?: continue - if (resolved.zone.numId != numId) { - lastErr = FabricError("verify", "reverse mismatch: expected $numId, got ${resolved.zone.numId}") + if (zone.numId != numId) { + lastErr = FabricError("verify", "reverse mismatch: expected $numId, got ${zone.numId}") continue } - return resolved + return zone } return null } - fun searchAddr(name: String, addr: String): ResolvedBatch { + fun searchAddr(name: String, addr: String): List { bootstrap() val urls = pool.shuffledUrls(4) var lastErr: Exception = FabricError("no_peers", "address search failed") @@ -274,7 +266,7 @@ class Fabric( if (result.handles.isEmpty()) continue val revNames = result.handles.map { it.rev } - val batch = try { + val zones = try { resolveAll(revNames) } catch (e: Exception) { lastErr = e @@ -282,7 +274,7 @@ class Fabric( } // Filter to zones that actually contain the matching addr record - val matching = batch.zones.filter { z -> + val matching = zones.filter { z -> z.records?.let { bytes -> try { val rs = RecordSet(bytes) @@ -293,16 +285,15 @@ class Fabric( } ?: false } if (matching.isEmpty()) continue - return ResolvedBatch(matching, batch.roots) + return matching } throw lastErr } - fun resolveAll(handles: List): ResolvedBatch { + fun resolveAll(handles: List): List { val lookup = Lookup(handles) val allZones = mutableListOf() - val roots = mutableListOf() var prevBatch = emptyList() var batch = lookup.start() @@ -313,10 +304,9 @@ class Fabric( prevBatch = batch batch = lookup.advance(zones) allZones.addAll(zones) - roots.add(verified.rootId().toHexString()) } - return ResolvedBatch(lookup.expandZones(allZones), roots) + return lookup.expandZones(allZones) } fun export(handle: String): ByteArray { @@ -681,28 +671,22 @@ class Fabric( // -- Private trust helpers -- - private fun areRootsTrusted(roots: List): Boolean { + private fun isRootTrusted(anchorHash: String): Boolean { val ts = trusted ?: return false - return roots.all { root -> - val rootBytes = root.hexToByteArray() - ts.roots.any { it.contentEquals(rootBytes) } - } + val rootBytes = anchorHash.hexToByteArray() + return ts.roots.any { it.contentEquals(rootBytes) } } - private fun areRootsObserved(roots: List): Boolean { + private fun isRootObserved(anchorHash: String): Boolean { val ts = observed ?: return false - return roots.all { root -> - val rootBytes = root.hexToByteArray() - ts.roots.any { it.contentEquals(rootBytes) } - } + val rootBytes = anchorHash.hexToByteArray() + return ts.roots.any { it.contentEquals(rootBytes) } } - private fun areRootsSemiTrusted(roots: List): Boolean { + private fun isRootSemiTrusted(anchorHash: String): Boolean { val ts = semiTrusted ?: return false - return roots.all { root -> - val rootBytes = root.hexToByteArray() - ts.roots.any { it.contentEquals(rootBytes) } - } + val rootBytes = anchorHash.hexToByteArray() + return ts.roots.any { it.contentEquals(rootBytes) } } } diff --git a/fabric/python/fabric/__init__.py b/fabric/python/fabric/__init__.py index 5e9b920..702b0cd 100644 --- a/fabric/python/fabric/__init__.py +++ b/fabric/python/fabric/__init__.py @@ -1,8 +1,6 @@ from .client import ( Fabric, FabricError, - Resolved, - ResolvedBatch, BADGE_ORANGE, BADGE_UNVERIFIED, BADGE_NONE, @@ -17,8 +15,6 @@ __all__ = [ "Fabric", "FabricError", - "Resolved", - "ResolvedBatch", "BADGE_ORANGE", "BADGE_UNVERIFIED", "BADGE_NONE", diff --git a/fabric/python/fabric/client.py b/fabric/python/fabric/client.py index 200a2e5..2cdf17c 100644 --- a/fabric/python/fabric/client.py +++ b/fabric/python/fabric/client.py @@ -35,24 +35,6 @@ def __init__(self, code: str, message: str, status: int = 0): else f"{code} ({status}): {message}") -@dataclass -class Resolved: - zone: lv.Zone - roots: list[str] # hex-encoded root IDs - - -@dataclass -class ResolvedBatch: - zones: list[lv.Zone] - roots: list[str] # hex-encoded root IDs - - def get(self, handle: str) -> Resolved | None: - """Look up a specific handle from the batch.""" - zone = next((z for z in self.zones if z.handle == handle), None) - if zone is None: - return None - return Resolved(zone=zone, roots=self.roots) - @dataclass class _EpochHint: @@ -211,31 +193,28 @@ def clear_trusted(self) -> None: """Clear the pinned trusted state.""" self._trusted = None - def badge(self, resolved: Resolved) -> str: - """Return the verification badge for a Resolved handle.""" - return self.badge_for(resolved.zone.sovereignty, resolved.roots) + def badge(self, zone: lv.Zone) -> str: + """Return the verification badge for a Zone.""" + return self.badge_for(zone.sovereignty, zone.anchor_hash) - def badge_for(self, sovereignty: str, roots: list[str]) -> str: - """Return the verification badge given sovereignty and root IDs.""" + def badge_for(self, sovereignty: str, anchor_hash: str) -> str: + """Return the verification badge given sovereignty and an anchor hash.""" if self._trusted is None and self._observed is None and self._semi_trusted is None: return BADGE_UNVERIFIED - is_trusted = self._are_roots_trusted(roots) - is_observed = is_trusted or self._are_roots_observed(roots) - is_semi_trusted = is_trusted or self._are_roots_semi_trusted(roots) + is_trusted = self._is_root_trusted(anchor_hash) + is_observed = is_trusted or self._is_root_observed(anchor_hash) + is_semi_trusted = is_trusted or self._is_root_semi_trusted(anchor_hash) if is_trusted and sovereignty == "sovereign": return BADGE_ORANGE if is_observed and not is_trusted and not is_semi_trusted: return BADGE_UNVERIFIED return BADGE_NONE - def resolve(self, handle: str) -> Resolved | None: - batch = self.resolve_all([handle]) - zone = next((z for z in batch.zones if z.handle == handle), None) - if zone is None: - return None - return Resolved(zone=zone, roots=batch.roots) + def resolve(self, handle: str) -> lv.Zone | None: + zones = self.resolve_all([handle]) + return next((z for z in zones if z.handle == handle), None) - def resolve_by_id(self, num_id: str) -> Resolved | None: + def resolve_by_id(self, num_id: str) -> lv.Zone | None: """Resolve a numeric ID to a verified handle. Returns None if not found.""" self.bootstrap() urls = self._pool.shuffled_urls(4) @@ -258,20 +237,20 @@ def resolve_by_id(self, num_id: str) -> Resolved | None: if entry is None: continue - resolved = self.resolve(entry["name"]) - if resolved is None: + zone = self.resolve(entry["name"]) + if zone is None: continue - if getattr(resolved.zone, "num_id", None) != num_id: + if getattr(zone, "num_id", None) != num_id: last_err = FabricError("verify", f"num_id mismatch: expected {num_id}") continue self._pool.mark_alive(u) - return resolved + return zone return None - def search_addr(self, name: str, addr: str) -> ResolvedBatch: + def search_addr(self, name: str, addr: str) -> list[lv.Zone]: """Search for handles by address record, verify via forward resolution.""" self.bootstrap() urls = self._pool.shuffled_urls(4) @@ -296,14 +275,14 @@ def search_addr(self, name: str, addr: str) -> ResolvedBatch: rev_names = [h["rev"] for h in handles] try: - batch = self.resolve_all(rev_names) + zones = self.resolve_all(rev_names) except Exception as e: last_err = e continue # Filter to zones that actually contain the matching addr record matching = [] - for z in batch.zones: + for z in zones: if z.records is not None: try: rs = lv.RecordSet(z.records) @@ -319,14 +298,13 @@ def search_addr(self, name: str, addr: str) -> ResolvedBatch: continue self._pool.mark_alive(u) - return ResolvedBatch(zones=matching, roots=batch.roots) + return matching raise last_err - def resolve_all(self, handles: list[str]) -> ResolvedBatch: + def resolve_all(self, handles: list[str]) -> list[lv.Zone]: lookup = lv.Lookup(handles) all_zones: list[lv.Zone] = [] - roots: list[str] = [] prev_batch: list[str] = [] batch = lookup.start() @@ -338,10 +316,8 @@ def resolve_all(self, handles: list[str]) -> ResolvedBatch: prev_batch = batch batch = lookup.advance(zones) all_zones.extend(zones) - roots.append(bytes(verified.root_id()).hex()) - expanded = lookup.expand_zones(all_zones) - return ResolvedBatch(zones=expanded, roots=roots) + return lookup.expand_zones(all_zones) def export(self, handle: str) -> bytes: """Export a certificate chain for a handle in .spacecert format.""" @@ -447,32 +423,26 @@ def refresh_peers(self): # -- Internal -- - def _are_roots_trusted(self, roots: list[str]) -> bool: + def _is_root_trusted(self, anchor_hash: str) -> bool: ts = self._trusted if ts is None: return False - return all( - any(bytes(r) == bytes.fromhex(root) for r in ts.roots) - for root in roots - ) + root_bytes = bytes.fromhex(anchor_hash) + return any(bytes(r) == root_bytes for r in ts.roots) - def _are_roots_observed(self, roots: list[str]) -> bool: + def _is_root_observed(self, anchor_hash: str) -> bool: ts = self._observed if ts is None: return False - return all( - any(bytes(r) == bytes.fromhex(root) for r in ts.roots) - for root in roots - ) + root_bytes = bytes.fromhex(anchor_hash) + return any(bytes(r) == root_bytes for r in ts.roots) - def _are_roots_semi_trusted(self, roots: list[str]) -> bool: + def _is_root_semi_trusted(self, anchor_hash: str) -> bool: ts = self._semi_trusted if ts is None: return False - return all( - any(bytes(r) == bytes.fromhex(root) for r in ts.roots) - for root in roots - ) + root_bytes = bytes.fromhex(anchor_hash) + return any(bytes(r) == root_bytes for r in ts.roots) def _bootstrap_peers(self): urls: set[str] = set() diff --git a/fabric/python/pyproject.toml b/fabric/python/pyproject.toml index 84d7dd5..0d1c1cd 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.1.4"] +dependencies = ["libveritas>=0.2.0"] [project.optional-dependencies] signing = ["coincurve"] diff --git a/fabric/rust/src/client.rs b/fabric/rust/src/client.rs index 507347d..f1718b7 100644 --- a/fabric/rust/src/client.rs +++ b/fabric/rust/src/client.rs @@ -164,34 +164,6 @@ impl fmt::Display for Badge { } } -#[derive(Clone, Serialize, Deserialize)] -pub struct ResolvedBatch { - pub zones: Vec, - pub roots: Vec, - pub relays: Vec, -} - -impl ResolvedBatch { - /// Look up a specific handle from the batch. - pub fn get(&self, handle: &str) -> Option { - self.zones - .iter() - .find(|z| z.handle.to_string() == handle) - .map(|z| Resolved { - zone: z.clone(), - roots: self.roots.clone(), - relays: self.relays.clone(), - }) - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct Resolved { - pub zone: Zone, - pub roots: Vec, - pub relays: Vec, -} - impl Default for Fabric { fn default() -> Self { Self::new() @@ -327,11 +299,7 @@ impl Fabric { true } - pub fn badge(&self, resolved: &Resolved) -> Badge { - self.badge_for(resolved.zone.sovereignty, resolved.roots.as_slice()) - } - - pub fn badge_for(&self, sov: SovereigntyState, roots: &[TrustId]) -> Badge { + pub fn badge(&self, zone: &Zone) -> Badge { let has_any_pool = self.trusted.lock().unwrap().is_some() || self.observed.lock().unwrap().is_some() || self.semi_trusted.lock().unwrap().is_some(); @@ -339,11 +307,12 @@ impl Fabric { return Badge::Unverified; } - let is_trusted = self.are_roots_trusted(roots); - let is_observed = is_trusted || self.are_roots_observed(roots); - let is_semi_trusted = is_trusted || self.are_roots_semi_trusted(roots); + let root = TrustId::from(zone.anchor_hash); + let is_trusted = self.are_roots_trusted(&[root]); + let is_observed = is_trusted || self.are_roots_observed(&[root]); + let is_semi_trusted = is_trusted || self.are_roots_semi_trusted(&[root]); - if is_trusted && matches!(sov, SovereigntyState::Sovereign) { + if is_trusted && matches!(zone.sovereignty, SovereigntyState::Sovereign) { Badge::Orange } else if is_observed && !is_trusted && !is_semi_trusted { Badge::Unverified @@ -527,32 +496,24 @@ impl Fabric { } /// Resolve a single handle and return its verified Zone. + /// Returns None if the handle doesn't exist. /// Supports dotted names like `hello.alice@bitcoin`. - pub async fn resolve(&self, handle: &str) -> Result> { - let rb = self.resolve_all(&[handle]).await?; - let zone = rb - .zones - .into_iter() - .find(|z| z.handle.to_string() == handle); - Ok(zone.map(|z| Resolved { - zone: z, - roots: rb.roots, - relays: rb.relays, - })) + pub async fn resolve(&self, handle: &str) -> Result> { + let zones = self.resolve_all(&[handle]).await?; + Ok(zones.into_iter().find(|z| z.handle.to_string() == handle)) } /// Reverse-resolve by a num id to retrieve its human-readable name. /// /// Queries relays for the reverse mapping, resolves the forward name, /// and verifies the zone's num_id matches. - pub async fn resolve_by_id(&self, num_id: &str) -> Result> { + pub async fn resolve_by_id(&self, num_id: &str) -> Result> { self.bootstrap().await?; let relays = self.pool.shuffled_urls_n(4); let mut last_err: Option = None; let mut any_responded = false; for url in &relays { - // 1. Fetch reverse mapping let reverse_url = format!("{url}/reverse?ids={num_id}"); let records: Vec = match self.http.get(&reverse_url).send().await { @@ -569,9 +530,8 @@ impl Fabric { continue; }; - // 2. Resolve forward - let resolved = match self.resolve(&entry.name).await { - Ok(Some(r)) => r, + let zone = match self.resolve(&entry.name).await { + Ok(Some(z)) => z, Ok(None) => continue, Err(e) => { last_err = Some(e); @@ -579,8 +539,7 @@ impl Fabric { } }; - // 3. Verify num_id matches - let zone_num_id = resolved.zone.num_id.as_ref().map(|id| id.to_string()); + let zone_num_id = zone.num_id.as_ref().map(|id| id.to_string()); if zone_num_id.as_deref() != Some(num_id) { last_err = Some(Error::Decode(std::io::Error::new( std::io::ErrorKind::InvalidData, @@ -589,10 +548,9 @@ impl Fabric { continue; } - return Ok(Some(resolved)); + return Ok(Some(zone)); } - // If relays responded but had no mapping, assume not found if any_responded && last_err.is_none() { return Ok(None); } @@ -604,13 +562,12 @@ impl Fabric { /// /// Queries relays for handles claiming the address, resolves them forward, /// and filters to zones that actually contain the matching addr record. - pub async fn search_addr(&self, name: &str, addr: &str) -> Result { + pub async fn search_addr(&self, name: &str, addr: &str) -> Result> { self.bootstrap().await?; let relays = self.pool.shuffled_urls_n(4); let mut last_err = Error::NoPeers; for url in &relays { - // 1. Fetch addr index let addr_url = format!("{url}/addrs?name={name}&addr={addr}"); let addr_match: crate::AddrMatch = match self.http.get(&addr_url).send().await { Ok(resp) if resp.status().is_success() => match resp.json().await { @@ -624,20 +581,17 @@ impl Fabric { continue; } - // 2. Resolve forward using the rev names let rev_names: Vec = addr_match.handles.iter().map(|e| e.rev.clone()).collect(); let refs: Vec<&str> = rev_names.iter().map(|s| s.as_str()).collect(); - let batch = match self.resolve_all(&refs).await { - Ok(b) => b, + let zones = match self.resolve_all(&refs).await { + Ok(z) => z, Err(e) => { last_err = e; continue; } }; - // 3. Filter to zones that actually have the matching addr record - let matching_zones: Vec = batch - .zones + let matching: Vec = zones .into_iter() .filter(|zone| { zone.records @@ -652,7 +606,7 @@ impl Fabric { }) .collect(); - if matching_zones.is_empty() { + if matching.is_empty() { last_err = Error::Decode(std::io::Error::new( std::io::ErrorKind::NotFound, "no verified matches", @@ -660,18 +614,14 @@ impl Fabric { continue; } - return Ok(ResolvedBatch { - zones: matching_zones, - roots: batch.roots, - relays: batch.relays, - }); + return Ok(matching); } Err(last_err) } /// Resolve multiple handles, including nested names like `hello.alice@bitcoin`. - pub async fn resolve_all(&self, handles: &[&str]) -> Result { + pub async fn resolve_all(&self, handles: &[&str]) -> Result> { let snames: Vec = handles .iter() .filter_map(|h| SName::try_from(*h).ok()) @@ -679,35 +629,23 @@ impl Fabric { let lookup = libveritas::names::Lookup::new(snames); let mut all_zones: Vec = Vec::new(); - let mut roots: Vec = Vec::new(); - let mut relays: Vec = Vec::new(); let mut prev_batch: Vec = Vec::new(); let mut batch: Vec = lookup.start(); while !batch.is_empty() { - // If advance returned the same batch, no progress — break 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, true).await?; + let (verified, _relay_url) = self.resolve_flat(&refs, true).await?; prev_batch = batch; batch = lookup.advance(&verified.zones); all_zones.extend(verified.zones); - roots.push(TrustId::from(verified.root_id)); - if !relays.contains(&relay_url) { - relays.push(relay_url); - } } lookup.expand_zones(&mut all_zones); - - Ok(ResolvedBatch { - zones: all_zones, - roots, - relays, - }) + Ok(all_zones) } /// Export a certificate chain for a handle in `.spacecert` format. diff --git a/fabric/rust/src/main.rs b/fabric/rust/src/main.rs index fa2f778..8f76526 100644 --- a/fabric/rust/src/main.rs +++ b/fabric/rust/src/main.rs @@ -64,9 +64,9 @@ async fn main() { let handle_refs: Vec<&str> = handles.iter().map(|s| s.as_ref()).collect(); match fabric.resolve_all(&handle_refs).await { - Ok(batch) => { + Ok(zones) => { for handle in &handles { - match batch.zones.iter().find(|z| z.handle.to_string() == *handle) { + match zones.iter().find(|z| z.handle.to_string() == *handle) { Some(zone) => println!("{}", serde_json::to_string(zone).unwrap()), None => eprintln!("{handle}: not found"), } diff --git a/fabric/swift/Package.swift b/fabric/swift/Package.swift index b9475e5..ec0a041 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.1.4"), + .package(url: "https://github.com/spacesprotocol/libveritas-swift.git", exact: "0.2.0"), .package(url: "https://github.com/21-DOT-DEV/swift-secp256k1.git", exact: "0.17.0"), ], targets: [ diff --git a/fabric/swift/Sources/Fabric/Fabric.swift b/fabric/swift/Sources/Fabric/Fabric.swift index efa9a75..62986df 100644 --- a/fabric/swift/Sources/Fabric/Fabric.swift +++ b/fabric/swift/Sources/Fabric/Fabric.swift @@ -53,26 +53,6 @@ public enum VerificationBadge { case none } -// MARK: - Resolved types - -/// A resolved handle with its zone and verification roots. -public struct Resolved { - public let zone: Zone - public let roots: [String] // hex-encoded root IDs -} - -/// A batch of resolved handles with shared verification roots. -public struct ResolvedBatch { - public let zones: [Zone] - public let roots: [String] // hex-encoded root IDs - - /// Look up a specific handle from the batch. - public func get(_ handle: String) -> Resolved? { - guard let zone = zones.first(where: { $0.handle == handle }) else { return nil } - return Resolved(zone: zone, roots: roots) - } -} - // MARK: - Trust kind private enum TrustKind { @@ -262,21 +242,21 @@ public final class Fabric: @unchecked Sendable { lock.lock(); trusted = nil; lock.unlock() } - /// Badge for a Resolved handle. - public func badge(_ resolved: Resolved) -> VerificationBadge { - badgeFor(sovereignty: resolved.zone.sovereignty, roots: resolved.roots) + /// Badge for a Zone. + public func badge(_ zone: Zone) -> VerificationBadge { + badgeFor(sovereignty: zone.sovereignty, anchorHash: zone.anchorHash) } - /// Badge given sovereignty and roots. - public func badgeFor(sovereignty: String, roots: [String]) -> VerificationBadge { + /// Badge given sovereignty and an anchor hash. + public func badgeFor(sovereignty: String, anchorHash: String) -> VerificationBadge { lock.lock() let hasAny = trusted != nil || observed != nil || semiTrusted != nil lock.unlock() if !hasAny { return .unverified } - let isTrusted = areRootsTrusted(roots) - let isObserved = isTrusted || areRootsObserved(roots) - let isSemiTrusted = isTrusted || areRootsSemiTrusted(roots) + let isTrusted = isRootTrusted(anchorHash) + let isObserved = isTrusted || isRootObserved(anchorHash) + let isSemiTrusted = isTrusted || isRootSemiTrusted(anchorHash) if isTrusted && sovereignty == "sovereign" { return .orange } if isObserved && !isTrusted && !isSemiTrusted { return .unverified } return .none @@ -334,16 +314,13 @@ public final class Fabric: @unchecked Sendable { // MARK: - Resolution /// Resolve a single handle. Returns nil if not found. Supports dotted names like `hello.alice@bitcoin`. - public func resolve(_ handle: String) async throws -> Resolved? { - let batch = try await resolveAll([handle]) - guard let zone = batch.zones.first(where: { $0.handle == handle }) else { - return nil - } - return Resolved(zone: zone, roots: batch.roots) + public func resolve(_ handle: String) async throws -> Zone? { + let zones = try await resolveAll([handle]) + return zones.first(where: { $0.handle == handle }) } /// Resolve a numeric ID to a verified handle. Returns nil if not found. - public func resolveById(_ numId: String) async throws -> Resolved? { + public func resolveById(_ numId: String) async throws -> Zone? { try await bootstrap() let relays = pool.shuffledUrls(4) @@ -358,17 +335,17 @@ public final class Fabric: @unchecked Sendable { guard let entry = entries.first(where: { $0.id == numId }) else { continue } - guard let resolved = try await resolve(entry.name) else { continue } + guard let zone = try await resolve(entry.name) else { continue } - guard resolved.zone.numId == numId else { continue } - return resolved + guard zone.numId == numId else { continue } + return zone } return nil } /// Search for handles by address record, verify via forward resolution. - public func searchAddr(_ name: String, addr: String) async throws -> ResolvedBatch { + public func searchAddr(_ name: String, addr: String) async throws -> [Zone] { try await bootstrap() let relays = pool.shuffledUrls(4) @@ -384,13 +361,13 @@ public final class Fabric: @unchecked Sendable { if result.handles.isEmpty { continue } let revNames = result.handles.map(\.rev) - let batch: ResolvedBatch + let zones: [Zone] do { - batch = try await resolveAll(revNames) + zones = try await resolveAll(revNames) } catch { continue } // Filter to zones that actually contain the matching addr record - let matching = batch.zones.filter { z in + let matching = zones.filter { z in do { let rs = RecordSet(data: z.records) let records = try rs.unpack() @@ -403,7 +380,7 @@ public final class Fabric: @unchecked Sendable { } catch { return false } } if matching.isEmpty { continue } - return ResolvedBatch(zones: matching, roots: batch.roots) + return matching } throw FabricError.noPeers @@ -413,10 +390,9 @@ public final class Fabric: @unchecked Sendable { /// /// Returns expanded zones for all requested handles. /// Uses the Lookup type from libveritas for dotted-name resolution. - public func resolveAll(_ handles: [String]) async throws -> ResolvedBatch { + public func resolveAll(_ handles: [String]) async throws -> [Zone] { let lookup = try Lookup(names: handles) var allZones = [Zone]() - var roots = [String]() var prevBatch = [String]() var batch = lookup.start() @@ -427,11 +403,9 @@ public final class Fabric: @unchecked Sendable { prevBatch = batch batch = try lookup.advance(zones: zones) allZones.append(contentsOf: zones) - roots.append(Data(verified.rootId()).hexString) } - let expanded = try lookup.expandZones(zones: allZones) - return ResolvedBatch(zones: expanded, roots: roots) + return try lookup.expandZones(zones: allZones) } /// Export a certificate chain for a handle in `.spacecert` format. @@ -727,31 +701,25 @@ public final class Fabric: @unchecked Sendable { // MARK: - Trust helpers (private) - private func areRootsTrusted(_ roots: [String]) -> Bool { + private func isRootTrusted(_ anchorHash: String) -> Bool { lock.lock(); defer { lock.unlock() } guard let ts = trusted else { return false } - return roots.allSatisfy { root in - guard let rootBytes = Data(hexString: root) else { return false } - return ts.roots.contains { Data($0) == rootBytes } - } + guard let rootBytes = Data(hexString: anchorHash) else { return false } + return ts.roots.contains { Data($0) == rootBytes } } - private func areRootsObserved(_ roots: [String]) -> Bool { + private func isRootObserved(_ anchorHash: String) -> Bool { lock.lock(); defer { lock.unlock() } guard let ts = observed else { return false } - return roots.allSatisfy { root in - guard let rootBytes = Data(hexString: root) else { return false } - return ts.roots.contains { Data($0) == rootBytes } - } + guard let rootBytes = Data(hexString: anchorHash) else { return false } + return ts.roots.contains { Data($0) == rootBytes } } - private func areRootsSemiTrusted(_ roots: [String]) -> Bool { + private func isRootSemiTrusted(_ anchorHash: String) -> Bool { lock.lock(); defer { lock.unlock() } guard let ts = semiTrusted else { return false } - return roots.allSatisfy { root in - guard let rootBytes = Data(hexString: root) else { return false } - return ts.roots.contains { Data($0) == rootBytes } - } + guard let rootBytes = Data(hexString: anchorHash) else { return false } + return ts.roots.contains { Data($0) == rootBytes } } // MARK: - Internal fetch helpers From d7a0989c8325fddfd9e0fe6048d0fc39d86306bb Mon Sep 17 00:00:00 2001 From: Buffrr Date: Fri, 24 Apr 2026 04:20:03 +0200 Subject: [PATCH 3/3] fix: skip rate limits in dev_mode, fix tests for libveritas 0.2 - Handler skips per-space and per-handle rate limits in dev_mode, fixing test_incremental_zone_replacement which was blocked by the 1-per-5-min handle rate limiter - Update test_anchors_endpoint to use compute_trust_set instead of removed trust_id/root_matches on AnchorSet - Update fabric_client tests for Zone-based resolve API --- relay/src/handler.rs | 16 +++++++++------- relay/tests/integration_tests.rs | 7 ++++--- tests/tests/fabric_client.rs | 33 ++++++++++++++------------------ 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/relay/src/handler.rs b/relay/src/handler.rs index 682e8c3..3481c76 100644 --- a/relay/src/handler.rs +++ b/relay/src/handler.rs @@ -304,13 +304,15 @@ impl Handler { let space = cert.subject.space()?.to_string(); // Rate limit per space (100 handle updates/min) and per handle (1 per 5 min) - if self.space_rate.check_key(&space).is_err() { - tracing::warn!("{}: space rate limited, skipping", space); - return None; - } - if self.handle_rate.check_key(&handle_str).is_err() { - tracing::warn!("{}: handle rate limited, skipping", handle_str); - return None; + if !self.dev_mode { + if self.space_rate.check_key(&space).is_err() { + tracing::warn!("{}: space rate limited, skipping", space); + return None; + } + if self.handle_rate.check_key(&handle_str).is_err() { + tracing::warn!("{}: handle rate limited, skipping", handle_str); + return None; + } } let epoch_height = epoch_map.get(&space).copied().unwrap_or(0); let offchain_seq = zone.records.seq().unwrap_or(0); diff --git a/relay/tests/integration_tests.rs b/relay/tests/integration_tests.rs index ce98eae..49db249 100644 --- a/relay/tests/integration_tests.rs +++ b/relay/tests/integration_tests.rs @@ -455,10 +455,10 @@ async fn test_anchors_endpoint() { .await .expect("anchors response should be valid JSON AnchorSet"); assert!(!anchor_set.entries.is_empty(), "should have anchor entries"); - assert!(anchor_set.root_matches(), "anchor root hash should match"); // GET /anchors?root= should return the same set - let root_hex = hex::encode(anchor_set.trust_id); + let trust_set = libveritas::compute_trust_set(&anchor_set.entries); + let root_hex = hex::encode(trust_set.id); let resp = client .get(format!("{}/anchors?root={}", url, root_hex)) .send() @@ -470,8 +470,9 @@ async fn test_anchors_endpoint() { .json() .await .expect("anchors response with root param should be valid JSON"); + let fetched_trust = libveritas::compute_trust_set(&fetched.entries); assert_eq!( - fetched.trust_id, anchor_set.trust_id, + fetched_trust.id, trust_set.id, "fetched anchor root should match" ); diff --git a/tests/tests/fabric_client.rs b/tests/tests/fabric_client.rs index 2dc4bf5..86e5f28 100644 --- a/tests/tests/fabric_client.rs +++ b/tests/tests/fabric_client.rs @@ -155,20 +155,16 @@ async fn test_resolve_all() { // Resolve multiple handles let fabric = Fabric::with_seeds(&[url.as_str()]); - let batch = fabric + let zones = fabric .resolve_all(&["alice@sovereign", "bob@sovereign"]) .await .expect("should resolve multiple handles"); assert!( - batch - .zones + zones .iter() .any(|z| z.handle.to_string() == "alice@sovereign") - || batch - .zones - .iter() - .any(|z| z.handle.to_string() == "@sovereign"), + || zones.iter().any(|z| z.handle.to_string() == "@sovereign"), "should contain alice or root zone" ); } @@ -183,9 +179,12 @@ async fn test_resolve_nonexistent() { let fabric = Fabric::with_seeds(&[url.as_str()]); - // Resolve a handle that was never broadcast - let result = fabric.resolve("nobody@sovereign").await; - assert!(result.is_err(), "resolving nonexistent handle should fail"); + // Resolve a handle that was never broadcast — returns None + let result = fabric.resolve("nobody@sovereign").await.unwrap(); + assert!( + result.is_none(), + "resolving nonexistent handle should return None" + ); } #[tokio::test(flavor = "multi_thread")] @@ -212,23 +211,19 @@ async fn test_resolve_all_partial() { // Resolve one existing and one nonexistent handle let fabric = Fabric::with_seeds(&[url.as_str()]); fabric.set_prefer_latest(false); - let batch = fabric + let zones = fabric .resolve_all(&["alice@sovereign", "nobody@sovereign"]) .await .expect("resolve_all should succeed with partial results"); // Should return the existing handle, not the missing one assert!( - !batch - .zones + !zones .iter() .any(|z| z.handle.to_string() == "nobody@sovereign"), "nonexistent handle should not be in results" ); - assert!( - batch.zones.len() >= 1, - "should have at least the existing handle" - ); + assert!(zones.len() >= 1, "should have at least the existing handle"); } #[tokio::test(flavor = "multi_thread")] @@ -257,9 +252,9 @@ async fn test_broadcast_then_resolve() { .await .expect("should resolve alice after broadcast"); - let batch = fabric + let zones = fabric .resolve_all(&["alice@sovereign", "bob@sovereign"]) .await .expect("should resolve all after broadcast"); - assert!(batch.zones.len() >= 2, "should have at least 2 zones"); + assert!(zones.len() >= 2, "should have at least 2 zones"); }