diff --git a/bench/fixture/discovery.go b/bench/fixture/discovery.go new file mode 100644 index 000000000..995a5bad1 --- /dev/null +++ b/bench/fixture/discovery.go @@ -0,0 +1,184 @@ +package fixture + +import ( + "context" + "sync/atomic" + "testing" + + "github.com/lightninglabs/taproot-assets/universe" +) + +const ( + // approxRootWireBytes approximates the wire size of one enumerated + // universe root: 32-byte asset ID (or group key hash), 1-byte proof + // type, 32-byte node hash, 8-byte sum. + approxRootWireBytes = 73 + + // approxLeafKeyWireBytes approximates the wire size of one + // enumerated leaf key: 36-byte outpoint plus 33-byte script key. + approxLeafKeyWireBytes = 69 +) + +// DiscoveryMetrics counts the remote DiffEngine traffic generated by a +// sync run, split by discovery phase. The fixture is in-process, but in +// production every DiffEngine call is an RPC round trip (or one page +// thereof), so the call counters approximate round trips and the byte +// counters approximate wire bytes. +type DiscoveryMetrics struct { + // RootPages counts RootNodes calls (one per enumeration page). + RootPages atomic.Int64 + + // RootsEnumerated counts roots returned across all RootNodes pages. + RootsEnumerated atomic.Int64 + + // RootLookups counts RootNode point lookups. + RootLookups atomic.Int64 + + // LeafKeyPages counts UniverseLeafKeys calls (one per page). + LeafKeyPages atomic.Int64 + + // LeafKeysEnumerated counts leaf keys returned across all pages. + LeafKeysEnumerated atomic.Int64 + + // ProofFetches counts FetchProofLeaf calls. + ProofFetches atomic.Int64 + + // ProofBytes sums the raw proof blob sizes returned by + // FetchProofLeaf and by delta pages. A lower bound on proof wire + // bytes (inclusion proof overhead is excluded). + ProofBytes atomic.Int64 + + // DeltaPages counts SyncDelta calls (one per page round trip). + DeltaPages atomic.Int64 + + // DeltaItems counts leaves returned across all delta pages. + DeltaItems atomic.Int64 +} + +// Report emits the counters, the derived per-phase byte estimates, and +// the discovery share of total bytes via b.ReportMetric. +func (m *DiscoveryMetrics) Report(b *testing.B) { + b.Helper() + + rootBytes := m.RootsEnumerated.Load() * approxRootWireBytes + keyBytes := m.LeafKeysEnumerated.Load() * approxLeafKeyWireBytes + proofBytes := m.ProofBytes.Load() + + discoveryBytes := rootBytes + keyBytes + share := float64(0) + if total := discoveryBytes + proofBytes; total > 0 { + share = float64(discoveryBytes) / float64(total) + } + + b.ReportMetric(float64(m.RootPages.Load()), "root_pages") + b.ReportMetric(float64(m.RootsEnumerated.Load()), "roots_enum") + b.ReportMetric(float64(m.RootLookups.Load()), "root_lookups") + b.ReportMetric(float64(m.LeafKeyPages.Load()), "leafkey_pages") + b.ReportMetric(float64(m.LeafKeysEnumerated.Load()), "leafkeys_enum") + b.ReportMetric(float64(m.ProofFetches.Load()), "proof_fetches") + b.ReportMetric(float64(m.DeltaPages.Load()), "delta_pages") + b.ReportMetric(float64(m.DeltaItems.Load()), "delta_items") + b.ReportMetric(float64(rootBytes), "root_bytes") + b.ReportMetric(float64(keyBytes), "leafkey_bytes") + b.ReportMetric(float64(proofBytes), "proof_bytes") + b.ReportMetric(share, "discovery_share") +} + +// countingDiffEngine wraps a universe.DiffEngine and tallies each call +// into a DiscoveryMetrics. It is installed around the fixture's remote +// archive so only remote-side (would-be wire) traffic is counted; the +// syncer's local enumeration is a DB scan, not a round trip. +type countingDiffEngine struct { + inner universe.DiffEngine + metrics *DiscoveryMetrics +} + +var _ universe.DiffEngine = (*countingDiffEngine)(nil) + +// RootNode delegates to the wrapped engine, counting the lookup. +// +// NOTE: part of the universe.DiffEngine interface. +func (c *countingDiffEngine) RootNode(ctx context.Context, + id universe.Identifier) (universe.Root, error) { + + c.metrics.RootLookups.Add(1) + return c.inner.RootNode(ctx, id) +} + +// RootNodes delegates to the wrapped engine, tallying the page and the +// roots it enumerates. +// +// NOTE: part of the universe.DiffEngine interface. +func (c *countingDiffEngine) RootNodes(ctx context.Context, + q universe.RootNodesQuery) ([]universe.Root, error) { + + roots, err := c.inner.RootNodes(ctx, q) + c.metrics.RootPages.Add(1) + c.metrics.RootsEnumerated.Add(int64(len(roots))) + return roots, err +} + +// UniverseLeafKeys delegates to the wrapped engine, tallying the page +// and the leaf entries it enumerates. +// +// NOTE: part of the universe.DiffEngine interface. +func (c *countingDiffEngine) UniverseLeafKeys(ctx context.Context, + q universe.UniverseLeafKeysQuery) ([]universe.LeafEntry, error) { + + keys, err := c.inner.UniverseLeafKeys(ctx, q) + c.metrics.LeafKeyPages.Add(1) + c.metrics.LeafKeysEnumerated.Add(int64(len(keys))) + return keys, err +} + +// FetchProofLeaf delegates to the wrapped engine, counting the fetch +// and the proof bytes it returns. +// +// NOTE: part of the universe.DiffEngine interface. +func (c *countingDiffEngine) FetchProofLeaf(ctx context.Context, + id universe.Identifier, + key universe.LeafKey) ([]*universe.Proof, error) { + + proofs, err := c.inner.FetchProofLeaf(ctx, id, key) + c.metrics.ProofFetches.Add(1) + for _, p := range proofs { + if p.Leaf != nil { + c.metrics.ProofBytes.Add(int64(len(p.Leaf.RawProof))) + } + } + return proofs, err +} + +// SyncDelta delegates to the wrapped engine's delta implementation, +// tallying pages, items, and payload bytes. +// +// NOTE: part of the universe.DeltaEngine interface. +func (c *countingDiffEngine) SyncDelta(ctx context.Context, + sinceSeq uint64, pageSize int32) (*universe.DeltaPage, error) { + + deltaEngine, ok := c.inner.(universe.DeltaEngine) + if !ok { + return nil, universe.ErrDeltaUnsupported + } + + page, err := deltaEngine.SyncDelta(ctx, sinceSeq, pageSize) + c.metrics.DeltaPages.Add(1) + if page != nil { + c.metrics.DeltaItems.Add(int64(len(page.Items))) + for _, item := range page.Items { + if item.Leaf != nil { + c.metrics.ProofBytes.Add( + int64(len(item.Leaf.RawProof)), + ) + } + } + } + return page, err +} + +// Close closes the wrapped engine. +// +// NOTE: part of the universe.DiffEngine interface. +func (c *countingDiffEngine) Close() error { + return c.inner.Close() +} diff --git a/bench/fixture/sync.go b/bench/fixture/sync.go index 87212726c..44f11641a 100644 --- a/bench/fixture/sync.go +++ b/bench/fixture/sync.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "math" "math/rand" "strings" "sync/atomic" @@ -104,6 +105,13 @@ type SeedSpec struct { // the local side starts empty; one means the two sides are already // identical (and sync should be a no-op). LocalOverlap Fraction + + // DivergentRoots, when non-zero, limits the LocalOverlap shortfall + // to the first DivergentRoots roots of each sweep; the remaining + // roots are seeded fully overlapped (locally identical). This + // models a steady-state federation where most universes are quiet + // and only a few gained new leaves since the last tick. + DivergentRoots int } // universePair is one side of the sync fixture. Each side has its own @@ -253,6 +261,16 @@ type SyncFixture struct { Remote *universePair Syncer *universe.SimpleSyncer Metrics *SyncMetrics + + // Discovery tallies the remote DiffEngine traffic (round trips and + // approximate wire bytes) the syncer generates while finding out + // what to fetch. + Discovery *DiscoveryMetrics + + // seededGens records the asset genesis used for each seeded root, + // keyed by proof type, in creation order. This is what allows a + // bench to grow a specific already-seeded universe after the fact. + seededGens map[universe.ProofType][]asset.Genesis } // NewSyncFixture constructs an unseeded SyncFixture. Call Seed to @@ -270,6 +288,7 @@ func NewSyncFixture(tb testing.TB, opts SyncFixtureOpts) *SyncFixture { remote := newUniversePair(tb, clk) metrics := &SyncMetrics{} + discovery := &DiscoveryMetrics{} syncer := universe.NewSimpleSyncer(universe.SimpleSyncCfg{ LocalDiffEngine: local.Archive, @@ -280,17 +299,24 @@ func NewSyncFixture(tb testing.TB, opts SyncFixtureOpts) *SyncFixture { NewRemoteDiffEngine: func( _ universe.ServerAddr) (universe.DiffEngine, error) { - return remote.Archive, nil + return &countingDiffEngine{ + inner: remote.Archive, + metrics: discovery, + }, nil }, SyncBatchSize: opts.SyncBatchSize, SyncRootConcurrency: opts.SyncRootConcurrency, }) return &SyncFixture{ - Local: local, - Remote: remote, - Syncer: syncer, - Metrics: metrics, + Local: local, + Remote: remote, + Syncer: syncer, + Metrics: metrics, + Discovery: discovery, + seededGens: make( + map[universe.ProofType][]asset.Genesis, + ), } } @@ -324,14 +350,15 @@ func (f *SyncFixture) Seed(tb testing.TB, spec SeedSpec) { ctx := context.Background() seedType(tb, ctx, f, universe.ProofTypeIssuance, spec.Issuance, - spec.LocalOverlap) + spec.LocalOverlap, spec.DivergentRoots) seedType(tb, ctx, f, universe.ProofTypeTransfer, spec.Transfer, - spec.LocalOverlap) + spec.LocalOverlap, spec.DivergentRoots) } // seedType is the per-proof-type worker used by Seed. func seedType(tb testing.TB, ctx context.Context, f *SyncFixture, - pt universe.ProofType, sweep RootSweep, overlap Fraction) { + pt universe.ProofType, sweep RootSweep, overlap Fraction, + divergentRoots int) { tb.Helper() @@ -342,6 +369,12 @@ func seedType(tb testing.TB, ctx context.Context, f *SyncFixture, localCount := int(float64(sweep.Leaves) * float64(overlap)) for r := 0; r < sweep.Roots; r++ { + // Roots beyond the divergent prefix are seeded fully + // overlapped: the local side already has every leaf. + rootLocalCount := localCount + if divergentRoots > 0 && r >= divergentRoots { + rootLocalCount = sweep.Leaves + } // All leaves under one root share a single asset genesis; the // universe identifier is derived from that same genesis so // insert-time and query-time namespaces agree. Deriving id @@ -353,6 +386,7 @@ func seedType(tb testing.TB, ctx context.Context, f *SyncFixture, AssetID: assetGen.ID(), ProofType: pt, } + f.seededGens[pt] = append(f.seededGens[pt], assetGen) remoteItems := make([]*universe.Item, sweep.Leaves) for i := 0; i < sweep.Leaves; i++ { @@ -370,17 +404,58 @@ func seedType(tb testing.TB, ctx context.Context, f *SyncFixture, ) require.NoError(tb, err) - if localCount == 0 { + if rootLocalCount == 0 { continue } err = f.Local.Multiverse.UpsertProofLeafBatch( - ctx, remoteItems[:localCount], + ctx, remoteItems[:rootLocalCount], ) require.NoError(tb, err) } } +// AddRemoteDivergence grows an already-seeded universe on the remote +// side only: n new leaves are appended to the rootIdx-th root of the +// given proof type's sweep. This models the steady-state shape where a +// previously synced universe gains new leaves between federation ticks. +func (f *SyncFixture) AddRemoteDivergence(tb testing.TB, + pt universe.ProofType, rootIdx, n int) { + + tb.Helper() + + gens := f.seededGens[pt] + require.Less(tb, rootIdx, len(gens), "root index out of range") + + assetGen := gens[rootIdx] + id := universe.Identifier{ + AssetID: assetGen.ID(), + ProofType: pt, + } + + ctx := context.Background() + for i := 0; i < n; i++ { + _, err := f.Remote.Multiverse.UpsertProofLeaf( + ctx, id, randLeafKey(tb), + randMintingLeafFor(tb, assetGen), nil, + ) + require.NoError(tb, err) + } +} + +// RemoteMaxSeq returns the remote side's current insertion-journal +// high-water mark, i.e. the cursor a fully synced peer would hold. +func (f *SyncFixture) RemoteMaxSeq(tb testing.TB) uint64 { + tb.Helper() + + _, maxSeq, err := f.Remote.Multiverse.FetchLeavesSince( + context.Background(), 0, int32(math.MaxInt32), + ) + require.NoError(tb, err) + + return maxSeq +} + // randLeafKey returns a random universe leaf key. Each call allocates // a fresh *asset.ScriptKey, which is exactly the shape that reveals // the pointer-identity diff bug when the returned keys are diffed via diff --git a/bench/scenario/universe_sync_steady_bench_test.go b/bench/scenario/universe_sync_steady_bench_test.go new file mode 100644 index 000000000..d17e2a5dc --- /dev/null +++ b/bench/scenario/universe_sync_steady_bench_test.go @@ -0,0 +1,140 @@ +package scenario + +import ( + "context" + "fmt" + "testing" + + "github.com/lightninglabs/taproot-assets/bench/fixture" + "github.com/lightninglabs/taproot-assets/universe" +) + +// steadyStateDelta is the number of leaves each divergent root is +// missing locally: the "d" in the steady-state cost model, i.e. what a +// mostly-synced node actually needs from one federation tick. +const steadyStateDelta = 4 + +// BenchmarkUniverseSync_SteadyState measures discovery cost on a +// mostly-synced node: all universes are locally present, one gained +// steadyStateDelta new leaves remotely since the last tick. The +// interesting output is not ns/op but the discovery metrics — how many +// round trips and bytes of enumeration it takes to find d new leaves. +// The delta-sync work (SOLUTION.md) should collapse discovery_share +// towards zero; the enumeration baseline reported here is the "before" +// column. +func BenchmarkUniverseSync_SteadyState(b *testing.B) { + for _, roots := range []int{10, 50} { + for _, leaves := range []int{100, 400} { + name := fmt.Sprintf("roots=%d/leaves=%d/delta=%d", + roots, leaves, steadyStateDelta) + b.Run(name, func(b *testing.B) { + overlap := float64(leaves-steadyStateDelta) / + float64(leaves) + runSteadySyncBench(b, fixture.SeedSpec{ + Issuance: fixture.RootSweep{ + Roots: roots, + Leaves: leaves, + }, + LocalOverlap: fixture.NewFraction( + overlap, + ), + DivergentRoots: 1, + }) + }) + } + } +} + +// BenchmarkUniverseSync_SteadyStateDelta is the delta-sync counterpart +// of BenchmarkUniverseSync_SteadyState: both sides start fully synced +// (cursor at the remote's high-water mark), the remote gains +// steadyStateDelta new leaves in one universe, and one tick runs via +// SyncUniverseDelta. Discovery cost should be O(delta): one delta page +// round trip carrying exactly the new leaves, no root or leaf-key +// enumeration at all. +func BenchmarkUniverseSync_SteadyStateDelta(b *testing.B) { + for _, roots := range []int{10, 50} { + for _, leaves := range []int{100, 400} { + name := fmt.Sprintf("roots=%d/leaves=%d/delta=%d", + roots, leaves, steadyStateDelta) + b.Run(name, func(b *testing.B) { + runDeltaSteadyBench(b, fixture.SeedSpec{ + Issuance: fixture.RootSweep{ + Roots: roots, + Leaves: leaves, + }, + LocalOverlap: fixture.NewFraction(1), + }) + }) + } + } +} + +// runSteadySyncBench mirrors runSyncBench but additionally reports the +// discovery metrics, which are the point of the steady-state scenario. +func runSteadySyncBench(b *testing.B, spec fixture.SeedSpec) { + b.Helper() + + ctx := context.Background() + cfg := fixture.GlobalSyncConfig() + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + f := fixture.NewSyncFixture(b, fixture.SyncFixtureOpts{}) + f.Seed(b, spec) + b.StartTimer() + + _, err := f.Syncer.SyncUniverse( + ctx, universe.ServerAddr{}, universe.SyncFull, cfg, + ) + if err != nil { + b.Fatalf("sync: %v", err) + } + + b.StopTimer() + f.Metrics.Report(b) + f.Discovery.Report(b) + b.StartTimer() + } +} + +// runDeltaSteadyBench seeds a fully synced fixture, records the cursor, +// applies steadyStateDelta new remote leaves to the first issuance +// universe, then times a single delta sync tick. +func runDeltaSteadyBench(b *testing.B, spec fixture.SeedSpec) { + b.Helper() + + ctx := context.Background() + cfg := fixture.GlobalSyncConfig() + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + f := fixture.NewSyncFixture(b, fixture.SyncFixtureOpts{}) + f.Seed(b, spec) + cursor := f.RemoteMaxSeq(b) + f.AddRemoteDivergence( + b, universe.ProofTypeIssuance, 0, steadyStateDelta, + ) + b.StartTimer() + + res, err := f.Syncer.SyncUniverseDelta( + ctx, universe.ServerAddr{}, cursor, cfg, + ) + if err != nil { + b.Fatalf("delta sync: %v", err) + } + + b.StopTimer() + if res.NewCursor <= cursor { + b.Fatalf("cursor did not advance: %d -> %d", cursor, + res.NewCursor) + } + f.Metrics.Report(b) + f.Discovery.Report(b) + b.StartTimer() + } +} diff --git a/docs/release-notes/release-notes-0.8.2.md b/docs/release-notes/release-notes-0.8.2.md deleted file mode 100644 index 158580ab3..000000000 --- a/docs/release-notes/release-notes-0.8.2.md +++ /dev/null @@ -1,71 +0,0 @@ -# Release Notes -- [Bug Fixes](#bug-fixes) -- [New Features](#new-features) - - [Functional Enhancements](#functional-enhancements) - - [RPC Additions](#rpc-additions) - - [tapcli Additions](#tapcli-additions) -- [Improvements](#improvements) - - [Functional Updates](#functional-updates) - - [RPC Updates](#rpc-updates) - - [tapcli Updates](#tapcli-updates) - - [Config Changes](#config-changes) - - [Breaking Changes](#breaking-changes) - - [Performance Improvements](#performance-improvements) - - [Deprecations](#deprecations) -- [Technical and Architectural Updates](#technical-and-architectural-updates) - - [BIP/bLIP Spec Updates](#bipblip-spec-updates) - - [Testing](#testing) - - [Database](#database) - - [Code Health](#code-health) - - [Tooling and Documentation](#tooling-and-documentation) - -# Bug Fixes - -# New Features - -## Functional Enhancements - -## RPC Additions - -## tapcli Additions - -# Improvements - -## Functional Updates - -## RPC Updates - -## tapcli Updates - -## Config Changes - -## Code Health - -## Breaking Changes - -## Performance Improvements - -- [PR#2184](https://github.com/lightninglabs/taproot-assets/pull/2184) - dramatically improves the performance of universe federation proof push. - -- [PR#2183](https://github.com/lightninglabs/taproot-assets/pull/2183) - dramatically improves the performance of MS-SMT proof verification. - -- [PR#2188](https://github.com/lightninglabs/taproot-assets/pull/2188) - dramatically improves the performance of batched MS-SMT insertions. - -## Deprecations - -# Technical and Architectural Updates - -## BIP/bLIP Spec Updates - -## Testing - -## Database - -## Code Health - -## Tooling and Documentation - -# Contributors (Alphabetical Order) diff --git a/docs/release-notes/release-notes-0.9.0.md b/docs/release-notes/release-notes-0.9.0.md index bb6b1b5df..c968586cb 100644 --- a/docs/release-notes/release-notes-0.9.0.md +++ b/docs/release-notes/release-notes-0.9.0.md @@ -37,18 +37,46 @@ ## Functional Updates +- [PR#2202](https://github.com/lightninglabs/taproot-assets/pull/2202) + adds cursor-based delta sync to the universe federation. Each server + exposes its insertion-ordered leaf journal via the new `SyncDelta` + RPC; a peer that remembers the last sequence number it applied can + fetch exactly the leaves it lacks, instead of enumerating every leaf + key of every divergent universe to compute a set difference. In the + fully synced steady state this reduces per-tick sync traffic from + O(universes + leaves) enumeration to a single round trip carrying + only the new proofs (measured discovery overhead drops from ~88% of + transferred bytes at 400 leaves/universe to zero). Convergence is + still verified by comparing local and remote universe roots after + each delta; any mismatch falls back to the existing enumeration + sync, and servers that don't support the new RPC are synced exactly + as before. + ## RPC Updates ## tapcli Updates ## Config Changes +- The new `--universe.no-delta-sync` flag forces the federation syncer + to always use full enumeration sync, serving as a kill switch for + the cursor-based delta sync mechanism. + ## Code Health ## Breaking Changes ## Performance Improvements +- [PR#2184](https://github.com/lightninglabs/taproot-assets/pull/2184) + dramatically improves the performance of universe federation proof push. + +- [PR#2183](https://github.com/lightninglabs/taproot-assets/pull/2183) + dramatically improves the performance of MS-SMT proof verification. + +- [PR#2188](https://github.com/lightninglabs/taproot-assets/pull/2188) + dramatically improves the performance of batched MS-SMT insertions. + ## Deprecations # Technical and Architectural Updates diff --git a/rpcserver/rpcserver.go b/rpcserver/rpcserver.go index 473bb1265..aa4a02b2d 100644 --- a/rpcserver/rpcserver.go +++ b/rpcserver/rpcserver.go @@ -8197,6 +8197,176 @@ func (r *RPCServer) SyncUniverse(ctx context.Context, return r.marshalUniverseDiff(ctx, universeDiff) } +// syncDeltaByteBudget caps the approximate payload size of a single +// SyncDelta response page, keeping it comfortably under the default +// 4 MiB gRPC message size limit. A variable rather than a constant so +// tests can exercise the page-splitting path with small payloads. +var syncDeltaByteBudget = 3 * 1024 * 1024 + +// syncDeltaItemOverhead is the approximate per-item wire overhead of a +// SyncDeltaItem beyond its raw proof and inclusion proof blobs +// (universe ID, leaf key, decoded asset fields, framing). +const syncDeltaItemOverhead = 512 + +// SyncDelta returns the universe leaves inserted on this server after the +// given sequence number, in insertion order, together with the current +// roots of the universes the delta touches. Leaves of universes with +// proof export disabled are omitted from the response but still advance +// latest_seq: the sequence is a position in this server's insertion log, +// not a count of exported items. +func (r *RPCServer) SyncDelta(ctx context.Context, + req *unirpc.SyncDeltaRequest) (*unirpc.SyncDeltaResponse, error) { + + pageSize := req.PageSize + switch { + case pageSize == 0: + pageSize = universe.RequestPageSize + + case pageSize < 0: + return nil, fmt.Errorf("invalid page size %d", pageSize) + + case pageSize > universe.MaxPageSize: + return nil, fmt.Errorf("page size %d exceeds maximum %d", + pageSize, universe.MaxPageSize) + } + + // Obtain the export gating config once for the whole page. + globalConfigs, uniSyncConfigs, err := + r.cfg.FederationDB.QueryFederationSyncConfigs(ctx) + if err != nil { + return nil, fmt.Errorf("query federation sync configs: %w", + err) + } + syncConfigs := universe.SyncConfigs{ + GlobalSyncConfigs: globalConfigs, + UniSyncConfigs: uniSyncConfigs, + } + + // A delta page is a single request that can carry many proofs, so we + // wait on the proof query rate limiter once per request rather than + // once per item. + if err := r.proofQueryRateLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("rate limiter: %w", err) + } + + items, _, err := r.cfg.UniverseArchive.FetchLeavesSince( + ctx, req.SinceSeq, pageSize, + ) + if err != nil { + return nil, fmt.Errorf("fetch leaves since seq=%d: %w", + req.SinceSeq, err) + } + + resp := &unirpc.SyncDeltaResponse{ + LatestSeq: req.SinceSeq, + } + + var ( + bytesUsed int + rootsSeen = make(map[universe.IdentifierKey]struct{}) + ) + + for i := range items { + item := items[i] + + // Universes with export disabled are omitted, but their + // leaves still advance the cursor. + if !syncConfigs.IsSyncExportEnabled(item.ID) { + resp.LatestSeq = item.Seq + continue + } + + // Fetch the leaf's inclusion proof along with its universe + // root. + proofs, err := r.cfg.UniverseArchive.FetchProofLeaf( + ctx, item.ID, item.Key, + ) + switch { + // The leaf disappeared between the delta query and the proof + // fetch (e.g. an administrative delete). Skipping it is + // safe: the caller's root comparison reconciles whatever + // divergence remains. + case errors.Is(err, universe.ErrNoUniverseProofFound): + rpcsLog.Warnf("SyncDelta: leaf at seq=%d vanished "+ + "before proof fetch, skipping", item.Seq) + resp.LatestSeq = item.Seq + continue + + case err != nil: + return nil, fmt.Errorf("fetch proof leaf "+ + "(seq=%d): %w", item.Seq, err) + } + firstProof := proofs[0] + + inclusionProof, err := marshalMssmtProof( + firstProof.UniverseInclusionProof, + ) + if err != nil { + return nil, err + } + + // Stop before this item if it would push the page past the + // response byte budget; latest_seq then points at the last + // included item and the caller simply pages again. + itemSize := len(item.Leaf.RawProof) + len(inclusionProof) + + syncDeltaItemOverhead + if len(resp.Items) > 0 && + bytesUsed+itemSize > syncDeltaByteBudget { + + break + } + + decDisplay, err := r.cfg.AddrBook.DecDisplayForAssetID( + ctx, item.Leaf.ID(), + ) + if err != nil { + return nil, err + } + + assetLeaf, err := r.marshalAssetLeaf( + ctx, item.Leaf, decDisplay, + ) + if err != nil { + return nil, err + } + + uniID, err := MarshalUniID(item.ID) + if err != nil { + return nil, err + } + + resp.Items = append(resp.Items, &unirpc.SyncDeltaItem{ + UniverseId: uniID, + Key: marshalLeafKey(item.Key), + Leaf: assetLeaf, + UniverseInclusionProof: inclusionProof, + Seq: item.Seq, + }) + bytesUsed += itemSize + resp.LatestSeq = item.Seq + + // Include this universe's root once per page. + idKey := item.ID.Key() + if _, ok := rootsSeen[idKey]; !ok { + rootsSeen[idKey] = struct{}{} + + uniRoot, err := marshalUniverseRoot(universe.Root{ + ID: item.ID, + Node: firstProof.UniverseRoot, + }) + if err != nil { + return nil, err + } + + resp.UniverseRoots = append( + resp.UniverseRoots, uniRoot, + ) + } + } + + return resp, nil +} + func marshalUniverseServer( server universe.ServerAddr) *unirpc.UniverseFederationServer { diff --git a/rpcserver/syncdelta_test.go b/rpcserver/syncdelta_test.go new file mode 100644 index 000000000..a9515478d --- /dev/null +++ b/rpcserver/syncdelta_test.go @@ -0,0 +1,396 @@ +package rpcserver + +import ( + "bytes" + "context" + "database/sql" + "encoding/hex" + "os" + "strings" + "testing" + "time" + + "github.com/btcsuite/btcd/wire/v2" + "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/internal/test" + "github.com/lightninglabs/taproot-assets/mssmt" + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/tapconfig" + "github.com/lightninglabs/taproot-assets/tapdb" + unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc" + "github.com/lightninglabs/taproot-assets/universe" + "github.com/lightningnetwork/lnd/clock" + lfn "github.com/lightningnetwork/lnd/fn/v2" + "github.com/stretchr/testify/require" + "golang.org/x/time/rate" +) + +// syncDeltaHarness bundles a minimally wired RPCServer whose universe +// archive is seeded with random leaves, together with the federation DB +// that controls export gating. +type syncDeltaHarness struct { + rpc *RPCServer + fedDB *tapdb.UniverseFederationDB + arch *universe.Archive +} + +// loadOddBlock loads the shared odd-transaction-count block fixture +// that proof.RandProof anchors its proofs in. +func loadOddBlock(t *testing.T) wire.MsgBlock { + t.Helper() + + oddTxBlockHex, err := os.ReadFile( + "../proof/testdata/odd-block.hex", + ) + require.NoError(t, err) + + oddTxBlockBytes, err := hex.DecodeString( + strings.Trim(string(oddTxBlockHex), "\n"), + ) + require.NoError(t, err) + + var oddTxBlock wire.MsgBlock + err = oddTxBlock.Deserialize(bytes.NewReader(oddTxBlockBytes)) + require.NoError(t, err) + + return oddTxBlock +} + +// seedUniverse inserts n random leaves into the given universe of the +// harness archive's multiverse store. +func seedUniverse(t *testing.T, mv *tapdb.MultiverseStore, + id universe.Identifier, assetGen asset.Genesis, block wire.MsgBlock, + n int) { + + t.Helper() + + ctx := context.Background() + for i := 0; i < n; i++ { + scriptKey := test.RandPubKey(t) + p := proof.RandProof(t, assetGen, scriptKey, block, 0, 1) + + proofBytes, err := p.Bytes() + require.NoError(t, err) + + leaf := &universe.Leaf{ + GenesisWithGroup: universe.GenesisWithGroup{ + Genesis: assetGen, + }, + RawProof: proofBytes, + Asset: &p.Asset, + Amt: p.Asset.Amount, + } + key := universe.BaseLeafKey{ + OutPoint: test.RandOp(t), + ScriptKey: fn.Ptr( + asset.NewScriptKey(scriptKey), + ), + } + + _, err = mv.UpsertProofLeaf(ctx, id, key, leaf, nil) + require.NoError(t, err) + } +} + +// newSyncDeltaHarness wires the minimal RPCServer dependencies the +// SyncDelta handler touches: the universe archive, the federation sync +// config DB, and the address book (for asset leaf marshaling). The +// archive is seeded with 3 issuance universes of 4 leaves each and 2 +// transfer universes of 3 leaves each. +func newSyncDeltaHarness(t *testing.T) *syncDeltaHarness { + t.Helper() + + clk := clock.NewTestClock(time.Unix(1_700_000_000, 0)) + db := tapdb.NewTestDB(t) + + multiverseTx := tapdb.NewTransactionExecutor( + db.BaseDB, func(tx *sql.Tx) tapdb.BaseMultiverseStore { + return db.WithTx(tx) + }, + ) + mv, err := tapdb.NewMultiverseStore( + multiverseTx, tapdb.DefaultMultiverseStoreConfig(), + ) + require.NoError(t, err) + + uniTx := tapdb.NewTransactionExecutor( + db.BaseDB, func(tx *sql.Tx) tapdb.BaseUniverseStore { + return db.WithTx(tx) + }, + ) + statsTx := tapdb.NewTransactionExecutor( + db.BaseDB, func(tx *sql.Tx) tapdb.UniverseStatsStore { + return db.WithTx(tx) + }, + ) + + arch := universe.NewArchive(universe.ArchiveConfig{ + NewBaseTree: func( + id universe.Identifier) universe.StorageBackend { + + return tapdb.NewBaseUniverseTree(uniTx, id) + }, + HeaderVerifier: proof.MockHeaderVerifier, + MerkleVerifier: proof.DefaultMerkleVerifier, + GroupVerifier: proof.MockGroupVerifier, + ChainLookupGenerator: proof.MockChainLookup, + Multiverse: mv, + UniverseStats: tapdb.NewUniverseStats(statsTx, clk), + IgnoreChecker: lfn.None[proof.IgnoreChecker](), + }) + + // Seed: 3 issuance universes x 4 leaves, 2 transfer universes x 3 + // leaves. Universe IDs derive from each universe's asset genesis, + // matching production. + oddBlock := loadOddBlock(t) + for i := 0; i < 3; i++ { + assetGen := asset.RandGenesis(t, asset.Normal) + seedUniverse(t, mv, universe.Identifier{ + AssetID: assetGen.ID(), + ProofType: universe.ProofTypeIssuance, + }, assetGen, oddBlock, 4) + } + for i := 0; i < 2; i++ { + assetGen := asset.RandGenesis(t, asset.Normal) + seedUniverse(t, mv, universe.Identifier{ + AssetID: assetGen.ID(), + ProofType: universe.ProofTypeTransfer, + }, assetGen, oddBlock, 3) + } + + fedTx := tapdb.NewTransactionExecutor( + db.BaseDB, func(tx *sql.Tx) tapdb.UniverseServerStore { + return db.WithTx(tx) + }, + ) + fedDB := tapdb.NewUniverseFederationDB(fedTx, clk) + + addrTx := tapdb.NewTransactionExecutor( + db.BaseDB, func(tx *sql.Tx) tapdb.AddrBook { + return db.WithTx(tx) + }, + ) + tapAddrBook := tapdb.NewTapAddressBook( + addrTx, &address.RegressionNetTap, clk, + ) + addrBook := address.NewBook(address.BookConfig{ + Store: tapAddrBook, + Chain: address.RegressionNetTap, + }) + + r := NewRPCServer() + r.cfg = &tapconfig.Config{ + DatabaseConfig: &tapconfig.DatabaseConfig{ + FederationDB: fedDB, + }, + UniverseArchive: arch, + AddrBook: addrBook, + } + r.proofQueryRateLimiter = rate.NewLimiter(rate.Inf, 1) + + // Enable export globally for both proof types, matching a fresh + // tapd's default. + ctx := context.Background() + err = fedDB.UpsertFederationSyncConfig( + ctx, []*universe.FedGlobalSyncConfig{ + { + ProofType: universe.ProofTypeIssuance, + AllowSyncInsert: true, + AllowSyncExport: true, + }, + { + ProofType: universe.ProofTypeTransfer, + AllowSyncInsert: true, + AllowSyncExport: true, + }, + }, nil, + ) + require.NoError(t, err) + + return &syncDeltaHarness{ + rpc: r, + fedDB: fedDB, + arch: arch, + } +} + +// serverRoots enumerates the universe roots of the harness archive. +func (h *syncDeltaHarness) serverRoots(t *testing.T) []universe.Root { + t.Helper() + + roots, err := h.arch.RootNodes( + context.Background(), universe.RootNodesQuery{Limit: 500}, + ) + require.NoError(t, err) + return roots +} + +// TestSyncDelta exercises the SyncDelta handler: full fetch, export +// gating, paging via the cursor, and inclusion proof validity. +func TestSyncDelta(t *testing.T) { + ctx := context.Background() + h := newSyncDeltaHarness(t) + + // The fixture seeds 3 issuance universes with 4 leaves each and 2 + // transfer universes with 3 leaves each. + const totalLeaves = 3*4 + 2*3 + + roots := h.serverRoots(t) + require.Len(t, roots, 5) + + // Disable export for one issuance universe; its leaves must be + // omitted from every response while still advancing the cursor. + var disabled universe.Identifier + for _, root := range roots { + if root.ID.ProofType == universe.ProofTypeIssuance { + disabled = root.ID + break + } + } + err := h.fedDB.UpsertFederationSyncConfig( + ctx, nil, []*universe.FedUniSyncConfig{{ + UniverseID: disabled, + AllowSyncInsert: true, + AllowSyncExport: false, + }}, + ) + require.NoError(t, err) + + // A single large page returns every exportable leaf. + full, err := h.rpc.SyncDelta(ctx, &unirpc.SyncDeltaRequest{ + SinceSeq: 0, + PageSize: 100, + }) + require.NoError(t, err) + require.Len(t, full.Items, totalLeaves-4) + + // Sequence numbers are strictly increasing and the disabled + // universe never appears. + rootsByUni := make(map[universe.IdentifierKey]*unirpc.UniverseRoot) + for _, rpcRoot := range full.UniverseRoots { + id, err := UnmarshalUniID(rpcRoot.Id) + require.NoError(t, err) + rootsByUni[id.Key()] = rpcRoot + } + require.Len(t, rootsByUni, 4) + require.NotContains(t, rootsByUni, disabled.Key()) + + var lastSeq uint64 + for _, item := range full.Items { + require.Greater(t, item.Seq, lastSeq) + lastSeq = item.Seq + + id, err := UnmarshalUniID(item.UniverseId) + require.NoError(t, err) + require.False(t, id.IsEqual(disabled)) + + // Every item's inclusion proof must bind its leaf to the + // universe root reported in the same response. + rpcRoot, ok := rootsByUni[id.Key()] + require.True(t, ok) + + leafKey, err := unmarshalLeafKey(item.Key) + require.NoError(t, err) + + leaf, err := unmarshalAssetLeaf(item.Leaf) + require.NoError(t, err) + + var compressedProof mssmt.CompressedProof + require.NoError(t, compressedProof.Decode( + bytes.NewReader(item.UniverseInclusionProof), + )) + inclusionProof, err := compressedProof.Decompress() + require.NoError(t, err) + + uniRoot, err := unmarshalUniverseRoot(rpcRoot) + require.NoError(t, err) + + uniProof := &universe.Proof{ + LeafKey: leafKey, + UniverseRoot: uniRoot, + UniverseInclusionProof: inclusionProof, + Leaf: leaf, + } + require.True(t, uniProof.VerifyRoot(uniRoot)) + } + + // The cursor is a position in the insertion log: filtered leaves + // advance it too, so resuming from latest_seq yields nothing new. + require.GreaterOrEqual(t, full.LatestSeq, lastSeq) + empty, err := h.rpc.SyncDelta(ctx, &unirpc.SyncDeltaRequest{ + SinceSeq: full.LatestSeq, + }) + require.NoError(t, err) + require.Empty(t, empty.Items) + require.Equal(t, full.LatestSeq, empty.LatestSeq) + + // Paging with a small page size walks the same item sequence. + var ( + paged []*unirpc.SyncDeltaItem + cursor uint64 + ) + for { + page, err := h.rpc.SyncDelta(ctx, &unirpc.SyncDeltaRequest{ + SinceSeq: cursor, + PageSize: 5, + }) + require.NoError(t, err) + + paged = append(paged, page.Items...) + if page.LatestSeq == cursor { + break + } + cursor = page.LatestSeq + } + require.Len(t, paged, len(full.Items)) + for i := range paged { + require.Equal(t, full.Items[i].Seq, paged[i].Seq) + } + + // Invalid page sizes are rejected. + _, err = h.rpc.SyncDelta(ctx, &unirpc.SyncDeltaRequest{ + PageSize: -1, + }) + require.ErrorContains(t, err, "invalid page size") + + _, err = h.rpc.SyncDelta(ctx, &unirpc.SyncDeltaRequest{ + PageSize: universe.MaxPageSize + 1, + }) + require.ErrorContains(t, err, "exceeds maximum") +} + +// TestSyncDeltaByteBudget pins that a page is cut short once the +// response byte budget is exhausted, while always admitting at least +// one item so the caller can make progress. +func TestSyncDeltaByteBudget(t *testing.T) { + ctx := context.Background() + h := newSyncDeltaHarness(t) + + // Shrink the budget so any single item exceeds it. + oldBudget := syncDeltaByteBudget + syncDeltaByteBudget = 1 + t.Cleanup(func() { + syncDeltaByteBudget = oldBudget + }) + + // Every page must carry exactly one item: the first item is always + // admitted, and any further item would exceed the budget. + page, err := h.rpc.SyncDelta(ctx, &unirpc.SyncDeltaRequest{ + SinceSeq: 0, + PageSize: 100, + }) + require.NoError(t, err) + require.Len(t, page.Items, 1) + + // The cursor points at the single included item, so the next page + // resumes right after it. + next, err := h.rpc.SyncDelta(ctx, &unirpc.SyncDeltaRequest{ + SinceSeq: page.LatestSeq, + PageSize: 100, + }) + require.NoError(t, err) + require.Len(t, next.Items, 1) + require.Greater(t, next.Items[0].Seq, page.Items[0].Seq) +} diff --git a/rpcserver/universe_rpc_diff.go b/rpcserver/universe_rpc_diff.go index e79c5a557..296fcb80e 100644 --- a/rpcserver/universe_rpc_diff.go +++ b/rpcserver/universe_rpc_diff.go @@ -12,6 +12,8 @@ import ( unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "github.com/lightninglabs/taproot-assets/universe" "golang.org/x/exp/maps" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // RpcUniverseDiff is an implementation of the universe.DiffEngine interface @@ -266,6 +268,91 @@ func (r *RpcUniverseDiff) FetchProofLeaf(ctx context.Context, return []*universe.Proof{uniProof}, nil } +// SyncDelta returns the page of leaves inserted on the remote server +// after sinceSeq, in insertion order. If the remote server predates the +// delta sync RPC, universe.ErrDeltaUnsupported is returned and the +// caller should fall back to enumeration-based sync. +// +// NOTE: this is part of the universe.DeltaEngine interface. +func (r *RpcUniverseDiff) SyncDelta(ctx context.Context, sinceSeq uint64, + pageSize int32) (*universe.DeltaPage, error) { + + resp, err := r.conn.SyncDelta(ctx, &universerpc.SyncDeltaRequest{ + SinceSeq: sinceSeq, + PageSize: pageSize, + }) + if status.Code(err) == codes.Unimplemented { + return nil, universe.ErrDeltaUnsupported + } + if err != nil { + return nil, err + } + + page := &universe.DeltaPage{ + Roots: make( + map[universe.IdentifierKey]universe.Root, + len(resp.UniverseRoots), + ), + LatestSeq: resp.LatestSeq, + } + + for _, rpcRoot := range resp.UniverseRoots { + root, err := unmarshalUniverseRoot(rpcRoot) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal "+ + "universe root: %w", err) + } + + page.Roots[root.ID.Key()] = root + } + + for _, rpcItem := range resp.Items { + uniID, err := UnmarshalUniID(rpcItem.UniverseId) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal "+ + "universe ID (seq=%d): %w", rpcItem.Seq, err) + } + + leafKey, err := unmarshalLeafKey(rpcItem.Key) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal leaf "+ + "key (seq=%d): %w", rpcItem.Seq, err) + } + + leaf, err := unmarshalAssetLeaf(rpcItem.Leaf) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal asset "+ + "leaf (seq=%d): %w", rpcItem.Seq, err) + } + + var compressedProof mssmt.CompressedProof + err = compressedProof.Decode( + bytes.NewReader(rpcItem.UniverseInclusionProof), + ) + if err != nil { + return nil, fmt.Errorf("unable to decode inclusion "+ + "proof (seq=%d): %w", rpcItem.Seq, err) + } + + inclusionProof, err := compressedProof.Decompress() + if err != nil { + return nil, fmt.Errorf("unable to decompress "+ + "inclusion proof (seq=%d): %w", rpcItem.Seq, + err) + } + + page.Items = append(page.Items, universe.DeltaLeafItem{ + Seq: rpcItem.Seq, + ID: uniID, + Key: leafKey, + Leaf: leaf, + InclusionProof: inclusionProof, + }) + } + + return page, nil +} + // Close closes the underlying RPC connection to the remote universe server. func (r *RpcUniverseDiff) Close() error { if err := r.conn.Close(); err != nil { @@ -280,3 +367,7 @@ func (r *RpcUniverseDiff) Close() error { // A compile time interface to ensure that RpcUniverseDiff implements the // universe.DiffEngine interface. var _ universe.DiffEngine = (*RpcUniverseDiff)(nil) + +// A compile time interface to ensure that RpcUniverseDiff implements the +// universe.DeltaEngine interface. +var _ universe.DeltaEngine = (*RpcUniverseDiff)(nil) diff --git a/rpcserver/universe_rpc_diff_test.go b/rpcserver/universe_rpc_diff_test.go new file mode 100644 index 000000000..a9296f84f --- /dev/null +++ b/rpcserver/universe_rpc_diff_test.go @@ -0,0 +1,163 @@ +package rpcserver + +import ( + "context" + "net" + "testing" + + unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc" + "github.com/lightninglabs/taproot-assets/universe" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" +) + +// deltaServerStub exposes only the SyncDelta method by delegating to +// the given handler function; every other universe RPC inherits the +// Unimplemented status. +type deltaServerStub struct { + unirpc.UnimplementedUniverseServer + + syncDelta func(context.Context, + *unirpc.SyncDeltaRequest) (*unirpc.SyncDeltaResponse, error) +} + +func (s *deltaServerStub) SyncDelta(ctx context.Context, + req *unirpc.SyncDeltaRequest) (*unirpc.SyncDeltaResponse, error) { + + return s.syncDelta(ctx, req) +} + +// newDeltaClient spins up an in-process gRPC server for the given +// universe server implementation and returns an RpcUniverseDiff dialed +// into it over a bufconn. +func newDeltaClient(t *testing.T, + srv unirpc.UniverseServer) *RpcUniverseDiff { + + t.Helper() + + lis := bufconn.Listen(1024 * 1024) + grpcServer := grpc.NewServer() + unirpc.RegisterUniverseServer(grpcServer, srv) + + go func() { + _ = grpcServer.Serve(lis) + }() + t.Cleanup(grpcServer.Stop) + + //nolint:staticcheck + conn, err := grpc.DialContext( + context.Background(), "bufnet", + grpc.WithContextDialer(func(ctx context.Context, + _ string) (net.Conn, error) { + + return lis.DialContext(ctx) + }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + require.NoError(t, err) + t.Cleanup(func() { + _ = conn.Close() + }) + + return &RpcUniverseDiff{ + conn: &universeClientConn{ + ClientConn: conn, + UniverseClient: unirpc.NewUniverseClient(conn), + }, + } +} + +// TestRpcUniverseDiffSyncDelta round-trips a delta page through the +// real SyncDelta handler over an in-process gRPC connection and checks +// the client-side unmarshaling, including inclusion proof validity +// against the returned roots. +func TestRpcUniverseDiffSyncDelta(t *testing.T) { + ctx := context.Background() + h := newSyncDeltaHarness(t) + + client := newDeltaClient(t, &deltaServerStub{ + syncDelta: h.rpc.SyncDelta, + }) + + page, err := client.SyncDelta(ctx, 0, 100) + require.NoError(t, err) + + // The harness seeds 3 issuance universes x 4 leaves and 2 transfer + // universes x 3 leaves, all exportable by default. + require.Len(t, page.Items, 3*4+2*3) + require.Len(t, page.Roots, 5) + require.Equal(t, page.Items[len(page.Items)-1].Seq, page.LatestSeq) + + var lastSeq uint64 + for _, item := range page.Items { + require.Greater(t, item.Seq, lastSeq) + lastSeq = item.Seq + + root, ok := page.Roots[item.ID.Key()] + require.True(t, ok) + + uniProof := &universe.Proof{ + LeafKey: item.Key, + UniverseRoot: root.Node, + UniverseInclusionProof: item.InclusionProof, + Leaf: item.Leaf, + } + require.True(t, uniProof.VerifyRoot(root.Node)) + } + + // Resuming from the returned cursor yields an empty page with the + // same cursor. + empty, err := client.SyncDelta(ctx, page.LatestSeq, 100) + require.NoError(t, err) + require.Empty(t, empty.Items) + require.Equal(t, page.LatestSeq, empty.LatestSeq) +} + +// TestRpcUniverseDiffSyncDeltaUnsupported pins that a remote server +// without the SyncDelta RPC surfaces universe.ErrDeltaUnsupported, the +// signal for callers to fall back to enumeration-based sync. +func TestRpcUniverseDiffSyncDeltaUnsupported(t *testing.T) { + client := newDeltaClient( + t, &unirpc.UnimplementedUniverseServer{}, + ) + + _, err := client.SyncDelta(context.Background(), 0, 100) + require.ErrorIs(t, err, universe.ErrDeltaUnsupported) +} + +// TestArchiveSyncDelta exercises the local (in-process) DeltaEngine +// implementation on the universe Archive, which serves pages without +// export gating. +func TestArchiveSyncDelta(t *testing.T) { + ctx := context.Background() + h := newSyncDeltaHarness(t) + + page, err := h.arch.SyncDelta(ctx, 0, 100) + require.NoError(t, err) + + require.Len(t, page.Items, 3*4+2*3) + require.Len(t, page.Roots, 5) + require.Equal(t, page.Items[len(page.Items)-1].Seq, page.LatestSeq) + + for _, item := range page.Items { + root, ok := page.Roots[item.ID.Key()] + require.True(t, ok) + + uniProof := &universe.Proof{ + LeafKey: item.Key, + UniverseRoot: root.Node, + UniverseInclusionProof: item.InclusionProof, + Leaf: item.Leaf, + } + require.True(t, uniProof.VerifyRoot(root.Node)) + } + + // Paging from the middle returns the suffix. + mid := page.Items[8].Seq + suffix, err := h.arch.SyncDelta(ctx, mid, 100) + require.NoError(t, err) + require.Len(t, suffix.Items, len(page.Items)-9) + require.Equal(t, page.LatestSeq, suffix.LatestSeq) +} diff --git a/sample-tapd.conf b/sample-tapd.conf index bb3abef03..afd7dd717 100644 --- a/sample-tapd.conf +++ b/sample-tapd.conf @@ -355,6 +355,10 @@ ; If set, the federation syncer will default to syncing all assets ; universe.sync-all-assets=false +; If set, the federation syncer will always use full enumeration sync instead +; of cursor-based delta sync, even against servers that support the latter +; universe.no-delta-sync=false + ; The public access mode for the universe server, controlling whether remote ; parties can read from and/or write to this universe server over RPC if ; exposed to a public network interface diff --git a/tapcfg/config.go b/tapcfg/config.go index c6ec91b34..dd5cd2d90 100644 --- a/tapcfg/config.go +++ b/tapcfg/config.go @@ -336,6 +336,8 @@ type UniverseConfig struct { SyncAllAssets bool `long:"sync-all-assets" description:"If set, the federation syncer will default to syncing all assets."` + NoDeltaSync bool `long:"no-delta-sync" description:"If set, the federation syncer will always use full enumeration sync instead of cursor-based delta sync, even against servers that support the latter."` + PublicAccess string `long:"public-access" description:"The public access mode for the universe server, controlling whether remote parties can read from and/or write to this universe server over RPC if exposed to a public network interface. This can be unset, 'r', 'w', or 'rw'. If unset, public access is not enabled for the universe server. If 'r' is included, public access is allowed for read-only endpoints. If 'w' is included, public access is allowed for write endpoints."` StatsCacheDuration time.Duration `long:"stats-cache-duration" description:"The amount of time to cache stats for before refreshing them. Valid time units are {s, m, h}."` diff --git a/tapcfg/server.go b/tapcfg/server.go index 49961d8e3..5ae545de3 100644 --- a/tapcfg/server.go +++ b/tapcfg/server.go @@ -468,6 +468,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, UniverseSyncer: universeSyncer, LocalRegistrar: uniArchive, SyncInterval: cfg.Universe.SyncInterval, + DisableDeltaSync: cfg.Universe.NoDeltaSync, NewRemoteRegistrar: pooledRegistrar, StaticFederationMembers: federationMembers, ServerChecker: func(addr universe.ServerAddr) error { diff --git a/tapdb/migrations.go b/tapdb/migrations.go index bb75bdabf..d9770b43a 100644 --- a/tapdb/migrations.go +++ b/tapdb/migrations.go @@ -24,7 +24,7 @@ const ( // daemon. // // NOTE: This MUST be updated when a new migration is added. - LatestMigrationVersion = 60 + LatestMigrationVersion = 61 ) // DatabaseBackend is an interface that contains all methods our different diff --git a/tapdb/multiverse.go b/tapdb/multiverse.go index bc2f65d4e..8592145fd 100644 --- a/tapdb/multiverse.go +++ b/tapdb/multiverse.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/wire/v2" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/mssmt" @@ -51,6 +52,14 @@ type ( // QueryMultiverseLeaves is used to query for a set of leaves based on // the proof type and asset ID (or group key) QueryMultiverseLeaves = sqlc.QueryMultiverseLeavesParams + + // UniverseLeavesSinceQuery is used to query for the insertion-ordered + // set of leaves inserted after a given sequence number. + UniverseLeavesSinceQuery = sqlc.FetchUniverseLeavesSinceParams + + // UniverseLeafDeltaRow is one row of the insertion-ordered leaf + // delta query. + UniverseLeafDeltaRow = sqlc.FetchUniverseLeavesSinceRow ) // BaseMultiverseStore is used to interact with a set of base universe @@ -72,6 +81,12 @@ type BaseMultiverseStore interface { // given target namespace (proof type in this case). FetchMultiverseRoot(ctx context.Context, proofNamespace string) (MultiverseRoot, error) + + // FetchUniverseLeavesSince returns leaves inserted after the given + // sequence number, in insertion order, across all issuance and + // transfer universes. + FetchUniverseLeavesSince(ctx context.Context, + arg UniverseLeavesSinceQuery) ([]UniverseLeafDeltaRow, error) } // BaseMultiverseOptions is the set of options for multiverse queries. @@ -1172,6 +1187,144 @@ func (b *MultiverseStore) FetchLeaves(ctx context.Context, return leaves, nil } +// FetchLeavesSince returns up to limit leaves inserted after sinceSeq +// across all issuance and transfer universes, in insertion order, along +// with the highest sequence number seen. If no leaves qualify, the +// returned sequence number equals sinceSeq. +// +// NOTE: an upsert that rewrites an existing leaf (re-org replacement) +// keeps its original sequence number and is therefore invisible to this +// query. Callers must not treat the delta as the sole source of +// divergence; root comparison remains authoritative. +func (b *MultiverseStore) FetchLeavesSince(ctx context.Context, + sinceSeq uint64, limit int32) ([]universe.DeltaLeafItem, uint64, + error) { + + if limit <= 0 { + limit = universe.RequestPageSize + } + + var ( + readTx = NewBaseUniverseReadTx() + items []universe.DeltaLeafItem + maxSeq = sinceSeq + ) + dbErr := b.db.ExecTx(ctx, &readTx, func(q BaseMultiverseStore) error { + items = nil + maxSeq = sinceSeq + + rows, err := q.FetchUniverseLeavesSince( + ctx, UniverseLeavesSinceQuery{ + SinceSeq: int64(sinceSeq), + NumLimit: limit, + }, + ) + if err != nil { + return err + } + + for _, row := range rows { + item, err := unmarshalLeafDeltaRow(ctx, q, row) + if err != nil { + return err + } + + items = append(items, *item) + maxSeq = item.Seq + } + + return nil + }) + if dbErr != nil { + return nil, 0, dbErr + } + + return items, maxSeq, nil +} + +// unmarshalLeafDeltaRow converts one row of the leaf delta query into a +// LeafDeltaItem. +func unmarshalLeafDeltaRow(ctx context.Context, q BaseMultiverseStore, + row UniverseLeafDeltaRow) (*universe.DeltaLeafItem, error) { + + if !row.ProofType.Valid { + return nil, fmt.Errorf("delta row %d: missing proof type", + row.Seq) + } + + uniID, err := universe.NewUniIDFromRawArgs( + row.RootAssetID, row.RootGroupKey, row.ProofType.String, + ) + if err != nil { + return nil, fmt.Errorf("delta row %d: %w", row.Seq, err) + } + + scriptKeyPub, err := schnorr.ParsePubKey(row.ScriptKeyBytes) + if err != nil { + return nil, fmt.Errorf("delta row %d: %w", row.Seq, err) + } + scriptKey := asset.NewScriptKey(scriptKeyPub) + + var genPoint wire.OutPoint + err = readOutPoint(bytes.NewReader(row.MintingPoint), 0, 0, &genPoint) + if err != nil { + return nil, fmt.Errorf("delta row %d: %w", row.Seq, err) + } + + leafKey := universe.BaseLeafKey{ + OutPoint: genPoint, + ScriptKey: &scriptKey, + } + + leafAssetGen, err := fetchGenesis(ctx, q, row.AssetGenesisID) + if err != nil { + return nil, fmt.Errorf("delta row %d: %w", row.Seq, err) + } + + // We only need the asset record for the leaf, so sparse decode just + // that from the raw proof. + var leafAsset asset.Asset + assetRecord := proof.AssetLeafRecord(&leafAsset) + err = proof.SparseDecode( + bytes.NewReader(row.GenesisProof), assetRecord, + ) + if err != nil { + return nil, fmt.Errorf("delta row %d: unable to decode "+ + "proof: %w", row.Seq, err) + } + + leaf := &universe.Leaf{ + GenesisWithGroup: universe.GenesisWithGroup{ + Genesis: leafAssetGen, + }, + RawProof: row.GenesisProof, + Asset: &leafAsset, + Amt: uint64(row.SumAmt), + } + + if uniID.GroupKey != nil { + leafAssetGroup, err := fetchGroupByGenesis( + ctx, q, row.AssetGenesisID, + ) + if err != nil { + return nil, fmt.Errorf("delta row %d: %w", row.Seq, + err) + } + + leaf.GroupKey = &asset.GroupKey{ + GroupPubKey: *uniID.GroupKey, + Witness: leafAssetGroup.Witness, + } + } + + return &universe.DeltaLeafItem{ + Seq: uint64(row.Seq), + ID: uniID, + Key: leafKey, + Leaf: leaf, + }, nil +} + // RegisterSubscriber adds a new subscriber for receiving events. The // deliverExisting boolean indicates whether already existing items should be // sent to the NewItemCreated channel when the subscription is started. An diff --git a/tapdb/multiverse_delta_test.go b/tapdb/multiverse_delta_test.go new file mode 100644 index 000000000..6d275e93e --- /dev/null +++ b/tapdb/multiverse_delta_test.go @@ -0,0 +1,144 @@ +package tapdb + +import ( + "context" + "testing" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/internal/test" + "github.com/lightninglabs/taproot-assets/universe" + "github.com/stretchr/testify/require" +) + +// deltaUniverse pairs a universe identifier with the asset genesis all +// of its leaves share. The identifier is derived from the genesis +// (matching production, where a non-group universe ID is the genesis +// asset ID), so the id recovered from the delta rows agrees with the +// one used at insert time. +type deltaUniverse struct { + id universe.Identifier + gen asset.Genesis +} + +// newDeltaUniverse creates a universe descriptor of the given proof +// type, optionally group-keyed. +func newDeltaUniverse(t *testing.T, proofType universe.ProofType, + withGroup bool) deltaUniverse { + + assetGen := asset.RandGenesis(t, asset.Normal) + id := universe.Identifier{ + AssetID: assetGen.ID(), + ProofType: proofType, + } + if withGroup { + id.GroupKey = test.RandPubKey(t) + } + + return deltaUniverse{id: id, gen: assetGen} +} + +// insertDeltaLeaf inserts a single random minting leaf into the given +// universe and returns the key and leaf that were inserted. +func insertDeltaLeaf(t *testing.T, ctx context.Context, + store *MultiverseStore, u deltaUniverse) (universe.LeafKey, + universe.Leaf) { + + key := randLeafKey(t) + leaf := randMintingLeaf(t, u.gen, u.id.GroupKey) + + _, err := store.UpsertProofLeaf(ctx, u.id, key, &leaf, nil) + require.NoError(t, err) + + return key, leaf +} + +// TestMultiverseFetchLeavesSince tests that the insertion-ordered leaf +// delta query returns exactly the leaves inserted after a given +// sequence number, in insertion order, across universes. +func TestMultiverseFetchLeavesSince(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store, _ := newTestMultiverse(t) + + // Create three universes: a plain issuance universe, a group-keyed + // issuance universe, and a transfer universe. + universes := []deltaUniverse{ + newDeltaUniverse(t, universe.ProofTypeIssuance, false), + newDeltaUniverse(t, universe.ProofTypeIssuance, true), + newDeltaUniverse(t, universe.ProofTypeTransfer, false), + } + + // Insert three leaves per universe, interleaved, so insertion order + // crosses universe boundaries. + type inserted struct { + id universe.Identifier + key universe.LeafKey + leaf universe.Leaf + } + var all []inserted + for i := 0; i < 3; i++ { + for _, u := range universes { + key, leaf := insertDeltaLeaf(t, ctx, store, u) + all = append(all, inserted{ + id: u.id, key: key, leaf: leaf, + }) + } + } + + // Fetching since zero should return every leaf, in insertion order. + items, maxSeq, err := store.FetchLeavesSince(ctx, 0, 0) + require.NoError(t, err) + require.Len(t, items, len(all)) + + for i, item := range items { + require.Equal(t, all[i].id.Key(), item.ID.Key()) + require.Equal( + t, all[i].key.UniverseKey(), item.Key.UniverseKey(), + ) + require.Equal( + t, []byte(all[i].leaf.RawProof), + []byte(item.Leaf.RawProof), + ) + + // Sequence numbers must be strictly increasing. + if i > 0 { + require.Greater(t, item.Seq, items[i-1].Seq) + } + } + require.Equal(t, items[len(items)-1].Seq, maxSeq) + + // Fetching since a mid-point sequence returns exactly the suffix. + midSeq := items[4].Seq + suffix, suffixMax, err := store.FetchLeavesSince(ctx, midSeq, 0) + require.NoError(t, err) + require.Len(t, suffix, len(all)-5) + require.Equal(t, items[5].Seq, suffix[0].Seq) + require.Equal(t, maxSeq, suffixMax) + + // A limited fetch returns only the first leaves of the range. + limited, limitedMax, err := store.FetchLeavesSince(ctx, 0, 4) + require.NoError(t, err) + require.Len(t, limited, 4) + require.Equal(t, items[3].Seq, limitedMax) + + // Fetching from the high-water mark returns nothing and reports the + // same sequence back. + empty, emptyMax, err := store.FetchLeavesSince(ctx, maxSeq, 0) + require.NoError(t, err) + require.Empty(t, empty) + require.Equal(t, maxSeq, emptyMax) + + // Re-upserting an existing leaf must not produce a new sequence + // number: re-org style rewrites are invisible to the delta, which is + // why root comparison stays authoritative. + _, err = store.UpsertProofLeaf( + ctx, all[0].id, all[0].key, &all[0].leaf, nil, + ) + require.NoError(t, err) + + empty, emptyMax, err = store.FetchLeavesSince(ctx, maxSeq, 0) + require.NoError(t, err) + require.Empty(t, empty) + require.Equal(t, maxSeq, emptyMax) +} diff --git a/tapdb/sqlc/migrations/000061_universe_server_sync_seq.down.sql b/tapdb/sqlc/migrations/000061_universe_server_sync_seq.down.sql new file mode 100644 index 000000000..346213d55 --- /dev/null +++ b/tapdb/sqlc/migrations/000061_universe_server_sync_seq.down.sql @@ -0,0 +1 @@ +ALTER TABLE universe_servers DROP COLUMN last_sync_seq; diff --git a/tapdb/sqlc/migrations/000061_universe_server_sync_seq.up.sql b/tapdb/sqlc/migrations/000061_universe_server_sync_seq.up.sql new file mode 100644 index 000000000..fcd12bf5f --- /dev/null +++ b/tapdb/sqlc/migrations/000061_universe_server_sync_seq.up.sql @@ -0,0 +1,6 @@ +-- The delta sync cursor: the highest insertion sequence number of the +-- remote server's universe_leaves table that we have fully applied and +-- verified. Zero means no delta sync has completed yet, in which case +-- the next sync starts from the beginning (bootstrap). +ALTER TABLE universe_servers + ADD COLUMN last_sync_seq BIGINT NOT NULL DEFAULT 0; diff --git a/tapdb/sqlc/models.go b/tapdb/sqlc/models.go index 6b46ad300..2573903c4 100644 --- a/tapdb/sqlc/models.go +++ b/tapdb/sqlc/models.go @@ -543,6 +543,7 @@ type UniverseServer struct { ID int64 ServerHost string LastSyncTime time.Time + LastSyncSeq int64 } type UniverseStat struct { diff --git a/tapdb/sqlc/querier.go b/tapdb/sqlc/querier.go index 74cf768b2..8bd76ed6d 100644 --- a/tapdb/sqlc/querier.go +++ b/tapdb/sqlc/querier.go @@ -128,6 +128,7 @@ type Querier interface { // computed from mssmt_nodes.value (the leaf's RawProof bytes) and // mssmt_nodes.sum. Callers compute the hash from these two columns. FetchUniverseKeys(ctx context.Context, arg FetchUniverseKeysParams) ([]FetchUniverseKeysRow, error) + FetchUniverseLeavesSince(ctx context.Context, arg FetchUniverseLeavesSinceParams) ([]FetchUniverseLeavesSinceRow, error) FetchUniverseRoot(ctx context.Context, namespace string) (FetchUniverseRootRow, error) FetchUniverseSupplyRoot(ctx context.Context, namespaceRoot string) (FetchUniverseSupplyRootRow, error) FetchUnknownTypeScriptKeys(ctx context.Context) ([]FetchUnknownTypeScriptKeysRow, error) @@ -213,6 +214,7 @@ type Querier interface { // Join on mssmt_nodes to get leaf related fields. // Join on genesis_info_view to get leaf related fields. QueryFederationProofSyncLog(ctx context.Context, arg QueryFederationProofSyncLogParams) ([]QueryFederationProofSyncLogRow, error) + QueryFederationSyncCursor(ctx context.Context, targetServer string) (int64, error) QueryFederationUniSyncConfigs(ctx context.Context) ([]QueryFederationUniSyncConfigsRow, error) QueryForwards(ctx context.Context, arg QueryForwardsParams) ([]QueryForwardsRow, error) QueryLastEventHeight(ctx context.Context, version int16) (int64, error) @@ -269,6 +271,7 @@ type Querier interface { UpsertChainTx(ctx context.Context, arg UpsertChainTxParams) (int64, error) UpsertFederationGlobalSyncConfig(ctx context.Context, arg UpsertFederationGlobalSyncConfigParams) error UpsertFederationProofSyncLog(ctx context.Context, arg UpsertFederationProofSyncLogParams) (int64, error) + UpsertFederationSyncCursor(ctx context.Context, arg UpsertFederationSyncCursorParams) error UpsertFederationUniSyncConfig(ctx context.Context, arg UpsertFederationUniSyncConfigParams) error UpsertForward(ctx context.Context, arg UpsertForwardParams) (int64, error) UpsertGenesisAsset(ctx context.Context, arg UpsertGenesisAssetParams) (int64, error) diff --git a/tapdb/sqlc/queries/universe.sql b/tapdb/sqlc/queries/universe.sql index 2b72ae851..a0d07660a 100644 --- a/tapdb/sqlc/queries/universe.sql +++ b/tapdb/sqlc/queries/universe.sql @@ -103,6 +103,25 @@ LIMIT @num_limit OFFSET @num_offset; -- name: UniverseLeaves :many SELECT * FROM universe_leaves; +-- name: FetchUniverseLeavesSince :many +SELECT leaves.id AS seq, leaves.minting_point, leaves.script_key_bytes, + leaves.asset_genesis_id, nodes.value AS genesis_proof, + nodes.sum AS sum_amt, roots.asset_id AS root_asset_id, + roots.group_key AS root_group_key, roots.proof_type +FROM universe_leaves AS leaves +JOIN universe_roots AS roots + ON leaves.universe_root_id = roots.id +JOIN mssmt_nodes AS nodes + ON leaves.leaf_node_key = nodes.key + AND leaves.leaf_node_namespace = nodes.namespace +WHERE leaves.id > @since_seq + -- The insertion-ordered delta serves federation sync, whose + -- domain is issuance and transfer universes; other proof types + -- flow through dedicated syncers. + AND roots.proof_type IN ('issuance', 'transfer') +ORDER BY leaves.id ASC +LIMIT @num_limit; + -- name: UniverseRoots :many SELECT universe_roots.asset_id, group_key, proof_type, mssmt_roots.root_hash AS root_hash, mssmt_nodes.sum AS root_sum, @@ -136,6 +155,15 @@ UPDATE universe_servers SET last_sync_time = @new_sync_time WHERE server_host = @target_server; +-- name: UpsertFederationSyncCursor :exec +UPDATE universe_servers +SET last_sync_seq = @last_sync_seq +WHERE server_host = @target_server; + +-- name: QueryFederationSyncCursor :one +SELECT last_sync_seq FROM universe_servers +WHERE server_host = @target_server; + -- name: QueryUniverseServers :many SELECT * FROM universe_servers WHERE (id = sqlc.narg('id') OR sqlc.narg('id') IS NULL) AND diff --git a/tapdb/sqlc/schemas/generated_schema.sql b/tapdb/sqlc/schemas/generated_schema.sql index 1991d0dc7..5295e6568 100644 --- a/tapdb/sqlc/schemas/generated_schema.sql +++ b/tapdb/sqlc/schemas/generated_schema.sql @@ -1257,7 +1257,7 @@ CREATE TABLE universe_servers ( -- TODO(roasbeef): can also add stuff like filters re which items to sync, -- etc? also sync mode, ones that should get everything pushed, etc -); +, last_sync_seq BIGINT NOT NULL DEFAULT 0); CREATE INDEX universe_servers_host ON universe_servers(server_host); diff --git a/tapdb/sqlc/universe.sql.go b/tapdb/sqlc/universe.sql.go index bed8edfd3..9e3600a5a 100644 --- a/tapdb/sqlc/universe.sql.go +++ b/tapdb/sqlc/universe.sql.go @@ -238,6 +238,76 @@ func (q *Queries) FetchUniverseKeys(ctx context.Context, arg FetchUniverseKeysPa return items, nil } +const FetchUniverseLeavesSince = `-- name: FetchUniverseLeavesSince :many +SELECT leaves.id AS seq, leaves.minting_point, leaves.script_key_bytes, + leaves.asset_genesis_id, nodes.value AS genesis_proof, + nodes.sum AS sum_amt, roots.asset_id AS root_asset_id, + roots.group_key AS root_group_key, roots.proof_type +FROM universe_leaves AS leaves +JOIN universe_roots AS roots + ON leaves.universe_root_id = roots.id +JOIN mssmt_nodes AS nodes + ON leaves.leaf_node_key = nodes.key + AND leaves.leaf_node_namespace = nodes.namespace +WHERE leaves.id > $1 + -- The insertion-ordered delta serves federation sync, whose + -- domain is issuance and transfer universes; other proof types + -- flow through dedicated syncers. + AND roots.proof_type IN ('issuance', 'transfer') +ORDER BY leaves.id ASC +LIMIT $2 +` + +type FetchUniverseLeavesSinceParams struct { + SinceSeq int64 + NumLimit int32 +} + +type FetchUniverseLeavesSinceRow struct { + Seq int64 + MintingPoint []byte + ScriptKeyBytes []byte + AssetGenesisID int64 + GenesisProof []byte + SumAmt int64 + RootAssetID []byte + RootGroupKey []byte + ProofType sql.NullString +} + +func (q *Queries) FetchUniverseLeavesSince(ctx context.Context, arg FetchUniverseLeavesSinceParams) ([]FetchUniverseLeavesSinceRow, error) { + rows, err := q.db.QueryContext(ctx, FetchUniverseLeavesSince, arg.SinceSeq, arg.NumLimit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchUniverseLeavesSinceRow + for rows.Next() { + var i FetchUniverseLeavesSinceRow + if err := rows.Scan( + &i.Seq, + &i.MintingPoint, + &i.ScriptKeyBytes, + &i.AssetGenesisID, + &i.GenesisProof, + &i.SumAmt, + &i.RootAssetID, + &i.RootGroupKey, + &i.ProofType, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const FetchUniverseRoot = `-- name: FetchUniverseRoot :one SELECT universe_roots.asset_id, group_key, proof_type, mssmt_nodes.hash_key root_hash, mssmt_nodes.sum root_sum, @@ -639,6 +709,18 @@ func (q *Queries) QueryFederationProofSyncLog(ctx context.Context, arg QueryFede return items, nil } +const QueryFederationSyncCursor = `-- name: QueryFederationSyncCursor :one +SELECT last_sync_seq FROM universe_servers +WHERE server_host = $1 +` + +func (q *Queries) QueryFederationSyncCursor(ctx context.Context, targetServer string) (int64, error) { + row := q.db.QueryRowContext(ctx, QueryFederationSyncCursor, targetServer) + var last_sync_seq int64 + err := row.Scan(&last_sync_seq) + return last_sync_seq, err +} + const QueryFederationUniSyncConfigs = `-- name: QueryFederationUniSyncConfigs :many SELECT namespace, asset_id, group_key, proof_type, allow_sync_insert, allow_sync_export FROM federation_uni_sync_config @@ -979,7 +1061,7 @@ func (q *Queries) QueryUniverseLeaves(ctx context.Context, arg QueryUniverseLeav } const QueryUniverseServers = `-- name: QueryUniverseServers :many -SELECT id, server_host, last_sync_time FROM universe_servers +SELECT id, server_host, last_sync_time, last_sync_seq FROM universe_servers WHERE (id = $1 OR $1 IS NULL) AND (server_host = $2 OR $2 IS NULL) @@ -999,7 +1081,12 @@ func (q *Queries) QueryUniverseServers(ctx context.Context, arg QueryUniverseSer var items []UniverseServer for rows.Next() { var i UniverseServer - if err := rows.Scan(&i.ID, &i.ServerHost, &i.LastSyncTime); err != nil { + if err := rows.Scan( + &i.ID, + &i.ServerHost, + &i.LastSyncTime, + &i.LastSyncSeq, + ); err != nil { return nil, err } items = append(items, i) @@ -1265,6 +1352,22 @@ func (q *Queries) UpsertFederationProofSyncLog(ctx context.Context, arg UpsertFe return id, err } +const UpsertFederationSyncCursor = `-- name: UpsertFederationSyncCursor :exec +UPDATE universe_servers +SET last_sync_seq = $1 +WHERE server_host = $2 +` + +type UpsertFederationSyncCursorParams struct { + LastSyncSeq int64 + TargetServer string +} + +func (q *Queries) UpsertFederationSyncCursor(ctx context.Context, arg UpsertFederationSyncCursorParams) error { + _, err := q.db.ExecContext(ctx, UpsertFederationSyncCursor, arg.LastSyncSeq, arg.TargetServer) + return err +} + const UpsertFederationUniSyncConfig = `-- name: UpsertFederationUniSyncConfig :exec INSERT INTO federation_uni_sync_config ( namespace, asset_id, group_key, proof_type, allow_sync_insert, allow_sync_export diff --git a/tapdb/universe_federation.go b/tapdb/universe_federation.go index 73ca74091..ec3f519ef 100644 --- a/tapdb/universe_federation.go +++ b/tapdb/universe_federation.go @@ -139,6 +139,16 @@ type UniverseServerStore interface { // LogServerSync marks that a server was just synced in the DB. LogServerSync(ctx context.Context, arg sqlc.LogServerSyncParams) error + // UpsertFederationSyncCursor updates the delta sync cursor stored + // for a server. + UpsertFederationSyncCursor(ctx context.Context, + arg sqlc.UpsertFederationSyncCursorParams) error + + // QueryFederationSyncCursor returns the delta sync cursor stored + // for a server. + QueryFederationSyncCursor(ctx context.Context, + targetServer string) (int64, error) + // QueryUniverseServers returns a set of universe servers. QueryUniverseServers(ctx context.Context, arg sqlc.QueryUniverseServersParams) ([]sqlc.UniverseServer, @@ -302,6 +312,49 @@ func (u *UniverseFederationDB) LogNewSyncs(ctx context.Context, }) } +// UpsertSyncCursor updates the delta sync cursor stored for the given +// server. +// +// NOTE: this is part of the universe.FederationLog interface. +func (u *UniverseFederationDB) UpsertSyncCursor(ctx context.Context, + addr universe.ServerAddr, seq uint64) error { + + var writeTx UniverseFederationOptions + return u.db.ExecTx(ctx, &writeTx, func(db UniverseServerStore) error { + return db.UpsertFederationSyncCursor( + ctx, sqlc.UpsertFederationSyncCursorParams{ + LastSyncSeq: int64(seq), + TargetServer: addr.HostStr(), + }, + ) + }) +} + +// FetchSyncCursor returns the delta sync cursor stored for the given +// server. A server that has never delta-synced reports cursor zero. +// +// NOTE: this is part of the universe.FederationLog interface. +func (u *UniverseFederationDB) FetchSyncCursor(ctx context.Context, + addr universe.ServerAddr) (uint64, error) { + + var ( + readTx UniverseFederationOptions + cursor int64 + ) + dbErr := u.db.ExecTx(ctx, &readTx, func(db UniverseServerStore) error { + var err error + cursor, err = db.QueryFederationSyncCursor( + ctx, addr.HostStr(), + ) + return err + }) + if dbErr != nil { + return 0, dbErr + } + + return uint64(cursor), nil +} + // UpsertFederationProofSyncLog upserts a federation proof sync log entry for a // given universe server and proof. func (u *UniverseFederationDB) UpsertFederationProofSyncLog( diff --git a/tapdb/universe_federation_test.go b/tapdb/universe_federation_test.go index 3a25f520d..ca1b88ae1 100644 --- a/tapdb/universe_federation_test.go +++ b/tapdb/universe_federation_test.go @@ -87,6 +87,53 @@ func TestUniverseFederationCRUD(t *testing.T) { require.NoError(t, err) } +// TestFederationSyncCursor tests the per-server delta sync cursor +// round-trip, and that removing and re-adding a server resets its +// cursor to zero. +func TestFederationSyncCursor(t *testing.T) { + t.Parallel() + + var ( + ctx = context.Background() + db = NewDbHandle(t) + fedDB = db.UniverseFederationStore + ) + + addrs := db.AddRandomServerAddrs(t, 2) + target, other := addrs[0], addrs[1] + + // A freshly added server starts at cursor zero. + cursor, err := fedDB.FetchSyncCursor(ctx, target) + require.NoError(t, err) + require.Zero(t, cursor) + + // The cursor round-trips, and only for the targeted server. + err = fedDB.UpsertSyncCursor(ctx, target, 42) + require.NoError(t, err) + + cursor, err = fedDB.FetchSyncCursor(ctx, target) + require.NoError(t, err) + require.EqualValues(t, 42, cursor) + + cursor, err = fedDB.FetchSyncCursor(ctx, other) + require.NoError(t, err) + require.Zero(t, cursor) + + // Removing and re-adding a server implicitly resets its cursor: + // the fresh row starts from zero (bootstrap). + err = fedDB.RemoveServers(ctx, target) + require.NoError(t, err) + + err = fedDB.AddServers( + ctx, universe.NewServerAddrFromStr(target.HostStr()), + ) + require.NoError(t, err) + + cursor, err = fedDB.FetchSyncCursor(ctx, target) + require.NoError(t, err) + require.Zero(t, cursor) +} + // TestFederationProofSyncLogCRUD tests that we can add, modify, and remove // proof sync log entries from the Universe DB. func TestFederationProofSyncLogCRUD(t *testing.T) { diff --git a/taprpc/perms.go b/taprpc/perms.go index b9e30bc4c..04fff9583 100644 --- a/taprpc/perms.go +++ b/taprpc/perms.go @@ -247,6 +247,10 @@ var ( Entity: "universe", Action: "write", }}, + "/universerpc.Universe/SyncDelta": {{ + Entity: "universe", + Action: "read", + }}, "/universerpc.Universe/ListFederationServers": {{ Entity: "universe", Action: "read", @@ -423,6 +427,7 @@ func MacaroonWhitelist(allowUniPublicAccessRead bool, "/universerpc.Universe/AssetLeafKeys", "/universerpc.Universe/AssetLeaves", "/universerpc.Universe/QueryProof", + "/universerpc.Universe/SyncDelta", "/universerpc.Universe/FetchSupplyCommit", "/universerpc.Universe/FetchSupplyLeaves", diff --git a/taprpc/universerpc/universe.pb.go b/taprpc/universerpc/universe.pb.go index 0d2a8dc20..55b6e14f8 100644 --- a/taprpc/universerpc/universe.pb.go +++ b/taprpc/universerpc/universe.pb.go @@ -2269,6 +2269,214 @@ func (x *SyncResponse) GetSyncedUniverses() []*SyncedUniverse { return nil } +type SyncDeltaRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Return leaves inserted after this sequence number. Zero means from + // the beginning (bootstrap). + SinceSeq uint64 `protobuf:"varint,1,opt,name=since_seq,json=sinceSeq,proto3" json:"since_seq,omitempty"` + // The maximum number of leaves to return in one response. Clamped + // server-side; the server may additionally cut the page short to + // respect its response size budget. Callers should page until a + // response returns fewer items than requested and latest_seq stops + // advancing. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncDeltaRequest) Reset() { + *x = SyncDeltaRequest{} + mi := &file_universerpc_universe_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncDeltaRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncDeltaRequest) ProtoMessage() {} + +func (x *SyncDeltaRequest) ProtoReflect() protoreflect.Message { + mi := &file_universerpc_universe_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncDeltaRequest.ProtoReflect.Descriptor instead. +func (*SyncDeltaRequest) Descriptor() ([]byte, []int) { + return file_universerpc_universe_proto_rawDescGZIP(), []int{34} +} + +func (x *SyncDeltaRequest) GetSinceSeq() uint64 { + if x != nil { + return x.SinceSeq + } + return 0 +} + +func (x *SyncDeltaRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +type SyncDeltaItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The universe the leaf belongs to. + UniverseId *ID `protobuf:"bytes,1,opt,name=universe_id,json=universeId,proto3" json:"universe_id,omitempty"` + // The leaf key the leaf is stored at. + Key *AssetKey `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + // The leaf payload: the asset and its raw issuance/transfer proof. + Leaf *AssetLeaf `protobuf:"bytes,3,opt,name=leaf,proto3" json:"leaf,omitempty"` + // The universe inclusion proof binding the leaf to its universe root + // (one of the roots in the enclosing response). + UniverseInclusionProof []byte `protobuf:"bytes,4,opt,name=universe_inclusion_proof,json=universeInclusionProof,proto3" json:"universe_inclusion_proof,omitempty"` + // The insertion sequence number of the leaf on the serving universe. + Seq uint64 `protobuf:"varint,5,opt,name=seq,proto3" json:"seq,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncDeltaItem) Reset() { + *x = SyncDeltaItem{} + mi := &file_universerpc_universe_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncDeltaItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncDeltaItem) ProtoMessage() {} + +func (x *SyncDeltaItem) ProtoReflect() protoreflect.Message { + mi := &file_universerpc_universe_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncDeltaItem.ProtoReflect.Descriptor instead. +func (*SyncDeltaItem) Descriptor() ([]byte, []int) { + return file_universerpc_universe_proto_rawDescGZIP(), []int{35} +} + +func (x *SyncDeltaItem) GetUniverseId() *ID { + if x != nil { + return x.UniverseId + } + return nil +} + +func (x *SyncDeltaItem) GetKey() *AssetKey { + if x != nil { + return x.Key + } + return nil +} + +func (x *SyncDeltaItem) GetLeaf() *AssetLeaf { + if x != nil { + return x.Leaf + } + return nil +} + +func (x *SyncDeltaItem) GetUniverseInclusionProof() []byte { + if x != nil { + return x.UniverseInclusionProof + } + return nil +} + +func (x *SyncDeltaItem) GetSeq() uint64 { + if x != nil { + return x.Seq + } + return 0 +} + +type SyncDeltaResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The leaves inserted after the requested sequence number, in + // insertion order. Leaves whose universes have proof export disabled + // are omitted, but still advance latest_seq. + Items []*SyncDeltaItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + // The current roots of every universe that appears in items. + UniverseRoots []*UniverseRoot `protobuf:"bytes,2,rep,name=universe_roots,json=universeRoots,proto3" json:"universe_roots,omitempty"` + // The high-water sequence mark of this page: the cursor value the + // caller should persist once the page's items are applied and + // verified. + LatestSeq uint64 `protobuf:"varint,3,opt,name=latest_seq,json=latestSeq,proto3" json:"latest_seq,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncDeltaResponse) Reset() { + *x = SyncDeltaResponse{} + mi := &file_universerpc_universe_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncDeltaResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncDeltaResponse) ProtoMessage() {} + +func (x *SyncDeltaResponse) ProtoReflect() protoreflect.Message { + mi := &file_universerpc_universe_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncDeltaResponse.ProtoReflect.Descriptor instead. +func (*SyncDeltaResponse) Descriptor() ([]byte, []int) { + return file_universerpc_universe_proto_rawDescGZIP(), []int{36} +} + +func (x *SyncDeltaResponse) GetItems() []*SyncDeltaItem { + if x != nil { + return x.Items + } + return nil +} + +func (x *SyncDeltaResponse) GetUniverseRoots() []*UniverseRoot { + if x != nil { + return x.UniverseRoots + } + return nil +} + +func (x *SyncDeltaResponse) GetLatestSeq() uint64 { + if x != nil { + return x.LatestSeq + } + return 0 +} + type UniverseFederationServer struct { state protoimpl.MessageState `protogen:"open.v1"` // The host of the federation server, which is used to connect to the @@ -2283,7 +2491,7 @@ type UniverseFederationServer struct { func (x *UniverseFederationServer) Reset() { *x = UniverseFederationServer{} - mi := &file_universerpc_universe_proto_msgTypes[34] + mi := &file_universerpc_universe_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2295,7 +2503,7 @@ func (x *UniverseFederationServer) String() string { func (*UniverseFederationServer) ProtoMessage() {} func (x *UniverseFederationServer) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[34] + mi := &file_universerpc_universe_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2308,7 +2516,7 @@ func (x *UniverseFederationServer) ProtoReflect() protoreflect.Message { // Deprecated: Use UniverseFederationServer.ProtoReflect.Descriptor instead. func (*UniverseFederationServer) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{34} + return file_universerpc_universe_proto_rawDescGZIP(), []int{37} } func (x *UniverseFederationServer) GetHost() string { @@ -2333,7 +2541,7 @@ type ListFederationServersRequest struct { func (x *ListFederationServersRequest) Reset() { *x = ListFederationServersRequest{} - mi := &file_universerpc_universe_proto_msgTypes[35] + mi := &file_universerpc_universe_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2345,7 +2553,7 @@ func (x *ListFederationServersRequest) String() string { func (*ListFederationServersRequest) ProtoMessage() {} func (x *ListFederationServersRequest) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[35] + mi := &file_universerpc_universe_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2358,7 +2566,7 @@ func (x *ListFederationServersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListFederationServersRequest.ProtoReflect.Descriptor instead. func (*ListFederationServersRequest) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{35} + return file_universerpc_universe_proto_rawDescGZIP(), []int{38} } type ListFederationServersResponse struct { @@ -2372,7 +2580,7 @@ type ListFederationServersResponse struct { func (x *ListFederationServersResponse) Reset() { *x = ListFederationServersResponse{} - mi := &file_universerpc_universe_proto_msgTypes[36] + mi := &file_universerpc_universe_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2384,7 +2592,7 @@ func (x *ListFederationServersResponse) String() string { func (*ListFederationServersResponse) ProtoMessage() {} func (x *ListFederationServersResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[36] + mi := &file_universerpc_universe_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2397,7 +2605,7 @@ func (x *ListFederationServersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListFederationServersResponse.ProtoReflect.Descriptor instead. func (*ListFederationServersResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{36} + return file_universerpc_universe_proto_rawDescGZIP(), []int{39} } func (x *ListFederationServersResponse) GetServers() []*UniverseFederationServer { @@ -2417,7 +2625,7 @@ type AddFederationServerRequest struct { func (x *AddFederationServerRequest) Reset() { *x = AddFederationServerRequest{} - mi := &file_universerpc_universe_proto_msgTypes[37] + mi := &file_universerpc_universe_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2429,7 +2637,7 @@ func (x *AddFederationServerRequest) String() string { func (*AddFederationServerRequest) ProtoMessage() {} func (x *AddFederationServerRequest) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[37] + mi := &file_universerpc_universe_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2442,7 +2650,7 @@ func (x *AddFederationServerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddFederationServerRequest.ProtoReflect.Descriptor instead. func (*AddFederationServerRequest) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{37} + return file_universerpc_universe_proto_rawDescGZIP(), []int{40} } func (x *AddFederationServerRequest) GetServers() []*UniverseFederationServer { @@ -2460,7 +2668,7 @@ type AddFederationServerResponse struct { func (x *AddFederationServerResponse) Reset() { *x = AddFederationServerResponse{} - mi := &file_universerpc_universe_proto_msgTypes[38] + mi := &file_universerpc_universe_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2472,7 +2680,7 @@ func (x *AddFederationServerResponse) String() string { func (*AddFederationServerResponse) ProtoMessage() {} func (x *AddFederationServerResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[38] + mi := &file_universerpc_universe_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2485,7 +2693,7 @@ func (x *AddFederationServerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddFederationServerResponse.ProtoReflect.Descriptor instead. func (*AddFederationServerResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{38} + return file_universerpc_universe_proto_rawDescGZIP(), []int{41} } type DeleteFederationServerRequest struct { @@ -2498,7 +2706,7 @@ type DeleteFederationServerRequest struct { func (x *DeleteFederationServerRequest) Reset() { *x = DeleteFederationServerRequest{} - mi := &file_universerpc_universe_proto_msgTypes[39] + mi := &file_universerpc_universe_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2510,7 +2718,7 @@ func (x *DeleteFederationServerRequest) String() string { func (*DeleteFederationServerRequest) ProtoMessage() {} func (x *DeleteFederationServerRequest) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[39] + mi := &file_universerpc_universe_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2523,7 +2731,7 @@ func (x *DeleteFederationServerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteFederationServerRequest.ProtoReflect.Descriptor instead. func (*DeleteFederationServerRequest) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{39} + return file_universerpc_universe_proto_rawDescGZIP(), []int{42} } func (x *DeleteFederationServerRequest) GetServers() []*UniverseFederationServer { @@ -2541,7 +2749,7 @@ type DeleteFederationServerResponse struct { func (x *DeleteFederationServerResponse) Reset() { *x = DeleteFederationServerResponse{} - mi := &file_universerpc_universe_proto_msgTypes[40] + mi := &file_universerpc_universe_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2553,7 +2761,7 @@ func (x *DeleteFederationServerResponse) String() string { func (*DeleteFederationServerResponse) ProtoMessage() {} func (x *DeleteFederationServerResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[40] + mi := &file_universerpc_universe_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2566,7 +2774,7 @@ func (x *DeleteFederationServerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteFederationServerResponse.ProtoReflect.Descriptor instead. func (*DeleteFederationServerResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{40} + return file_universerpc_universe_proto_rawDescGZIP(), []int{43} } type StatsResponse struct { @@ -2581,7 +2789,7 @@ type StatsResponse struct { func (x *StatsResponse) Reset() { *x = StatsResponse{} - mi := &file_universerpc_universe_proto_msgTypes[41] + mi := &file_universerpc_universe_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2593,7 +2801,7 @@ func (x *StatsResponse) String() string { func (*StatsResponse) ProtoMessage() {} func (x *StatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[41] + mi := &file_universerpc_universe_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2606,7 +2814,7 @@ func (x *StatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StatsResponse.ProtoReflect.Descriptor instead. func (*StatsResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{41} + return file_universerpc_universe_proto_rawDescGZIP(), []int{44} } func (x *StatsResponse) GetNumTotalAssets() int64 { @@ -2665,7 +2873,7 @@ type AssetStatsQuery struct { func (x *AssetStatsQuery) Reset() { *x = AssetStatsQuery{} - mi := &file_universerpc_universe_proto_msgTypes[42] + mi := &file_universerpc_universe_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2677,7 +2885,7 @@ func (x *AssetStatsQuery) String() string { func (*AssetStatsQuery) ProtoMessage() {} func (x *AssetStatsQuery) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[42] + mi := &file_universerpc_universe_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2690,7 +2898,7 @@ func (x *AssetStatsQuery) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetStatsQuery.ProtoReflect.Descriptor instead. func (*AssetStatsQuery) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{42} + return file_universerpc_universe_proto_rawDescGZIP(), []int{45} } func (x *AssetStatsQuery) GetAssetNameFilter() string { @@ -2768,7 +2976,7 @@ type AssetStatsSnapshot struct { func (x *AssetStatsSnapshot) Reset() { *x = AssetStatsSnapshot{} - mi := &file_universerpc_universe_proto_msgTypes[43] + mi := &file_universerpc_universe_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2780,7 +2988,7 @@ func (x *AssetStatsSnapshot) String() string { func (*AssetStatsSnapshot) ProtoMessage() {} func (x *AssetStatsSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[43] + mi := &file_universerpc_universe_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2793,7 +3001,7 @@ func (x *AssetStatsSnapshot) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetStatsSnapshot.ProtoReflect.Descriptor instead. func (*AssetStatsSnapshot) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{43} + return file_universerpc_universe_proto_rawDescGZIP(), []int{46} } func (x *AssetStatsSnapshot) GetGroupKey() []byte { @@ -2870,7 +3078,7 @@ type AssetStatsAsset struct { func (x *AssetStatsAsset) Reset() { *x = AssetStatsAsset{} - mi := &file_universerpc_universe_proto_msgTypes[44] + mi := &file_universerpc_universe_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2882,7 +3090,7 @@ func (x *AssetStatsAsset) String() string { func (*AssetStatsAsset) ProtoMessage() {} func (x *AssetStatsAsset) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[44] + mi := &file_universerpc_universe_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2895,7 +3103,7 @@ func (x *AssetStatsAsset) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetStatsAsset.ProtoReflect.Descriptor instead. func (*AssetStatsAsset) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{44} + return file_universerpc_universe_proto_rawDescGZIP(), []int{47} } func (x *AssetStatsAsset) GetAssetId() []byte { @@ -2974,7 +3182,7 @@ type UniverseAssetStats struct { func (x *UniverseAssetStats) Reset() { *x = UniverseAssetStats{} - mi := &file_universerpc_universe_proto_msgTypes[45] + mi := &file_universerpc_universe_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2986,7 +3194,7 @@ func (x *UniverseAssetStats) String() string { func (*UniverseAssetStats) ProtoMessage() {} func (x *UniverseAssetStats) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[45] + mi := &file_universerpc_universe_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2999,7 +3207,7 @@ func (x *UniverseAssetStats) ProtoReflect() protoreflect.Message { // Deprecated: Use UniverseAssetStats.ProtoReflect.Descriptor instead. func (*UniverseAssetStats) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{45} + return file_universerpc_universe_proto_rawDescGZIP(), []int{48} } func (x *UniverseAssetStats) GetAssetStats() []*AssetStatsSnapshot { @@ -3028,7 +3236,7 @@ type QueryEventsRequest struct { func (x *QueryEventsRequest) Reset() { *x = QueryEventsRequest{} - mi := &file_universerpc_universe_proto_msgTypes[46] + mi := &file_universerpc_universe_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3040,7 +3248,7 @@ func (x *QueryEventsRequest) String() string { func (*QueryEventsRequest) ProtoMessage() {} func (x *QueryEventsRequest) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[46] + mi := &file_universerpc_universe_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3053,7 +3261,7 @@ func (x *QueryEventsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryEventsRequest.ProtoReflect.Descriptor instead. func (*QueryEventsRequest) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{46} + return file_universerpc_universe_proto_rawDescGZIP(), []int{49} } func (x *QueryEventsRequest) GetStartTimestamp() int64 { @@ -3082,7 +3290,7 @@ type QueryEventsResponse struct { func (x *QueryEventsResponse) Reset() { *x = QueryEventsResponse{} - mi := &file_universerpc_universe_proto_msgTypes[47] + mi := &file_universerpc_universe_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3094,7 +3302,7 @@ func (x *QueryEventsResponse) String() string { func (*QueryEventsResponse) ProtoMessage() {} func (x *QueryEventsResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[47] + mi := &file_universerpc_universe_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3107,7 +3315,7 @@ func (x *QueryEventsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryEventsResponse.ProtoReflect.Descriptor instead. func (*QueryEventsResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{47} + return file_universerpc_universe_proto_rawDescGZIP(), []int{50} } func (x *QueryEventsResponse) GetEvents() []*GroupedUniverseEvents { @@ -3131,7 +3339,7 @@ type GroupedUniverseEvents struct { func (x *GroupedUniverseEvents) Reset() { *x = GroupedUniverseEvents{} - mi := &file_universerpc_universe_proto_msgTypes[48] + mi := &file_universerpc_universe_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3143,7 +3351,7 @@ func (x *GroupedUniverseEvents) String() string { func (*GroupedUniverseEvents) ProtoMessage() {} func (x *GroupedUniverseEvents) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[48] + mi := &file_universerpc_universe_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3156,7 +3364,7 @@ func (x *GroupedUniverseEvents) ProtoReflect() protoreflect.Message { // Deprecated: Use GroupedUniverseEvents.ProtoReflect.Descriptor instead. func (*GroupedUniverseEvents) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{48} + return file_universerpc_universe_proto_rawDescGZIP(), []int{51} } func (x *GroupedUniverseEvents) GetDate() string { @@ -3192,7 +3400,7 @@ type SetFederationSyncConfigRequest struct { func (x *SetFederationSyncConfigRequest) Reset() { *x = SetFederationSyncConfigRequest{} - mi := &file_universerpc_universe_proto_msgTypes[49] + mi := &file_universerpc_universe_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3204,7 +3412,7 @@ func (x *SetFederationSyncConfigRequest) String() string { func (*SetFederationSyncConfigRequest) ProtoMessage() {} func (x *SetFederationSyncConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[49] + mi := &file_universerpc_universe_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3217,7 +3425,7 @@ func (x *SetFederationSyncConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetFederationSyncConfigRequest.ProtoReflect.Descriptor instead. func (*SetFederationSyncConfigRequest) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{49} + return file_universerpc_universe_proto_rawDescGZIP(), []int{52} } func (x *SetFederationSyncConfigRequest) GetGlobalSyncConfigs() []*GlobalFederationSyncConfig { @@ -3242,7 +3450,7 @@ type SetFederationSyncConfigResponse struct { func (x *SetFederationSyncConfigResponse) Reset() { *x = SetFederationSyncConfigResponse{} - mi := &file_universerpc_universe_proto_msgTypes[50] + mi := &file_universerpc_universe_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3254,7 +3462,7 @@ func (x *SetFederationSyncConfigResponse) String() string { func (*SetFederationSyncConfigResponse) ProtoMessage() {} func (x *SetFederationSyncConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[50] + mi := &file_universerpc_universe_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3267,7 +3475,7 @@ func (x *SetFederationSyncConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetFederationSyncConfigResponse.ProtoReflect.Descriptor instead. func (*SetFederationSyncConfigResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{50} + return file_universerpc_universe_proto_rawDescGZIP(), []int{53} } // GlobalFederationSyncConfig is a global proof type specific configuration @@ -3290,7 +3498,7 @@ type GlobalFederationSyncConfig struct { func (x *GlobalFederationSyncConfig) Reset() { *x = GlobalFederationSyncConfig{} - mi := &file_universerpc_universe_proto_msgTypes[51] + mi := &file_universerpc_universe_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3302,7 +3510,7 @@ func (x *GlobalFederationSyncConfig) String() string { func (*GlobalFederationSyncConfig) ProtoMessage() {} func (x *GlobalFederationSyncConfig) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[51] + mi := &file_universerpc_universe_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3315,7 +3523,7 @@ func (x *GlobalFederationSyncConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use GlobalFederationSyncConfig.ProtoReflect.Descriptor instead. func (*GlobalFederationSyncConfig) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{51} + return file_universerpc_universe_proto_rawDescGZIP(), []int{54} } func (x *GlobalFederationSyncConfig) GetProofType() ProofType { @@ -3359,7 +3567,7 @@ type AssetFederationSyncConfig struct { func (x *AssetFederationSyncConfig) Reset() { *x = AssetFederationSyncConfig{} - mi := &file_universerpc_universe_proto_msgTypes[52] + mi := &file_universerpc_universe_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3371,7 +3579,7 @@ func (x *AssetFederationSyncConfig) String() string { func (*AssetFederationSyncConfig) ProtoMessage() {} func (x *AssetFederationSyncConfig) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[52] + mi := &file_universerpc_universe_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3384,7 +3592,7 @@ func (x *AssetFederationSyncConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetFederationSyncConfig.ProtoReflect.Descriptor instead. func (*AssetFederationSyncConfig) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{52} + return file_universerpc_universe_proto_rawDescGZIP(), []int{55} } func (x *AssetFederationSyncConfig) GetId() *ID { @@ -3418,7 +3626,7 @@ type QueryFederationSyncConfigRequest struct { func (x *QueryFederationSyncConfigRequest) Reset() { *x = QueryFederationSyncConfigRequest{} - mi := &file_universerpc_universe_proto_msgTypes[53] + mi := &file_universerpc_universe_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3430,7 +3638,7 @@ func (x *QueryFederationSyncConfigRequest) String() string { func (*QueryFederationSyncConfigRequest) ProtoMessage() {} func (x *QueryFederationSyncConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[53] + mi := &file_universerpc_universe_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3443,7 +3651,7 @@ func (x *QueryFederationSyncConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryFederationSyncConfigRequest.ProtoReflect.Descriptor instead. func (*QueryFederationSyncConfigRequest) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{53} + return file_universerpc_universe_proto_rawDescGZIP(), []int{56} } func (x *QueryFederationSyncConfigRequest) GetId() []*ID { @@ -3465,7 +3673,7 @@ type QueryFederationSyncConfigResponse struct { func (x *QueryFederationSyncConfigResponse) Reset() { *x = QueryFederationSyncConfigResponse{} - mi := &file_universerpc_universe_proto_msgTypes[54] + mi := &file_universerpc_universe_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3477,7 +3685,7 @@ func (x *QueryFederationSyncConfigResponse) String() string { func (*QueryFederationSyncConfigResponse) ProtoMessage() {} func (x *QueryFederationSyncConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[54] + mi := &file_universerpc_universe_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3490,7 +3698,7 @@ func (x *QueryFederationSyncConfigResponse) ProtoReflect() protoreflect.Message // Deprecated: Use QueryFederationSyncConfigResponse.ProtoReflect.Descriptor instead. func (*QueryFederationSyncConfigResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{54} + return file_universerpc_universe_proto_rawDescGZIP(), []int{57} } func (x *QueryFederationSyncConfigResponse) GetGlobalSyncConfigs() []*GlobalFederationSyncConfig { @@ -3519,7 +3727,7 @@ type IgnoreAssetOutPointRequest struct { func (x *IgnoreAssetOutPointRequest) Reset() { *x = IgnoreAssetOutPointRequest{} - mi := &file_universerpc_universe_proto_msgTypes[55] + mi := &file_universerpc_universe_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3531,7 +3739,7 @@ func (x *IgnoreAssetOutPointRequest) String() string { func (*IgnoreAssetOutPointRequest) ProtoMessage() {} func (x *IgnoreAssetOutPointRequest) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[55] + mi := &file_universerpc_universe_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3544,7 +3752,7 @@ func (x *IgnoreAssetOutPointRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use IgnoreAssetOutPointRequest.ProtoReflect.Descriptor instead. func (*IgnoreAssetOutPointRequest) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{55} + return file_universerpc_universe_proto_rawDescGZIP(), []int{58} } func (x *IgnoreAssetOutPointRequest) GetAssetOutPoint() *taprpc.AssetOutPoint { @@ -3574,7 +3782,7 @@ type IgnoreAssetOutPointResponse struct { func (x *IgnoreAssetOutPointResponse) Reset() { *x = IgnoreAssetOutPointResponse{} - mi := &file_universerpc_universe_proto_msgTypes[56] + mi := &file_universerpc_universe_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3586,7 +3794,7 @@ func (x *IgnoreAssetOutPointResponse) String() string { func (*IgnoreAssetOutPointResponse) ProtoMessage() {} func (x *IgnoreAssetOutPointResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[56] + mi := &file_universerpc_universe_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3599,7 +3807,7 @@ func (x *IgnoreAssetOutPointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use IgnoreAssetOutPointResponse.ProtoReflect.Descriptor instead. func (*IgnoreAssetOutPointResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{56} + return file_universerpc_universe_proto_rawDescGZIP(), []int{59} } func (x *IgnoreAssetOutPointResponse) GetLeafKey() []byte { @@ -3632,7 +3840,7 @@ type UpdateSupplyCommitRequest struct { func (x *UpdateSupplyCommitRequest) Reset() { *x = UpdateSupplyCommitRequest{} - mi := &file_universerpc_universe_proto_msgTypes[57] + mi := &file_universerpc_universe_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3644,7 +3852,7 @@ func (x *UpdateSupplyCommitRequest) String() string { func (*UpdateSupplyCommitRequest) ProtoMessage() {} func (x *UpdateSupplyCommitRequest) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[57] + mi := &file_universerpc_universe_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3657,7 +3865,7 @@ func (x *UpdateSupplyCommitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateSupplyCommitRequest.ProtoReflect.Descriptor instead. func (*UpdateSupplyCommitRequest) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{57} + return file_universerpc_universe_proto_rawDescGZIP(), []int{60} } func (x *UpdateSupplyCommitRequest) GetGroupKey() isUpdateSupplyCommitRequest_GroupKey { @@ -3712,7 +3920,7 @@ type UpdateSupplyCommitResponse struct { func (x *UpdateSupplyCommitResponse) Reset() { *x = UpdateSupplyCommitResponse{} - mi := &file_universerpc_universe_proto_msgTypes[58] + mi := &file_universerpc_universe_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3724,7 +3932,7 @@ func (x *UpdateSupplyCommitResponse) String() string { func (*UpdateSupplyCommitResponse) ProtoMessage() {} func (x *UpdateSupplyCommitResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[58] + mi := &file_universerpc_universe_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3737,7 +3945,7 @@ func (x *UpdateSupplyCommitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateSupplyCommitResponse.ProtoReflect.Descriptor instead. func (*UpdateSupplyCommitResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{58} + return file_universerpc_universe_proto_rawDescGZIP(), []int{61} } type FetchSupplyCommitRequest struct { @@ -3765,7 +3973,7 @@ type FetchSupplyCommitRequest struct { func (x *FetchSupplyCommitRequest) Reset() { *x = FetchSupplyCommitRequest{} - mi := &file_universerpc_universe_proto_msgTypes[59] + mi := &file_universerpc_universe_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3777,7 +3985,7 @@ func (x *FetchSupplyCommitRequest) String() string { func (*FetchSupplyCommitRequest) ProtoMessage() {} func (x *FetchSupplyCommitRequest) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[59] + mi := &file_universerpc_universe_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3790,7 +3998,7 @@ func (x *FetchSupplyCommitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchSupplyCommitRequest.ProtoReflect.Descriptor instead. func (*FetchSupplyCommitRequest) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{59} + return file_universerpc_universe_proto_rawDescGZIP(), []int{62} } func (x *FetchSupplyCommitRequest) GetGroupKey() isFetchSupplyCommitRequest_GroupKey { @@ -3940,7 +4148,7 @@ type SupplyCommitSubtreeRoot struct { func (x *SupplyCommitSubtreeRoot) Reset() { *x = SupplyCommitSubtreeRoot{} - mi := &file_universerpc_universe_proto_msgTypes[60] + mi := &file_universerpc_universe_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3952,7 +4160,7 @@ func (x *SupplyCommitSubtreeRoot) String() string { func (*SupplyCommitSubtreeRoot) ProtoMessage() {} func (x *SupplyCommitSubtreeRoot) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[60] + mi := &file_universerpc_universe_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3965,7 +4173,7 @@ func (x *SupplyCommitSubtreeRoot) ProtoReflect() protoreflect.Message { // Deprecated: Use SupplyCommitSubtreeRoot.ProtoReflect.Descriptor instead. func (*SupplyCommitSubtreeRoot) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{60} + return file_universerpc_universe_proto_rawDescGZIP(), []int{63} } func (x *SupplyCommitSubtreeRoot) GetType() string { @@ -4038,7 +4246,7 @@ type FetchSupplyCommitResponse struct { func (x *FetchSupplyCommitResponse) Reset() { *x = FetchSupplyCommitResponse{} - mi := &file_universerpc_universe_proto_msgTypes[61] + mi := &file_universerpc_universe_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4050,7 +4258,7 @@ func (x *FetchSupplyCommitResponse) String() string { func (*FetchSupplyCommitResponse) ProtoMessage() {} func (x *FetchSupplyCommitResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[61] + mi := &file_universerpc_universe_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4063,7 +4271,7 @@ func (x *FetchSupplyCommitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchSupplyCommitResponse.ProtoReflect.Descriptor instead. func (*FetchSupplyCommitResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{61} + return file_universerpc_universe_proto_rawDescGZIP(), []int{64} } func (x *FetchSupplyCommitResponse) GetChainData() *SupplyCommitChainData { @@ -4172,7 +4380,7 @@ type FetchSupplyLeavesRequest struct { func (x *FetchSupplyLeavesRequest) Reset() { *x = FetchSupplyLeavesRequest{} - mi := &file_universerpc_universe_proto_msgTypes[62] + mi := &file_universerpc_universe_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4184,7 +4392,7 @@ func (x *FetchSupplyLeavesRequest) String() string { func (*FetchSupplyLeavesRequest) ProtoMessage() {} func (x *FetchSupplyLeavesRequest) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[62] + mi := &file_universerpc_universe_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4197,7 +4405,7 @@ func (x *FetchSupplyLeavesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchSupplyLeavesRequest.ProtoReflect.Descriptor instead. func (*FetchSupplyLeavesRequest) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{62} + return file_universerpc_universe_proto_rawDescGZIP(), []int{65} } func (x *FetchSupplyLeavesRequest) GetGroupKey() isFetchSupplyLeavesRequest_GroupKey { @@ -4297,7 +4505,7 @@ type SupplyLeafKey struct { func (x *SupplyLeafKey) Reset() { *x = SupplyLeafKey{} - mi := &file_universerpc_universe_proto_msgTypes[63] + mi := &file_universerpc_universe_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4309,7 +4517,7 @@ func (x *SupplyLeafKey) String() string { func (*SupplyLeafKey) ProtoMessage() {} func (x *SupplyLeafKey) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[63] + mi := &file_universerpc_universe_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4322,7 +4530,7 @@ func (x *SupplyLeafKey) ProtoReflect() protoreflect.Message { // Deprecated: Use SupplyLeafKey.ProtoReflect.Descriptor instead. func (*SupplyLeafKey) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{63} + return file_universerpc_universe_proto_rawDescGZIP(), []int{66} } func (x *SupplyLeafKey) GetOutpoint() *Outpoint { @@ -4363,7 +4571,7 @@ type SupplyLeafEntry struct { func (x *SupplyLeafEntry) Reset() { *x = SupplyLeafEntry{} - mi := &file_universerpc_universe_proto_msgTypes[64] + mi := &file_universerpc_universe_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4375,7 +4583,7 @@ func (x *SupplyLeafEntry) String() string { func (*SupplyLeafEntry) ProtoMessage() {} func (x *SupplyLeafEntry) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[64] + mi := &file_universerpc_universe_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4388,7 +4596,7 @@ func (x *SupplyLeafEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use SupplyLeafEntry.ProtoReflect.Descriptor instead. func (*SupplyLeafEntry) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{64} + return file_universerpc_universe_proto_rawDescGZIP(), []int{67} } func (x *SupplyLeafEntry) GetLeafKey() *SupplyLeafKey { @@ -4431,7 +4639,7 @@ type SupplyLeafBlockHeader struct { func (x *SupplyLeafBlockHeader) Reset() { *x = SupplyLeafBlockHeader{} - mi := &file_universerpc_universe_proto_msgTypes[65] + mi := &file_universerpc_universe_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4443,7 +4651,7 @@ func (x *SupplyLeafBlockHeader) String() string { func (*SupplyLeafBlockHeader) ProtoMessage() {} func (x *SupplyLeafBlockHeader) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[65] + mi := &file_universerpc_universe_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4456,7 +4664,7 @@ func (x *SupplyLeafBlockHeader) ProtoReflect() protoreflect.Message { // Deprecated: Use SupplyLeafBlockHeader.ProtoReflect.Descriptor instead. func (*SupplyLeafBlockHeader) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{65} + return file_universerpc_universe_proto_rawDescGZIP(), []int{68} } func (x *SupplyLeafBlockHeader) GetTimestamp() int64 { @@ -4498,7 +4706,7 @@ type FetchSupplyLeavesResponse struct { func (x *FetchSupplyLeavesResponse) Reset() { *x = FetchSupplyLeavesResponse{} - mi := &file_universerpc_universe_proto_msgTypes[66] + mi := &file_universerpc_universe_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4510,7 +4718,7 @@ func (x *FetchSupplyLeavesResponse) String() string { func (*FetchSupplyLeavesResponse) ProtoMessage() {} func (x *FetchSupplyLeavesResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[66] + mi := &file_universerpc_universe_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4523,7 +4731,7 @@ func (x *FetchSupplyLeavesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FetchSupplyLeavesResponse.ProtoReflect.Descriptor instead. func (*FetchSupplyLeavesResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{66} + return file_universerpc_universe_proto_rawDescGZIP(), []int{69} } func (x *FetchSupplyLeavesResponse) GetIssuanceLeaves() []*SupplyLeafEntry { @@ -4615,7 +4823,7 @@ type SupplyCommitChainData struct { func (x *SupplyCommitChainData) Reset() { *x = SupplyCommitChainData{} - mi := &file_universerpc_universe_proto_msgTypes[67] + mi := &file_universerpc_universe_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4627,7 +4835,7 @@ func (x *SupplyCommitChainData) String() string { func (*SupplyCommitChainData) ProtoMessage() {} func (x *SupplyCommitChainData) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[67] + mi := &file_universerpc_universe_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4640,7 +4848,7 @@ func (x *SupplyCommitChainData) ProtoReflect() protoreflect.Message { // Deprecated: Use SupplyCommitChainData.ProtoReflect.Descriptor instead. func (*SupplyCommitChainData) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{67} + return file_universerpc_universe_proto_rawDescGZIP(), []int{70} } func (x *SupplyCommitChainData) GetTxn() []byte { @@ -4754,7 +4962,7 @@ type InsertSupplyCommitRequest struct { func (x *InsertSupplyCommitRequest) Reset() { *x = InsertSupplyCommitRequest{} - mi := &file_universerpc_universe_proto_msgTypes[68] + mi := &file_universerpc_universe_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4766,7 +4974,7 @@ func (x *InsertSupplyCommitRequest) String() string { func (*InsertSupplyCommitRequest) ProtoMessage() {} func (x *InsertSupplyCommitRequest) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[68] + mi := &file_universerpc_universe_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4779,7 +4987,7 @@ func (x *InsertSupplyCommitRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InsertSupplyCommitRequest.ProtoReflect.Descriptor instead. func (*InsertSupplyCommitRequest) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{68} + return file_universerpc_universe_proto_rawDescGZIP(), []int{71} } func (x *InsertSupplyCommitRequest) GetGroupKey() isInsertSupplyCommitRequest_GroupKey { @@ -4869,7 +5077,7 @@ type InsertSupplyCommitResponse struct { func (x *InsertSupplyCommitResponse) Reset() { *x = InsertSupplyCommitResponse{} - mi := &file_universerpc_universe_proto_msgTypes[69] + mi := &file_universerpc_universe_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4881,7 +5089,7 @@ func (x *InsertSupplyCommitResponse) String() string { func (*InsertSupplyCommitResponse) ProtoMessage() {} func (x *InsertSupplyCommitResponse) ProtoReflect() protoreflect.Message { - mi := &file_universerpc_universe_proto_msgTypes[69] + mi := &file_universerpc_universe_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4894,7 +5102,7 @@ func (x *InsertSupplyCommitResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InsertSupplyCommitResponse.ProtoReflect.Descriptor instead. func (*InsertSupplyCommitResponse) Descriptor() ([]byte, []int) { - return file_universerpc_universe_proto_rawDescGZIP(), []int{69} + return file_universerpc_universe_proto_rawDescGZIP(), []int{72} } var File_universerpc_universe_proto protoreflect.FileDescriptor @@ -5032,7 +5240,22 @@ const file_universerpc_universe_proto_rawDesc = "" + "\x10new_asset_leaves\x18\x03 \x03(\v2\x16.universerpc.AssetLeafR\x0enewAssetLeaves\"\x0e\n" + "\fStatsRequest\"V\n" + "\fSyncResponse\x12F\n" + - "\x10synced_universes\x18\x01 \x03(\v2\x1b.universerpc.SyncedUniverseR\x0fsyncedUniverses\">\n" + + "\x10synced_universes\x18\x01 \x03(\v2\x1b.universerpc.SyncedUniverseR\x0fsyncedUniverses\"L\n" + + "\x10SyncDeltaRequest\x12\x1b\n" + + "\tsince_seq\x18\x01 \x01(\x04R\bsinceSeq\x12\x1b\n" + + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\"\xe2\x01\n" + + "\rSyncDeltaItem\x120\n" + + "\vuniverse_id\x18\x01 \x01(\v2\x0f.universerpc.IDR\n" + + "universeId\x12'\n" + + "\x03key\x18\x02 \x01(\v2\x15.universerpc.AssetKeyR\x03key\x12*\n" + + "\x04leaf\x18\x03 \x01(\v2\x16.universerpc.AssetLeafR\x04leaf\x128\n" + + "\x18universe_inclusion_proof\x18\x04 \x01(\fR\x16universeInclusionProof\x12\x10\n" + + "\x03seq\x18\x05 \x01(\x04R\x03seq\"\xa6\x01\n" + + "\x11SyncDeltaResponse\x120\n" + + "\x05items\x18\x01 \x03(\v2\x1a.universerpc.SyncDeltaItemR\x05items\x12@\n" + + "\x0euniverse_roots\x18\x02 \x03(\v2\x19.universerpc.UniverseRootR\runiverseRoots\x12\x1d\n" + + "\n" + + "latest_seq\x18\x03 \x01(\x04R\tlatestSeq\">\n" + "\x18UniverseFederationServer\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\x12\x0e\n" + "\x02id\x18\x02 \x01(\x05R\x02id\"\x1e\n" + @@ -5236,7 +5459,7 @@ const file_universerpc_universe_proto_rawDesc = "" + "\x0fAssetTypeFilter\x12\x15\n" + "\x11FILTER_ASSET_NONE\x10\x00\x12\x17\n" + "\x13FILTER_ASSET_NORMAL\x10\x01\x12\x1c\n" + - "\x18FILTER_ASSET_COLLECTIBLE\x10\x022\xe4\x11\n" + + "\x18FILTER_ASSET_COLLECTIBLE\x10\x022\xb0\x12\n" + "\bUniverse\x12Y\n" + "\x0eMultiverseRoot\x12\".universerpc.MultiverseRootRequest\x1a#.universerpc.MultiverseRootResponse\x12K\n" + "\n" + @@ -5251,7 +5474,8 @@ const file_universerpc_universe_proto_rawDesc = "" + "\vInsertProof\x12\x17.universerpc.AssetProof\x1a\x1f.universerpc.AssetProofResponse\x12J\n" + "\tPushProof\x12\x1d.universerpc.PushProofRequest\x1a\x1e.universerpc.PushProofResponse\x12;\n" + "\x04Info\x12\x18.universerpc.InfoRequest\x1a\x19.universerpc.InfoResponse\x12C\n" + - "\fSyncUniverse\x12\x18.universerpc.SyncRequest\x1a\x19.universerpc.SyncResponse\x12n\n" + + "\fSyncUniverse\x12\x18.universerpc.SyncRequest\x1a\x19.universerpc.SyncResponse\x12J\n" + + "\tSyncDelta\x12\x1d.universerpc.SyncDeltaRequest\x1a\x1e.universerpc.SyncDeltaResponse\x12n\n" + "\x15ListFederationServers\x12).universerpc.ListFederationServersRequest\x1a*.universerpc.ListFederationServersResponse\x12h\n" + "\x13AddFederationServer\x12'.universerpc.AddFederationServerRequest\x1a(.universerpc.AddFederationServerResponse\x12q\n" + "\x16DeleteFederationServer\x12*.universerpc.DeleteFederationServerRequest\x1a+.universerpc.DeleteFederationServerResponse\x12F\n" + @@ -5279,7 +5503,7 @@ func file_universerpc_universe_proto_rawDescGZIP() []byte { } var file_universerpc_universe_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_universerpc_universe_proto_msgTypes = make([]protoimpl.MessageInfo, 74) +var file_universerpc_universe_proto_msgTypes = make([]protoimpl.MessageInfo, 77) var file_universerpc_universe_proto_goTypes = []any{ (ProofType)(0), // 0: universerpc.ProofType (UniverseSyncMode)(0), // 1: universerpc.UniverseSyncMode @@ -5319,65 +5543,68 @@ var file_universerpc_universe_proto_goTypes = []any{ (*SyncedUniverse)(nil), // 35: universerpc.SyncedUniverse (*StatsRequest)(nil), // 36: universerpc.StatsRequest (*SyncResponse)(nil), // 37: universerpc.SyncResponse - (*UniverseFederationServer)(nil), // 38: universerpc.UniverseFederationServer - (*ListFederationServersRequest)(nil), // 39: universerpc.ListFederationServersRequest - (*ListFederationServersResponse)(nil), // 40: universerpc.ListFederationServersResponse - (*AddFederationServerRequest)(nil), // 41: universerpc.AddFederationServerRequest - (*AddFederationServerResponse)(nil), // 42: universerpc.AddFederationServerResponse - (*DeleteFederationServerRequest)(nil), // 43: universerpc.DeleteFederationServerRequest - (*DeleteFederationServerResponse)(nil), // 44: universerpc.DeleteFederationServerResponse - (*StatsResponse)(nil), // 45: universerpc.StatsResponse - (*AssetStatsQuery)(nil), // 46: universerpc.AssetStatsQuery - (*AssetStatsSnapshot)(nil), // 47: universerpc.AssetStatsSnapshot - (*AssetStatsAsset)(nil), // 48: universerpc.AssetStatsAsset - (*UniverseAssetStats)(nil), // 49: universerpc.UniverseAssetStats - (*QueryEventsRequest)(nil), // 50: universerpc.QueryEventsRequest - (*QueryEventsResponse)(nil), // 51: universerpc.QueryEventsResponse - (*GroupedUniverseEvents)(nil), // 52: universerpc.GroupedUniverseEvents - (*SetFederationSyncConfigRequest)(nil), // 53: universerpc.SetFederationSyncConfigRequest - (*SetFederationSyncConfigResponse)(nil), // 54: universerpc.SetFederationSyncConfigResponse - (*GlobalFederationSyncConfig)(nil), // 55: universerpc.GlobalFederationSyncConfig - (*AssetFederationSyncConfig)(nil), // 56: universerpc.AssetFederationSyncConfig - (*QueryFederationSyncConfigRequest)(nil), // 57: universerpc.QueryFederationSyncConfigRequest - (*QueryFederationSyncConfigResponse)(nil), // 58: universerpc.QueryFederationSyncConfigResponse - (*IgnoreAssetOutPointRequest)(nil), // 59: universerpc.IgnoreAssetOutPointRequest - (*IgnoreAssetOutPointResponse)(nil), // 60: universerpc.IgnoreAssetOutPointResponse - (*UpdateSupplyCommitRequest)(nil), // 61: universerpc.UpdateSupplyCommitRequest - (*UpdateSupplyCommitResponse)(nil), // 62: universerpc.UpdateSupplyCommitResponse - (*FetchSupplyCommitRequest)(nil), // 63: universerpc.FetchSupplyCommitRequest - (*SupplyCommitSubtreeRoot)(nil), // 64: universerpc.SupplyCommitSubtreeRoot - (*FetchSupplyCommitResponse)(nil), // 65: universerpc.FetchSupplyCommitResponse - (*FetchSupplyLeavesRequest)(nil), // 66: universerpc.FetchSupplyLeavesRequest - (*SupplyLeafKey)(nil), // 67: universerpc.SupplyLeafKey - (*SupplyLeafEntry)(nil), // 68: universerpc.SupplyLeafEntry - (*SupplyLeafBlockHeader)(nil), // 69: universerpc.SupplyLeafBlockHeader - (*FetchSupplyLeavesResponse)(nil), // 70: universerpc.FetchSupplyLeavesResponse - (*SupplyCommitChainData)(nil), // 71: universerpc.SupplyCommitChainData - (*InsertSupplyCommitRequest)(nil), // 72: universerpc.InsertSupplyCommitRequest - (*InsertSupplyCommitResponse)(nil), // 73: universerpc.InsertSupplyCommitResponse - nil, // 74: universerpc.UniverseRoot.AmountsByAssetIdEntry - nil, // 75: universerpc.AssetRootResponse.UniverseRootsEntry - nil, // 76: universerpc.FetchSupplyCommitResponse.BlockHeadersEntry - nil, // 77: universerpc.FetchSupplyLeavesResponse.BlockHeadersEntry - (taprpc.SortDirection)(0), // 78: taprpc.SortDirection - (*taprpc.Asset)(nil), // 79: taprpc.Asset - (*taprpc.AssetMeta)(nil), // 80: taprpc.AssetMeta - (*taprpc.GenesisReveal)(nil), // 81: taprpc.GenesisReveal - (*taprpc.GroupKeyReveal)(nil), // 82: taprpc.GroupKeyReveal - (taprpc.AssetType)(0), // 83: taprpc.AssetType - (*taprpc.AssetOutPoint)(nil), // 84: taprpc.AssetOutPoint - (*taprpc.OutPoint)(nil), // 85: taprpc.OutPoint + (*SyncDeltaRequest)(nil), // 38: universerpc.SyncDeltaRequest + (*SyncDeltaItem)(nil), // 39: universerpc.SyncDeltaItem + (*SyncDeltaResponse)(nil), // 40: universerpc.SyncDeltaResponse + (*UniverseFederationServer)(nil), // 41: universerpc.UniverseFederationServer + (*ListFederationServersRequest)(nil), // 42: universerpc.ListFederationServersRequest + (*ListFederationServersResponse)(nil), // 43: universerpc.ListFederationServersResponse + (*AddFederationServerRequest)(nil), // 44: universerpc.AddFederationServerRequest + (*AddFederationServerResponse)(nil), // 45: universerpc.AddFederationServerResponse + (*DeleteFederationServerRequest)(nil), // 46: universerpc.DeleteFederationServerRequest + (*DeleteFederationServerResponse)(nil), // 47: universerpc.DeleteFederationServerResponse + (*StatsResponse)(nil), // 48: universerpc.StatsResponse + (*AssetStatsQuery)(nil), // 49: universerpc.AssetStatsQuery + (*AssetStatsSnapshot)(nil), // 50: universerpc.AssetStatsSnapshot + (*AssetStatsAsset)(nil), // 51: universerpc.AssetStatsAsset + (*UniverseAssetStats)(nil), // 52: universerpc.UniverseAssetStats + (*QueryEventsRequest)(nil), // 53: universerpc.QueryEventsRequest + (*QueryEventsResponse)(nil), // 54: universerpc.QueryEventsResponse + (*GroupedUniverseEvents)(nil), // 55: universerpc.GroupedUniverseEvents + (*SetFederationSyncConfigRequest)(nil), // 56: universerpc.SetFederationSyncConfigRequest + (*SetFederationSyncConfigResponse)(nil), // 57: universerpc.SetFederationSyncConfigResponse + (*GlobalFederationSyncConfig)(nil), // 58: universerpc.GlobalFederationSyncConfig + (*AssetFederationSyncConfig)(nil), // 59: universerpc.AssetFederationSyncConfig + (*QueryFederationSyncConfigRequest)(nil), // 60: universerpc.QueryFederationSyncConfigRequest + (*QueryFederationSyncConfigResponse)(nil), // 61: universerpc.QueryFederationSyncConfigResponse + (*IgnoreAssetOutPointRequest)(nil), // 62: universerpc.IgnoreAssetOutPointRequest + (*IgnoreAssetOutPointResponse)(nil), // 63: universerpc.IgnoreAssetOutPointResponse + (*UpdateSupplyCommitRequest)(nil), // 64: universerpc.UpdateSupplyCommitRequest + (*UpdateSupplyCommitResponse)(nil), // 65: universerpc.UpdateSupplyCommitResponse + (*FetchSupplyCommitRequest)(nil), // 66: universerpc.FetchSupplyCommitRequest + (*SupplyCommitSubtreeRoot)(nil), // 67: universerpc.SupplyCommitSubtreeRoot + (*FetchSupplyCommitResponse)(nil), // 68: universerpc.FetchSupplyCommitResponse + (*FetchSupplyLeavesRequest)(nil), // 69: universerpc.FetchSupplyLeavesRequest + (*SupplyLeafKey)(nil), // 70: universerpc.SupplyLeafKey + (*SupplyLeafEntry)(nil), // 71: universerpc.SupplyLeafEntry + (*SupplyLeafBlockHeader)(nil), // 72: universerpc.SupplyLeafBlockHeader + (*FetchSupplyLeavesResponse)(nil), // 73: universerpc.FetchSupplyLeavesResponse + (*SupplyCommitChainData)(nil), // 74: universerpc.SupplyCommitChainData + (*InsertSupplyCommitRequest)(nil), // 75: universerpc.InsertSupplyCommitRequest + (*InsertSupplyCommitResponse)(nil), // 76: universerpc.InsertSupplyCommitResponse + nil, // 77: universerpc.UniverseRoot.AmountsByAssetIdEntry + nil, // 78: universerpc.AssetRootResponse.UniverseRootsEntry + nil, // 79: universerpc.FetchSupplyCommitResponse.BlockHeadersEntry + nil, // 80: universerpc.FetchSupplyLeavesResponse.BlockHeadersEntry + (taprpc.SortDirection)(0), // 81: taprpc.SortDirection + (*taprpc.Asset)(nil), // 82: taprpc.Asset + (*taprpc.AssetMeta)(nil), // 83: taprpc.AssetMeta + (*taprpc.GenesisReveal)(nil), // 84: taprpc.GenesisReveal + (*taprpc.GroupKeyReveal)(nil), // 85: taprpc.GroupKeyReveal + (taprpc.AssetType)(0), // 86: taprpc.AssetType + (*taprpc.AssetOutPoint)(nil), // 87: taprpc.AssetOutPoint + (*taprpc.OutPoint)(nil), // 88: taprpc.OutPoint } var file_universerpc_universe_proto_depIdxs = []int32{ 0, // 0: universerpc.MultiverseRootRequest.proof_type:type_name -> universerpc.ProofType 8, // 1: universerpc.MultiverseRootRequest.specific_ids:type_name -> universerpc.ID 7, // 2: universerpc.MultiverseRootResponse.multiverse_root:type_name -> universerpc.MerkleSumNode - 78, // 3: universerpc.AssetRootRequest.direction:type_name -> taprpc.SortDirection + 81, // 3: universerpc.AssetRootRequest.direction:type_name -> taprpc.SortDirection 0, // 4: universerpc.ID.proof_type:type_name -> universerpc.ProofType 8, // 5: universerpc.UniverseRoot.id:type_name -> universerpc.ID 7, // 6: universerpc.UniverseRoot.mssmt_root:type_name -> universerpc.MerkleSumNode - 74, // 7: universerpc.UniverseRoot.amounts_by_asset_id:type_name -> universerpc.UniverseRoot.AmountsByAssetIdEntry - 75, // 8: universerpc.AssetRootResponse.universe_roots:type_name -> universerpc.AssetRootResponse.UniverseRootsEntry + 77, // 7: universerpc.UniverseRoot.amounts_by_asset_id:type_name -> universerpc.UniverseRoot.AmountsByAssetIdEntry + 78, // 8: universerpc.AssetRootResponse.universe_roots:type_name -> universerpc.AssetRootResponse.UniverseRootsEntry 8, // 9: universerpc.AssetRootQuery.id:type_name -> universerpc.ID 9, // 10: universerpc.QueryRootResponse.issuance_root:type_name -> universerpc.UniverseRoot 9, // 11: universerpc.QueryRootResponse.transfer_root:type_name -> universerpc.UniverseRoot @@ -5385,13 +5612,13 @@ var file_universerpc_universe_proto_depIdxs = []int32{ 25, // 13: universerpc.DeleteAssetLeafRequest.key:type_name -> universerpc.UniverseKey 17, // 14: universerpc.AssetKey.op:type_name -> universerpc.Outpoint 8, // 15: universerpc.AssetLeafKeysRequest.id:type_name -> universerpc.ID - 78, // 16: universerpc.AssetLeafKeysRequest.direction:type_name -> taprpc.SortDirection + 81, // 16: universerpc.AssetLeafKeysRequest.direction:type_name -> taprpc.SortDirection 8, // 17: universerpc.AssetLeavesRequest.id:type_name -> universerpc.ID - 78, // 18: universerpc.AssetLeavesRequest.direction:type_name -> taprpc.SortDirection + 81, // 18: universerpc.AssetLeavesRequest.direction:type_name -> taprpc.SortDirection 18, // 19: universerpc.AssetLeafEntry.asset_key:type_name -> universerpc.AssetKey 18, // 20: universerpc.AssetLeafKeyResponse.asset_keys:type_name -> universerpc.AssetKey 21, // 21: universerpc.AssetLeafKeyResponse.entries:type_name -> universerpc.AssetLeafEntry - 79, // 22: universerpc.AssetLeaf.asset:type_name -> taprpc.Asset + 82, // 22: universerpc.AssetLeaf.asset:type_name -> taprpc.Asset 23, // 23: universerpc.AssetLeafResponse.leaves:type_name -> universerpc.AssetLeaf 8, // 24: universerpc.UniverseKey.id:type_name -> universerpc.ID 18, // 25: universerpc.UniverseKey.leaf_key:type_name -> universerpc.AssetKey @@ -5400,13 +5627,13 @@ var file_universerpc_universe_proto_depIdxs = []int32{ 23, // 28: universerpc.AssetProofResponse.asset_leaf:type_name -> universerpc.AssetLeaf 7, // 29: universerpc.AssetProofResponse.multiverse_root:type_name -> universerpc.MerkleSumNode 27, // 30: universerpc.AssetProofResponse.issuance_data:type_name -> universerpc.IssuanceData - 80, // 31: universerpc.IssuanceData.meta_reveal:type_name -> taprpc.AssetMeta - 81, // 32: universerpc.IssuanceData.genesis_reveal:type_name -> taprpc.GenesisReveal - 82, // 33: universerpc.IssuanceData.group_key_reveal:type_name -> taprpc.GroupKeyReveal + 83, // 31: universerpc.IssuanceData.meta_reveal:type_name -> taprpc.AssetMeta + 84, // 32: universerpc.IssuanceData.genesis_reveal:type_name -> taprpc.GenesisReveal + 85, // 33: universerpc.IssuanceData.group_key_reveal:type_name -> taprpc.GroupKeyReveal 25, // 34: universerpc.AssetProof.key:type_name -> universerpc.UniverseKey 23, // 35: universerpc.AssetProof.asset_leaf:type_name -> universerpc.AssetLeaf 25, // 36: universerpc.PushProofRequest.key:type_name -> universerpc.UniverseKey - 38, // 37: universerpc.PushProofRequest.server:type_name -> universerpc.UniverseFederationServer + 41, // 37: universerpc.PushProofRequest.server:type_name -> universerpc.UniverseFederationServer 25, // 38: universerpc.PushProofResponse.key:type_name -> universerpc.UniverseKey 8, // 39: universerpc.SyncTarget.id:type_name -> universerpc.ID 1, // 40: universerpc.SyncRequest.sync_mode:type_name -> universerpc.UniverseSyncMode @@ -5415,108 +5642,115 @@ var file_universerpc_universe_proto_depIdxs = []int32{ 9, // 43: universerpc.SyncedUniverse.new_asset_root:type_name -> universerpc.UniverseRoot 23, // 44: universerpc.SyncedUniverse.new_asset_leaves:type_name -> universerpc.AssetLeaf 35, // 45: universerpc.SyncResponse.synced_universes:type_name -> universerpc.SyncedUniverse - 38, // 46: universerpc.ListFederationServersResponse.servers:type_name -> universerpc.UniverseFederationServer - 38, // 47: universerpc.AddFederationServerRequest.servers:type_name -> universerpc.UniverseFederationServer - 38, // 48: universerpc.DeleteFederationServerRequest.servers:type_name -> universerpc.UniverseFederationServer - 3, // 49: universerpc.AssetStatsQuery.asset_type_filter:type_name -> universerpc.AssetTypeFilter - 2, // 50: universerpc.AssetStatsQuery.sort_by:type_name -> universerpc.AssetQuerySort - 78, // 51: universerpc.AssetStatsQuery.direction:type_name -> taprpc.SortDirection - 48, // 52: universerpc.AssetStatsSnapshot.group_anchor:type_name -> universerpc.AssetStatsAsset - 48, // 53: universerpc.AssetStatsSnapshot.asset:type_name -> universerpc.AssetStatsAsset - 83, // 54: universerpc.AssetStatsAsset.asset_type:type_name -> taprpc.AssetType - 47, // 55: universerpc.UniverseAssetStats.asset_stats:type_name -> universerpc.AssetStatsSnapshot - 52, // 56: universerpc.QueryEventsResponse.events:type_name -> universerpc.GroupedUniverseEvents - 55, // 57: universerpc.SetFederationSyncConfigRequest.global_sync_configs:type_name -> universerpc.GlobalFederationSyncConfig - 56, // 58: universerpc.SetFederationSyncConfigRequest.asset_sync_configs:type_name -> universerpc.AssetFederationSyncConfig - 0, // 59: universerpc.GlobalFederationSyncConfig.proof_type:type_name -> universerpc.ProofType - 8, // 60: universerpc.AssetFederationSyncConfig.id:type_name -> universerpc.ID - 8, // 61: universerpc.QueryFederationSyncConfigRequest.id:type_name -> universerpc.ID - 55, // 62: universerpc.QueryFederationSyncConfigResponse.global_sync_configs:type_name -> universerpc.GlobalFederationSyncConfig - 56, // 63: universerpc.QueryFederationSyncConfigResponse.asset_sync_configs:type_name -> universerpc.AssetFederationSyncConfig - 84, // 64: universerpc.IgnoreAssetOutPointRequest.asset_out_point:type_name -> taprpc.AssetOutPoint - 7, // 65: universerpc.IgnoreAssetOutPointResponse.leaf:type_name -> universerpc.MerkleSumNode - 85, // 66: universerpc.FetchSupplyCommitRequest.commit_outpoint:type_name -> taprpc.OutPoint - 85, // 67: universerpc.FetchSupplyCommitRequest.spent_commit_outpoint:type_name -> taprpc.OutPoint - 7, // 68: universerpc.SupplyCommitSubtreeRoot.root_node:type_name -> universerpc.MerkleSumNode - 71, // 69: universerpc.FetchSupplyCommitResponse.chain_data:type_name -> universerpc.SupplyCommitChainData - 64, // 70: universerpc.FetchSupplyCommitResponse.issuance_subtree_root:type_name -> universerpc.SupplyCommitSubtreeRoot - 64, // 71: universerpc.FetchSupplyCommitResponse.burn_subtree_root:type_name -> universerpc.SupplyCommitSubtreeRoot - 64, // 72: universerpc.FetchSupplyCommitResponse.ignore_subtree_root:type_name -> universerpc.SupplyCommitSubtreeRoot - 68, // 73: universerpc.FetchSupplyCommitResponse.issuance_leaves:type_name -> universerpc.SupplyLeafEntry - 68, // 74: universerpc.FetchSupplyCommitResponse.burn_leaves:type_name -> universerpc.SupplyLeafEntry - 68, // 75: universerpc.FetchSupplyCommitResponse.ignore_leaves:type_name -> universerpc.SupplyLeafEntry - 85, // 76: universerpc.FetchSupplyCommitResponse.spent_commitment_outpoint:type_name -> taprpc.OutPoint - 76, // 77: universerpc.FetchSupplyCommitResponse.block_headers:type_name -> universerpc.FetchSupplyCommitResponse.BlockHeadersEntry - 17, // 78: universerpc.SupplyLeafKey.outpoint:type_name -> universerpc.Outpoint - 67, // 79: universerpc.SupplyLeafEntry.leaf_key:type_name -> universerpc.SupplyLeafKey - 7, // 80: universerpc.SupplyLeafEntry.leaf_node:type_name -> universerpc.MerkleSumNode - 68, // 81: universerpc.FetchSupplyLeavesResponse.issuance_leaves:type_name -> universerpc.SupplyLeafEntry - 68, // 82: universerpc.FetchSupplyLeavesResponse.burn_leaves:type_name -> universerpc.SupplyLeafEntry - 68, // 83: universerpc.FetchSupplyLeavesResponse.ignore_leaves:type_name -> universerpc.SupplyLeafEntry - 77, // 84: universerpc.FetchSupplyLeavesResponse.block_headers:type_name -> universerpc.FetchSupplyLeavesResponse.BlockHeadersEntry - 71, // 85: universerpc.InsertSupplyCommitRequest.chain_data:type_name -> universerpc.SupplyCommitChainData - 85, // 86: universerpc.InsertSupplyCommitRequest.spent_commitment_outpoint:type_name -> taprpc.OutPoint - 68, // 87: universerpc.InsertSupplyCommitRequest.issuance_leaves:type_name -> universerpc.SupplyLeafEntry - 68, // 88: universerpc.InsertSupplyCommitRequest.burn_leaves:type_name -> universerpc.SupplyLeafEntry - 68, // 89: universerpc.InsertSupplyCommitRequest.ignore_leaves:type_name -> universerpc.SupplyLeafEntry - 9, // 90: universerpc.AssetRootResponse.UniverseRootsEntry.value:type_name -> universerpc.UniverseRoot - 69, // 91: universerpc.FetchSupplyCommitResponse.BlockHeadersEntry.value:type_name -> universerpc.SupplyLeafBlockHeader - 69, // 92: universerpc.FetchSupplyLeavesResponse.BlockHeadersEntry.value:type_name -> universerpc.SupplyLeafBlockHeader - 4, // 93: universerpc.Universe.MultiverseRoot:input_type -> universerpc.MultiverseRootRequest - 6, // 94: universerpc.Universe.AssetRoots:input_type -> universerpc.AssetRootRequest - 11, // 95: universerpc.Universe.QueryAssetRoots:input_type -> universerpc.AssetRootQuery - 13, // 96: universerpc.Universe.DeleteAssetRoot:input_type -> universerpc.DeleteRootQuery - 15, // 97: universerpc.Universe.DeleteAssetLeaf:input_type -> universerpc.DeleteAssetLeafRequest - 19, // 98: universerpc.Universe.AssetLeafKeys:input_type -> universerpc.AssetLeafKeysRequest - 20, // 99: universerpc.Universe.AssetLeaves:input_type -> universerpc.AssetLeavesRequest - 25, // 100: universerpc.Universe.QueryProof:input_type -> universerpc.UniverseKey - 28, // 101: universerpc.Universe.InsertProof:input_type -> universerpc.AssetProof - 29, // 102: universerpc.Universe.PushProof:input_type -> universerpc.PushProofRequest - 31, // 103: universerpc.Universe.Info:input_type -> universerpc.InfoRequest - 34, // 104: universerpc.Universe.SyncUniverse:input_type -> universerpc.SyncRequest - 39, // 105: universerpc.Universe.ListFederationServers:input_type -> universerpc.ListFederationServersRequest - 41, // 106: universerpc.Universe.AddFederationServer:input_type -> universerpc.AddFederationServerRequest - 43, // 107: universerpc.Universe.DeleteFederationServer:input_type -> universerpc.DeleteFederationServerRequest - 36, // 108: universerpc.Universe.UniverseStats:input_type -> universerpc.StatsRequest - 46, // 109: universerpc.Universe.QueryAssetStats:input_type -> universerpc.AssetStatsQuery - 50, // 110: universerpc.Universe.QueryEvents:input_type -> universerpc.QueryEventsRequest - 53, // 111: universerpc.Universe.SetFederationSyncConfig:input_type -> universerpc.SetFederationSyncConfigRequest - 57, // 112: universerpc.Universe.QueryFederationSyncConfig:input_type -> universerpc.QueryFederationSyncConfigRequest - 59, // 113: universerpc.Universe.IgnoreAssetOutPoint:input_type -> universerpc.IgnoreAssetOutPointRequest - 61, // 114: universerpc.Universe.UpdateSupplyCommit:input_type -> universerpc.UpdateSupplyCommitRequest - 63, // 115: universerpc.Universe.FetchSupplyCommit:input_type -> universerpc.FetchSupplyCommitRequest - 66, // 116: universerpc.Universe.FetchSupplyLeaves:input_type -> universerpc.FetchSupplyLeavesRequest - 72, // 117: universerpc.Universe.InsertSupplyCommit:input_type -> universerpc.InsertSupplyCommitRequest - 5, // 118: universerpc.Universe.MultiverseRoot:output_type -> universerpc.MultiverseRootResponse - 10, // 119: universerpc.Universe.AssetRoots:output_type -> universerpc.AssetRootResponse - 12, // 120: universerpc.Universe.QueryAssetRoots:output_type -> universerpc.QueryRootResponse - 14, // 121: universerpc.Universe.DeleteAssetRoot:output_type -> universerpc.DeleteRootResponse - 16, // 122: universerpc.Universe.DeleteAssetLeaf:output_type -> universerpc.DeleteAssetLeafResponse - 22, // 123: universerpc.Universe.AssetLeafKeys:output_type -> universerpc.AssetLeafKeyResponse - 24, // 124: universerpc.Universe.AssetLeaves:output_type -> universerpc.AssetLeafResponse - 26, // 125: universerpc.Universe.QueryProof:output_type -> universerpc.AssetProofResponse - 26, // 126: universerpc.Universe.InsertProof:output_type -> universerpc.AssetProofResponse - 30, // 127: universerpc.Universe.PushProof:output_type -> universerpc.PushProofResponse - 32, // 128: universerpc.Universe.Info:output_type -> universerpc.InfoResponse - 37, // 129: universerpc.Universe.SyncUniverse:output_type -> universerpc.SyncResponse - 40, // 130: universerpc.Universe.ListFederationServers:output_type -> universerpc.ListFederationServersResponse - 42, // 131: universerpc.Universe.AddFederationServer:output_type -> universerpc.AddFederationServerResponse - 44, // 132: universerpc.Universe.DeleteFederationServer:output_type -> universerpc.DeleteFederationServerResponse - 45, // 133: universerpc.Universe.UniverseStats:output_type -> universerpc.StatsResponse - 49, // 134: universerpc.Universe.QueryAssetStats:output_type -> universerpc.UniverseAssetStats - 51, // 135: universerpc.Universe.QueryEvents:output_type -> universerpc.QueryEventsResponse - 54, // 136: universerpc.Universe.SetFederationSyncConfig:output_type -> universerpc.SetFederationSyncConfigResponse - 58, // 137: universerpc.Universe.QueryFederationSyncConfig:output_type -> universerpc.QueryFederationSyncConfigResponse - 60, // 138: universerpc.Universe.IgnoreAssetOutPoint:output_type -> universerpc.IgnoreAssetOutPointResponse - 62, // 139: universerpc.Universe.UpdateSupplyCommit:output_type -> universerpc.UpdateSupplyCommitResponse - 65, // 140: universerpc.Universe.FetchSupplyCommit:output_type -> universerpc.FetchSupplyCommitResponse - 70, // 141: universerpc.Universe.FetchSupplyLeaves:output_type -> universerpc.FetchSupplyLeavesResponse - 73, // 142: universerpc.Universe.InsertSupplyCommit:output_type -> universerpc.InsertSupplyCommitResponse - 118, // [118:143] is the sub-list for method output_type - 93, // [93:118] is the sub-list for method input_type - 93, // [93:93] is the sub-list for extension type_name - 93, // [93:93] is the sub-list for extension extendee - 0, // [0:93] is the sub-list for field type_name + 8, // 46: universerpc.SyncDeltaItem.universe_id:type_name -> universerpc.ID + 18, // 47: universerpc.SyncDeltaItem.key:type_name -> universerpc.AssetKey + 23, // 48: universerpc.SyncDeltaItem.leaf:type_name -> universerpc.AssetLeaf + 39, // 49: universerpc.SyncDeltaResponse.items:type_name -> universerpc.SyncDeltaItem + 9, // 50: universerpc.SyncDeltaResponse.universe_roots:type_name -> universerpc.UniverseRoot + 41, // 51: universerpc.ListFederationServersResponse.servers:type_name -> universerpc.UniverseFederationServer + 41, // 52: universerpc.AddFederationServerRequest.servers:type_name -> universerpc.UniverseFederationServer + 41, // 53: universerpc.DeleteFederationServerRequest.servers:type_name -> universerpc.UniverseFederationServer + 3, // 54: universerpc.AssetStatsQuery.asset_type_filter:type_name -> universerpc.AssetTypeFilter + 2, // 55: universerpc.AssetStatsQuery.sort_by:type_name -> universerpc.AssetQuerySort + 81, // 56: universerpc.AssetStatsQuery.direction:type_name -> taprpc.SortDirection + 51, // 57: universerpc.AssetStatsSnapshot.group_anchor:type_name -> universerpc.AssetStatsAsset + 51, // 58: universerpc.AssetStatsSnapshot.asset:type_name -> universerpc.AssetStatsAsset + 86, // 59: universerpc.AssetStatsAsset.asset_type:type_name -> taprpc.AssetType + 50, // 60: universerpc.UniverseAssetStats.asset_stats:type_name -> universerpc.AssetStatsSnapshot + 55, // 61: universerpc.QueryEventsResponse.events:type_name -> universerpc.GroupedUniverseEvents + 58, // 62: universerpc.SetFederationSyncConfigRequest.global_sync_configs:type_name -> universerpc.GlobalFederationSyncConfig + 59, // 63: universerpc.SetFederationSyncConfigRequest.asset_sync_configs:type_name -> universerpc.AssetFederationSyncConfig + 0, // 64: universerpc.GlobalFederationSyncConfig.proof_type:type_name -> universerpc.ProofType + 8, // 65: universerpc.AssetFederationSyncConfig.id:type_name -> universerpc.ID + 8, // 66: universerpc.QueryFederationSyncConfigRequest.id:type_name -> universerpc.ID + 58, // 67: universerpc.QueryFederationSyncConfigResponse.global_sync_configs:type_name -> universerpc.GlobalFederationSyncConfig + 59, // 68: universerpc.QueryFederationSyncConfigResponse.asset_sync_configs:type_name -> universerpc.AssetFederationSyncConfig + 87, // 69: universerpc.IgnoreAssetOutPointRequest.asset_out_point:type_name -> taprpc.AssetOutPoint + 7, // 70: universerpc.IgnoreAssetOutPointResponse.leaf:type_name -> universerpc.MerkleSumNode + 88, // 71: universerpc.FetchSupplyCommitRequest.commit_outpoint:type_name -> taprpc.OutPoint + 88, // 72: universerpc.FetchSupplyCommitRequest.spent_commit_outpoint:type_name -> taprpc.OutPoint + 7, // 73: universerpc.SupplyCommitSubtreeRoot.root_node:type_name -> universerpc.MerkleSumNode + 74, // 74: universerpc.FetchSupplyCommitResponse.chain_data:type_name -> universerpc.SupplyCommitChainData + 67, // 75: universerpc.FetchSupplyCommitResponse.issuance_subtree_root:type_name -> universerpc.SupplyCommitSubtreeRoot + 67, // 76: universerpc.FetchSupplyCommitResponse.burn_subtree_root:type_name -> universerpc.SupplyCommitSubtreeRoot + 67, // 77: universerpc.FetchSupplyCommitResponse.ignore_subtree_root:type_name -> universerpc.SupplyCommitSubtreeRoot + 71, // 78: universerpc.FetchSupplyCommitResponse.issuance_leaves:type_name -> universerpc.SupplyLeafEntry + 71, // 79: universerpc.FetchSupplyCommitResponse.burn_leaves:type_name -> universerpc.SupplyLeafEntry + 71, // 80: universerpc.FetchSupplyCommitResponse.ignore_leaves:type_name -> universerpc.SupplyLeafEntry + 88, // 81: universerpc.FetchSupplyCommitResponse.spent_commitment_outpoint:type_name -> taprpc.OutPoint + 79, // 82: universerpc.FetchSupplyCommitResponse.block_headers:type_name -> universerpc.FetchSupplyCommitResponse.BlockHeadersEntry + 17, // 83: universerpc.SupplyLeafKey.outpoint:type_name -> universerpc.Outpoint + 70, // 84: universerpc.SupplyLeafEntry.leaf_key:type_name -> universerpc.SupplyLeafKey + 7, // 85: universerpc.SupplyLeafEntry.leaf_node:type_name -> universerpc.MerkleSumNode + 71, // 86: universerpc.FetchSupplyLeavesResponse.issuance_leaves:type_name -> universerpc.SupplyLeafEntry + 71, // 87: universerpc.FetchSupplyLeavesResponse.burn_leaves:type_name -> universerpc.SupplyLeafEntry + 71, // 88: universerpc.FetchSupplyLeavesResponse.ignore_leaves:type_name -> universerpc.SupplyLeafEntry + 80, // 89: universerpc.FetchSupplyLeavesResponse.block_headers:type_name -> universerpc.FetchSupplyLeavesResponse.BlockHeadersEntry + 74, // 90: universerpc.InsertSupplyCommitRequest.chain_data:type_name -> universerpc.SupplyCommitChainData + 88, // 91: universerpc.InsertSupplyCommitRequest.spent_commitment_outpoint:type_name -> taprpc.OutPoint + 71, // 92: universerpc.InsertSupplyCommitRequest.issuance_leaves:type_name -> universerpc.SupplyLeafEntry + 71, // 93: universerpc.InsertSupplyCommitRequest.burn_leaves:type_name -> universerpc.SupplyLeafEntry + 71, // 94: universerpc.InsertSupplyCommitRequest.ignore_leaves:type_name -> universerpc.SupplyLeafEntry + 9, // 95: universerpc.AssetRootResponse.UniverseRootsEntry.value:type_name -> universerpc.UniverseRoot + 72, // 96: universerpc.FetchSupplyCommitResponse.BlockHeadersEntry.value:type_name -> universerpc.SupplyLeafBlockHeader + 72, // 97: universerpc.FetchSupplyLeavesResponse.BlockHeadersEntry.value:type_name -> universerpc.SupplyLeafBlockHeader + 4, // 98: universerpc.Universe.MultiverseRoot:input_type -> universerpc.MultiverseRootRequest + 6, // 99: universerpc.Universe.AssetRoots:input_type -> universerpc.AssetRootRequest + 11, // 100: universerpc.Universe.QueryAssetRoots:input_type -> universerpc.AssetRootQuery + 13, // 101: universerpc.Universe.DeleteAssetRoot:input_type -> universerpc.DeleteRootQuery + 15, // 102: universerpc.Universe.DeleteAssetLeaf:input_type -> universerpc.DeleteAssetLeafRequest + 19, // 103: universerpc.Universe.AssetLeafKeys:input_type -> universerpc.AssetLeafKeysRequest + 20, // 104: universerpc.Universe.AssetLeaves:input_type -> universerpc.AssetLeavesRequest + 25, // 105: universerpc.Universe.QueryProof:input_type -> universerpc.UniverseKey + 28, // 106: universerpc.Universe.InsertProof:input_type -> universerpc.AssetProof + 29, // 107: universerpc.Universe.PushProof:input_type -> universerpc.PushProofRequest + 31, // 108: universerpc.Universe.Info:input_type -> universerpc.InfoRequest + 34, // 109: universerpc.Universe.SyncUniverse:input_type -> universerpc.SyncRequest + 38, // 110: universerpc.Universe.SyncDelta:input_type -> universerpc.SyncDeltaRequest + 42, // 111: universerpc.Universe.ListFederationServers:input_type -> universerpc.ListFederationServersRequest + 44, // 112: universerpc.Universe.AddFederationServer:input_type -> universerpc.AddFederationServerRequest + 46, // 113: universerpc.Universe.DeleteFederationServer:input_type -> universerpc.DeleteFederationServerRequest + 36, // 114: universerpc.Universe.UniverseStats:input_type -> universerpc.StatsRequest + 49, // 115: universerpc.Universe.QueryAssetStats:input_type -> universerpc.AssetStatsQuery + 53, // 116: universerpc.Universe.QueryEvents:input_type -> universerpc.QueryEventsRequest + 56, // 117: universerpc.Universe.SetFederationSyncConfig:input_type -> universerpc.SetFederationSyncConfigRequest + 60, // 118: universerpc.Universe.QueryFederationSyncConfig:input_type -> universerpc.QueryFederationSyncConfigRequest + 62, // 119: universerpc.Universe.IgnoreAssetOutPoint:input_type -> universerpc.IgnoreAssetOutPointRequest + 64, // 120: universerpc.Universe.UpdateSupplyCommit:input_type -> universerpc.UpdateSupplyCommitRequest + 66, // 121: universerpc.Universe.FetchSupplyCommit:input_type -> universerpc.FetchSupplyCommitRequest + 69, // 122: universerpc.Universe.FetchSupplyLeaves:input_type -> universerpc.FetchSupplyLeavesRequest + 75, // 123: universerpc.Universe.InsertSupplyCommit:input_type -> universerpc.InsertSupplyCommitRequest + 5, // 124: universerpc.Universe.MultiverseRoot:output_type -> universerpc.MultiverseRootResponse + 10, // 125: universerpc.Universe.AssetRoots:output_type -> universerpc.AssetRootResponse + 12, // 126: universerpc.Universe.QueryAssetRoots:output_type -> universerpc.QueryRootResponse + 14, // 127: universerpc.Universe.DeleteAssetRoot:output_type -> universerpc.DeleteRootResponse + 16, // 128: universerpc.Universe.DeleteAssetLeaf:output_type -> universerpc.DeleteAssetLeafResponse + 22, // 129: universerpc.Universe.AssetLeafKeys:output_type -> universerpc.AssetLeafKeyResponse + 24, // 130: universerpc.Universe.AssetLeaves:output_type -> universerpc.AssetLeafResponse + 26, // 131: universerpc.Universe.QueryProof:output_type -> universerpc.AssetProofResponse + 26, // 132: universerpc.Universe.InsertProof:output_type -> universerpc.AssetProofResponse + 30, // 133: universerpc.Universe.PushProof:output_type -> universerpc.PushProofResponse + 32, // 134: universerpc.Universe.Info:output_type -> universerpc.InfoResponse + 37, // 135: universerpc.Universe.SyncUniverse:output_type -> universerpc.SyncResponse + 40, // 136: universerpc.Universe.SyncDelta:output_type -> universerpc.SyncDeltaResponse + 43, // 137: universerpc.Universe.ListFederationServers:output_type -> universerpc.ListFederationServersResponse + 45, // 138: universerpc.Universe.AddFederationServer:output_type -> universerpc.AddFederationServerResponse + 47, // 139: universerpc.Universe.DeleteFederationServer:output_type -> universerpc.DeleteFederationServerResponse + 48, // 140: universerpc.Universe.UniverseStats:output_type -> universerpc.StatsResponse + 52, // 141: universerpc.Universe.QueryAssetStats:output_type -> universerpc.UniverseAssetStats + 54, // 142: universerpc.Universe.QueryEvents:output_type -> universerpc.QueryEventsResponse + 57, // 143: universerpc.Universe.SetFederationSyncConfig:output_type -> universerpc.SetFederationSyncConfigResponse + 61, // 144: universerpc.Universe.QueryFederationSyncConfig:output_type -> universerpc.QueryFederationSyncConfigResponse + 63, // 145: universerpc.Universe.IgnoreAssetOutPoint:output_type -> universerpc.IgnoreAssetOutPointResponse + 65, // 146: universerpc.Universe.UpdateSupplyCommit:output_type -> universerpc.UpdateSupplyCommitResponse + 68, // 147: universerpc.Universe.FetchSupplyCommit:output_type -> universerpc.FetchSupplyCommitResponse + 73, // 148: universerpc.Universe.FetchSupplyLeaves:output_type -> universerpc.FetchSupplyLeavesResponse + 76, // 149: universerpc.Universe.InsertSupplyCommit:output_type -> universerpc.InsertSupplyCommitResponse + 124, // [124:150] is the sub-list for method output_type + 98, // [98:124] is the sub-list for method input_type + 98, // [98:98] is the sub-list for extension type_name + 98, // [98:98] is the sub-list for extension extendee + 0, // [0:98] is the sub-list for field type_name } func init() { file_universerpc_universe_proto_init() } @@ -5536,11 +5770,11 @@ func file_universerpc_universe_proto_init() { (*AssetKey_ScriptKeyBytes)(nil), (*AssetKey_ScriptKeyStr)(nil), } - file_universerpc_universe_proto_msgTypes[57].OneofWrappers = []any{ + file_universerpc_universe_proto_msgTypes[60].OneofWrappers = []any{ (*UpdateSupplyCommitRequest_GroupKeyBytes)(nil), (*UpdateSupplyCommitRequest_GroupKeyStr)(nil), } - file_universerpc_universe_proto_msgTypes[59].OneofWrappers = []any{ + file_universerpc_universe_proto_msgTypes[62].OneofWrappers = []any{ (*FetchSupplyCommitRequest_GroupKeyBytes)(nil), (*FetchSupplyCommitRequest_GroupKeyStr)(nil), (*FetchSupplyCommitRequest_CommitOutpoint)(nil), @@ -5548,11 +5782,11 @@ func file_universerpc_universe_proto_init() { (*FetchSupplyCommitRequest_VeryFirst)(nil), (*FetchSupplyCommitRequest_Latest)(nil), } - file_universerpc_universe_proto_msgTypes[62].OneofWrappers = []any{ + file_universerpc_universe_proto_msgTypes[65].OneofWrappers = []any{ (*FetchSupplyLeavesRequest_GroupKeyBytes)(nil), (*FetchSupplyLeavesRequest_GroupKeyStr)(nil), } - file_universerpc_universe_proto_msgTypes[68].OneofWrappers = []any{ + file_universerpc_universe_proto_msgTypes[71].OneofWrappers = []any{ (*InsertSupplyCommitRequest_GroupKeyBytes)(nil), (*InsertSupplyCommitRequest_GroupKeyStr)(nil), } @@ -5562,7 +5796,7 @@ func file_universerpc_universe_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_universerpc_universe_proto_rawDesc), len(file_universerpc_universe_proto_rawDesc)), NumEnums: 4, - NumMessages: 74, + NumMessages: 77, NumExtensions: 0, NumServices: 1, }, diff --git a/taprpc/universerpc/universe.pb.gw.go b/taprpc/universerpc/universe.pb.gw.go index 83964f0f2..6a9956761 100644 --- a/taprpc/universerpc/universe.pb.gw.go +++ b/taprpc/universerpc/universe.pb.gw.go @@ -1369,6 +1369,42 @@ func local_request_Universe_SyncUniverse_0(ctx context.Context, marshaler runtim } +var ( + filter_Universe_SyncDelta_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_Universe_SyncDelta_0(ctx context.Context, marshaler runtime.Marshaler, client UniverseClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SyncDeltaRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Universe_SyncDelta_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.SyncDelta(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Universe_SyncDelta_0(ctx context.Context, marshaler runtime.Marshaler, server UniverseServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SyncDeltaRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Universe_SyncDelta_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.SyncDelta(ctx, &protoReq) + return msg, metadata, err + +} + func request_Universe_ListFederationServers_0(ctx context.Context, marshaler runtime.Marshaler, client UniverseClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ListFederationServersRequest var metadata runtime.ServerMetadata @@ -2383,6 +2419,31 @@ func RegisterUniverseHandlerServer(ctx context.Context, mux *runtime.ServeMux, s }) + mux.Handle("GET", pattern_Universe_SyncDelta_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/universerpc.Universe/SyncDelta", runtime.WithHTTPPathPattern("/v1/taproot-assets/universe/sync/delta")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Universe_SyncDelta_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Universe_SyncDelta_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_Universe_ListFederationServers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -3145,6 +3206,28 @@ func RegisterUniverseHandlerClient(ctx context.Context, mux *runtime.ServeMux, c }) + mux.Handle("GET", pattern_Universe_SyncDelta_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/universerpc.Universe/SyncDelta", runtime.WithHTTPPathPattern("/v1/taproot-assets/universe/sync/delta")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Universe_SyncDelta_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Universe_SyncDelta_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_Universe_ListFederationServers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -3471,6 +3554,8 @@ var ( pattern_Universe_SyncUniverse_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "taproot-assets", "universe", "sync"}, "")) + pattern_Universe_SyncDelta_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"v1", "taproot-assets", "universe", "sync", "delta"}, "")) + pattern_Universe_ListFederationServers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "taproot-assets", "universe", "federation"}, "")) pattern_Universe_AddFederationServer_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "taproot-assets", "universe", "federation"}, "")) @@ -3535,6 +3620,8 @@ var ( forward_Universe_SyncUniverse_0 = runtime.ForwardResponseMessage + forward_Universe_SyncDelta_0 = runtime.ForwardResponseMessage + forward_Universe_ListFederationServers_0 = runtime.ForwardResponseMessage forward_Universe_AddFederationServer_0 = runtime.ForwardResponseMessage diff --git a/taprpc/universerpc/universe.pb.json.go b/taprpc/universerpc/universe.pb.json.go index d986e7792..3cea26272 100644 --- a/taprpc/universerpc/universe.pb.json.go +++ b/taprpc/universerpc/universe.pb.json.go @@ -321,6 +321,31 @@ func RegisterUniverseJSONCallbacks(registry map[string]func(ctx context.Context, callback(string(respBytes), nil) } + registry["universerpc.Universe.SyncDelta"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &SyncDeltaRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewUniverseClient(conn) + resp, err := client.SyncDelta(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + registry["universerpc.Universe.ListFederationServers"] = func(ctx context.Context, conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { diff --git a/taprpc/universerpc/universe.proto b/taprpc/universerpc/universe.proto index df79266a6..420c39ac5 100644 --- a/taprpc/universerpc/universe.proto +++ b/taprpc/universerpc/universe.proto @@ -107,6 +107,17 @@ service Universe { // TODO(roasebeef): streaming response, so can give feedback? ^ + /* + SyncDelta returns the universe leaves inserted on this server after + the given sequence number, in insertion order. A federation peer + that tracks a per-server cursor can fetch exactly the leaves it + hasn't seen yet, instead of enumerating all leaf keys to compute a + set difference. The response carries the current roots of every + universe touched by the delta so the caller can verify convergence + after applying it. + */ + rpc SyncDelta (SyncDeltaRequest) returns (SyncDeltaResponse); + /* tapcli: `universe federation list` ListFederationServers lists the set of servers that make up the federation of the local Universe server. This servers are used to push out new proofs, @@ -564,6 +575,52 @@ message SyncResponse { repeated SyncedUniverse synced_universes = 1; } +message SyncDeltaRequest { + // Return leaves inserted after this sequence number. Zero means from + // the beginning (bootstrap). + uint64 since_seq = 1; + + // The maximum number of leaves to return in one response. Clamped + // server-side; the server may additionally cut the page short to + // respect its response size budget. Callers should page until a + // response returns fewer items than requested and latest_seq stops + // advancing. + int32 page_size = 2; +} + +message SyncDeltaItem { + // The universe the leaf belongs to. + ID universe_id = 1; + + // The leaf key the leaf is stored at. + AssetKey key = 2; + + // The leaf payload: the asset and its raw issuance/transfer proof. + AssetLeaf leaf = 3; + + // The universe inclusion proof binding the leaf to its universe root + // (one of the roots in the enclosing response). + bytes universe_inclusion_proof = 4; + + // The insertion sequence number of the leaf on the serving universe. + uint64 seq = 5; +} + +message SyncDeltaResponse { + // The leaves inserted after the requested sequence number, in + // insertion order. Leaves whose universes have proof export disabled + // are omitted, but still advance latest_seq. + repeated SyncDeltaItem items = 1; + + // The current roots of every universe that appears in items. + repeated UniverseRoot universe_roots = 2; + + // The high-water sequence mark of this page: the cursor value the + // caller should persist once the page's items are applied and + // verified. + uint64 latest_seq = 3; +} + message UniverseFederationServer { // The host of the federation server, which is used to connect to the // server to push proofs and sync new proofs. diff --git a/taprpc/universerpc/universe.swagger.json b/taprpc/universerpc/universe.swagger.json index 91b471e26..cd3f41b58 100644 --- a/taprpc/universerpc/universe.swagger.json +++ b/taprpc/universerpc/universe.swagger.json @@ -1873,6 +1873,47 @@ "Universe" ] } + }, + "/v1/taproot-assets/universe/sync/delta": { + "get": { + "summary": "SyncDelta returns the universe leaves inserted on this server after\nthe given sequence number, in insertion order. A federation peer\nthat tracks a per-server cursor can fetch exactly the leaves it\nhasn't seen yet, instead of enumerating all leaf keys to compute a\nset difference. The response carries the current roots of every\nuniverse touched by the delta so the caller can verify convergence\nafter applying it.", + "operationId": "Universe_SyncDelta", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/universerpcSyncDeltaResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "since_seq", + "description": "Return leaves inserted after this sequence number. Zero means from\nthe beginning (bootstrap).", + "in": "query", + "required": false, + "type": "string", + "format": "uint64" + }, + { + "name": "page_size", + "description": "The maximum number of leaves to return in one response. Clamped\nserver-side; the server may additionally cut the page short to\nrespect its response size budget. Callers should page until a\nresponse returns fewer items than requested and latest_seq stops\nadvancing.", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + } + ], + "tags": [ + "Universe" + ] + } } }, "definitions": { @@ -3305,6 +3346,59 @@ }, "description": "SupplyLeafKey identifies a supply leaf entry. It contains the components\nused to derive the key, which is computed as a hash of these fields." }, + "universerpcSyncDeltaItem": { + "type": "object", + "properties": { + "universe_id": { + "$ref": "#/definitions/universerpcID", + "description": "The universe the leaf belongs to." + }, + "key": { + "$ref": "#/definitions/universerpcAssetKey", + "description": "The leaf key the leaf is stored at." + }, + "leaf": { + "$ref": "#/definitions/universerpcAssetLeaf", + "description": "The leaf payload: the asset and its raw issuance/transfer proof." + }, + "universe_inclusion_proof": { + "type": "string", + "format": "byte", + "description": "The universe inclusion proof binding the leaf to its universe root\n(one of the roots in the enclosing response)." + }, + "seq": { + "type": "string", + "format": "uint64", + "description": "The insertion sequence number of the leaf on the serving universe." + } + } + }, + "universerpcSyncDeltaResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/universerpcSyncDeltaItem" + }, + "description": "The leaves inserted after the requested sequence number, in\ninsertion order. Leaves whose universes have proof export disabled\nare omitted, but still advance latest_seq." + }, + "universe_roots": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/universerpcUniverseRoot" + }, + "description": "The current roots of every universe that appears in items." + }, + "latest_seq": { + "type": "string", + "format": "uint64", + "description": "The high-water sequence mark of this page: the cursor value the\ncaller should persist once the page's items are applied and\nverified." + } + } + }, "universerpcSyncRequest": { "type": "object", "properties": { diff --git a/taprpc/universerpc/universe.yaml b/taprpc/universerpc/universe.yaml index e8c70f0c6..e610ddd4e 100644 --- a/taprpc/universerpc/universe.yaml +++ b/taprpc/universerpc/universe.yaml @@ -51,6 +51,9 @@ http: post: "/v1/taproot-assets/universe/sync" body: "*" + - selector: universerpc.Universe.SyncDelta + get: "/v1/taproot-assets/universe/sync/delta" + - selector: universerpc.Universe.SetFederationSyncConfig post: "/v1/taproot-assets/universe/sync/config" body: "*" diff --git a/taprpc/universerpc/universe_grpc.pb.go b/taprpc/universerpc/universe_grpc.pb.go index e40412b9d..81dc80cdd 100644 --- a/taprpc/universerpc/universe_grpc.pb.go +++ b/taprpc/universerpc/universe_grpc.pb.go @@ -85,6 +85,14 @@ type UniverseClient interface { // the latest known root for each asset, performing tree based reconciliation // to arrive at a new shared root. SyncUniverse(ctx context.Context, in *SyncRequest, opts ...grpc.CallOption) (*SyncResponse, error) + // SyncDelta returns the universe leaves inserted on this server after + // the given sequence number, in insertion order. A federation peer + // that tracks a per-server cursor can fetch exactly the leaves it + // hasn't seen yet, instead of enumerating all leaf keys to compute a + // set difference. The response carries the current roots of every + // universe touched by the delta so the caller can verify convergence + // after applying it. + SyncDelta(ctx context.Context, in *SyncDeltaRequest, opts ...grpc.CallOption) (*SyncDeltaResponse, error) // tapcli: `universe federation list` // ListFederationServers lists the set of servers that make up the federation // of the local Universe server. This servers are used to push out new proofs, @@ -262,6 +270,15 @@ func (c *universeClient) SyncUniverse(ctx context.Context, in *SyncRequest, opts return out, nil } +func (c *universeClient) SyncDelta(ctx context.Context, in *SyncDeltaRequest, opts ...grpc.CallOption) (*SyncDeltaResponse, error) { + out := new(SyncDeltaResponse) + err := c.cc.Invoke(ctx, "/universerpc.Universe/SyncDelta", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *universeClient) ListFederationServers(ctx context.Context, in *ListFederationServersRequest, opts ...grpc.CallOption) (*ListFederationServersResponse, error) { out := new(ListFederationServersResponse) err := c.cc.Invoke(ctx, "/universerpc.Universe/ListFederationServers", in, out, opts...) @@ -450,6 +467,14 @@ type UniverseServer interface { // the latest known root for each asset, performing tree based reconciliation // to arrive at a new shared root. SyncUniverse(context.Context, *SyncRequest) (*SyncResponse, error) + // SyncDelta returns the universe leaves inserted on this server after + // the given sequence number, in insertion order. A federation peer + // that tracks a per-server cursor can fetch exactly the leaves it + // hasn't seen yet, instead of enumerating all leaf keys to compute a + // set difference. The response carries the current roots of every + // universe touched by the delta so the caller can verify convergence + // after applying it. + SyncDelta(context.Context, *SyncDeltaRequest) (*SyncDeltaResponse, error) // tapcli: `universe federation list` // ListFederationServers lists the set of servers that make up the federation // of the local Universe server. This servers are used to push out new proofs, @@ -552,6 +577,9 @@ func (UnimplementedUniverseServer) Info(context.Context, *InfoRequest) (*InfoRes func (UnimplementedUniverseServer) SyncUniverse(context.Context, *SyncRequest) (*SyncResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SyncUniverse not implemented") } +func (UnimplementedUniverseServer) SyncDelta(context.Context, *SyncDeltaRequest) (*SyncDeltaResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SyncDelta not implemented") +} func (UnimplementedUniverseServer) ListFederationServers(context.Context, *ListFederationServersRequest) (*ListFederationServersResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListFederationServers not implemented") } @@ -820,6 +848,24 @@ func _Universe_SyncUniverse_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _Universe_SyncDelta_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SyncDeltaRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UniverseServer).SyncDelta(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/universerpc.Universe/SyncDelta", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UniverseServer).SyncDelta(ctx, req.(*SyncDeltaRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Universe_ListFederationServers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListFederationServersRequest) if err := dec(in); err != nil { @@ -1109,6 +1155,10 @@ var Universe_ServiceDesc = grpc.ServiceDesc{ MethodName: "SyncUniverse", Handler: _Universe_SyncUniverse_Handler, }, + { + MethodName: "SyncDelta", + Handler: _Universe_SyncDelta_Handler, + }, { MethodName: "ListFederationServers", Handler: _Universe_ListFederationServers_Handler, diff --git a/universe/archive.go b/universe/archive.go index 20c540c81..e198622f7 100644 --- a/universe/archive.go +++ b/universe/archive.go @@ -697,6 +697,75 @@ func (a *Archive) getPrevAssetSnapshot(ctx context.Context, }, nil } +// FetchLeavesSince returns up to limit leaves inserted after sinceSeq +// across all issuance and transfer universes, in insertion order, along +// with the highest sequence number seen. +func (a *Archive) FetchLeavesSince(ctx context.Context, sinceSeq uint64, + limit int32) ([]DeltaLeafItem, uint64, error) { + + log.Tracef("Fetching leaf delta since seq=%v (limit=%v)", sinceSeq, + limit) + + return a.cfg.Multiverse.FetchLeavesSince(ctx, sinceSeq, limit) +} + +// SyncDelta returns the page of leaves inserted after sinceSeq, in +// insertion order, each with the inclusion proof binding it to its +// universe root. +// +// NOTE: proof export gating is an RPC-layer policy; this method serves +// every leaf. This is part of the universe.DeltaEngine interface. +func (a *Archive) SyncDelta(ctx context.Context, sinceSeq uint64, + pageSize int32) (*DeltaPage, error) { + + items, latestSeq, err := a.FetchLeavesSince(ctx, sinceSeq, pageSize) + if err != nil { + return nil, fmt.Errorf("unable to fetch leaf delta: %w", err) + } + + page := &DeltaPage{ + Roots: make(map[IdentifierKey]Root), + LatestSeq: latestSeq, + } + + for i := range items { + item := items[i] + + proofs, err := a.FetchProofLeaf(ctx, item.ID, item.Key) + switch { + // The leaf disappeared between the delta query and the proof + // fetch; the caller's root comparison reconciles whatever + // divergence remains. + case errors.Is(err, ErrNoUniverseProofFound): + log.Warnf("SyncDelta: leaf at seq=%d vanished "+ + "before proof fetch, skipping", item.Seq) + continue + + case err != nil: + return nil, fmt.Errorf("unable to fetch proof leaf "+ + "(seq=%d): %w", item.Seq, err) + } + firstProof := proofs[0] + + item.InclusionProof = firstProof.UniverseInclusionProof + page.Items = append(page.Items, item) + + idKey := item.ID.Key() + if _, ok := page.Roots[idKey]; !ok { + page.Roots[idKey] = Root{ + ID: item.ID, + Node: firstProof.UniverseRoot, + } + } + } + + return page, nil +} + +// A compile-time assertion to ensure Archive satisfies the DeltaEngine +// interface. +var _ DeltaEngine = (*Archive)(nil) + // FetchProofLeaf attempts to fetch a proof leaf for the target leaf key // and given a universe identifier (assetID/groupKey). func (a *Archive) FetchProofLeaf(ctx context.Context, id Identifier, diff --git a/universe/archive_test.go b/universe/archive_test.go index 55bac61da..26cb5a206 100644 --- a/universe/archive_test.go +++ b/universe/archive_test.go @@ -84,6 +84,12 @@ func (m *mockMultiverse) MultiverseRootNode(context.Context, return fn.None[MultiverseRoot](), nil } +func (m *mockMultiverse) FetchLeavesSince(_ context.Context, + sinceSeq uint64, _ int32) ([]DeltaLeafItem, uint64, error) { + + return nil, sinceSeq, nil +} + // mockStorageBackend implements StorageBackend as a no-op. type mockStorageBackend struct{} diff --git a/universe/federation.go b/universe/federation.go index 3e249797a..70e81aa57 100644 --- a/universe/federation.go +++ b/universe/federation.go @@ -2,6 +2,7 @@ package universe import ( "context" + "errors" "fmt" "sync" "time" @@ -44,6 +45,11 @@ type FederationConfig struct { // set of Universe servers. SyncInterval time.Duration + // DisableDeltaSync forces the envoy to use full enumeration sync + // even against servers that support cursor-based delta sync. This + // is a kill switch for the delta sync mechanism. + DisableDeltaSync bool + // ErrChan is the main error channel the custodian will report back // critical errors to the main server. ErrChan chan<- error @@ -193,12 +199,35 @@ func (f *FederationEnvoy) Stop() error { // syncServerState attempts to sync Universe state with the target server. // If the sync is successful (even if no diff is generated), then a new sync -// event will be logged. +// event will be logged. Cursor-based delta sync is attempted first when +// available; full enumeration sync remains the fallback for servers that +// don't support it (and the always-correct path on any delta failure). func (f *FederationEnvoy) syncServerState(ctx context.Context, addr ServerAddr, syncConfigs SyncConfigs) error { log.Infof("Syncing Universe state with server=%s", addr.HostStr()) + deltaSyncer, canDelta := f.cfg.UniverseSyncer.(DeltaSyncer) + if canDelta && !f.cfg.DisableDeltaSync { + done, diffSize, err := f.tryDeltaSync( + ctx, deltaSyncer, addr, syncConfigs, + ) + if err != nil { + // The delta path is an optimization: on failure we + // log and let the enumeration path below have a go. + log.Warnf("Delta sync with server=%v failed, "+ + "falling back to enumeration sync: %v", + addr.HostStr(), err) + } + if done { + if diffSize > 0 { + f.logSyncEvent(addr, diffSize) + } + + return nil + } + } + // Attempt to sync with the remote Universe server, if this errors then // we'll bail out early as something wrong happened. diff, err := f.cfg.UniverseSyncer.SyncUniverse( @@ -212,9 +241,63 @@ func (f *FederationEnvoy) syncServerState(ctx context.Context, return nil } - // If we synced anything from the server, then we'll log that here. + f.logSyncEvent(addr, len(diff)) + + return nil +} + +// tryDeltaSync runs a cursor-based delta sync against the target server, +// persisting the advanced cursor on success. It reports done=false when +// the remote doesn't support delta sync, signaling the caller to use the +// enumeration path instead. +func (f *FederationEnvoy) tryDeltaSync(ctx context.Context, + deltaSyncer DeltaSyncer, addr ServerAddr, + syncConfigs SyncConfigs) (bool, int, error) { + + cursor, err := f.cfg.FederationDB.FetchSyncCursor(ctx, addr) + if err != nil { + return false, 0, fmt.Errorf("unable to fetch sync cursor: %w", + err) + } + + res, err := deltaSyncer.SyncUniverseDelta( + ctx, addr, cursor, syncConfigs, + ) + switch { + // The remote predates delta sync; the enumeration path takes over. + case errors.Is(err, ErrDeltaUnsupported): + log.Debugf("Server=%v does not support delta sync", + addr.HostStr()) + return false, 0, nil + + case err != nil: + return false, 0, err + } + + // A successful run means every universe the delta touched has been + // verified as converged, so the cursor may be persisted. + if res.NewCursor != cursor { + err := f.cfg.FederationDB.UpsertSyncCursor( + ctx, addr, res.NewCursor, + ) + if err != nil { + return false, 0, fmt.Errorf("unable to persist sync "+ + "cursor: %w", err) + } + } + + log.Infof("Delta sync with server=%v complete: cursor %d -> %d, "+ + "diff_size=%d", addr.HostStr(), cursor, res.NewCursor, + len(res.Diffs)) + + return true, len(res.Diffs), nil +} + +// logSyncEvent records a successful sync with the given server in the +// background. +func (f *FederationEnvoy) logSyncEvent(addr ServerAddr, diffSize int) { log.Infof("Synced new Universe leaves from server=%v, diff_size=%v", - spew.Sdump(addr), len(diff)) + spew.Sdump(addr), diffSize) // Log a new sync event in the background now that we know we were able // to contract the remote server. @@ -230,8 +313,6 @@ func (f *FederationEnvoy) syncServerState(ctx context.Context, log.Warnf("unable to log new sync: %v", err) } }() - - return nil } // pushProofToServer attempts to push out a new proof to the target server. diff --git a/universe/federation_delta_test.go b/universe/federation_delta_test.go new file mode 100644 index 000000000..e3d962328 --- /dev/null +++ b/universe/federation_delta_test.go @@ -0,0 +1,260 @@ +package universe + +import ( + "context" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/require" +) + +// mockFederationDB is a minimal in-memory FederationDB carrying just +// enough state for the envoy's delta sync path: per-server cursors. +type mockFederationDB struct { + mu sync.Mutex + cursors map[string]uint64 +} + +func newMockFederationDB() *mockFederationDB { + return &mockFederationDB{ + cursors: make(map[string]uint64), + } +} + +func (m *mockFederationDB) UniverseServers( + _ context.Context) ([]ServerAddr, error) { + + return nil, nil +} + +func (m *mockFederationDB) AddServers(_ context.Context, + _ ...ServerAddr) error { + + return nil +} + +func (m *mockFederationDB) RemoveServers(_ context.Context, + _ ...ServerAddr) error { + + return nil +} + +func (m *mockFederationDB) LogNewSyncs(_ context.Context, + _ ...ServerAddr) error { + + return nil +} + +func (m *mockFederationDB) UpsertSyncCursor(_ context.Context, + addr ServerAddr, seq uint64) error { + + m.mu.Lock() + defer m.mu.Unlock() + m.cursors[addr.HostStr()] = seq + return nil +} + +func (m *mockFederationDB) FetchSyncCursor(_ context.Context, + addr ServerAddr) (uint64, error) { + + m.mu.Lock() + defer m.mu.Unlock() + return m.cursors[addr.HostStr()], nil +} + +func (m *mockFederationDB) UpsertFederationProofSyncLog(_ context.Context, + _ Identifier, _ LeafKey, _ ServerAddr, _ SyncDirection, + _ ProofSyncStatus, _ bool) (int64, error) { + + return 0, nil +} + +func (m *mockFederationDB) QueryFederationProofSyncLog(_ context.Context, + _ Identifier, _ LeafKey, _ SyncDirection, + _ ProofSyncStatus) ([]*ProofSyncLogEntry, error) { + + return nil, nil +} + +func (m *mockFederationDB) FetchPendingProofsSyncLog(_ context.Context, + _ *SyncDirection) ([]*ProofSyncLogEntry, error) { + + return nil, nil +} + +func (m *mockFederationDB) DeleteProofsSyncLogEntries(_ context.Context, + _ ...ServerAddr) error { + + return nil +} + +func (m *mockFederationDB) QueryFederationSyncConfigs( + _ context.Context) ([]*FedGlobalSyncConfig, []*FedUniSyncConfig, + error) { + + return nil, nil, nil +} + +func (m *mockFederationDB) UpsertFederationSyncConfig(_ context.Context, + _ []*FedGlobalSyncConfig, _ []*FedUniSyncConfig) error { + + return nil +} + +var _ FederationDB = (*mockFederationDB)(nil) + +// countingDeltaSet wraps a memUniverseSet and counts SyncDelta calls, +// so tests can observe which sync path the envoy chose. +type countingDeltaSet struct { + *memUniverseSet + + deltaCalls atomic.Int32 +} + +func (c *countingDeltaSet) SyncDelta(ctx context.Context, sinceSeq uint64, + pageSize int32) (*DeltaPage, error) { + + c.deltaCalls.Add(1) + return c.memUniverseSet.SyncDelta(ctx, sinceSeq, pageSize) +} + +// newDeltaEnvoy builds a federation envoy whose syncer talks to the +// given remote engine and whose local side is a fresh in-memory set. +func newDeltaEnvoy(fedDB FederationDB, remoteFn func() DiffEngine, + disableDelta bool) (*FederationEnvoy, *memUniverseSet) { + + local := newMemUniverseSet() + syncer := newMemSyncer(local, remoteFn, 50) + + envoy := NewFederationEnvoy(FederationConfig{ + FederationDB: fedDB, + UniverseSyncer: syncer, + LocalRegistrar: local, + DisableDeltaSync: disableDelta, + }) + + return envoy, local +} + +// seedRemote populates a remote universe set with a couple of +// universes. +func seedRemote(t *testing.T) *memUniverseSet { + t.Helper() + + remote := newMemUniverseSet() + issuanceID := Identifier{ProofType: ProofTypeIssuance} + issuanceID.AssetID[0] = 1 + transferID := Identifier{ProofType: ProofTypeTransfer} + transferID.AssetID[0] = 2 + + for i := 0; i < 3; i++ { + require.NoError(t, remote.insert( + issuanceID, randomTestLeafKey(t), randomTestLeaf(t), + )) + require.NoError(t, remote.insert( + transferID, randomTestLeafKey(t), randomTestLeaf(t), + )) + } + + return remote +} + +// TestEnvoyDeltaSync pins the envoy's happy path: a delta-capable +// remote is synced via the cursor, which is then persisted, and a +// second pass is an incremental no-op that still advances the cursor +// for newly arrived leaves. +func TestEnvoyDeltaSync(t *testing.T) { + t.Parallel() + + ctx := context.Background() + remote := seedRemote(t) + counting := &countingDeltaSet{memUniverseSet: remote} + fedDB := newMockFederationDB() + + envoy, local := newDeltaEnvoy( + fedDB, func() DiffEngine { return counting }, false, + ) + addr := NewServerAddrFromStr("delta-peer:10029") + + err := envoy.syncServerState(ctx, addr, allowAllConfigs()) + require.NoError(t, err) + require.Positive(t, counting.deltaCalls.Load()) + + requireConverged(t, local, remote) + + cursor, err := fedDB.FetchSyncCursor(ctx, addr) + require.NoError(t, err) + require.Equal(t, remote.maxSeq(), cursor) + + // New activity on the remote: the next tick picks up exactly the + // new leaves and advances the cursor. + newID := Identifier{ProofType: ProofTypeIssuance} + newID.AssetID[0] = 3 + require.NoError(t, remote.insert( + newID, randomTestLeafKey(t), randomTestLeaf(t), + )) + + err = envoy.syncServerState(ctx, addr, allowAllConfigs()) + require.NoError(t, err) + + requireConverged(t, local, remote) + + cursor, err = fedDB.FetchSyncCursor(ctx, addr) + require.NoError(t, err) + require.Equal(t, remote.maxSeq(), cursor) + + require.NoError(t, envoy.Stop()) +} + +// TestEnvoyDeltaSyncUnsupported pins the compatibility fallback: a +// remote without delta support is synced via enumeration, converging +// without ever advancing the cursor. +func TestEnvoyDeltaSyncUnsupported(t *testing.T) { + t.Parallel() + + ctx := context.Background() + remote := seedRemote(t) + fedDB := newMockFederationDB() + + envoy, local := newDeltaEnvoy(fedDB, func() DiffEngine { + return &diffOnlyEngine{inner: remote} + }, false) + addr := NewServerAddrFromStr("legacy-peer:10029") + + err := envoy.syncServerState(ctx, addr, allowAllConfigs()) + require.NoError(t, err) + + requireConverged(t, local, remote) + + cursor, err := fedDB.FetchSyncCursor(ctx, addr) + require.NoError(t, err) + require.Zero(t, cursor) + + require.NoError(t, envoy.Stop()) +} + +// TestEnvoyDeltaSyncKillSwitch pins the config kill switch: with +// DisableDeltaSync set, the envoy uses enumeration sync even though +// both sides support deltas. +func TestEnvoyDeltaSyncKillSwitch(t *testing.T) { + t.Parallel() + + ctx := context.Background() + remote := seedRemote(t) + counting := &countingDeltaSet{memUniverseSet: remote} + fedDB := newMockFederationDB() + + envoy, local := newDeltaEnvoy( + fedDB, func() DiffEngine { return counting }, true, + ) + addr := NewServerAddrFromStr("delta-peer:10029") + + err := envoy.syncServerState(ctx, addr, allowAllConfigs()) + require.NoError(t, err) + require.Zero(t, counting.deltaCalls.Load()) + + requireConverged(t, local, remote) + + require.NoError(t, envoy.Stop()) +} diff --git a/universe/interface.go b/universe/interface.go index cb9cfa57a..c3b6d39a0 100644 --- a/universe/interface.go +++ b/universe/interface.go @@ -526,6 +526,62 @@ type MultiverseLeaf struct { *mssmt.LeafNode } +// ErrDeltaUnsupported is returned by a delta-capable diff engine when +// the serving side does not implement delta sync. Callers must fall +// back to enumeration-based sync via the DiffEngine interface. +var ErrDeltaUnsupported = fmt.Errorf("delta sync not supported by server") + +// DeltaLeafItem is one entry of an insertion-ordered universe leaf +// delta: a leaf, the universe it belongs to, and the sequence number +// under which it was inserted on the serving store. +type DeltaLeafItem struct { + // Seq is the leaf's insertion sequence number. + Seq uint64 + + // ID identifies the universe the leaf belongs to. + ID Identifier + + // Key is the universe leaf key the leaf is stored at. + Key LeafKey + + // Leaf is the leaf payload. + Leaf *Leaf + + // InclusionProof binds the leaf to its universe's root as reported + // in the enclosing DeltaPage. It is populated when the item is + // served by a DeltaEngine and nil at the storage layer. + InclusionProof *mssmt.Proof +} + +// DeltaPage is one page of an insertion-ordered universe leaf delta, as +// served by a delta-capable diff engine. +type DeltaPage struct { + // Items are the delta leaves in insertion order, each carrying the + // inclusion proof that binds it to its universe's root in Roots. + Items []DeltaLeafItem + + // Roots are the current universe roots of every universe that + // appears in Items, keyed by universe identifier. + Roots map[IdentifierKey]Root + + // LatestSeq is the high-water cursor value the caller should + // persist once the page's items have been applied and verified. + // Equal to the requested since-seq when the delta is empty. + LatestSeq uint64 +} + +// DeltaEngine is implemented by diff engines that can serve +// insertion-ordered leaf deltas. Serving sides that don't support delta +// sync return ErrDeltaUnsupported, in which case callers fall back to +// enumeration via DiffEngine. +type DeltaEngine interface { + // SyncDelta returns the page of leaves inserted after sinceSeq, in + // insertion order, along with the roots of the universes the page + // touches. + SyncDelta(ctx context.Context, sinceSeq uint64, + pageSize int32) (*DeltaPage, error) +} + // MultiverseArchive is an interface for tracking the set of known universe // roots. While the StorageBackend interface operates on a single universe, this // interface provides aggregate access across multiple universes. @@ -581,6 +637,14 @@ type MultiverseArchive interface { // proof type. MultiverseRootNode(ctx context.Context, proofType ProofType) (fn.Option[MultiverseRoot], error) + + // FetchLeavesSince returns up to limit leaves inserted after + // sinceSeq across all issuance and transfer universes, in insertion + // order, along with the highest sequence number seen. Rewrites of + // existing leaves keep their sequence number and are invisible + // here; root comparison remains the authority on divergence. + FetchLeavesSince(ctx context.Context, sinceSeq uint64, + limit int32) ([]DeltaLeafItem, uint64, error) } // Registrar is an interface that allows a caller to upsert a proof leaf in a @@ -796,6 +860,17 @@ type Syncer interface { idsToSync ...Identifier) ([]AssetSyncDiff, error) } +// DeltaSyncer is implemented by syncers that can additionally perform +// cursor-based delta sync against delta-capable servers. +type DeltaSyncer interface { + // SyncUniverseDelta attempts a cursor-based delta sync against the + // given host, returning ErrDeltaUnsupported if the remote cannot + // serve deltas. + SyncUniverseDelta(ctx context.Context, host ServerAddr, + sinceSeq uint64, + syncConfigs SyncConfigs) (*DeltaSyncResult, error) +} + // DiffEngine is a Universe diff engine that can be used to compare the state // of two universes and find the set of assets that are different between them. type DiffEngine interface { @@ -842,6 +917,17 @@ type FederationLog interface { // LogNewSyncs logs a new sync event for each server. This can be used // to keep track of the last time we synced with a remote server. LogNewSyncs(ctx context.Context, addrs ...ServerAddr) error + + // UpsertSyncCursor updates the delta sync cursor stored for the + // given server: the highest remote insertion sequence number that + // has been fully applied and verified locally. + UpsertSyncCursor(ctx context.Context, addr ServerAddr, + seq uint64) error + + // FetchSyncCursor returns the delta sync cursor stored for the + // given server. A server that has never delta-synced reports + // cursor zero. + FetchSyncCursor(ctx context.Context, addr ServerAddr) (uint64, error) } // ProofType is an enum that describes the type of proof which can be stored in diff --git a/universe/syncer_delta.go b/universe/syncer_delta.go new file mode 100644 index 000000000..210697ebb --- /dev/null +++ b/universe/syncer_delta.go @@ -0,0 +1,322 @@ +package universe + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/mssmt" +) + +// DeltaSyncResult summarizes a successful delta sync run against a +// single remote server. +type DeltaSyncResult struct { + // NewCursor is the cursor value the caller should persist for the + // server. A successful run implies every universe the delta touched + // ended the run verified — either directly, or via fallback + // enumeration — so the cursor always reflects the final page. + NewCursor uint64 + + // Diffs describes the per-universe changes applied during the run. + Diffs []AssetSyncDiff +} + +// SyncUniverseDelta attempts a cursor-based delta sync against the given +// host: fetch the leaves inserted on the remote after sinceSeq, verify +// each against the remote's claimed universe roots, insert them locally +// in remote insertion order, and then verify convergence per universe by +// comparing the recomputed local root against the remote root. Universes +// that fail verification are demoted to the enumeration-based syncRoot +// path. If the remote does not support delta sync, ErrDeltaUnsupported +// is returned and the caller should use SyncUniverse instead. +// +// The delta is an optimization of enumeration sync, never a semantic +// change: the cursor is an unauthenticated hint, and every acceptance +// decision rests on the same inclusion-proof, chain-verification, and +// root-equality checks the enumeration path uses. +// +// One caveat follows from only examining universes the delta touches: a +// universe with no post-cursor activity is not root-checked, so local +// divergence in a quiet universe (e.g. an administrative delete, or a +// leaf missed under a stale cursor) heals only when that universe next +// gains a leaf, or when a full enumeration sync runs. Callers that need +// a hard bound on divergence should schedule occasional full syncs as +// an audit. +func (s *SimpleSyncer) SyncUniverseDelta(ctx context.Context, + host ServerAddr, sinceSeq uint64, + syncConfigs SyncConfigs) (*DeltaSyncResult, error) { + + log.Infof("Attempting delta sync: host=%v, since_seq=%v", + host.HostStr(), sinceSeq) + + diffEngine, err := s.cfg.NewRemoteDiffEngine(host) + if err != nil { + return nil, fmt.Errorf("unable to create remote diff "+ + "engine: %w", err) + } + defer diffEngine.Close() + + deltaEngine, ok := diffEngine.(DeltaEngine) + if !ok { + return nil, ErrDeltaUnsupported + } + + // Prevent concurrent syncs, mirroring executeSync. The flag also + // acts as the global write-pressure throttle: at most one sync run + // feeds the local registrar at a time. + if !s.isSyncing.CompareAndSwap(false, true) { + return nil, fmt.Errorf("sync is already in progress, please " + + "wait for it to finish") + } + defer func() { + s.isSyncing.Store(false) + }() + + // insertFilter mirrors executeSync's uniIdSyncFilter for a full + // sync: only issuance and transfer universes with insert enabled + // are accepted. + insertFilter := func(id Identifier) bool { + switch id.ProofType { + case ProofTypeIssuance, ProofTypeTransfer: + + case ProofTypeUnspecified, ProofTypeIgnore, ProofTypeBurn, + ProofTypeMintSupply: + + return false + } + + return syncConfigs.IsSyncInsertEnabled(id) + } + + var ( + cursor = sinceSeq + remoteRoots = make(map[IdentifierKey]Root) + tainted = make(map[IdentifierKey]struct{}) + touched = make(map[IdentifierKey]Identifier) + oldLocalRoots = make(map[IdentifierKey]Root) + newLeaves = make(map[IdentifierKey][]*Leaf) + ) + + for { + page, err := deltaEngine.SyncDelta(ctx, cursor, defaultPageSize) + if err != nil { + return nil, fmt.Errorf("unable to fetch delta page "+ + "(since_seq=%d): %w", cursor, err) + } + + // Merge the page roots: later pages carry fresher roots for + // universes that keep growing while we page. + for key, root := range page.Roots { + remoteRoots[key] = root + } + + accepted := make([]*Item, 0, len(page.Items)) + for i := range page.Items { + item := page.Items[i] + idKey := item.ID.Key() + + if !insertFilter(item.ID) { + continue + } + if _, isTainted := tainted[idKey]; isTainted { + continue + } + + // Record the universe's pre-sync local root the first + // time the delta touches it, so the final diff reports + // the actual transition. + if _, seen := touched[idKey]; !seen { + local := s.cfg.LocalDiffEngine + localRoot, err := local.RootNode( + ctx, item.ID, + ) + switch { + // This universe is new to us. + case errors.Is(err, ErrNoUniverseRoot): + + case err != nil: + return nil, fmt.Errorf("unable to "+ + "fetch local root: %w", err) + + default: + oldLocalRoots[idKey] = localRoot + } + + touched[idKey] = item.ID + } + + // Verify the item's inclusion proof against the + // remote's claimed root for its universe. A failure + // taints the universe: its remaining items are + // dropped and it is queued for fallback enumeration. + root, haveRoot := remoteRoots[idKey] + leafProof := &Proof{ + LeafKey: item.Key, + UniverseRoot: root.Node, + UniverseInclusionProof: item.InclusionProof, + Leaf: item.Leaf, + } + if !haveRoot || item.InclusionProof == nil || + !leafProof.VerifyRoot(root.Node) { + + log.Warnf("Delta item (seq=%d) failed "+ + "universe root verification, "+ + "demoting UniverseRoot(%v) to "+ + "enumeration sync", item.Seq, + item.ID.String()) + + tainted[idKey] = struct{}{} + continue + } + + accepted = append(accepted, &Item{ + ID: item.ID, + Key: item.Key, + Leaf: item.Leaf, + }) + } + + // Insert the accepted items in remote insertion order. That + // order respects proof dependencies by construction: the + // remote could only have inserted a leaf after the leaves it + // depends on, so no re-sorting or proof-type partitioning is + // needed here. + err = s.insertDeltaItems(ctx, accepted) + if err != nil { + return nil, err + } + + for _, item := range accepted { + idKey := item.ID.Key() + newLeaves[idKey] = append(newLeaves[idKey], item.Leaf) + } + + // The cursor no longer advancing means the remote has nothing + // further for us. This also covers byte-budget-shortened + // pages, which advance the cursor and simply require another + // round trip. + if page.LatestSeq == cursor { + break + } + cursor = page.LatestSeq + } + + // With the delta applied, verify convergence per touched universe: + // the recomputed local root must equal the freshest remote root the + // pages reported. Mismatches (tainted universes, in-place re-org + // rewrites, journal anomalies) are demoted to enumeration sync. + var ( + diffs []AssetSyncDiff + demoted []Root + ) + for idKey, id := range touched { + remoteRoot, haveRoot := remoteRoots[idKey] + if !haveRoot { + return nil, fmt.Errorf("no remote root reported for "+ + "universe %v", id.String()) + } + + localRoot, err := s.cfg.LocalDiffEngine.RootNode(ctx, id) + if err == nil && mssmt.IsEqualNode(localRoot, remoteRoot) { + // The diff reports the measured local root, so the + // completion report is adequate to what was actually + // attained. + diffs = append(diffs, AssetSyncDiff{ + OldUniverseRoot: oldLocalRoots[idKey], + NewUniverseRoot: localRoot, + NewLeafProofs: newLeaves[idKey], + }) + continue + } + if err != nil && !errors.Is(err, ErrNoUniverseRoot) { + return nil, fmt.Errorf("unable to fetch local "+ + "root: %w", err) + } + + demoted = append(demoted, remoteRoot) + } + + // Fallback: enumeration sync for the demoted universes, issuance + // first, bounded by the usual root concurrency. Any failure here + // fails the whole run, so a successful return always implies every + // touched universe converged. + if len(demoted) > 0 { + log.Infof("Delta sync demoting %d universes to enumeration "+ + "sync", len(demoted)) + + syncDiffs := make(chan AssetSyncDiff, len(demoted)) + sorted := partitionByProofType(demoted) + for _, roots := range [][]Root{ + sorted.Issuance, sorted.Transfer, sorted.Other, + } { + err := s.syncRoots(ctx, roots, diffEngine, syncDiffs) + if err != nil { + return nil, fmt.Errorf("delta fallback "+ + "sync failed: %w", err) + } + } + + diffs = append(diffs, fn.Collect(syncDiffs)...) + } + + result := &DeltaSyncResult{ + NewCursor: cursor, + Diffs: diffs, + } + + // Notify subscribers of the universes whose roots changed. + var events []fn.Event + for idx := range diffs { + diff := diffs[idx] + + rootChange, err := diff.HasRootChanged() + if err != nil { + return nil, fmt.Errorf("unable to determine if root "+ + "has changed: %w", err) + } + + if rootChange { + events = append(events, &SyncDiffEvent{ + timestamp: time.Now().UTC(), + SyncDiff: diff, + }) + } + } + s.eventDistributor.NotifySubscribers(events...) + + log.Infof("Delta sync complete: host=%v, new_cursor=%v, "+ + "universes_synced=%d (%d via fallback)", host.HostStr(), + cursor, len(touched), len(demoted)) + + return result, nil +} + +// insertDeltaItems feeds the given items to the local registrar in +// SyncBatchSize chunks, preserving their order. +func (s *SimpleSyncer) insertDeltaItems(ctx context.Context, + items []*Item) error { + + batchSize := s.cfg.SyncBatchSize + if batchSize <= 0 { + batchSize = 200 + } + + for start := 0; start < len(items); start += batchSize { + end := start + batchSize + if end > len(items) { + end = len(items) + } + + err := s.cfg.LocalRegistrar.UpsertProofLeafBatch( + ctx, items[start:end], + ) + if err != nil { + return fmt.Errorf("unable to register proofs: %w", + err) + } + } + + return nil +} diff --git a/universe/syncer_delta_test.go b/universe/syncer_delta_test.go new file mode 100644 index 000000000..d8bd00701 --- /dev/null +++ b/universe/syncer_delta_test.go @@ -0,0 +1,691 @@ +package universe + +import ( + "bytes" + "context" + "fmt" + "sync" + "testing" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/internal/test" + "github.com/lightninglabs/taproot-assets/mssmt" + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +// memUniverse is a single in-memory universe backed by a real MS-SMT +// tree, so roots and inclusion proofs are genuine. +type memUniverse struct { + id Identifier + tree mssmt.Tree + keys []LeafKey + leaves map[[32]byte]*Leaf +} + +// memUniverseSet implements DiffEngine, DeltaEngine and BatchRegistrar +// over a set of in-memory universes plus an insertion journal. It +// stands in for either side of a sync: the remote (diff/delta engine) +// or the local (diff engine and registrar). +type memUniverseSet struct { + mu sync.Mutex + unis map[IdentifierKey]*memUniverse + uniIDs []Identifier + nextSeq uint64 + journal []DeltaLeafItem +} + +func newMemUniverseSet() *memUniverseSet { + return &memUniverseSet{ + unis: make(map[IdentifierKey]*memUniverse), + } +} + +// insert upserts a leaf. Re-inserting an existing (key, same content) +// pair is a no-op that produces no new journal entry, mirroring the +// production ON CONFLICT semantics. +func (m *memUniverseSet) insert(id Identifier, key LeafKey, + leaf *Leaf) error { + + m.mu.Lock() + defer m.mu.Unlock() + + idKey := id.Key() + uni, ok := m.unis[idKey] + if !ok { + uni = &memUniverse{ + id: id, + tree: mssmt.NewCompactedTree(mssmt.NewDefaultStore()), + leaves: make(map[[32]byte]*Leaf), + } + m.unis[idKey] = uni + m.uniIDs = append(m.uniIDs, id) + } + + uniKey := key.UniverseKey() + if existing, ok := uni.leaves[uniKey]; ok { + if bytes.Equal(existing.RawProof, leaf.RawProof) { + return nil + } + } else { + uni.keys = append(uni.keys, key) + } + uni.leaves[uniKey] = leaf + + _, err := uni.tree.Insert( + context.Background(), uniKey, leaf.SmtLeafNode(), + ) + if err != nil { + return err + } + + m.nextSeq++ + m.journal = append(m.journal, DeltaLeafItem{ + Seq: m.nextSeq, + ID: id, + Key: key, + Leaf: leaf, + }) + + return nil +} + +// rootLocked returns the universe root as a computed branch node, +// mirroring the RPC boundary (which transports only hash and sum). +// Handing out the tree's real linked root node would also make +// spew.Sdump trace statements in the syncer walk the entire empty +// subtree lattice. +func (m *memUniverseSet) rootLocked(uni *memUniverse) (mssmt.Node, error) { + root, err := uni.tree.Root(context.Background()) + if err != nil { + return nil, err + } + + return mssmt.NewComputedBranch(root.NodeHash(), root.NodeSum()), nil +} + +// RootNode returns the root of the given universe. +// +// NOTE: part of the DiffEngine interface. +func (m *memUniverseSet) RootNode(_ context.Context, + id Identifier) (Root, error) { + + m.mu.Lock() + defer m.mu.Unlock() + + uni, ok := m.unis[id.Key()] + if !ok { + return Root{}, ErrNoUniverseRoot + } + + root, err := m.rootLocked(uni) + if err != nil { + return Root{}, err + } + + return Root{ID: id, Node: root}, nil +} + +// RootNodes returns a page of all universe roots. +// +// NOTE: part of the DiffEngine interface. +func (m *memUniverseSet) RootNodes(_ context.Context, + q RootNodesQuery) ([]Root, error) { + + m.mu.Lock() + defer m.mu.Unlock() + + var roots []Root + for i := int(q.Offset); i < len(m.uniIDs); i++ { + if q.Limit > 0 && len(roots) >= int(q.Limit) { + break + } + + id := m.uniIDs[i] + uni := m.unis[id.Key()] + root, err := m.rootLocked(uni) + if err != nil { + return nil, err + } + roots = append(roots, Root{ID: id, Node: root}) + } + + return roots, nil +} + +// UniverseLeafKeys returns a page of the leaf entries of one universe, +// each carrying its canonical leaf node hash. +// +// NOTE: part of the DiffEngine interface. +func (m *memUniverseSet) UniverseLeafKeys(_ context.Context, + q UniverseLeafKeysQuery) ([]LeafEntry, error) { + + m.mu.Lock() + defer m.mu.Unlock() + + uni, ok := m.unis[q.Id.Key()] + if !ok { + return nil, nil + } + + var entries []LeafEntry + for i := int(q.Offset); i < len(uni.keys); i++ { + if q.Limit > 0 && len(entries) >= int(q.Limit) { + break + } + + key := uni.keys[i] + leaf := uni.leaves[key.UniverseKey()] + entries = append(entries, LeafEntry{ + Key: key, + NodeHash: fn.Some( + leaf.SmtLeafNode().NodeHash(), + ), + }) + } + + return entries, nil +} + +// FetchProofLeaf returns the leaf at the given key together with its +// inclusion proof and the universe root. +// +// NOTE: part of the DiffEngine interface. +func (m *memUniverseSet) FetchProofLeaf(_ context.Context, id Identifier, + key LeafKey) ([]*Proof, error) { + + m.mu.Lock() + defer m.mu.Unlock() + + uni, ok := m.unis[id.Key()] + if !ok { + return nil, fmt.Errorf("%w: unknown universe", + ErrNoUniverseProofFound) + } + + leaf, ok := uni.leaves[key.UniverseKey()] + if !ok { + return nil, fmt.Errorf("%w: unknown leaf", + ErrNoUniverseProofFound) + } + + ctx := context.Background() + root, err := m.rootLocked(uni) + if err != nil { + return nil, err + } + inclusionProof, err := uni.tree.MerkleProof(ctx, key.UniverseKey()) + if err != nil { + return nil, err + } + + return []*Proof{{ + LeafKey: key, + UniverseRoot: root, + UniverseInclusionProof: inclusionProof, + Leaf: leaf, + }}, nil +} + +// SyncDelta returns the journal entries after sinceSeq with fresh +// inclusion proofs, plus the roots of the universes they touch. +// +// NOTE: part of the DeltaEngine interface. +func (m *memUniverseSet) SyncDelta(_ context.Context, sinceSeq uint64, + pageSize int32) (*DeltaPage, error) { + + m.mu.Lock() + defer m.mu.Unlock() + + page := &DeltaPage{ + Roots: make(map[IdentifierKey]Root), + LatestSeq: sinceSeq, + } + + ctx := context.Background() + for _, entry := range m.journal { + if entry.Seq <= sinceSeq { + continue + } + if pageSize > 0 && len(page.Items) >= int(pageSize) { + break + } + + uni := m.unis[entry.ID.Key()] + inclusionProof, err := uni.tree.MerkleProof( + ctx, entry.Key.UniverseKey(), + ) + if err != nil { + return nil, err + } + + item := entry + item.InclusionProof = inclusionProof + page.Items = append(page.Items, item) + page.LatestSeq = entry.Seq + + idKey := entry.ID.Key() + if _, ok := page.Roots[idKey]; !ok { + root, err := m.rootLocked(uni) + if err != nil { + return nil, err + } + page.Roots[idKey] = Root{ID: entry.ID, Node: root} + } + } + + return page, nil +} + +// UpsertProofLeaf inserts a single leaf. +// +// NOTE: part of the BatchRegistrar interface. +func (m *memUniverseSet) UpsertProofLeaf(_ context.Context, id Identifier, + key LeafKey, leaf *Leaf) (*Proof, error) { + + if err := m.insert(id, key, leaf); err != nil { + return nil, err + } + + return nil, nil +} + +// UpsertProofLeafBatch inserts a batch of leaves in order. +// +// NOTE: part of the BatchRegistrar interface. +func (m *memUniverseSet) UpsertProofLeafBatch(_ context.Context, + items []*Item) error { + + for _, item := range items { + if err := m.insert(item.ID, item.Key, item.Leaf); err != nil { + return err + } + } + + return nil +} + +func (m *memUniverseSet) Close() error { return nil } + +var ( + _ DiffEngine = (*memUniverseSet)(nil) + _ DeltaEngine = (*memUniverseSet)(nil) + _ BatchRegistrar = (*memUniverseSet)(nil) +) + +// maxSeq returns the current journal high-water mark. +func (m *memUniverseSet) maxSeq() uint64 { + m.mu.Lock() + defer m.mu.Unlock() + return m.nextSeq +} + +// diffOnlyEngine hides the DeltaEngine implementation of the wrapped +// engine, modeling a remote that predates delta sync. +type diffOnlyEngine struct { + inner DiffEngine +} + +func (d *diffOnlyEngine) RootNode(ctx context.Context, + id Identifier) (Root, error) { + + return d.inner.RootNode(ctx, id) +} + +func (d *diffOnlyEngine) RootNodes(ctx context.Context, + q RootNodesQuery) ([]Root, error) { + + return d.inner.RootNodes(ctx, q) +} + +func (d *diffOnlyEngine) UniverseLeafKeys(ctx context.Context, + q UniverseLeafKeysQuery) ([]LeafEntry, error) { + + return d.inner.UniverseLeafKeys(ctx, q) +} + +func (d *diffOnlyEngine) FetchProofLeaf(ctx context.Context, + id Identifier, key LeafKey) ([]*Proof, error) { + + return d.inner.FetchProofLeaf(ctx, id, key) +} + +func (d *diffOnlyEngine) Close() error { return d.inner.Close() } + +// corruptDeltaEngine passes everything through but strips the +// inclusion proofs from delta items of one target universe, modeling a +// remote whose delta cannot be verified for that universe. +type corruptDeltaEngine struct { + *memUniverseSet + + target IdentifierKey +} + +func (c *corruptDeltaEngine) SyncDelta(ctx context.Context, + sinceSeq uint64, pageSize int32) (*DeltaPage, error) { + + page, err := c.memUniverseSet.SyncDelta(ctx, sinceSeq, pageSize) + if err != nil { + return nil, err + } + + for i := range page.Items { + if page.Items[i].ID.Key() == c.target { + page.Items[i].InclusionProof = nil + } + } + + return page, nil +} + +// deltaLeafGen generates a random universe leaf. +var deltaLeafGen = rapid.Custom(func(t *rapid.T) *Leaf { + gen := asset.GenesisGen.Draw(t, "genesis") + a := asset.AssetGen.Draw(t, "asset") + rawProof := rapid.SliceOfN( + rapid.Byte(), 8, 32, + ).Draw(t, "raw_proof") + + return &Leaf{ + GenesisWithGroup: GenesisWithGroup{Genesis: gen}, + RawProof: rawProof, + Asset: &a, + Amt: rapid.Uint64Range(1, 1_000).Draw(t, "amt"), + } +}) + +// deltaUniIDGen generates a random issuance- or transfer-typed +// universe identifier. +var deltaUniIDGen = rapid.Custom(func(t *rapid.T) Identifier { + return Identifier{ + AssetID: asset.AssetIDGen.Draw(t, "asset_id"), + ProofType: rapid.SampledFrom([]ProofType{ + ProofTypeIssuance, ProofTypeTransfer, + }).Draw(t, "proof_type"), + } +}) + +// allowAllConfigs enables global insert and export for both proof +// types. +func allowAllConfigs() SyncConfigs { + return SyncConfigs{ + GlobalSyncConfigs: []*FedGlobalSyncConfig{ + { + ProofType: ProofTypeIssuance, + AllowSyncInsert: true, + AllowSyncExport: true, + }, + { + ProofType: ProofTypeTransfer, + AllowSyncInsert: true, + AllowSyncExport: true, + }, + }, + } +} + +// newMemSyncer builds a SimpleSyncer whose local side is the given +// universe set and whose remote diff engine is served by remoteFn. +func newMemSyncer(local *memUniverseSet, + remoteFn func() DiffEngine, batchSize int) *SimpleSyncer { + + return NewSimpleSyncer(SimpleSyncCfg{ + LocalDiffEngine: local, + LocalRegistrar: local, + NewRemoteDiffEngine: func(_ ServerAddr) (DiffEngine, error) { + return remoteFn(), nil + }, + SyncBatchSize: batchSize, + SyncRootConcurrency: 2, + }) +} + +// requireConverged asserts that the local set holds exactly the same +// universes with exactly the same roots as the remote set. +func requireConverged(t require.TestingT, local, remote *memUniverseSet) { + ctx := context.Background() + + remoteRoots, err := remote.RootNodes(ctx, RootNodesQuery{}) + require.NoError(t, err) + + for _, remoteRoot := range remoteRoots { + localRoot, err := local.RootNode(ctx, remoteRoot.ID) + require.NoError(t, err) + require.True( + t, mssmt.IsEqualNode(localRoot.Node, remoteRoot.Node), + "universe %v diverged", remoteRoot.ID.String(), + ) + } +} + +// TestSyncUniverseDeltaEquivalence is the core invariant of delta sync: +// for any remote population and any honest cursor position (the local +// side holds everything at or below the cursor, and possibly more), +// delta sync with root-check fallback converges the local side to +// exactly the same state as full enumeration sync, and advances the +// cursor to the remote's high-water mark. +func TestSyncUniverseDeltaEquivalence(t *testing.T) { + t.Parallel() + + ctx := context.Background() + cfg := allowAllConfigs() + + rapid.Check(t, func(rt *rapid.T) { + remote := newMemUniverseSet() + + // Populate 1-3 universes with 1-6 leaves each. + numUnis := rapid.IntRange(1, 3).Draw(rt, "num_unis") + for u := 0; u < numUnis; u++ { + id := deltaUniIDGen.Draw(rt, fmt.Sprintf("uni_%d", u)) + numLeaves := rapid.IntRange(1, 6).Draw( + rt, fmt.Sprintf("num_leaves_%d", u), + ) + for l := 0; l < numLeaves; l++ { + key := baseLeafKeyGen.Draw( + rt, fmt.Sprintf("key_%d_%d", u, l), + ) + leaf := deltaLeafGen.Draw( + rt, fmt.Sprintf("leaf_%d_%d", u, l), + ) + require.NoError(rt, remote.insert( + id, key, leaf, + )) + } + } + + // Pick an honest cursor: both locals hold every journal + // entry at or below it, plus a random subset of the later + // ones (modeling re-delivery tolerance). + cursor := rapid.Uint64Range(0, remote.maxSeq()).Draw( + rt, "cursor", + ) + + localDelta := newMemUniverseSet() + localEnum := newMemUniverseSet() + for _, entry := range remote.journal { + have := entry.Seq <= cursor || rapid.Bool().Draw( + rt, fmt.Sprintf("have_%d", entry.Seq), + ) + if !have { + continue + } + + require.NoError(rt, localDelta.insert( + entry.ID, entry.Key, entry.Leaf, + )) + require.NoError(rt, localEnum.insert( + entry.ID, entry.Key, entry.Leaf, + )) + } + + batchSize := rapid.SampledFrom([]int{1, 3, 50}).Draw( + rt, "batch_size", + ) + + // Delta sync one local; enumeration sync the other. + deltaSyncer := newMemSyncer( + localDelta, func() DiffEngine { return remote }, + batchSize, + ) + res, err := deltaSyncer.SyncUniverseDelta( + ctx, ServerAddr{}, cursor, cfg, + ) + require.NoError(rt, err) + require.Equal(rt, remote.maxSeq(), res.NewCursor) + + enumSyncer := newMemSyncer( + localEnum, func() DiffEngine { return remote }, + batchSize, + ) + _, err = enumSyncer.SyncUniverse( + ctx, ServerAddr{}, SyncFull, cfg, + ) + require.NoError(rt, err) + + // Both must converge to the remote state exactly. + requireConverged(rt, localDelta, remote) + requireConverged(rt, localEnum, remote) + }) +} + +// TestSyncUniverseDeltaUnsupported pins the fallback signal: a remote +// engine that does not implement DeltaEngine yields +// ErrDeltaUnsupported. +func TestSyncUniverseDeltaUnsupported(t *testing.T) { + t.Parallel() + + remote := newMemUniverseSet() + local := newMemUniverseSet() + + syncer := newMemSyncer(local, func() DiffEngine { + return &diffOnlyEngine{inner: remote} + }, 50) + + _, err := syncer.SyncUniverseDelta( + context.Background(), ServerAddr{}, 0, allowAllConfigs(), + ) + require.ErrorIs(t, err, ErrDeltaUnsupported) +} + +// TestSyncUniverseDeltaInsertFilter pins that universes with insert +// disabled are skipped without blocking cursor advancement. +func TestSyncUniverseDeltaInsertFilter(t *testing.T) { + t.Parallel() + + ctx := context.Background() + remote := newMemUniverseSet() + + allowedID := Identifier{ProofType: ProofTypeIssuance} + allowedID.AssetID[0] = 1 + blockedID := Identifier{ProofType: ProofTypeIssuance} + blockedID.AssetID[0] = 2 + + insertRandomLeaves := func(id Identifier, n int) { + for i := 0; i < n; i++ { + key := randomTestLeafKey(t) + leaf := randomTestLeaf(t) + require.NoError(t, remote.insert(id, key, leaf)) + } + } + insertRandomLeaves(allowedID, 3) + insertRandomLeaves(blockedID, 2) + insertRandomLeaves(allowedID, 1) + + // Insert is enabled globally for issuance, but disabled + // specifically for the blocked universe. + cfg := allowAllConfigs() + cfg.UniSyncConfigs = []*FedUniSyncConfig{{ + UniverseID: blockedID, + AllowSyncInsert: false, + AllowSyncExport: true, + }} + + local := newMemUniverseSet() + syncer := newMemSyncer( + local, func() DiffEngine { return remote }, 50, + ) + + res, err := syncer.SyncUniverseDelta(ctx, ServerAddr{}, 0, cfg) + require.NoError(t, err) + + // The cursor covers the blocked universe's entries too. + require.Equal(t, remote.maxSeq(), res.NewCursor) + + // The allowed universe converged; the blocked one was never + // created locally. + allowedRoot, err := local.RootNode(ctx, allowedID) + require.NoError(t, err) + remoteRoot, err := remote.RootNode(ctx, allowedID) + require.NoError(t, err) + require.True(t, mssmt.IsEqualNode(allowedRoot.Node, remoteRoot.Node)) + + _, err = local.RootNode(ctx, blockedID) + require.ErrorIs(t, err, ErrNoUniverseRoot) +} + +// TestSyncUniverseDeltaTaintFallback pins the demotion path: when a +// universe's delta items fail inclusion-proof verification, the +// universe is synced via enumeration instead, and the run still +// converges and advances the cursor. +func TestSyncUniverseDeltaTaintFallback(t *testing.T) { + t.Parallel() + + ctx := context.Background() + remote := newMemUniverseSet() + + goodID := Identifier{ProofType: ProofTypeIssuance} + goodID.AssetID[0] = 1 + badID := Identifier{ProofType: ProofTypeTransfer} + badID.AssetID[0] = 2 + + for i := 0; i < 3; i++ { + require.NoError(t, remote.insert( + goodID, randomTestLeafKey(t), randomTestLeaf(t), + )) + require.NoError(t, remote.insert( + badID, randomTestLeafKey(t), randomTestLeaf(t), + )) + } + + local := newMemUniverseSet() + syncer := newMemSyncer(local, func() DiffEngine { + return &corruptDeltaEngine{ + memUniverseSet: remote, + target: badID.Key(), + } + }, 50) + + res, err := syncer.SyncUniverseDelta( + ctx, ServerAddr{}, 0, allowAllConfigs(), + ) + require.NoError(t, err) + require.Equal(t, remote.maxSeq(), res.NewCursor) + + // Both universes converged: the good one via the delta, the bad + // one via fallback enumeration. + requireConverged(t, local, remote) +} + +// randomTestLeafKey returns a random leaf key outside a rapid context. +func randomTestLeafKey(t *testing.T) LeafKey { + t.Helper() + + return BaseLeafKey{ + OutPoint: test.RandOp(t), + ScriptKey: fn.Ptr(asset.NewScriptKey(test.RandPubKey(t))), + } +} + +// randomTestLeaf returns a random leaf outside a rapid context. +func randomTestLeaf(t *testing.T) *Leaf { + t.Helper() + + a := asset.RandAsset(t, asset.Normal) + return &Leaf{ + GenesisWithGroup: GenesisWithGroup{Genesis: a.Genesis}, + RawProof: test.RandBytes(24), + Asset: a, + Amt: a.Amount, + } +}