From 07428a8bc70c13a726c831a8002625836c44937b Mon Sep 17 00:00:00 2001 From: Buffrr Date: Sun, 26 Apr 2026 12:39:50 +0200 Subject: [PATCH] fix: sort merged anchors by descending height, fix non-Rust client builds --- fabric/go/cmd/fabric/main.go | 7 ++- fabric/go/fabric.go | 61 ++++++++++++------- fabric/js/fabric-core/src/fabric.ts | 1 + .../org/spacesprotocol/fabric/Fabric.kt | 42 +++++++------ .../org/spacesprotocol/fabric/cli/Main.kt | 4 +- fabric/python/fabric/client.py | 4 ++ fabric/swift/Sources/Fabric/Fabric.swift | 44 +++++++------ .../swift/Sources/FabricCLI/FabricCLI.swift | 4 +- 8 files changed, 102 insertions(+), 65 deletions(-) diff --git a/fabric/go/cmd/fabric/main.go b/fabric/go/cmd/fabric/main.go index a9cc108..e27d78b 100644 --- a/fabric/go/cmd/fabric/main.go +++ b/fabric/go/cmd/fabric/main.go @@ -51,7 +51,8 @@ func main() { seeds = fabric.DefaultSeeds } - f := fabric.New(seeds) + f := fabric.New() + f.SetSeeds(seeds) if devMode { f.SetDevMode(true) } @@ -62,14 +63,14 @@ func main() { } } - batch, err := f.ResolveAll(handles) + zones, err := f.ResolveAll(handles) if err != nil { fmt.Fprintf(os.Stderr, "error: %s\n", err) os.Exit(1) } zoneMap := make(map[string]libveritas.Zone) - for _, z := range batch.Zones { + for _, z := range zones { if _, exists := zoneMap[z.Handle]; !exists { zoneMap[z.Handle] = z } diff --git a/fabric/go/fabric.go b/fabric/go/fabric.go index f6bf840..3ec3801 100644 --- a/fabric/go/fabric.go +++ b/fabric/go/fabric.go @@ -117,7 +117,36 @@ func (p *anchorPool) merged() (string, error) { } allEntries = append(allEntries, entries...) } - data, err := json.Marshal(allEntries) + + type entryWithHeight struct { + raw json.RawMessage + height uint64 + } + parsed := make([]entryWithHeight, 0, len(allEntries)) + seen := make(map[uint64]struct{}, len(allEntries)) + for _, e := range allEntries { + var meta struct { + Block struct { + Height uint64 `json:"height"` + } `json:"block"` + } + if err := json.Unmarshal(e, &meta); err != nil { + continue + } + if _, ok := seen[meta.Block.Height]; ok { + continue + } + seen[meta.Block.Height] = struct{}{} + parsed = append(parsed, entryWithHeight{raw: e, height: meta.Block.Height}) + } + sort.Slice(parsed, func(i, j int) bool { + return parsed[i].height > parsed[j].height + }) + out := make([]json.RawMessage, len(parsed)) + for i, e := range parsed { + out[i] = e.raw + } + data, err := json.Marshal(out) return string(data), err } @@ -242,7 +271,7 @@ func (f *Fabric) Badge(zone libveritas.Zone) VerificationBadge { } // BadgeFor returns the verification badge given sovereignty and an anchor hash. -func (f *Fabric) BadgeFor(sovereignty string, anchorHash string) VerificationBadge { +func (f *Fabric) BadgeFor(sovereignty string, anchorHash []byte) VerificationBadge { f.mu.Lock() hasAny := f.trusted != nil || f.observed != nil || f.semiTrusted != nil f.mu.Unlock() @@ -263,43 +292,31 @@ func (f *Fabric) BadgeFor(sovereignty string, anchorHash string) VerificationBad return BadgeNone } -func (f *Fabric) isRootTrusted(anchorHash string) bool { +func (f *Fabric) isRootTrusted(anchorHash []byte) bool { f.mu.Lock() defer f.mu.Unlock() if f.trusted == nil { return false } - rootBytes, err := hex.DecodeString(anchorHash) - if err != nil { - return false - } - return containsRoot(f.trusted.Roots, rootBytes) + return containsRoot(f.trusted.Roots, anchorHash) } -func (f *Fabric) isRootObserved(anchorHash string) bool { +func (f *Fabric) isRootObserved(anchorHash []byte) bool { f.mu.Lock() defer f.mu.Unlock() if f.observed == nil { return false } - rootBytes, err := hex.DecodeString(anchorHash) - if err != nil { - return false - } - return containsRoot(f.observed.Roots, rootBytes) + return containsRoot(f.observed.Roots, anchorHash) } -func (f *Fabric) isRootSemiTrusted(anchorHash string) bool { +func (f *Fabric) isRootSemiTrusted(anchorHash []byte) bool { f.mu.Lock() defer f.mu.Unlock() if f.semiTrusted == nil { return false } - rootBytes, err := hex.DecodeString(anchorHash) - if err != nil { - return false - } - return containsRoot(f.semiTrusted.Roots, rootBytes) + return containsRoot(f.semiTrusted.Roots, anchorHash) } func containsRoot(roots [][]byte, target []byte) bool { @@ -530,8 +547,8 @@ func (f *Fabric) SearchAddr(name, addr string) ([]libveritas.Zone, error) { // Filter to zones that actually contain the matching addr record var matching []libveritas.Zone for _, z := range zones { - if z.Records != nil { - rs := libveritas.NewRecordSet(*z.Records) + if len(z.Records) > 0 { + rs := libveritas.NewRecordSet(z.Records) records, err := rs.Unpack() if err != nil { continue diff --git a/fabric/js/fabric-core/src/fabric.ts b/fabric/js/fabric-core/src/fabric.ts index 4dabc92..5d2b457 100644 --- a/fabric/js/fabric-core/src/fabric.ts +++ b/fabric/js/fabric-core/src/fabric.ts @@ -144,6 +144,7 @@ export class Fabric { seen.add(h); return true; }); + deduped.sort((a, b) => (b.block?.height ?? b.height) - (a.block?.height ?? a.height)); const anchors = this.provider.createAnchors(deduped); this.veritas = this.provider.createVeritas(anchors); } 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 45a0ffb..4e05525 100644 --- a/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/Fabric.kt +++ b/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/Fabric.kt @@ -4,7 +4,10 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import org.spacesprotocol.libveritas.* import java.io.InputStreamReader import java.net.HttpURLConnection @@ -60,15 +63,23 @@ private class AnchorPool { var observed: String = "" // raw entries JSON array string fun merged(): String { - val parts = mutableListOf() + val all = mutableListOf() for (src in listOf(trusted, semiTrusted, observed)) { - if (src.isNotEmpty()) { - // Strip outer brackets and add contents - val inner = src.trim().removePrefix("[").removeSuffix("]").trim() - if (inner.isNotEmpty()) parts.add(inner) - } + if (src.isEmpty()) continue + try { + all.addAll(json.parseToJsonElement(src).jsonArray) + } catch (_: Exception) {} + } + val seen = mutableSetOf() + val deduped = mutableListOf>() + for (e in all) { + val h = (e as? kotlinx.serialization.json.JsonObject) + ?.get("block")?.jsonObject + ?.get("height")?.jsonPrimitive?.content?.toLongOrNull() ?: 0L + if (seen.add(h)) deduped.add(h to e) } - return "[${parts.joinToString(",")}]" + deduped.sortByDescending { it.first } + return "[${deduped.joinToString(",") { it.second.toString() }}]" } } @@ -126,7 +137,7 @@ class Fabric( fun badge(zone: Zone): VerificationBadge = badgeFor(zone.sovereignty, zone.anchorHash) - fun badgeFor(sovereignty: String, anchorHash: String): VerificationBadge { + fun badgeFor(sovereignty: String, anchorHash: ByteArray): VerificationBadge { val hasAny = synchronized(lock) { trusted != null || observed != null || semiTrusted != null } if (!hasAny) return VerificationBadge.Unverified @@ -671,22 +682,19 @@ class Fabric( // -- Private trust helpers -- - private fun isRootTrusted(anchorHash: String): Boolean { + private fun isRootTrusted(anchorHash: ByteArray): Boolean { val ts = trusted ?: return false - val rootBytes = anchorHash.hexToByteArray() - return ts.roots.any { it.contentEquals(rootBytes) } + return ts.roots.any { it.contentEquals(anchorHash) } } - private fun isRootObserved(anchorHash: String): Boolean { + private fun isRootObserved(anchorHash: ByteArray): Boolean { val ts = observed ?: return false - val rootBytes = anchorHash.hexToByteArray() - return ts.roots.any { it.contentEquals(rootBytes) } + return ts.roots.any { it.contentEquals(anchorHash) } } - private fun isRootSemiTrusted(anchorHash: String): Boolean { + private fun isRootSemiTrusted(anchorHash: ByteArray): Boolean { val ts = semiTrusted ?: return false - val rootBytes = anchorHash.hexToByteArray() - return ts.roots.any { it.contentEquals(rootBytes) } + return ts.roots.any { it.contentEquals(anchorHash) } } } diff --git a/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/cli/Main.kt b/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/cli/Main.kt index d19f12c..6b9b76a 100644 --- a/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/cli/Main.kt +++ b/fabric/kotlin/src/main/kotlin/org/spacesprotocol/fabric/cli/Main.kt @@ -53,7 +53,7 @@ fun main(args: Array) { } } - val batch = try { + val zones = try { fabric.resolveAll(handles) } catch (e: Exception) { System.err.println("error: $e") @@ -61,7 +61,7 @@ fun main(args: Array) { } for (handle in handles) { - val zone = batch.zones.find { it.handle == handle } + val zone = zones.find { it.handle == handle } if (zone == null) { System.err.println("$handle: not found") continue diff --git a/fabric/python/fabric/client.py b/fabric/python/fabric/client.py index 2cdf17c..c97bc2c 100644 --- a/fabric/python/fabric/client.py +++ b/fabric/python/fabric/client.py @@ -93,6 +93,10 @@ def merged(self) -> list: if h not in seen: seen.add(h) deduped.append(e) + deduped.sort( + key=lambda e: e.get("block", {}).get("height", 0) if isinstance(e, dict) else 0, + reverse=True, + ) return deduped diff --git a/fabric/swift/Sources/Fabric/Fabric.swift b/fabric/swift/Sources/Fabric/Fabric.swift index 62986df..e988423 100644 --- a/fabric/swift/Sources/Fabric/Fabric.swift +++ b/fabric/swift/Sources/Fabric/Fabric.swift @@ -84,19 +84,28 @@ private struct AnchorPool { var observed: String = "" // raw entries JSON array string func merged() -> String? { - var parts = [String]() + var all: [Any] = [] for src in [trusted, semiTrusted, observed] { if src.isEmpty { continue } - let inner = src.trimmingCharacters(in: .whitespaces) - .dropFirst() // remove [ - .dropLast() // remove ] - .trimmingCharacters(in: .whitespaces) - if !inner.isEmpty { - parts.append(String(inner)) + guard let data = src.data(using: .utf8), + let arr = try? JSONSerialization.jsonObject(with: data) as? [Any] else { continue } + all.append(contentsOf: arr) + } + if all.isEmpty { return nil } + var seen = Set() + var deduped: [(UInt64, Any)] = [] + for e in all { + let h = ((e as? [String: Any])?["block"] as? [String: Any])?["height"] as? UInt64 + ?? UInt64(((e as? [String: Any])?["block"] as? [String: Any])?["height"] as? Int ?? 0) + if seen.insert(h).inserted { + deduped.append((h, e)) } } - if parts.isEmpty { return nil } - return "[\(parts.joined(separator: ","))]" + deduped.sort { $0.0 > $1.0 } + let sorted = deduped.map { $0.1 } + guard let data = try? JSONSerialization.data(withJSONObject: sorted), + let str = String(data: data, encoding: .utf8) else { return nil } + return str } } @@ -248,7 +257,7 @@ public final class Fabric: @unchecked Sendable { } /// Badge given sovereignty and an anchor hash. - public func badgeFor(sovereignty: String, anchorHash: String) -> VerificationBadge { + public func badgeFor(sovereignty: String, anchorHash: Data) -> VerificationBadge { lock.lock() let hasAny = trusted != nil || observed != nil || semiTrusted != nil lock.unlock() @@ -701,25 +710,22 @@ public final class Fabric: @unchecked Sendable { // MARK: - Trust helpers (private) - private func isRootTrusted(_ anchorHash: String) -> Bool { + private func isRootTrusted(_ anchorHash: Data) -> Bool { lock.lock(); defer { lock.unlock() } guard let ts = trusted else { return false } - guard let rootBytes = Data(hexString: anchorHash) else { return false } - return ts.roots.contains { Data($0) == rootBytes } + return ts.roots.contains { Data($0) == anchorHash } } - private func isRootObserved(_ anchorHash: String) -> Bool { + private func isRootObserved(_ anchorHash: Data) -> Bool { lock.lock(); defer { lock.unlock() } guard let ts = observed else { return false } - guard let rootBytes = Data(hexString: anchorHash) else { return false } - return ts.roots.contains { Data($0) == rootBytes } + return ts.roots.contains { Data($0) == anchorHash } } - private func isRootSemiTrusted(_ anchorHash: String) -> Bool { + private func isRootSemiTrusted(_ anchorHash: Data) -> Bool { lock.lock(); defer { lock.unlock() } guard let ts = semiTrusted else { return false } - guard let rootBytes = Data(hexString: anchorHash) else { return false } - return ts.roots.contains { Data($0) == rootBytes } + return ts.roots.contains { Data($0) == anchorHash } } // MARK: - Internal fetch helpers diff --git a/fabric/swift/Sources/FabricCLI/FabricCLI.swift b/fabric/swift/Sources/FabricCLI/FabricCLI.swift index 48e3d49..5a73115 100644 --- a/fabric/swift/Sources/FabricCLI/FabricCLI.swift +++ b/fabric/swift/Sources/FabricCLI/FabricCLI.swift @@ -45,10 +45,10 @@ struct FabricCLI { try await fabric.trust(trustId) } - let batch = try await fabric.resolveAll(handles) + let zones = try await fabric.resolveAll(handles) for handle in handles { - guard let zone = batch.zones.first(where: { $0.handle == handle }) else { + guard let zone = zones.first(where: { $0.handle == handle }) else { fputs("\(handle): not found\n", stderr) continue }