feat(ipns): refactored IPNS package with lean records (#339)

This commit was moved from ipfs/boxo@417c5f7d61
This commit is contained in:
Henrique Dias 2023-06-20 14:08:22 +02:00 committed by GitHub
parent 0134124cdd
commit d6464864cb
5 changed files with 109 additions and 234 deletions

View File

@ -5,20 +5,13 @@ import (
"errors"
path "github.com/ipfs/boxo/coreiface/path"
"github.com/ipfs/boxo/ipns"
"github.com/ipfs/boxo/coreiface/options"
)
var ErrResolveFailed = errors.New("could not resolve name")
// IpnsEntry specifies the interface to IpnsEntries
type IpnsEntry interface {
// Name returns IpnsEntry name
Name() string
// Value returns IpnsEntry value
Value() path.Path
}
type IpnsResult struct {
path.Path
Err error
@ -34,7 +27,7 @@ type IpnsResult struct {
// You can use .Key API to list and generate more names and their respective keys.
type NameAPI interface {
// Publish announces new IPNS name
Publish(ctx context.Context, path path.Path, opts ...options.NamePublishOption) (IpnsEntry, error)
Publish(ctx context.Context, path path.Path, opts ...options.NamePublishOption) (ipns.Name, error)
// Resolve attempts to resolve the newest version of the specified name
Resolve(ctx context.Context, name string, opts ...options.NameResolveOption) (path.Path, error)

View File

@ -11,12 +11,11 @@ const (
)
type NamePublishSettings struct {
ValidTime time.Duration
Key string
TTL *time.Duration
AllowOffline bool
ValidTime time.Duration
Key string
TTL *time.Duration
CompatibleWithV1 bool
AllowOffline bool
}
type NameResolveSettings struct {
@ -104,6 +103,15 @@ func (nameOpts) TTL(ttl time.Duration) NamePublishOption {
}
}
// CompatibleWithV1 is an option for [Name.Publish] which specifies if the
// created record should be backwards compatible with V1 IPNS Records.
func (nameOpts) CompatibleWithV1(compatible bool) NamePublishOption {
return func(settings *NamePublishSettings) error {
settings.CompatibleWithV1 = compatible
return nil
}
}
// Cache is an option for Name.Resolve which specifies if cache should be used.
// Default value is true
func (nameOpts) Cache(cache bool) NameResolveOption {

View File

@ -84,8 +84,9 @@ func ProcessOpts(opts []ResolveOpt) ResolveOpts {
// PublishOptions specifies options for publishing an IPNS record.
type PublishOptions struct {
EOL time.Time
TTL time.Duration
EOL time.Time
TTL time.Duration
CompatibleWithV1 bool
}
// DefaultPublishOptions returns the default options for publishing an IPNS record.
@ -113,6 +114,13 @@ func PublishWithTTL(ttl time.Duration) PublishOption {
}
}
// 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()

View File

@ -8,12 +8,12 @@ import (
"testing"
"time"
path "github.com/ipfs/boxo/coreiface/path"
"github.com/ipfs/boxo/files"
coreiface "github.com/ipfs/boxo/coreiface"
opt "github.com/ipfs/boxo/coreiface/options"
path "github.com/ipfs/boxo/coreiface/path"
"github.com/ipfs/boxo/files"
"github.com/ipfs/boxo/ipns"
"github.com/stretchr/testify/require"
)
func (tp *TestSuite) TestName(t *testing.T) {
@ -44,138 +44,68 @@ func (tp *TestSuite) TestPublishResolve(t *testing.T) {
defer cancel()
init := func() (coreiface.CoreAPI, path.Path) {
apis, err := tp.MakeAPISwarm(t, ctx, 5)
if err != nil {
t.Fatal(err)
return nil, nil
}
require.NoError(t, err)
api := apis[0]
p, err := addTestObject(ctx, api)
if err != nil {
t.Fatal(err)
return nil, nil
}
require.NoError(t, err)
return api, p
}
run := func(t *testing.T, ropts []opt.NameResolveOption) {
t.Run("basic", func(t *testing.T) {
api, p := init()
e, err := api.Name().Publish(ctx, p)
if err != nil {
t.Fatal(err)
}
name, err := api.Name().Publish(ctx, p)
require.NoError(t, err)
self, err := api.Key().Self(ctx)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String())
if e.Name() != coreiface.FormatKeyID(self.ID()) {
t.Errorf("expected e.Name to equal '%s', got '%s'", coreiface.FormatKeyID(self.ID()), e.Name())
}
if e.Value().String() != p.String() {
t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String())
}
resPath, err := api.Name().Resolve(ctx, e.Name(), ropts...)
if err != nil {
t.Fatal(err)
}
if resPath.String() != p.String() {
t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String())
}
resPath, err := api.Name().Resolve(ctx, name.String(), ropts...)
require.NoError(t, err)
require.Equal(t, p.String(), resPath.String())
})
t.Run("publishPath", func(t *testing.T) {
api, p := init()
e, err := api.Name().Publish(ctx, appendPath(p, "/test"))
if err != nil {
t.Fatal(err)
}
name, err := api.Name().Publish(ctx, appendPath(p, "/test"))
require.NoError(t, err)
self, err := api.Key().Self(ctx)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String())
if e.Name() != coreiface.FormatKeyID(self.ID()) {
t.Errorf("expected e.Name to equal '%s', got '%s'", coreiface.FormatKeyID(self.ID()), e.Name())
}
if e.Value().String() != p.String()+"/test" {
t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String())
}
resPath, err := api.Name().Resolve(ctx, e.Name(), ropts...)
if err != nil {
t.Fatal(err)
}
if resPath.String() != p.String()+"/test" {
t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String()+"/test")
}
resPath, err := api.Name().Resolve(ctx, name.String(), ropts...)
require.NoError(t, err)
require.Equal(t, p.String()+"/test", resPath.String())
})
t.Run("revolvePath", func(t *testing.T) {
api, p := init()
e, err := api.Name().Publish(ctx, p)
if err != nil {
t.Fatal(err)
}
name, err := api.Name().Publish(ctx, p)
require.NoError(t, err)
self, err := api.Key().Self(ctx)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String())
if e.Name() != coreiface.FormatKeyID(self.ID()) {
t.Errorf("expected e.Name to equal '%s', got '%s'", coreiface.FormatKeyID(self.ID()), e.Name())
}
if e.Value().String() != p.String() {
t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String())
}
resPath, err := api.Name().Resolve(ctx, e.Name()+"/test", ropts...)
if err != nil {
t.Fatal(err)
}
if resPath.String() != p.String()+"/test" {
t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String()+"/test")
}
resPath, err := api.Name().Resolve(ctx, name.String()+"/test", ropts...)
require.NoError(t, err)
require.Equal(t, p.String()+"/test", resPath.String())
})
t.Run("publishRevolvePath", func(t *testing.T) {
api, p := init()
e, err := api.Name().Publish(ctx, appendPath(p, "/a"))
if err != nil {
t.Fatal(err)
}
name, err := api.Name().Publish(ctx, appendPath(p, "/a"))
require.NoError(t, err)
self, err := api.Key().Self(ctx)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String())
if e.Name() != coreiface.FormatKeyID(self.ID()) {
t.Errorf("expected e.Name to equal '%s', got '%s'", coreiface.FormatKeyID(self.ID()), e.Name())
}
if e.Value().String() != p.String()+"/a" {
t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String())
}
resPath, err := api.Name().Resolve(ctx, e.Name()+"/b", ropts...)
if err != nil {
t.Fatal(err)
}
if resPath.String() != p.String()+"/a/b" {
t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String()+"/a/b")
}
resPath, err := api.Name().Resolve(ctx, name.String()+"/b", ropts...)
require.NoError(t, err)
require.Equal(t, p.String()+"/a/b", resPath.String())
})
}
@ -192,42 +122,22 @@ func (tp *TestSuite) TestBasicPublishResolveKey(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
apis, err := tp.MakeAPISwarm(t, ctx, 5)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
api := apis[0]
k, err := api.Key().Generate(ctx, "foo")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
p, err := addTestObject(ctx, api)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
e, err := api.Name().Publish(ctx, p, opt.Name.Key(k.Name()))
if err != nil {
t.Fatal(err)
}
name, err := api.Name().Publish(ctx, p, opt.Name.Key(k.Name()))
require.NoError(t, err)
require.Equal(t, name.String(), ipns.NameFromPeer(k.ID()).String())
if e.Name() != coreiface.FormatKey(k) {
t.Errorf("expected e.Name to equal %s, got '%s'", e.Name(), coreiface.FormatKey(k))
}
if e.Value().String() != p.String() {
t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String())
}
resPath, err := api.Name().Resolve(ctx, e.Name())
if err != nil {
t.Fatal(err)
}
if resPath.String() != p.String() {
t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String())
}
resPath, err := api.Name().Resolve(ctx, name.String())
require.NoError(t, err)
require.Equal(t, p.String(), resPath.String())
}
func (tp *TestSuite) TestBasicPublishResolveTimeout(t *testing.T) {
@ -236,39 +146,22 @@ func (tp *TestSuite) TestBasicPublishResolveTimeout(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
apis, err := tp.MakeAPISwarm(t, ctx, 5)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
api := apis[0]
p, err := addTestObject(ctx, api)
if err != nil {
t.Fatal(err)
}
e, err := api.Name().Publish(ctx, p, opt.Name.ValidTime(time.Millisecond*100))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
self, err := api.Key().Self(ctx)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
if e.Name() != coreiface.FormatKeyID(self.ID()) {
t.Errorf("expected e.Name to equal '%s', got '%s'", coreiface.FormatKeyID(self.ID()), e.Name())
}
if e.Value().String() != p.String() {
t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String())
}
name, err := api.Name().Publish(ctx, p, opt.Name.ValidTime(time.Millisecond*100))
require.NoError(t, err)
require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String())
time.Sleep(time.Second)
_, err = api.Name().Resolve(ctx, e.Name())
if err == nil {
t.Fatal("Expected an error")
}
_, err = api.Name().Resolve(ctx, name.String())
require.NoError(t, err)
}
//TODO: When swarm api is created, add multinode tests

View File

@ -5,10 +5,11 @@ import (
"testing"
"time"
"github.com/gogo/protobuf/proto"
iface "github.com/ipfs/boxo/coreiface"
"github.com/ipfs/boxo/coreiface/options"
ipns_pb "github.com/ipfs/boxo/ipns/pb"
"github.com/ipfs/boxo/coreiface/path"
"github.com/ipfs/boxo/ipns"
"github.com/stretchr/testify/require"
)
func (tp *TestSuite) TestRouting(t *testing.T) {
@ -24,19 +25,15 @@ func (tp *TestSuite) TestRouting(t *testing.T) {
t.Run("TestRoutingPutOffline", tp.TestRoutingPutOffline)
}
func (tp *TestSuite) testRoutingPublishKey(t *testing.T, ctx context.Context, api iface.CoreAPI, opts ...options.NamePublishOption) iface.IpnsEntry {
func (tp *TestSuite) testRoutingPublishKey(t *testing.T, ctx context.Context, api iface.CoreAPI, opts ...options.NamePublishOption) (path.Path, ipns.Name) {
p, err := addTestObject(ctx, api)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
entry, err := api.Name().Publish(ctx, p, opts...)
if err != nil {
t.Fatal(err)
}
name, err := api.Name().Publish(ctx, p, opts...)
require.NoError(t, err)
time.Sleep(3 * time.Second)
return entry
return p, name
}
func (tp *TestSuite) TestRoutingGet(t *testing.T) {
@ -44,53 +41,39 @@ func (tp *TestSuite) TestRoutingGet(t *testing.T) {
defer cancel()
apis, err := tp.MakeAPISwarm(t, ctx, 2)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// Node 1: publishes an IPNS name
ipnsEntry := tp.testRoutingPublishKey(t, ctx, apis[0])
p, name := tp.testRoutingPublishKey(t, ctx, apis[0])
// Node 2: retrieves the best value for the IPNS name.
data, err := apis[1].Routing().Get(ctx, "/ipns/"+ipnsEntry.Name())
if err != nil {
t.Fatal(err)
}
data, err := apis[1].Routing().Get(ctx, ipns.NamespacePrefix+name.String())
require.NoError(t, err)
// Checks if values match.
var entry ipns_pb.IpnsEntry
err = proto.Unmarshal(data, &entry)
if err != nil {
t.Fatal(err)
}
rec, err := ipns.UnmarshalRecord(data)
require.NoError(t, err)
if string(entry.GetValue()) != ipnsEntry.Value().String() {
t.Fatalf("routing key has wrong value, expected %s, got %s", ipnsEntry.Value().String(), string(entry.GetValue()))
}
val, err := rec.Value()
require.NoError(t, err)
require.Equal(t, p.String(), val.String())
}
func (tp *TestSuite) TestRoutingPut(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
apis, err := tp.MakeAPISwarm(t, ctx, 2)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// Create and publish IPNS entry.
ipnsEntry := tp.testRoutingPublishKey(t, ctx, apis[0])
_, name := tp.testRoutingPublishKey(t, ctx, apis[0])
// Get valid routing value.
data, err := apis[0].Routing().Get(ctx, "/ipns/"+ipnsEntry.Name())
if err != nil {
t.Fatal(err)
}
data, err := apis[0].Routing().Get(ctx, ipns.NamespacePrefix+name.String())
require.NoError(t, err)
// Put routing value.
err = apis[1].Routing().Put(ctx, "/ipns/"+ipnsEntry.Name(), data)
if err != nil {
t.Fatal(err)
}
err = apis[1].Routing().Put(ctx, ipns.NamespacePrefix+name.String(), data)
require.NoError(t, err)
}
func (tp *TestSuite) TestRoutingPutOffline(t *testing.T) {
@ -99,29 +82,19 @@ func (tp *TestSuite) TestRoutingPutOffline(t *testing.T) {
// init a swarm & publish an IPNS entry to get a valid payload
apis, err := tp.MakeAPISwarm(t, ctx, 2)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
ipnsEntry := tp.testRoutingPublishKey(t, ctx, apis[0], options.Name.AllowOffline(true))
data, err := apis[0].Routing().Get(ctx, "/ipns/"+ipnsEntry.Name())
if err != nil {
t.Fatal(err)
}
_, name := tp.testRoutingPublishKey(t, ctx, apis[0], options.Name.AllowOffline(true))
data, err := apis[0].Routing().Get(ctx, ipns.NamespacePrefix+name.String())
require.NoError(t, err)
// init our offline node and try to put the payload
api, err := tp.makeAPIWithIdentityAndOffline(t, ctx)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
err = api.Routing().Put(ctx, "/ipns/"+ipnsEntry.Name(), data)
if err == nil {
t.Fatal("this operation should fail because we are offline")
}
err = api.Routing().Put(ctx, ipns.NamespacePrefix+name.String(), data)
require.Error(t, err, "this operation should fail because we are offline")
err = api.Routing().Put(ctx, "/ipns/"+ipnsEntry.Name(), data, options.Put.AllowOffline(true))
if err != nil {
t.Fatal(err)
}
err = api.Routing().Put(ctx, ipns.NamespacePrefix+name.String(), data, options.Put.AllowOffline(true))
require.NoError(t, err)
}