From c0f757a0b34fe32cf4f272fce22ac02f6bf413b4 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Mon, 4 Sep 2023 15:46:05 +0200 Subject: [PATCH] refactor!: namesys package - move namesys options out of coreiface/options/namesys into namesys directly. - rename options to make them consistent and make sense. - move code around to be in self-explanatory file names. --- coreiface/options/name.go | 6 +- coreiface/options/namesys/opts.go | 131 ------ gateway/blocks_backend.go | 3 +- gateway/utilities_test.go | 17 +- namesys/{dns.go => dns_resolver.go} | 59 ++- namesys/{dns_test.go => dns_resolver_test.go} | 51 +-- namesys/interface.go | 98 ----- namesys/{publisher.go => ipns_publisher.go} | 82 ++-- ...blisher_test.go => ipns_publisher_test.go} | 55 +-- namesys/{routing.go => ipns_resolver.go} | 43 +- ...{resolve_test.go => ipns_resolver_test.go} | 64 +-- namesys/mpns.go | 308 +++++++++++++ namesys/{cache.go => mpns_cache.go} | 6 +- namesys/namesys.go | 403 +++++++----------- namesys/namesys_test.go | 114 ++--- namesys/republisher/repub.go | 46 +- namesys/republisher/repub_test.go | 71 +-- namesys/resolve/resolve.go | 25 +- namesys/tracing.go | 13 - namesys/{base.go => utilities.go} | 37 +- 20 files changed, 747 insertions(+), 885 deletions(-) delete mode 100644 coreiface/options/namesys/opts.go rename namesys/{dns.go => dns_resolver.go} (75%) rename namesys/{dns_test.go => dns_resolver_test.go} (68%) delete mode 100644 namesys/interface.go rename namesys/{publisher.go => ipns_publisher.go} (68%) rename namesys/{publisher_test.go => ipns_publisher_test.go} (79%) rename namesys/{routing.go => ipns_resolver.go} (68%) rename namesys/{resolve_test.go => ipns_resolver_test.go} (75%) create mode 100644 namesys/mpns.go rename namesys/{cache.go => mpns_cache.go} (82%) delete mode 100644 namesys/tracing.go rename namesys/{base.go => utilities.go} (64%) diff --git a/coreiface/options/name.go b/coreiface/options/name.go index 35e78c394..7b4b6a8fd 100644 --- a/coreiface/options/name.go +++ b/coreiface/options/name.go @@ -3,7 +3,7 @@ package options import ( "time" - ropts "github.com/ipfs/boxo/coreiface/options/namesys" + "github.com/ipfs/boxo/namesys" ) const ( @@ -21,7 +21,7 @@ type NamePublishSettings struct { type NameResolveSettings struct { Cache bool - ResolveOpts []ropts.ResolveOpt + ResolveOpts []namesys.ResolveOption } type ( @@ -123,7 +123,7 @@ func (nameOpts) Cache(cache bool) NameResolveOption { } } -func (nameOpts) ResolveOption(opt ropts.ResolveOpt) NameResolveOption { +func (nameOpts) ResolveOption(opt namesys.ResolveOption) NameResolveOption { return func(settings *NameResolveSettings) error { settings.ResolveOpts = append(settings.ResolveOpts, opt) return nil diff --git a/coreiface/options/namesys/opts.go b/coreiface/options/namesys/opts.go deleted file mode 100644 index ed568200b..000000000 --- a/coreiface/options/namesys/opts.go +++ /dev/null @@ -1,131 +0,0 @@ -package nsopts - -import ( - "time" -) - -const ( - // DefaultDepthLimit is the default depth limit used by Resolve. - DefaultDepthLimit = 32 - - // UnlimitedDepth allows infinite recursion in Resolve. You - // probably don't want to use this, but it's here if you absolutely - // trust resolution to eventually complete and can't put an upper - // limit on how many steps it will take. - UnlimitedDepth = 0 - - // DefaultIPNSRecordTTL specifies the time that the record can be cached - // before checking if its validity again. - DefaultIPNSRecordTTL = time.Minute - - // DefaultIPNSRecordEOL specifies the time that the network will cache IPNS - // records after being published. Records should be re-published before this - // interval expires. We use the same default expiration as the DHT. - DefaultIPNSRecordEOL = 48 * time.Hour -) - -// ResolveOpts specifies options for resolving an IPNS path -type ResolveOpts struct { - // Recursion depth limit - Depth uint - // The number of IPNS records to retrieve from the DHT - // (the best record is selected from this set) - DhtRecordCount uint - // The amount of time to wait for DHT records to be fetched - // and verified. A zero value indicates that there is no explicit - // timeout (although there is an implicit timeout due to dial - // timeouts within the DHT) - DhtTimeout time.Duration -} - -// DefaultResolveOpts returns the default options for resolving -// an IPNS path -func DefaultResolveOpts() ResolveOpts { - return ResolveOpts{ - Depth: DefaultDepthLimit, - DhtRecordCount: 16, - DhtTimeout: time.Minute, - } -} - -// ResolveOpt is used to set an option -type ResolveOpt func(*ResolveOpts) - -// Depth is the recursion depth limit -func Depth(depth uint) ResolveOpt { - return func(o *ResolveOpts) { - o.Depth = depth - } -} - -// DhtRecordCount is the number of IPNS records to retrieve from the DHT -func DhtRecordCount(count uint) ResolveOpt { - return func(o *ResolveOpts) { - o.DhtRecordCount = count - } -} - -// DhtTimeout is the amount of time to wait for DHT records to be fetched -// and verified. A zero value indicates that there is no explicit timeout -func DhtTimeout(timeout time.Duration) ResolveOpt { - return func(o *ResolveOpts) { - o.DhtTimeout = timeout - } -} - -// ProcessOpts converts an array of ResolveOpt into a ResolveOpts object -func ProcessOpts(opts []ResolveOpt) ResolveOpts { - rsopts := DefaultResolveOpts() - for _, option := range opts { - option(&rsopts) - } - return rsopts -} - -// PublishOptions specifies options for publishing an IPNS record. -type PublishOptions struct { - EOL time.Time - TTL time.Duration - CompatibleWithV1 bool -} - -// DefaultPublishOptions returns the default options for publishing an IPNS record. -func DefaultPublishOptions() PublishOptions { - return PublishOptions{ - EOL: time.Now().Add(DefaultIPNSRecordEOL), - TTL: DefaultIPNSRecordTTL, - } -} - -// PublishOption is used to set an option for PublishOpts. -type PublishOption func(*PublishOptions) - -// PublishWithEOL sets an EOL. -func PublishWithEOL(eol time.Time) PublishOption { - return func(o *PublishOptions) { - o.EOL = eol - } -} - -// PublishWithEOL sets a TTL. -func PublishWithTTL(ttl time.Duration) PublishOption { - return func(o *PublishOptions) { - o.TTL = ttl - } -} - -// PublishCompatibleWithV1 sets compatibility with IPNS Records V1. -func PublishCompatibleWithV1(compatible bool) PublishOption { - return func(o *PublishOptions) { - o.CompatibleWithV1 = compatible - } -} - -// ProcessPublishOptions converts an array of PublishOpt into a PublishOpts object. -func ProcessPublishOptions(opts []PublishOption) PublishOptions { - rsopts := DefaultPublishOptions() - for _, option := range opts { - option(&rsopts) - } - return rsopts -} diff --git a/gateway/blocks_backend.go b/gateway/blocks_backend.go index 208c92062..82c580680 100644 --- a/gateway/blocks_backend.go +++ b/gateway/blocks_backend.go @@ -12,7 +12,6 @@ import ( "github.com/ipfs/boxo/blockservice" blockstore "github.com/ipfs/boxo/blockstore" - nsopts "github.com/ipfs/boxo/coreiface/options/namesys" ifacepath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/fetcher" bsfetcher "github.com/ipfs/boxo/fetcher/impl/blockservice" @@ -608,7 +607,7 @@ func (bb *BlocksBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, func (bb *BlocksBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (ifacepath.Path, error) { if bb.namesys != nil { - p, err := bb.namesys.Resolve(ctx, "/ipns/"+hostname, nsopts.Depth(1)) + p, err := bb.namesys.Resolve(ctx, "/ipns/"+hostname, namesys.ResolveWithDepth(1)) if err == namesys.ErrResolveRecursion { err = nil } diff --git a/gateway/utilities_test.go b/gateway/utilities_test.go index 27ba43a14..d19ec961f 100644 --- a/gateway/utilities_test.go +++ b/gateway/utilities_test.go @@ -13,7 +13,6 @@ import ( "testing" "github.com/ipfs/boxo/blockservice" - nsopts "github.com/ipfs/boxo/coreiface/options/namesys" ipath "github.com/ipfs/boxo/coreiface/path" offline "github.com/ipfs/boxo/exchange/offline" "github.com/ipfs/boxo/files" @@ -54,13 +53,13 @@ func mustDo(t *testing.T, req *http.Request) *http.Response { type mockNamesys map[string]path.Path -func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...nsopts.ResolveOpt) (value path.Path, err error) { - cfg := nsopts.DefaultResolveOpts() +func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...namesys.ResolveOption) (value path.Path, err error) { + cfg := namesys.DefaultResolveOptions() for _, o := range opts { o(&cfg) } depth := cfg.Depth - if depth == nsopts.UnlimitedDepth { + if depth == namesys.UnlimitedDepth { // max uint depth = ^uint(0) } @@ -80,15 +79,15 @@ func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...nsopts.Re return value, nil } -func (m mockNamesys) ResolveAsync(ctx context.Context, name string, opts ...nsopts.ResolveOpt) <-chan namesys.Result { - out := make(chan namesys.Result, 1) +func (m mockNamesys) ResolveAsync(ctx context.Context, name string, opts ...namesys.ResolveOption) <-chan namesys.ResolveResult { + out := make(chan namesys.ResolveResult, 1) v, err := m.Resolve(ctx, name, opts...) - out <- namesys.Result{Path: v, Err: err} + out <- namesys.ResolveResult{Path: v, Err: err} close(out) return out } -func (m mockNamesys) Publish(ctx context.Context, name crypto.PrivKey, value path.Path, opts ...nsopts.PublishOption) error { +func (m mockNamesys) Publish(ctx context.Context, name crypto.PrivKey, value path.Path, opts ...namesys.PublishOption) error { return errors.New("not implemented for mockNamesys") } @@ -163,7 +162,7 @@ func (mb *mockBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, er func (mb *mockBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (ipath.Path, error) { if mb.namesys != nil { - p, err := mb.namesys.Resolve(ctx, "/ipns/"+hostname, nsopts.Depth(1)) + p, err := mb.namesys.Resolve(ctx, "/ipns/"+hostname, namesys.ResolveWithDepth(1)) if err == namesys.ErrResolveRecursion { err = nil } diff --git a/namesys/dns.go b/namesys/dns_resolver.go similarity index 75% rename from namesys/dns.go rename to namesys/dns_resolver.go index 6f846fcda..128946aa9 100644 --- a/namesys/dns.go +++ b/namesys/dns_resolver.go @@ -8,7 +8,6 @@ import ( gpath "path" "strings" - opts "github.com/ipfs/boxo/coreiface/options/namesys" path "github.com/ipfs/boxo/path" dns "github.com/miekg/dns" "go.opentelemetry.io/otel/attribute" @@ -18,44 +17,36 @@ import ( // LookupTXTFunc is a function that lookups TXT record values. type LookupTXTFunc func(ctx context.Context, name string) (txt []string, err error) -// DNSResolver implements a Resolver on DNS domains +// DNSResolver implements [Resolver] on DNS domains. type DNSResolver struct { lookupTXT LookupTXTFunc // TODO: maybe some sort of caching? // cache would need a timeout } +var _ Resolver = &DNSResolver{} + // NewDNSResolver constructs a name resolver using DNS TXT records. func NewDNSResolver(lookup LookupTXTFunc) *DNSResolver { return &DNSResolver{lookupTXT: lookup} } -// Resolve implements Resolver. -func (r *DNSResolver) Resolve(ctx context.Context, name string, options ...opts.ResolveOpt) (path.Path, error) { - ctx, span := StartSpan(ctx, "DNSResolver.Resolve") +func (r *DNSResolver) Resolve(ctx context.Context, name string, options ...ResolveOption) (path.Path, error) { + ctx, span := startSpan(ctx, "DNSResolver.Resolve") defer span.End() - return resolve(ctx, r, name, opts.ProcessOpts(options)) + return resolve(ctx, r, name, ProcessResolveOptions(options)) } -// ResolveAsync implements Resolver. -func (r *DNSResolver) ResolveAsync(ctx context.Context, name string, options ...opts.ResolveOpt) <-chan Result { - ctx, span := StartSpan(ctx, "DNSResolver.ResolveAsync") +func (r *DNSResolver) ResolveAsync(ctx context.Context, name string, options ...ResolveOption) <-chan ResolveResult { + ctx, span := startSpan(ctx, "DNSResolver.ResolveAsync") defer span.End() - return resolveAsync(ctx, r, name, opts.ProcessOpts(options)) -} - -type lookupRes struct { - path path.Path - error error + return resolveAsync(ctx, r, name, ProcessResolveOptions(options)) } -// resolveOnce implements resolver. -// TXT records for a given domain name should contain a b58 -// encoded multihash. -func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult { - ctx, span := StartSpan(ctx, "DNSResolver.ResolveOnceAsync") +func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options ResolveOptions) <-chan onceResult { + ctx, span := startSpan(ctx, "DNSResolver.ResolveOnceAsync") defer span.End() var fqdn string @@ -76,10 +67,10 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options fqdn = domain + "." } - rootChan := make(chan lookupRes, 1) + rootChan := make(chan ResolveResult, 1) go workDomain(ctx, r, fqdn, rootChan) - subChan := make(chan lookupRes, 1) + subChan := make(chan ResolveResult, 1) go workDomain(ctx, r, "_dnslink."+fqdn, subChan) appendPath := func(p path.Path) (path.Path, error) { @@ -91,7 +82,7 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options go func() { defer close(out) - ctx, span := StartSpan(ctx, "DNSResolver.ResolveOnceAsync.Worker") + ctx, span := startSpan(ctx, "DNSResolver.ResolveOnceAsync.Worker") defer span.End() var rootResErr, subResErr error @@ -102,26 +93,26 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options subChan = nil break } - if subRes.error == nil { - p, err := appendPath(subRes.path) + if subRes.Err == nil { + p, err := appendPath(subRes.Path) emitOnceResult(ctx, out, onceResult{value: p, err: err}) // Return without waiting for rootRes, since this result // (for "_dnslink."+fqdn) takes precedence return } - subResErr = subRes.error + subResErr = subRes.Err case rootRes, ok := <-rootChan: if !ok { rootChan = nil break } - if rootRes.error == nil { - p, err := appendPath(rootRes.path) + if rootRes.Err == nil { + p, err := appendPath(rootRes.Path) emitOnceResult(ctx, out, onceResult{value: p, err: err}) // Do not return here. Wait for subRes so that it is // output last if good, thereby giving subRes precedence. } else { - rootResErr = rootRes.error + rootResErr = rootRes.Err } case <-ctx.Done(): return @@ -144,8 +135,8 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options return out } -func workDomain(ctx context.Context, r *DNSResolver, name string, res chan lookupRes) { - ctx, span := StartSpan(ctx, "DNSResolver.WorkDomain", trace.WithAttributes(attribute.String("Name", name))) +func workDomain(ctx context.Context, r *DNSResolver, name string, res chan ResolveResult) { + ctx, span := startSpan(ctx, "DNSResolver.WorkDomain", trace.WithAttributes(attribute.String("Name", name))) defer span.End() defer close(res) @@ -160,20 +151,20 @@ func workDomain(ctx context.Context, r *DNSResolver, name string, res chan looku } } // Could not look up any text records for name - res <- lookupRes{"", err} + res <- ResolveResult{"", err} return } for _, t := range txt { p, err := parseEntry(t) if err == nil { - res <- lookupRes{p, nil} + res <- ResolveResult{p, nil} return } } // There were no TXT records with a dnslink - res <- lookupRes{"", ErrResolveFailed} + res <- ResolveResult{"", ErrResolveFailed} } func parseEntry(txt string) (path.Path, error) { diff --git a/namesys/dns_test.go b/namesys/dns_resolver_test.go similarity index 68% rename from namesys/dns_test.go rename to namesys/dns_resolver_test.go index a31a53582..177c7a996 100644 --- a/namesys/dns_test.go +++ b/namesys/dns_resolver_test.go @@ -5,7 +5,7 @@ import ( "fmt" "testing" - opts "github.com/ipfs/boxo/coreiface/options/namesys" + "github.com/stretchr/testify/assert" ) type mockDNS struct { @@ -42,19 +42,12 @@ func TestDnsEntryParsing(t *testing.T) { for _, e := range goodEntries { _, err := parseEntry(e) - if err != nil { - t.Log("expected entry to parse correctly!") - t.Log(e) - t.Fatal(err) - } + assert.NoError(t, err) } for _, e := range badEntries { _, err := parseEntry(e) - if err == nil { - t.Log("expected entry parse to fail!") - t.Fatal(err) - } + assert.Error(t, err) } } @@ -145,36 +138,36 @@ func newMockDNS() *mockDNS { func TestDNSResolution(t *testing.T) { mock := newMockDNS() r := &DNSResolver{lookupTXT: mock.lookupTXT} - testResolution(t, r, "multihash.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) - testResolution(t, r, "ipfs.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) - testResolution(t, r, "dipfs.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) - testResolution(t, r, "dns1.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) + testResolution(t, r, "multihash.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) + testResolution(t, r, "ipfs.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) + testResolution(t, r, "dipfs.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) + testResolution(t, r, "dns1.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) testResolution(t, r, "dns1.example.com", 1, "/ipns/ipfs.example.com", ErrResolveRecursion) - testResolution(t, r, "dns2.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) + testResolution(t, r, "dns2.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) testResolution(t, r, "dns2.example.com", 1, "/ipns/dns1.example.com", ErrResolveRecursion) testResolution(t, r, "dns2.example.com", 2, "/ipns/ipfs.example.com", ErrResolveRecursion) - testResolution(t, r, "multi.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) + testResolution(t, r, "multi.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) testResolution(t, r, "multi.example.com", 1, "/ipns/dns1.example.com", ErrResolveRecursion) testResolution(t, r, "multi.example.com", 2, "/ipns/ipfs.example.com", ErrResolveRecursion) - testResolution(t, r, "equals.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/=equals", nil) + testResolution(t, r, "equals.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/=equals", nil) testResolution(t, r, "loop1.example.com", 1, "/ipns/loop2.example.com", ErrResolveRecursion) testResolution(t, r, "loop1.example.com", 2, "/ipns/loop1.example.com", ErrResolveRecursion) testResolution(t, r, "loop1.example.com", 3, "/ipns/loop2.example.com", ErrResolveRecursion) - testResolution(t, r, "loop1.example.com", opts.DefaultDepthLimit, "/ipns/loop1.example.com", ErrResolveRecursion) + testResolution(t, r, "loop1.example.com", DefaultDepthLimit, "/ipns/loop1.example.com", ErrResolveRecursion) testResolution(t, r, "dloop1.example.com", 1, "/ipns/loop2.example.com", ErrResolveRecursion) testResolution(t, r, "dloop1.example.com", 2, "/ipns/loop1.example.com", ErrResolveRecursion) testResolution(t, r, "dloop1.example.com", 3, "/ipns/loop2.example.com", ErrResolveRecursion) - testResolution(t, r, "dloop1.example.com", opts.DefaultDepthLimit, "/ipns/loop1.example.com", ErrResolveRecursion) - testResolution(t, r, "bad.example.com", opts.DefaultDepthLimit, "", ErrResolveFailed) - testResolution(t, r, "withsegment.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment", nil) - testResolution(t, r, "withrecsegment.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/subsub", nil) - testResolution(t, r, "withsegment.example.com/test1", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/test1", nil) - testResolution(t, r, "withrecsegment.example.com/test2", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/subsub/test2", nil) - testResolution(t, r, "withrecsegment.example.com/test3/", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/subsub/test3/", nil) - testResolution(t, r, "withtrailingrec.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/", nil) - testResolution(t, r, "double.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) - testResolution(t, r, "conflict.example.com", opts.DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjE", nil) - testResolution(t, r, "fqdn.example.com.", opts.DefaultDepthLimit, "/ipfs/QmYvMB9yrsSf7RKBghkfwmHJkzJhW2ZgVwq3LxBXXPasFr", nil) + testResolution(t, r, "dloop1.example.com", DefaultDepthLimit, "/ipns/loop1.example.com", ErrResolveRecursion) + testResolution(t, r, "bad.example.com", DefaultDepthLimit, "", ErrResolveFailed) + testResolution(t, r, "withsegment.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment", nil) + testResolution(t, r, "withrecsegment.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/subsub", nil) + testResolution(t, r, "withsegment.example.com/test1", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/test1", nil) + testResolution(t, r, "withrecsegment.example.com/test2", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/subsub/test2", nil) + testResolution(t, r, "withrecsegment.example.com/test3/", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/subsub/test3/", nil) + testResolution(t, r, "withtrailingrec.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/sub/segment/", nil) + testResolution(t, r, "double.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil) + testResolution(t, r, "conflict.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjE", nil) + testResolution(t, r, "fqdn.example.com.", DefaultDepthLimit, "/ipfs/QmYvMB9yrsSf7RKBghkfwmHJkzJhW2ZgVwq3LxBXXPasFr", nil) testResolution(t, r, "en.wikipedia-on-ipfs.org", 2, "/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze", nil) testResolution(t, r, "custom.non-icann.tldextravaganza.", 2, "/ipfs/bafybeieto6mcuvqlechv4iadoqvnffondeiwxc2bcfcewhvpsd2odvbmvm", nil) testResolution(t, r, "singlednslabelshouldbeok", 2, "/ipfs/bafybeih4a6ylafdki6ailjrdvmr7o4fbbeceeeuty4v3qyyouiz5koqlpi", nil) diff --git a/namesys/interface.go b/namesys/interface.go deleted file mode 100644 index 5d50936ee..000000000 --- a/namesys/interface.go +++ /dev/null @@ -1,98 +0,0 @@ -/* -Package namesys implements resolvers and publishers for the IPFS -naming system (IPNS). - -The core of IPFS is an immutable, content-addressable Merkle graph. -That works well for many use cases, but doesn't allow you to answer -questions like "what is Alice's current homepage?". The mutable name -system allows Alice to publish information like: - - The current homepage for alice.example.com is - /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj - -or: - - The current homepage for node - QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy - is - /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj - -The mutable name system also allows users to resolve those references -to find the immutable IPFS object currently referenced by a given -mutable name. - -For command-line bindings to this functionality, see: - - ipfs name - ipfs dns - ipfs resolve -*/ -package namesys - -import ( - "context" - "errors" - - opts "github.com/ipfs/boxo/coreiface/options/namesys" - "github.com/ipfs/boxo/path" - ci "github.com/libp2p/go-libp2p/core/crypto" -) - -// ErrResolveFailed signals an error when attempting to resolve. -var ErrResolveFailed = errors.New("could not resolve name") - -// ErrResolveRecursion signals a recursion-depth limit. -var ErrResolveRecursion = errors.New( - "could not resolve name (recursion limit exceeded)") - -// ErrPublishFailed signals an error when attempting to publish. -var ErrPublishFailed = errors.New("could not publish name") - -// NameSystem represents a cohesive name publishing and resolving system. -// -// Publishing a name is the process of establishing a mapping, a key-value -// pair, according to naming rules and databases. -// -// Resolving a name is the process of looking up the value associated with the -// key (name). -type NameSystem interface { - Resolver - Publisher -} - -// Result is the return type for Resolver.ResolveAsync. -type Result struct { - Path path.Path - Err error -} - -// Resolver is an object capable of resolving names. -type Resolver interface { - // Resolve performs a recursive lookup, returning the dereferenced - // path. For example, if ipfs.io has a DNS TXT record pointing to - // /ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy - // and there is a DHT IPNS entry for - // QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy - // -> /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj - // then - // Resolve(ctx, "/ipns/ipfs.io") - // will resolve both names, returning - // /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj - // - // There is a default depth-limit to avoid infinite recursion. Most - // users will be fine with this default limit, but if you need to - // adjust the limit you can specify it as an option. - Resolve(ctx context.Context, name string, options ...opts.ResolveOpt) (value path.Path, err error) - - // ResolveAsync performs recursive name lookup, like Resolve, but it returns - // entries as they are discovered in the DHT. Each returned result is guaranteed - // to be "better" (which usually means newer) than the previous one. - ResolveAsync(ctx context.Context, name string, options ...opts.ResolveOpt) <-chan Result -} - -// Publisher is an object capable of publishing particular names. -type Publisher interface { - // Publish establishes a name-value mapping. - // TODO make this not PrivKey specific. - Publish(ctx context.Context, name ci.PrivKey, value path.Path, options ...opts.PublishOption) error -} diff --git a/namesys/publisher.go b/namesys/ipns_publisher.go similarity index 68% rename from namesys/publisher.go rename to namesys/ipns_publisher.go index c913b0bbc..690191782 100644 --- a/namesys/publisher.go +++ b/namesys/ipns_publisher.go @@ -6,7 +6,6 @@ import ( "sync" "time" - opts "github.com/ipfs/boxo/coreiface/options/namesys" "github.com/ipfs/boxo/ipns" "github.com/ipfs/boxo/path" ds "github.com/ipfs/go-datastore" @@ -19,40 +18,39 @@ import ( "go.opentelemetry.io/otel/trace" ) -const ipnsPrefix = "/ipns/" - -// IpnsPublisher is capable of publishing and resolving names to the IPFS -// routing system. -type IpnsPublisher struct { +// IPNSPublisher implements [Publisher] for IPNS Records. +type IPNSPublisher struct { routing routing.ValueStore ds ds.Datastore - // Used to ensure we assign IPNS records *sequential* sequence numbers. + // Used to ensure we assign IPNS records sequential sequence numbers. mu sync.Mutex } -// NewIpnsPublisher constructs a publisher for the IPFS Routing name system. -func NewIpnsPublisher(route routing.ValueStore, ds ds.Datastore) *IpnsPublisher { +var _ Publisher = &IPNSPublisher{} + +// NewIPNSResolver constructs a new [IPNSResolver] from a [routing.ValueStore] and +// a [ds.Datastore]. +func NewIPNSPublisher(route routing.ValueStore, ds ds.Datastore) *IPNSPublisher { if ds == nil { panic("nil datastore") } - return &IpnsPublisher{routing: route, ds: ds} + + return &IPNSPublisher{routing: route, ds: ds} } -// Publish implements Publisher. Accepts a keypair and a value, -// and publishes it out to the routing system -func (p *IpnsPublisher) Publish(ctx context.Context, k crypto.PrivKey, value path.Path, options ...opts.PublishOption) error { +func (p *IPNSPublisher) Publish(ctx context.Context, priv crypto.PrivKey, value path.Path, options ...PublishOption) error { log.Debugf("Publish %s", value) - ctx, span := StartSpan(ctx, "IpnsPublisher.Publish", trace.WithAttributes(attribute.String("Value", value.String()))) + ctx, span := startSpan(ctx, "IpnsPublisher.Publish", trace.WithAttributes(attribute.String("Value", value.String()))) defer span.End() - record, err := p.updateRecord(ctx, k, value, options...) + record, err := p.updateRecord(ctx, priv, value, options...) if err != nil { return err } - return PutRecordToRouting(ctx, p.routing, k.GetPublic(), record) + return PublishRecord(ctx, p.routing, priv.GetPublic(), record) } // IpnsDsKey returns a datastore key given an IPNS identifier (peer @@ -66,9 +64,9 @@ func IpnsDsKey(id peer.ID) ds.Key { // // This method will not search the routing system for records published by other // nodes. -func (p *IpnsPublisher) ListPublished(ctx context.Context) (map[peer.ID]*ipns.Record, error) { +func (p *IPNSPublisher) ListPublished(ctx context.Context) (map[peer.ID]*ipns.Record, error) { query, err := p.ds.Query(ctx, dsquery.Query{ - Prefix: ipnsPrefix, + Prefix: ipns.NamespacePrefix, }) if err != nil { return nil, err @@ -91,11 +89,11 @@ func (p *IpnsPublisher) ListPublished(ctx context.Context) (map[peer.ID]*ipns.Re log.Error("found an invalid IPNS entry:", err) continue } - if !strings.HasPrefix(result.Key, ipnsPrefix) { - log.Errorf("datastore query for keys with prefix %s returned a key: %s", ipnsPrefix, result.Key) + if !strings.HasPrefix(result.Key, ipns.NamespacePrefix) { + log.Errorf("datastore query for keys with prefix %s returned a key: %s", ipns.NamespacePrefix, result.Key) continue } - k := result.Key[len(ipnsPrefix):] + k := result.Key[len(ipns.NamespacePrefix):] pid, err := base32.RawStdEncoding.DecodeString(k) if err != nil { log.Errorf("ipns ds key invalid: %s", result.Key) @@ -113,7 +111,7 @@ func (p *IpnsPublisher) ListPublished(ctx context.Context) (map[peer.ID]*ipns.Re // // If `checkRouting` is true and we have no existing record, this method will // check the routing system for any existing records. -func (p *IpnsPublisher) GetPublished(ctx context.Context, id peer.ID, checkRouting bool) (*ipns.Record, error) { +func (p *IPNSPublisher) GetPublished(ctx context.Context, id peer.ID, checkRouting bool) (*ipns.Record, error) { ctx, cancel := context.WithTimeout(ctx, time.Second*30) defer cancel() @@ -142,7 +140,7 @@ func (p *IpnsPublisher) GetPublished(ctx context.Context, id peer.ID, checkRouti return ipns.UnmarshalRecord(value) } -func (p *IpnsPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, value path.Path, options ...opts.PublishOption) (*ipns.Record, error) { +func (p *IPNSPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, value path.Path, options ...PublishOption) (*ipns.Record, error) { id, err := peer.IDFromPrivateKey(k) if err != nil { return nil, err @@ -175,7 +173,7 @@ func (p *IpnsPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, valu } } - opts := opts.ProcessPublishOptions(options) + opts := ProcessPublishOptions(options) // Create record r, err := ipns.NewRecord(k, value, seqno, opts.EOL, opts.TTL, ipns.WithV1Compatibility(opts.CompatibleWithV1)) @@ -199,11 +197,11 @@ func (p *IpnsPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, valu return r, nil } -// PutRecordToRouting publishes the given entry using the provided ValueStore, +// PublishRecord publishes the given entry using the provided ValueStore, // keyed on the ID associated with the provided public key. The public key is // also made available to the routing system so that entries can be verified. -func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k crypto.PubKey, rec *ipns.Record) error { - ctx, span := StartSpan(ctx, "PutRecordToRouting") +func PublishRecord(ctx context.Context, r routing.ValueStore, k crypto.PubKey, rec *ipns.Record) error { + ctx, span := startSpan(ctx, "PutRecordToRouting") defer span.End() ctx, cancel := context.WithCancel(ctx) @@ -217,7 +215,7 @@ func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k crypto.PubK } go func() { - errs <- PublishEntry(ctx, r, string(ipns.NameFromPeer(id).RoutingKey()), rec) + errs <- PublishIPNSRecord(ctx, r, ipns.NameFromPeer(id), rec) }() // Publish the public key if a public key cannot be extracted from the ID @@ -249,26 +247,26 @@ func waitOnErrChan(ctx context.Context, errs chan error) error { } } -// PublishPublicKey stores the given public key in the ValueStore with the -// given key. -func PublishPublicKey(ctx context.Context, r routing.ValueStore, k string, pubk crypto.PubKey) error { - ctx, span := StartSpan(ctx, "PublishPublicKey", trace.WithAttributes(attribute.String("Key", k))) +// PublishPublicKey stores the given [crypto.PubKey] for the given key in the [routing.ValueStore]. +func PublishPublicKey(ctx context.Context, r routing.ValueStore, key string, pubKey crypto.PubKey) error { + ctx, span := startSpan(ctx, "PublishPublicKey", trace.WithAttributes(attribute.String("Key", key))) defer span.End() - log.Debugf("Storing pubkey at: %s", k) - pkbytes, err := crypto.MarshalPublicKey(pubk) + log.Debugf("Storing pubkey at: %s", key) + bytes, err := crypto.MarshalPublicKey(pubKey) if err != nil { return err } // Store associated public key - return r.PutValue(ctx, k, pkbytes) + return r.PutValue(ctx, key, bytes) } -// PublishEntry stores the given IpnsEntry in the ValueStore with the given -// ipnskey. -func PublishEntry(ctx context.Context, r routing.ValueStore, ipnskey string, rec *ipns.Record) error { - ctx, span := StartSpan(ctx, "PublishEntry", trace.WithAttributes(attribute.String("IPNSKey", ipnskey))) +// PublishIPNSRecord stores the given [ipns.Record] for the given [ipns.Name] in the given [routing.ValueStore]. +func PublishIPNSRecord(ctx context.Context, r routing.ValueStore, name ipns.Name, rec *ipns.Record) error { + routingKey := string(name.RoutingKey()) + + ctx, span := startSpan(ctx, "PublishEntry", trace.WithAttributes(attribute.String("IPNSKey", routingKey))) defer span.End() data, err := ipns.MarshalRecord(rec) @@ -276,12 +274,12 @@ func PublishEntry(ctx context.Context, r routing.ValueStore, ipnskey string, rec return err } - log.Debugf("Storing ipns entry at: %x", ipnskey) + log.Debugf("Storing ipns entry at: %x", routingKey) // Store ipns entry at "/ipns/"+h(pubkey) - return r.PutValue(ctx, ipnskey, data) + return r.PutValue(ctx, routingKey, data) } -// PkKeyForID returns the public key routing key for the given peer ID. +// PkKeyForID returns the public key routing key for the given [peer.ID]. func PkKeyForID(id peer.ID) string { return "/pk/" + string(id) } diff --git a/namesys/publisher_test.go b/namesys/ipns_publisher_test.go similarity index 79% rename from namesys/publisher_test.go rename to namesys/ipns_publisher_test.go index ad975f59a..ceb959f12 100644 --- a/namesys/publisher_test.go +++ b/namesys/ipns_publisher_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/ipfs/boxo/path" + "github.com/stretchr/testify/require" dshelp "github.com/ipfs/boxo/datastore/dshelp" "github.com/ipfs/boxo/ipns" @@ -40,28 +41,16 @@ func (p *identity) PublicKey() ci.PubKey { } func testNamekeyPublisher(t *testing.T, keyType int, expectedErr error, expectedExistence bool) { - // Context ctx := context.Background() - // Private key privKey, pubKey, err := ci.GenerateKeyPairWithReader(keyType, 2048, rand.Reader) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - // ID id, err := peer.IDFromPublicKey(pubKey) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - // Value value := path.Path("ipfs/TESTING") - - // Seqnum - seqnum := uint64(0) - - // Eol + seq := uint64(0) eol := time.Now().Add(24 * time.Hour) // Routing value store @@ -76,33 +65,22 @@ func testNamekeyPublisher(t *testing.T, keyType int, expectedErr error, expected serv := mockrouting.NewServer() r := serv.ClientWithDatastore(context.Background(), &identity{p}, dstore) - rec, err := ipns.NewRecord(privKey, value, seqnum, eol, 0) - if err != nil { - t.Fatal(err) - } + rec, err := ipns.NewRecord(privKey, value, seq, eol, 0) + require.NoError(t, err) - err = PutRecordToRouting(ctx, r, pubKey, rec) - if err != nil { - t.Fatal(err) - } + err = PublishRecord(ctx, r, pubKey, rec) + require.NoError(t, err) // Check for namekey existence in value store namekey := PkKeyForID(id) _, err = r.GetValue(ctx, namekey) - if err != expectedErr { - t.Fatal(err) - } + require.ErrorIs(t, err, expectedErr) // Also check datastore for completeness key := dshelp.NewKeyFromBinary([]byte(namekey)) exists, err := dstore.Has(ctx, key) - if err != nil { - t.Fatal(err) - } - - if exists != expectedExistence { - t.Fatal("Unexpected key existence in datastore") - } + require.NoError(t, err) + require.Equal(t, expectedExistence, exists) } func TestRSAPublisher(t *testing.T) { @@ -122,17 +100,14 @@ func TestAsyncDS(t *testing.T) { Datastore: ds.NewMapDatastore(), syncKeys: make(map[ds.Key]struct{}), } - publisher := NewIpnsPublisher(rt, ds) + publisher := NewIPNSPublisher(rt, ds) ipnsFakeID := testutil.RandIdentityOrFatal(t) ipnsVal, err := path.ParsePath("/ipns/foo.bar") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if err := publisher.Publish(ctx, ipnsFakeID.PrivateKey(), ipnsVal); err != nil { - t.Fatal(err) - } + err = publisher.Publish(ctx, ipnsFakeID.PrivateKey(), ipnsVal) + require.NoError(t, err) ipnsKey := IpnsDsKey(ipnsFakeID.ID()) diff --git a/namesys/routing.go b/namesys/ipns_resolver.go similarity index 68% rename from namesys/routing.go rename to namesys/ipns_resolver.go index 6b706bd92..4f228af19 100644 --- a/namesys/routing.go +++ b/namesys/ipns_resolver.go @@ -5,10 +5,9 @@ import ( "strings" "time" - opts "github.com/ipfs/boxo/coreiface/options/namesys" "github.com/ipfs/boxo/ipns" "github.com/ipfs/boxo/path" - logging "github.com/ipfs/go-log/v2" + dht "github.com/libp2p/go-libp2p-kad-dht" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/routing" @@ -16,42 +15,40 @@ import ( "go.opentelemetry.io/otel/trace" ) -var log = logging.Logger("namesys") - -// IpnsResolver implements NSResolver for the main IPFS SFS-like naming -type IpnsResolver struct { +// IPNSResolver implements [Resolver] for IPNS Records. +type IPNSResolver struct { routing routing.ValueStore } -// NewIpnsResolver constructs a name resolver using the IPFS Routing system -// to implement SFS-like naming on top. -func NewIpnsResolver(route routing.ValueStore) *IpnsResolver { +var _ Resolver = &IPNSResolver{} + +// NewIPNSResolver constructs a new [IPNSResolver] from a [routing.ValueStore]. +func NewIPNSResolver(route routing.ValueStore) *IPNSResolver { if route == nil { panic("attempt to create resolver with nil routing system") } - return &IpnsResolver{ + + return &IPNSResolver{ routing: route, } } -// Resolve implements Resolver. -func (r *IpnsResolver) Resolve(ctx context.Context, name string, options ...opts.ResolveOpt) (path.Path, error) { - ctx, span := StartSpan(ctx, "IpnsResolver.Resolve", trace.WithAttributes(attribute.String("Name", name))) +func (r *IPNSResolver) Resolve(ctx context.Context, name string, options ...ResolveOption) (path.Path, error) { + ctx, span := startSpan(ctx, "IpnsResolver.Resolve", trace.WithAttributes(attribute.String("Name", name))) defer span.End() - return resolve(ctx, r, name, opts.ProcessOpts(options)) + + return resolve(ctx, r, name, ProcessResolveOptions(options)) } -// ResolveAsync implements Resolver. -func (r *IpnsResolver) ResolveAsync(ctx context.Context, name string, options ...opts.ResolveOpt) <-chan Result { - ctx, span := StartSpan(ctx, "IpnsResolver.ResolveAsync", trace.WithAttributes(attribute.String("Name", name))) +func (r *IPNSResolver) ResolveAsync(ctx context.Context, name string, options ...ResolveOption) <-chan ResolveResult { + ctx, span := startSpan(ctx, "IpnsResolver.ResolveAsync", trace.WithAttributes(attribute.String("Name", name))) defer span.End() - return resolveAsync(ctx, r, name, opts.ProcessOpts(options)) + + return resolveAsync(ctx, r, name, ProcessResolveOptions(options)) } -// resolveOnce implements resolver. Uses the IPFS routing system to -// resolve SFS-like names. -func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult { - ctx, span := StartSpan(ctx, "IpnsResolver.ResolveOnceAsync", trace.WithAttributes(attribute.String("Name", name))) +func (r *IPNSResolver) resolveOnceAsync(ctx context.Context, name string, options ResolveOptions) <-chan onceResult { + ctx, span := startSpan(ctx, "IpnsResolver.ResolveOnceAsync", trace.WithAttributes(attribute.String("Name", name))) defer span.End() out := make(chan onceResult, 1) @@ -91,7 +88,7 @@ func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, option go func() { defer cancel() defer close(out) - ctx, span := StartSpan(ctx, "IpnsResolver.ResolveOnceAsync.Worker") + ctx, span := startSpan(ctx, "IpnsResolver.ResolveOnceAsync.Worker") defer span.End() for { diff --git a/namesys/resolve_test.go b/namesys/ipns_resolver_test.go similarity index 75% rename from namesys/resolve_test.go rename to namesys/ipns_resolver_test.go index 3aecdccaf..17c140bef 100644 --- a/namesys/resolve_test.go +++ b/namesys/ipns_resolver_test.go @@ -12,6 +12,7 @@ import ( ds "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" tnet "github.com/libp2p/go-libp2p-testing/net" + "github.com/stretchr/testify/require" ) func TestRoutingResolve(t *testing.T) { @@ -20,33 +21,26 @@ func TestRoutingResolve(t *testing.T) { id := tnet.RandIdentityOrFatal(t) d := serv.ClientWithDatastore(context.Background(), id, dstore) - resolver := NewIpnsResolver(d) - publisher := NewIpnsPublisher(d, dstore) + resolver := NewIPNSResolver(d) + publisher := NewIPNSPublisher(d, dstore) identity := tnet.RandIdentityOrFatal(t) h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN") err := publisher.Publish(context.Background(), identity.PrivateKey(), h) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) res, err := resolver.Resolve(context.Background(), identity.ID().Pretty()) - if err != nil { - t.Fatal(err) - } - - if res != h { - t.Fatal("Got back incorrect value.") - } + require.NoError(t, err) + require.Equal(t, h, res) } func TestPrexistingExpiredRecord(t *testing.T) { dstore := dssync.MutexWrap(ds.NewMapDatastore()) d := mockrouting.NewServer().ClientWithDatastore(context.Background(), tnet.RandIdentityOrFatal(t), dstore) - resolver := NewIpnsResolver(d) - publisher := NewIpnsPublisher(d, dstore) + resolver := NewIPNSResolver(d) + publisher := NewIPNSPublisher(d, dstore) identity := tnet.RandIdentityOrFatal(t) @@ -55,32 +49,25 @@ func TestPrexistingExpiredRecord(t *testing.T) { eol := time.Now().Add(time.Hour * -1) entry, err := ipns.NewRecord(identity.PrivateKey(), h, 0, eol, 0) - if err != nil { - t.Fatal(err) - } - err = PutRecordToRouting(context.Background(), d, identity.PublicKey(), entry) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + + err = PublishRecord(context.Background(), d, identity.PublicKey(), entry) + require.NoError(t, err) // Now, with an old record in the system already, try and publish a new one err = publisher.Publish(context.Background(), identity.PrivateKey(), h) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) err = verifyCanResolve(resolver, identity.ID().Pretty(), h) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } func TestPrexistingRecord(t *testing.T) { dstore := dssync.MutexWrap(ds.NewMapDatastore()) d := mockrouting.NewServer().ClientWithDatastore(context.Background(), tnet.RandIdentityOrFatal(t), dstore) - resolver := NewIpnsResolver(d) - publisher := NewIpnsPublisher(d, dstore) + resolver := NewIPNSResolver(d) + publisher := NewIPNSPublisher(d, dstore) identity := tnet.RandIdentityOrFatal(t) @@ -88,24 +75,17 @@ func TestPrexistingRecord(t *testing.T) { h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN") eol := time.Now().Add(time.Hour) entry, err := ipns.NewRecord(identity.PrivateKey(), h, 0, eol, 0) - if err != nil { - t.Fatal(err) - } - err = PutRecordToRouting(context.Background(), d, identity.PublicKey(), entry) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + + err = PublishRecord(context.Background(), d, identity.PublicKey(), entry) + require.NoError(t, err) // Now, with an old record in the system already, try and publish a new one err = publisher.Publish(context.Background(), identity.PrivateKey(), h) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) err = verifyCanResolve(resolver, identity.ID().Pretty(), h) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } func verifyCanResolve(r Resolver, name string, exp path.Path) error { diff --git a/namesys/mpns.go b/namesys/mpns.go new file mode 100644 index 000000000..c06b9f74e --- /dev/null +++ b/namesys/mpns.go @@ -0,0 +1,308 @@ +package namesys + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + lru "github.com/hashicorp/golang-lru/v2" + "github.com/ipfs/boxo/ipns" + "github.com/ipfs/boxo/path" + "github.com/ipfs/go-cid" + ds "github.com/ipfs/go-datastore" + dssync "github.com/ipfs/go-datastore/sync" + ci "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/routing" + "github.com/miekg/dns" + madns "github.com/multiformats/go-multiaddr-dns" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// nameSys is a multi-protocol [NameSystem] that implements generic IPFS naming. +// It uses several [Resolver]s: +// +// 1. IPFS routing naming: SFS-like PKI names. +// 2. dns domains: resolves using links in DNS TXT records +// +// It can only publish to: 1. IPFS routing naming. +type nameSys struct { + ds ds.Datastore + + dnsResolver, ipnsResolver resolver + ipnsPublisher Publisher + + staticMap map[string]path.Path + cache *lru.Cache[string, any] +} + +var _ NameSystem = &nameSys{} + +type Option func(*nameSys) error + +// WithCache is an option that instructs the name system to use a (LRU) cache of the given size. +func WithCache(size int) Option { + return func(ns *nameSys) error { + if size <= 0 { + return fmt.Errorf("invalid cache size %d; must be > 0", size) + } + + cache, err := lru.New[string, any](size) + if err != nil { + return err + } + + ns.cache = cache + return nil + } +} + +// WithDNSResolver is an option that supplies a custom DNS resolver to use instead +// of the system default. +func WithDNSResolver(rslv madns.BasicResolver) Option { + return func(ns *nameSys) error { + ns.dnsResolver = NewDNSResolver(rslv.LookupTXT) + return nil + } +} + +// WithDatastore is an option that supplies a datastore to use instead of an in-memory map datastore. +// The datastore is used to store published IPNS Records and make them available for querying. +func WithDatastore(ds ds.Datastore) Option { + return func(ns *nameSys) error { + ns.ds = ds + return nil + } +} + +// NewNameSystem constructs an IPFS [NameSystem] based on the given [routing.ValueStore]. +func NewNameSystem(r routing.ValueStore, opts ...Option) (NameSystem, error) { + var staticMap map[string]path.Path + + // Prewarm namesys cache with static records for deterministic tests and debugging. + // Useful for testing things like DNSLink without real DNS lookup. + // Example: + // IPFS_NS_MAP="dnslink-test.example.com:/ipfs/bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am" + if list := os.Getenv("IPFS_NS_MAP"); list != "" { + staticMap = make(map[string]path.Path) + for _, pair := range strings.Split(list, ",") { + mapping := strings.SplitN(pair, ":", 2) + key := mapping[0] + value := path.FromString(mapping[1]) + staticMap[key] = value + } + } + + ns := &nameSys{ + staticMap: staticMap, + } + + for _, opt := range opts { + err := opt(ns) + if err != nil { + return nil, err + } + } + + if ns.ds == nil { + ns.ds = dssync.MutexWrap(ds.NewMapDatastore()) + } + + if ns.dnsResolver == nil { + ns.dnsResolver = NewDNSResolver(madns.DefaultResolver.LookupTXT) + } + + ns.ipnsResolver = NewIPNSResolver(r) + ns.ipnsPublisher = NewIPNSPublisher(r, ns.ds) + + return ns, nil +} + +// Resolve implements Resolver. +func (ns *nameSys) Resolve(ctx context.Context, name string, options ...ResolveOption) (path.Path, error) { + ctx, span := startSpan(ctx, "MPNS.Resolve", trace.WithAttributes(attribute.String("Name", name))) + defer span.End() + + if strings.HasPrefix(name, "/ipfs/") { + return path.ParsePath(name) + } + + if !strings.HasPrefix(name, "/") { + return path.ParsePath("/ipfs/" + name) + } + + return resolve(ctx, ns, name, ProcessResolveOptions(options)) +} + +func (ns *nameSys) ResolveAsync(ctx context.Context, name string, options ...ResolveOption) <-chan ResolveResult { + ctx, span := startSpan(ctx, "MPNS.ResolveAsync", trace.WithAttributes(attribute.String("Name", name))) + defer span.End() + + if strings.HasPrefix(name, "/ipfs/") { + p, err := path.ParsePath(name) + res := make(chan ResolveResult, 1) + res <- ResolveResult{p, err} + close(res) + return res + } + + if !strings.HasPrefix(name, "/") { + p, err := path.ParsePath("/ipfs/" + name) + res := make(chan ResolveResult, 1) + res <- ResolveResult{p, err} + close(res) + return res + } + + return resolveAsync(ctx, ns, name, ProcessResolveOptions(options)) +} + +// resolveOnce implements resolver. +func (ns *nameSys) resolveOnceAsync(ctx context.Context, name string, options ResolveOptions) <-chan onceResult { + ctx, span := startSpan(ctx, "MPNS.ResolveOnceAsync") + defer span.End() + + out := make(chan onceResult, 1) + + if !strings.HasPrefix(name, ipns.NamespacePrefix) { + name = ipns.NamespacePrefix + name + } + segments := strings.SplitN(name, "/", 4) + if len(segments) < 3 || segments[0] != "" { + log.Debugf("invalid name syntax for %s", name) + out <- onceResult{err: ErrResolveFailed} + close(out) + return out + } + + key := segments[2] + + // Resolver selection: + // 1. if it is a PeerID/CID/multihash resolve through "ipns". + // 2. if it is a domain name, resolve through "dns" + + var res resolver + ipnsKey, err := peer.Decode(key) + // CIDs in IPNS are expected to have libp2p-key multicodec + // We ease the transition by returning a more meaningful error with a valid CID + if err != nil { + ipnsCid, cidErr := cid.Decode(key) + if cidErr == nil && ipnsCid.Version() == 1 && ipnsCid.Type() != cid.Libp2pKey { + fixedCid := cid.NewCidV1(cid.Libp2pKey, ipnsCid.Hash()).String() + codecErr := fmt.Errorf("peer ID represented as CIDv1 require libp2p-key multicodec: retry with /ipns/%s", fixedCid) + log.Debugf("RoutingResolver: could not convert public key hash %q to peer ID: %s\n", key, codecErr) + out <- onceResult{err: codecErr} + close(out) + return out + } + } + + cacheKey := key + if err == nil { + cacheKey = string(ipnsKey) + } + + if p, ok := ns.cacheGet(cacheKey); ok { + var err error + if len(segments) > 3 { + p, err = path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[3]) + } + span.SetAttributes(attribute.Bool("CacheHit", true)) + span.RecordError(err) + + out <- onceResult{value: p, err: err} + close(out) + return out + } + span.SetAttributes(attribute.Bool("CacheHit", false)) + + if err == nil { + res = ns.ipnsResolver + } else if _, ok := dns.IsDomainName(key); ok { + res = ns.dnsResolver + } else { + out <- onceResult{err: fmt.Errorf("invalid IPNS root: %q", key)} + close(out) + return out + } + + resCh := res.resolveOnceAsync(ctx, key, options) + var best onceResult + go func() { + defer close(out) + for { + select { + case res, ok := <-resCh: + if !ok { + if best != (onceResult{}) { + ns.cacheSet(cacheKey, best.value, best.ttl) + } + return + } + if res.err == nil { + best = res + } + p := res.value + err := res.err + ttl := res.ttl + + // Attach rest of the path + if len(segments) > 3 { + p, err = path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[3]) + } + + emitOnceResult(ctx, out, onceResult{value: p, ttl: ttl, err: err}) + case <-ctx.Done(): + return + } + } + }() + + return out +} + +func emitOnceResult(ctx context.Context, outCh chan<- onceResult, r onceResult) { + select { + case outCh <- r: + case <-ctx.Done(): + } +} + +// Publish implements Publisher +func (ns *nameSys) Publish(ctx context.Context, name ci.PrivKey, value path.Path, options ...PublishOption) error { + ctx, span := startSpan(ctx, "MPNS.Publish") + defer span.End() + + // This is a bit hacky. We do this because the EOL is based on the current + // time, but also needed in the end of the function. Therefore, we parse + // the options immediately and add an option PublishWithEOL with the EOL + // calculated in this moment. + publishOpts := ProcessPublishOptions(options) + options = append(options, PublishWithEOL(publishOpts.EOL)) + + id, err := peer.IDFromPrivateKey(name) + if err != nil { + span.RecordError(err) + return err + } + span.SetAttributes(attribute.String("ID", id.String())) + if err := ns.ipnsPublisher.Publish(ctx, name, value, options...); err != nil { + // Invalidate the cache. Publishing may _partially_ succeed but + // still return an error. + ns.cacheInvalidate(string(id)) + span.RecordError(err) + return err + } + ttl := DefaultResolverCacheTTL + if publishOpts.TTL >= 0 { + ttl = publishOpts.TTL + } + if ttEOL := time.Until(publishOpts.EOL); ttEOL < ttl { + ttl = ttEOL + } + ns.cacheSet(string(id), value, ttl) + return nil +} diff --git a/namesys/cache.go b/namesys/mpns_cache.go similarity index 82% rename from namesys/cache.go rename to namesys/mpns_cache.go index 8b7f50794..9cd0dcb1d 100644 --- a/namesys/cache.go +++ b/namesys/mpns_cache.go @@ -6,7 +6,7 @@ import ( path "github.com/ipfs/boxo/path" ) -func (ns *mpns) cacheGet(name string) (path.Path, bool) { +func (ns *nameSys) cacheGet(name string) (path.Path, bool) { // existence of optional mapping defined via IPFS_NS_MAP is checked first if ns.staticMap != nil { val, ok := ns.staticMap[name] @@ -39,7 +39,7 @@ func (ns *mpns) cacheGet(name string) (path.Path, bool) { return "", false } -func (ns *mpns) cacheSet(name string, val path.Path, ttl time.Duration) { +func (ns *nameSys) cacheSet(name string, val path.Path, ttl time.Duration) { if ns.cache == nil || ttl <= 0 { return } @@ -49,7 +49,7 @@ func (ns *mpns) cacheSet(name string, val path.Path, ttl time.Duration) { }) } -func (ns *mpns) cacheInvalidate(name string) { +func (ns *nameSys) cacheInvalidate(name string) { if ns.cache == nil { return } diff --git a/namesys/namesys.go b/namesys/namesys.go index df4403570..6be310f83 100644 --- a/namesys/namesys.go +++ b/namesys/namesys.go @@ -14,307 +14,202 @@ package namesys import ( "context" - "fmt" - "os" - "strings" + "errors" "time" - lru "github.com/hashicorp/golang-lru/v2" - opts "github.com/ipfs/boxo/coreiface/options/namesys" "github.com/ipfs/boxo/path" - "github.com/ipfs/go-cid" - ds "github.com/ipfs/go-datastore" - dssync "github.com/ipfs/go-datastore/sync" + logging "github.com/ipfs/go-log/v2" ci "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/core/routing" - "github.com/miekg/dns" - madns "github.com/multiformats/go-multiaddr-dns" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" ) -// mpns (a multi-protocol NameSystem) implements generic IPFS naming. -// -// Uses several Resolvers: -// (a) IPFS routing naming: SFS-like PKI names. -// (b) dns domains: resolves using links in DNS TXT records -// -// It can only publish to: (a) IPFS routing naming. -type mpns struct { - ds ds.Datastore +var log = logging.Logger("namesys") - dnsResolver, ipnsResolver resolver - ipnsPublisher Publisher +var ( + // ErrResolveFailed signals an error when attempting to resolve. + ErrResolveFailed = errors.New("could not resolve name") - staticMap map[string]path.Path - cache *lru.Cache[string, any] -} + // ErrResolveRecursion signals a recursion-depth limit. + ErrResolveRecursion = errors.New("could not resolve name (recursion limit exceeded)") +) -type Option func(*mpns) error +const ( + // DefaultDepthLimit is the default depth limit used by [Resolver]. + DefaultDepthLimit = 32 -// WithCache is an option that instructs the name system to use a (LRU) cache of the given size. -func WithCache(size int) Option { - return func(ns *mpns) error { - if size <= 0 { - return fmt.Errorf("invalid cache size %d; must be > 0", size) - } + // UnlimitedDepth allows infinite recursion in [Resolver]. You probably don't want + // to use this, but it's here if you absolutely trust resolution to eventually + // complete and can't put an upper limit on how many steps it will take. + UnlimitedDepth = 0 - cache, err := lru.New[string, any](size) - if err != nil { - return err - } + // DefaultIPNSRecordTTL specifies the time that the record can be cached before + // checking if its validity again. + DefaultIPNSRecordTTL = time.Minute - ns.cache = cache - return nil - } -} + // DefaultIPNSRecordEOL specifies the time that the network will cache IPNS + // records after being published. Records should be re-published before this + // interval expires. We use the same default expiration as the DHT. + DefaultIPNSRecordEOL = 48 * time.Hour -// WithDNSResolver is an option that supplies a custom DNS resolver to use instead of the system -// default. -func WithDNSResolver(rslv madns.BasicResolver) Option { - return func(ns *mpns) error { - ns.dnsResolver = NewDNSResolver(rslv.LookupTXT) - return nil - } -} + // DefaultResolverCacheTTL defines max TTL of a record placed in [NameSystem] cache. + DefaultResolverCacheTTL = time.Minute +) -// WithDatastore is an option that supplies a datastore to use instead of an in-memory map datastore. The datastore is used to store published IPNS records and make them available for querying. -func WithDatastore(ds ds.Datastore) Option { - return func(ns *mpns) error { - ns.ds = ds - return nil - } +// NameSystem represents a cohesive name publishing and resolving system. +// +// Publishing a name is the process of establishing a mapping, a key-value +// pair, according to naming rules and databases. +// +// Resolving a name is the process of looking up the value associated with the +// key (name). +type NameSystem interface { + Resolver + Publisher } -// NewNameSystem will construct the IPFS naming system based on Routing -func NewNameSystem(r routing.ValueStore, opts ...Option) (NameSystem, error) { - var staticMap map[string]path.Path +// ResolveResult is the return type for [Resolver.ResolveAsync]. +type ResolveResult struct { + Path path.Path + Err error +} - // Prewarm namesys cache with static records for deterministic tests and debugging. - // Useful for testing things like DNSLink without real DNS lookup. - // Example: - // IPFS_NS_MAP="dnslink-test.example.com:/ipfs/bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am" - if list := os.Getenv("IPFS_NS_MAP"); list != "" { - staticMap = make(map[string]path.Path) - for _, pair := range strings.Split(list, ",") { - mapping := strings.SplitN(pair, ":", 2) - key := mapping[0] - value := path.FromString(mapping[1]) - staticMap[key] = value - } - } +// Resolver is an object capable of resolving names. +type Resolver interface { + // Resolve performs a recursive lookup, returning the dereferenced path. For example, + // if example.com has a DNS TXT record pointing to: + // + // /ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy + // + // and there is a DHT IPNS entry for + // + // QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy + // -> /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj + // + // then + // + // Resolve(ctx, "/ipns/ipfs.io") + // + // will resolve both names, returning + // + // /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj + // + // There is a default depth-limit to avoid infinite recursion. Most users will be fine with + // this default limit, but if you need to adjust the limit you can specify it as an option. + Resolve(ctx context.Context, name string, options ...ResolveOption) (value path.Path, err error) + + // ResolveAsync performs recursive name lookup, like Resolve, but it returns entries as + // they are discovered in the DHT. Each returned result is guaranteed to be "better" + // (which usually means newer) than the previous one. + ResolveAsync(ctx context.Context, name string, options ...ResolveOption) <-chan ResolveResult +} - ns := &mpns{ - staticMap: staticMap, - } +// ResolveOptions specifies options for resolving an IPNS Path. +type ResolveOptions struct { + // Depth is the recursion depth limit. + Depth uint - for _, opt := range opts { - err := opt(ns) - if err != nil { - return nil, err - } - } + // DhtRecordCount is the number of IPNS Records to retrieve from the DHT + // (the best record is selected from this set). + DhtRecordCount uint - if ns.ds == nil { - ns.ds = dssync.MutexWrap(ds.NewMapDatastore()) - } + // DhtTimeout is the amount of time to wait for DHT records to be fetched + // and verified. A zero value indicates that there is no explicit timeout + // (although there is an implicit timeout due to dial timeouts within the DHT). + DhtTimeout time.Duration +} - if ns.dnsResolver == nil { - ns.dnsResolver = NewDNSResolver(madns.DefaultResolver.LookupTXT) +// DefaultResolveOptions returns the default options for resolving an IPNS Path. +func DefaultResolveOptions() ResolveOptions { + return ResolveOptions{ + Depth: DefaultDepthLimit, + DhtRecordCount: 16, + DhtTimeout: time.Minute, } - - ns.ipnsResolver = NewIpnsResolver(r) - ns.ipnsPublisher = NewIpnsPublisher(r, ns.ds) - - return ns, nil } -// DefaultResolverCacheTTL defines max ttl of a record placed in namesys cache. -const DefaultResolverCacheTTL = time.Minute - -// Resolve implements Resolver. -func (ns *mpns) Resolve(ctx context.Context, name string, options ...opts.ResolveOpt) (path.Path, error) { - ctx, span := StartSpan(ctx, "MPNS.Resolve", trace.WithAttributes(attribute.String("Name", name))) - defer span.End() - - if strings.HasPrefix(name, "/ipfs/") { - return path.ParsePath(name) - } +// ResolveOption is used to set a resolve option. +type ResolveOption func(*ResolveOptions) - if !strings.HasPrefix(name, "/") { - return path.ParsePath("/ipfs/" + name) +// ResolveWithDepth sets [ResolveOptions.Depth]. +func ResolveWithDepth(depth uint) ResolveOption { + return func(o *ResolveOptions) { + o.Depth = depth } - - return resolve(ctx, ns, name, opts.ProcessOpts(options)) } -func (ns *mpns) ResolveAsync(ctx context.Context, name string, options ...opts.ResolveOpt) <-chan Result { - ctx, span := StartSpan(ctx, "MPNS.ResolveAsync", trace.WithAttributes(attribute.String("Name", name))) - defer span.End() - - if strings.HasPrefix(name, "/ipfs/") { - p, err := path.ParsePath(name) - res := make(chan Result, 1) - res <- Result{p, err} - close(res) - return res +// ResolveWithDhtRecordCount sets [ResolveOptions.DhtRecordCount]. +func ResolveWithDhtRecordCount(count uint) ResolveOption { + return func(o *ResolveOptions) { + o.DhtRecordCount = count } +} - if !strings.HasPrefix(name, "/") { - p, err := path.ParsePath("/ipfs/" + name) - res := make(chan Result, 1) - res <- Result{p, err} - close(res) - return res +// ResolveWithDhtTimeout sets [ResolveOptions.ResolveWithDhtTimeout]. +func ResolveWithDhtTimeout(timeout time.Duration) ResolveOption { + return func(o *ResolveOptions) { + o.DhtTimeout = timeout } - - return resolveAsync(ctx, ns, name, opts.ProcessOpts(options)) } -// resolveOnce implements resolver. -func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult { - ctx, span := StartSpan(ctx, "MPNS.ResolveOnceAsync") - defer span.End() - - out := make(chan onceResult, 1) - - if !strings.HasPrefix(name, ipnsPrefix) { - name = ipnsPrefix + name +// ProcessResolveOptions converts an array of [ResolveOption] into a [ResolveOptions] object. +func ProcessResolveOptions(opts []ResolveOption) ResolveOptions { + resolveOptions := DefaultResolveOptions() + for _, option := range opts { + option(&resolveOptions) } - segments := strings.SplitN(name, "/", 4) - if len(segments) < 3 || segments[0] != "" { - log.Debugf("invalid name syntax for %s", name) - out <- onceResult{err: ErrResolveFailed} - close(out) - return out - } - - key := segments[2] + return resolveOptions +} - // Resolver selection: - // 1. if it is a PeerID/CID/multihash resolve through "ipns". - // 2. if it is a domain name, resolve through "dns" +// Publisher is an object capable of publishing particular names. +type Publisher interface { + // Publish establishes a name-value mapping. + // TODO make this not PrivKey specific. + Publish(ctx context.Context, name ci.PrivKey, value path.Path, options ...PublishOption) error +} - var res resolver - ipnsKey, err := peer.Decode(key) - // CIDs in IPNS are expected to have libp2p-key multicodec - // We ease the transition by returning a more meaningful error with a valid CID - if err != nil { - ipnsCid, cidErr := cid.Decode(key) - if cidErr == nil && ipnsCid.Version() == 1 && ipnsCid.Type() != cid.Libp2pKey { - fixedCid := cid.NewCidV1(cid.Libp2pKey, ipnsCid.Hash()).String() - codecErr := fmt.Errorf("peer ID represented as CIDv1 require libp2p-key multicodec: retry with /ipns/%s", fixedCid) - log.Debugf("RoutingResolver: could not convert public key hash %q to peer ID: %s\n", key, codecErr) - out <- onceResult{err: codecErr} - close(out) - return out - } - } +// PublishOptions specifies options for publishing an IPNS Record. +type PublishOptions struct { + EOL time.Time + TTL time.Duration + CompatibleWithV1 bool +} - cacheKey := key - if err == nil { - cacheKey = string(ipnsKey) +// DefaultPublishOptions returns the default options for publishing an IPNS Record. +func DefaultPublishOptions() PublishOptions { + return PublishOptions{ + EOL: time.Now().Add(DefaultIPNSRecordEOL), + TTL: DefaultIPNSRecordTTL, } +} - if p, ok := ns.cacheGet(cacheKey); ok { - var err error - if len(segments) > 3 { - p, err = path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[3]) - } - span.SetAttributes(attribute.Bool("CacheHit", true)) - span.RecordError(err) +// PublishOption is used to set an option for [PublishOptions]. +type PublishOption func(*PublishOptions) - out <- onceResult{value: p, err: err} - close(out) - return out +// PublishWithEOL sets [PublishOptions.EOL]. +func PublishWithEOL(eol time.Time) PublishOption { + return func(o *PublishOptions) { + o.EOL = eol } - span.SetAttributes(attribute.Bool("CacheHit", false)) +} - if err == nil { - res = ns.ipnsResolver - } else if _, ok := dns.IsDomainName(key); ok { - res = ns.dnsResolver - } else { - out <- onceResult{err: fmt.Errorf("invalid IPNS root: %q", key)} - close(out) - return out +// PublishWithEOL sets [PublishOptions.TTL]. +func PublishWithTTL(ttl time.Duration) PublishOption { + return func(o *PublishOptions) { + o.TTL = ttl } - - resCh := res.resolveOnceAsync(ctx, key, options) - var best onceResult - go func() { - defer close(out) - for { - select { - case res, ok := <-resCh: - if !ok { - if best != (onceResult{}) { - ns.cacheSet(cacheKey, best.value, best.ttl) - } - return - } - if res.err == nil { - best = res - } - p := res.value - err := res.err - ttl := res.ttl - - // Attach rest of the path - if len(segments) > 3 { - p, err = path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[3]) - } - - emitOnceResult(ctx, out, onceResult{value: p, ttl: ttl, err: err}) - case <-ctx.Done(): - return - } - } - }() - - return out } -func emitOnceResult(ctx context.Context, outCh chan<- onceResult, r onceResult) { - select { - case outCh <- r: - case <-ctx.Done(): +// PublishCompatibleWithV1 sets [PublishOptions.CompatibleWithV1]. +func PublishCompatibleWithV1(compatible bool) PublishOption { + return func(o *PublishOptions) { + o.CompatibleWithV1 = compatible } } -// Publish implements Publisher -func (ns *mpns) Publish(ctx context.Context, name ci.PrivKey, value path.Path, options ...opts.PublishOption) error { - ctx, span := StartSpan(ctx, "MPNS.Publish") - defer span.End() - - // This is a bit hacky. We do this because the EOL is based on the current - // time, but also needed in the end of the function. Therefore, we parse - // the options immediately and add an option PublishWithEOL with the EOL - // calculated in this moment. - publishOpts := opts.ProcessPublishOptions(options) - options = append(options, opts.PublishWithEOL(publishOpts.EOL)) - - id, err := peer.IDFromPrivateKey(name) - if err != nil { - span.RecordError(err) - return err - } - span.SetAttributes(attribute.String("ID", id.String())) - if err := ns.ipnsPublisher.Publish(ctx, name, value, options...); err != nil { - // Invalidate the cache. Publishing may _partially_ succeed but - // still return an error. - ns.cacheInvalidate(string(id)) - span.RecordError(err) - return err - } - ttl := DefaultResolverCacheTTL - if publishOpts.TTL >= 0 { - ttl = publishOpts.TTL - } - if ttEOL := time.Until(publishOpts.EOL); ttEOL < ttl { - ttl = ttEOL +// ProcessPublishOptions converts an array of [PublishOption] into a [PublishOptions] object. +func ProcessPublishOptions(opts []PublishOption) PublishOptions { + publishOptions := DefaultPublishOptions() + for _, option := range opts { + option(&publishOptions) } - ns.cacheSet(string(id), value, ttl) - return nil + return publishOptions } diff --git a/namesys/namesys_test.go b/namesys/namesys_test.go index 52fce6794..8a1e5a483 100644 --- a/namesys/namesys_test.go +++ b/namesys/namesys_test.go @@ -2,12 +2,9 @@ package namesys import ( "context" - "errors" - "fmt" "testing" "time" - opts "github.com/ipfs/boxo/coreiface/options/namesys" "github.com/ipfs/boxo/ipns" "github.com/ipfs/boxo/path" offroute "github.com/ipfs/boxo/routing/offline" @@ -17,6 +14,7 @@ import ( ci "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" + "github.com/stretchr/testify/require" ) type mockResolver struct { @@ -25,20 +23,13 @@ type mockResolver struct { func testResolution(t *testing.T, resolver Resolver, name string, depth uint, expected string, expError error) { t.Helper() - p, err := resolver.Resolve(context.Background(), name, opts.Depth(depth)) - if !errors.Is(err, expError) { - t.Fatal(fmt.Errorf( - "expected %s with a depth of %d to have a '%s' error, but got '%s'", - name, depth, expError, err)) - } - if p.String() != expected { - t.Fatal(fmt.Errorf( - "%s with depth %d resolved to %s != %s", - name, depth, p.String(), expected)) - } + p, err := resolver.Resolve(context.Background(), name, ResolveWithDepth(depth)) + + require.ErrorIs(t, err, expError) + require.Equal(t, expected, p.String()) } -func (r *mockResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult { +func (r *mockResolver) resolveOnceAsync(ctx context.Context, name string, options ResolveOptions) <-chan onceResult { p, err := path.ParsePath(r.entries[name]) out := make(chan onceResult, 1) out <- onceResult{value: p, err: err} @@ -68,19 +59,19 @@ func mockResolverTwo() *mockResolver { } func TestNamesysResolution(t *testing.T) { - r := &mpns{ + r := &nameSys{ ipnsResolver: mockResolverOne(), dnsResolver: mockResolverTwo(), } - testResolution(t, r, "Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", opts.DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil) - testResolution(t, r, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", opts.DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil) - testResolution(t, r, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", opts.DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil) + testResolution(t, r, "Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil) + testResolution(t, r, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil) + testResolution(t, r, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil) testResolution(t, r, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", 1, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", ErrResolveRecursion) - testResolution(t, r, "/ipns/ipfs.io", opts.DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil) + testResolution(t, r, "/ipns/ipfs.io", DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil) testResolution(t, r, "/ipns/ipfs.io", 1, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion) testResolution(t, r, "/ipns/ipfs.io", 2, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", ErrResolveRecursion) - testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", opts.DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil) + testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil) testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 1, "/ipns/ipfs.io", ErrResolveRecursion) testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 2, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion) testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 3, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", ErrResolveRecursion) @@ -91,21 +82,16 @@ func TestNamesysResolution(t *testing.T) { func TestPublishWithCache0(t *testing.T) { dst := dssync.MutexWrap(ds.NewMapDatastore()) priv, _, err := ci.GenerateKeyPair(ci.RSA, 2048) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + ps, err := pstoremem.NewPeerstore() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + pid, err := peer.IDFromPrivateKey(priv) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + err = ps.AddPrivKey(pid, priv) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) routing := offroute.NewOfflineRouter(dst, record.NamespacedValidator{ "ipns": ipns.Validator{KeyBook: ps}, @@ -113,39 +99,29 @@ func TestPublishWithCache0(t *testing.T) { }) nsys, err := NewNameSystem(routing, WithDatastore(dst)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // CID is arbitrary. p, err := path.ParsePath("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + err = nsys.Publish(context.Background(), priv, p) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } func TestPublishWithTTL(t *testing.T) { dst := dssync.MutexWrap(ds.NewMapDatastore()) priv, _, err := ci.GenerateKeyPair(ci.RSA, 2048) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + ps, err := pstoremem.NewPeerstore() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + pid, err := peer.IDFromPrivateKey(priv) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + err = ps.AddPrivKey(pid, priv) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) routing := offroute.NewOfflineRouter(dst, record.NamespacedValidator{ "ipns": ipns.Validator{KeyBook: ps}, @@ -153,32 +129,22 @@ func TestPublishWithTTL(t *testing.T) { }) nsys, err := NewNameSystem(routing, WithDatastore(dst), WithCache(128)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // CID is arbitrary. p, err := path.ParsePath("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) ttl := 1 * time.Second eol := time.Now().Add(2 * time.Second) - err = nsys.Publish(context.Background(), priv, p, opts.PublishWithEOL(eol), opts.PublishWithTTL(ttl)) - if err != nil { - t.Fatal(err) - } - ientry, ok := nsys.(*mpns).cache.Get(string(pid)) - if !ok { - t.Fatal("cache get failed") - } + err = nsys.Publish(context.Background(), priv, p, PublishWithEOL(eol), PublishWithTTL(ttl)) + require.NoError(t, err) + + ientry, ok := nsys.(*nameSys).cache.Get(string(pid)) + require.True(t, ok) + entry, ok := ientry.(cacheEntry) - if !ok { - t.Fatal("bad cache item returned") - } - if entry.eol.Sub(eol) > 10*time.Millisecond { - t.Fatalf("bad cache ttl: expected %s, got %s", eol, entry.eol) - } + require.True(t, ok) + require.LessOrEqual(t, entry.eol.Sub(eol), 10*time.Millisecond) } diff --git a/namesys/republisher/repub.go b/namesys/republisher/repub.go index 87200ff5c..6fc87230f 100644 --- a/namesys/republisher/repub.go +++ b/namesys/republisher/repub.go @@ -5,14 +5,16 @@ package republisher import ( "context" "errors" + "fmt" "time" - keystore "github.com/ipfs/boxo/keystore" + "github.com/ipfs/boxo/keystore" "github.com/ipfs/boxo/namesys" "github.com/ipfs/boxo/path" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" - opts "github.com/ipfs/boxo/coreiface/options/namesys" "github.com/ipfs/boxo/ipns" ds "github.com/ipfs/go-datastore" logging "github.com/ipfs/go-log/v2" @@ -22,24 +24,27 @@ import ( "github.com/libp2p/go-libp2p/core/peer" ) -var errNoEntry = errors.New("no previous entry") - -var log = logging.Logger("ipns-repub") +var ( + errNoEntry = errors.New("no previous entry") + log = logging.Logger("ipns/repub") +) -// DefaultRebroadcastInterval is the default interval at which we rebroadcast IPNS records -var DefaultRebroadcastInterval = time.Hour * 4 +var ( + // DefaultRebroadcastInterval is the default interval at which we rebroadcast IPNS records + DefaultRebroadcastInterval = time.Hour * 4 -// InitialRebroadcastDelay is the delay before first broadcasting IPNS records on start -var InitialRebroadcastDelay = time.Minute * 1 + // InitialRebroadcastDelay is the delay before first broadcasting IPNS records on start + InitialRebroadcastDelay = time.Minute * 1 -// FailureRetryInterval is the interval at which we retry IPNS records broadcasts (when they fail) -var FailureRetryInterval = time.Minute * 5 + // FailureRetryInterval is the interval at which we retry IPNS records broadcasts (when they fail) + FailureRetryInterval = time.Minute * 5 +) // DefaultRecordLifetime is the default lifetime for IPNS records const DefaultRecordLifetime = time.Hour * 24 // Republisher facilitates the regular publishing of all the IPNS records -// associated to keys in a Keystore. +// associated to keys in a [keystore.Keystore]. type Republisher struct { ns namesys.Publisher ds ds.Datastore @@ -52,7 +57,7 @@ type Republisher struct { RecordLifetime time.Duration } -// NewRepublisher creates a new Republisher +// NewRepublisher creates a new [Republisher] from the given options. func NewRepublisher(ns namesys.Publisher, ds ds.Datastore, self ic.PrivKey, ks keystore.Keystore) *Republisher { return &Republisher{ ns: ns, @@ -64,8 +69,7 @@ func NewRepublisher(ns namesys.Publisher, ds ds.Datastore, self ic.PrivKey, ks k } } -// Run starts the republisher facility. It can be stopped by stopping the -// provided proc. +// Run starts the republisher facility. It can be stopped by stopping the provided proc. func (rp *Republisher) Run(proc goprocess.Process) { timer := time.NewTimer(InitialRebroadcastDelay) defer timer.Stop() @@ -93,7 +97,7 @@ func (rp *Republisher) Run(proc goprocess.Process) { func (rp *Republisher) republishEntries(p goprocess.Process) error { ctx, cancel := context.WithCancel(gpctx.OnClosingContext(p)) defer cancel() - ctx, span := namesys.StartSpan(ctx, "Republisher.RepublishEntries") + ctx, span := startSpan(ctx, "Republisher.RepublishEntries") defer span.End() // TODO: Use rp.ipns.ListPublished(). We can't currently *do* that @@ -127,7 +131,7 @@ func (rp *Republisher) republishEntries(p goprocess.Process) error { } func (rp *Republisher) republishEntry(ctx context.Context, priv ic.PrivKey) error { - ctx, span := namesys.StartSpan(ctx, "Republisher.RepublishEntry") + ctx, span := startSpan(ctx, "Republisher.RepublishEntry") defer span.End() id, err := peer.IDFromPrivateKey(priv) if err != nil { @@ -165,7 +169,7 @@ func (rp *Republisher) republishEntry(ctx context.Context, priv ic.PrivKey) erro if prevEol.After(eol) { eol = prevEol } - err = rp.ns.Publish(ctx, priv, path.Path(p.String()), opts.PublishWithEOL(eol)) + err = rp.ns.Publish(ctx, priv, path.Path(p.String()), namesys.PublishWithEOL(eol)) span.RecordError(err) return err } @@ -183,3 +187,9 @@ func (rp *Republisher) getLastIPNSRecord(ctx context.Context, id peer.ID) (*ipns return ipns.UnmarshalRecord(val) } + +var tracer = otel.Tracer("boxo/namesys/republisher") + +func startSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return tracer.Start(ctx, fmt.Sprintf("Namesys.%s", name)) +} diff --git a/namesys/republisher/repub_test.go b/namesys/republisher/repub_test.go index d6c7b0d85..bc50f29d7 100644 --- a/namesys/republisher/repub_test.go +++ b/namesys/republisher/repub_test.go @@ -13,8 +13,8 @@ import ( host "github.com/libp2p/go-libp2p/core/host" peer "github.com/libp2p/go-libp2p/core/peer" routing "github.com/libp2p/go-libp2p/core/routing" + "github.com/stretchr/testify/require" - opts "github.com/ipfs/boxo/coreiface/options/namesys" "github.com/ipfs/boxo/ipns" "github.com/ipfs/boxo/path" ds "github.com/ipfs/go-datastore" @@ -47,9 +47,7 @@ func getMockNode(t *testing.T, ctx context.Context) *mockNode { return rt, err }), ) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return &mockNode{ h: h, @@ -72,9 +70,7 @@ func TestRepublish(t *testing.T) { for i := 0; i < 10; i++ { n := getMockNode(t, ctx) ns, err := namesys.NewNameSystem(n.dht, namesys.WithDatastore(n.store)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) nsystems = append(nsystems, ns) nodes = append(nodes, n) @@ -83,16 +79,15 @@ func TestRepublish(t *testing.T) { pinfo := host.InfoFromHost(nodes[0].h) for _, n := range nodes[1:] { - if err := n.h.Connect(ctx, *pinfo); err != nil { - t.Fatal(err) - } + err := n.h.Connect(ctx, *pinfo) + require.NoError(t, err) } // have one node publish a record that is valid for 1 second publisher := nodes[3] p := path.FromString("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") // does not need to be valid - rp := namesys.NewIpnsPublisher(publisher.dht, publisher.store) + rp := namesys.NewIPNSPublisher(publisher.dht, publisher.store) name := "/ipns/" + publisher.id // Retry in case the record expires before we can fetch it. This can @@ -101,10 +96,8 @@ func TestRepublish(t *testing.T) { timeout := time.Second for { expiration = time.Now().Add(time.Second) - err := rp.Publish(ctx, publisher.privKey, p, opts.PublishWithEOL(expiration)) - if err != nil { - t.Fatal(err) - } + err := rp.Publish(ctx, publisher.privKey, p, namesys.PublishWithEOL(expiration)) + require.NoError(t, err) err = verifyResolution(nsystems, name, p) if err == nil { @@ -120,9 +113,8 @@ func TestRepublish(t *testing.T) { // Now wait a second, the records will be invalid and we should fail to resolve time.Sleep(timeout) - if err := verifyResolutionFails(nsystems, name); err != nil { - t.Fatal(err) - } + err := verifyResolutionFails(nsystems, name) + require.NoError(t, err) // The republishers that are contained within the nodes have their timeout set // to 12 hours. Instead of trying to tweak those, we're just going to pretend @@ -138,9 +130,8 @@ func TestRepublish(t *testing.T) { time.Sleep(time.Second * 2) // we should be able to resolve them now - if err := verifyResolution(nsystems, name, p); err != nil { - t.Fatal(err) - } + err = verifyResolution(nsystems, name, p) + require.NoError(t, err) } func TestLongEOLRepublish(t *testing.T) { @@ -154,9 +145,7 @@ func TestLongEOLRepublish(t *testing.T) { for i := 0; i < 10; i++ { n := getMockNode(t, ctx) ns, err := namesys.NewNameSystem(n.dht, namesys.WithDatastore(n.store)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) nsystems = append(nsystems, ns) nodes = append(nodes, n) @@ -165,27 +154,22 @@ func TestLongEOLRepublish(t *testing.T) { pinfo := host.InfoFromHost(nodes[0].h) for _, n := range nodes[1:] { - if err := n.h.Connect(ctx, *pinfo); err != nil { - t.Fatal(err) - } + err := n.h.Connect(ctx, *pinfo) + require.NoError(t, err) } // have one node publish a record that is valid for 1 second publisher := nodes[3] p := path.FromString("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") // does not need to be valid - rp := namesys.NewIpnsPublisher(publisher.dht, publisher.store) + rp := namesys.NewIPNSPublisher(publisher.dht, publisher.store) name := "/ipns/" + publisher.id expiration := time.Now().Add(time.Hour) - err := rp.Publish(ctx, publisher.privKey, p, opts.PublishWithEOL(expiration)) - if err != nil { - t.Fatal(err) - } + err := rp.Publish(ctx, publisher.privKey, p, namesys.PublishWithEOL(expiration)) + require.NoError(t, err) err = verifyResolution(nsystems, name, p) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // The republishers that are contained within the nodes have their timeout set // to 12 hours. Instead of trying to tweak those, we're just going to pretend @@ -201,23 +185,14 @@ func TestLongEOLRepublish(t *testing.T) { time.Sleep(time.Second * 2) err = verifyResolution(nsystems, name, p) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) rec, err := getLastIPNSRecord(ctx, publisher.store, publisher.h.ID()) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) finalEol, err := rec.Validity() - if err != nil { - t.Fatal(err) - } - - if !finalEol.Equal(expiration) { - t.Fatal("expiration time modified") - } + require.NoError(t, err) + require.Equal(t, expiration.UTC(), finalEol.UTC()) } func getLastIPNSRecord(ctx context.Context, dstore ds.Datastore, id peer.ID) (*ipns.Record, error) { diff --git a/namesys/resolve/resolve.go b/namesys/resolve/resolve.go index b2acf0602..09ac1bf83 100644 --- a/namesys/resolve/resolve.go +++ b/namesys/resolve/resolve.go @@ -6,7 +6,9 @@ import ( "fmt" "strings" + "github.com/ipfs/boxo/ipns" "github.com/ipfs/boxo/path" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -15,16 +17,16 @@ import ( // ErrNoNamesys is an explicit error for when an IPFS node doesn't // (yet) have a name system -var ErrNoNamesys = errors.New( - "core/resolve: no Namesys on IpfsNode - can't resolve ipns entry") +var ErrNoNamesys = errors.New("core/resolve: no Namesys on IpfsNode - can't resolve ipns entry") // ResolveIPNS resolves /ipns paths -func ResolveIPNS(ctx context.Context, nsys namesys.NameSystem, p path.Path) (path.Path, error) { - ctx, span := namesys.StartSpan(ctx, "ResolveIPNS", trace.WithAttributes(attribute.String("Path", p.String()))) +func ResolveIPNS(ctx context.Context, ns namesys.NameSystem, p path.Path) (path.Path, error) { + ctx, span := startSpan(ctx, "ResolveIPNS", trace.WithAttributes(attribute.String("Path", p.String()))) defer span.End() - if strings.HasPrefix(p.String(), "/ipns/") { + + if strings.HasPrefix(p.String(), ipns.NamespacePrefix) { // TODO(cryptix): we should be able to query the local cache for the path - if nsys == nil { + if ns == nil { return "", ErrNoNamesys } @@ -41,16 +43,23 @@ func ResolveIPNS(ctx context.Context, nsys namesys.NameSystem, p path.Path) (pat return "", err } - respath, err := nsys.Resolve(ctx, resolvable.String()) + resolvedPath, err := ns.Resolve(ctx, resolvable.String()) if err != nil { return "", err } - segments := append(respath.Segments(), extensions...) + segments := append(resolvedPath.Segments(), extensions...) p, err = path.FromSegments("/", segments...) if err != nil { return "", err } } + return p, nil } + +var tracer = otel.Tracer("boxo/namesys/resolve") + +func startSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return tracer.Start(ctx, fmt.Sprintf("Namesys.%s", name)) +} diff --git a/namesys/tracing.go b/namesys/tracing.go deleted file mode 100644 index 4ef84294a..000000000 --- a/namesys/tracing.go +++ /dev/null @@ -1,13 +0,0 @@ -package namesys - -import ( - "context" - "fmt" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" -) - -func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { - return otel.Tracer("go-namesys").Start(ctx, fmt.Sprintf("Namesys.%s", name)) -} diff --git a/namesys/base.go b/namesys/utilities.go similarity index 64% rename from namesys/base.go rename to namesys/utilities.go index 06b24bedc..7a175c3dd 100644 --- a/namesys/base.go +++ b/namesys/utilities.go @@ -2,11 +2,14 @@ package namesys import ( "context" + "fmt" "strings" "time" - opts "github.com/ipfs/boxo/coreiface/options/namesys" + "github.com/ipfs/boxo/ipns" path "github.com/ipfs/boxo/path" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" ) type onceResult struct { @@ -16,11 +19,11 @@ type onceResult struct { } type resolver interface { - resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult + resolveOnceAsync(ctx context.Context, name string, options ResolveOptions) <-chan onceResult } // resolve is a helper for implementing Resolver.ResolveN using resolveOnce. -func resolve(ctx context.Context, r resolver, name string, options opts.ResolveOpts) (path.Path, error) { +func resolve(ctx context.Context, r resolver, name string, options ResolveOptions) (path.Path, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -39,20 +42,20 @@ func resolve(ctx context.Context, r resolver, name string, options opts.ResolveO return p, err } -func resolveAsync(ctx context.Context, r resolver, name string, options opts.ResolveOpts) <-chan Result { - ctx, span := StartSpan(ctx, "ResolveAsync") +func resolveAsync(ctx context.Context, r resolver, name string, options ResolveOptions) <-chan ResolveResult { + ctx, span := startSpan(ctx, "ResolveAsync") defer span.End() resCh := r.resolveOnceAsync(ctx, name, options) depth := options.Depth - outCh := make(chan Result, 1) + outCh := make(chan ResolveResult, 1) go func() { defer close(outCh) - ctx, span := StartSpan(ctx, "ResolveAsync.Worker") + ctx, span := startSpan(ctx, "ResolveAsync.Worker") defer span.End() - var subCh <-chan Result + var subCh <-chan ResolveResult var cancelSub context.CancelFunc defer func() { if cancelSub != nil { @@ -69,17 +72,17 @@ func resolveAsync(ctx context.Context, r resolver, name string, options opts.Res } if res.err != nil { - emitResult(ctx, outCh, Result{Err: res.err}) + emitResult(ctx, outCh, ResolveResult{Err: res.err}) return } log.Debugf("resolved %s to %s", name, res.value.String()) - if !strings.HasPrefix(res.value.String(), ipnsPrefix) { - emitResult(ctx, outCh, Result{Path: res.value}) + if !strings.HasPrefix(res.value.String(), ipns.NamespacePrefix) { + emitResult(ctx, outCh, ResolveResult{Path: res.value}) break } if depth == 1 { - emitResult(ctx, outCh, Result{Path: res.value, Err: ErrResolveRecursion}) + emitResult(ctx, outCh, ResolveResult{Path: res.value, Err: ErrResolveRecursion}) break } @@ -96,7 +99,7 @@ func resolveAsync(ctx context.Context, r resolver, name string, options opts.Res subCtx, cancelSub = context.WithCancel(ctx) _ = cancelSub - p := strings.TrimPrefix(res.value.String(), ipnsPrefix) + p := strings.TrimPrefix(res.value.String(), ipns.NamespacePrefix) subCh = resolveAsync(subCtx, r, p, subopts) case res, ok := <-subCh: if !ok { @@ -118,9 +121,15 @@ func resolveAsync(ctx context.Context, r resolver, name string, options opts.Res return outCh } -func emitResult(ctx context.Context, outCh chan<- Result, r Result) { +func emitResult(ctx context.Context, outCh chan<- ResolveResult, r ResolveResult) { select { case outCh <- r: case <-ctx.Done(): } } + +var tracer = otel.Tracer("boxo/namesys") + +func startSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return tracer.Start(ctx, fmt.Sprintf("Namesys.%s", name)) +}