Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ The following emojis are used to highlight certain changes:

### Changed

* 🛠 The `ipns` package has been refactored. You should no longer use the direct Protobuf
version of the IPNS Record. Instead, we have a shiny new `ipns.Record` type that wraps
all the required functionality to work the best as possible with IPNS v2 Records. Please
check the [documentation](https://pkg.go.dev/github.com/ipfs/boxo/ipns) for more information,
and follow [ipfs/specs#376](https://github.com/ipfs/specs/issues/376) for related IPIP.

### Removed

### Fixed
Expand Down
11 changes: 2 additions & 9 deletions coreiface/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
20 changes: 14 additions & 6 deletions coreiface/options/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 10 additions & 2 deletions coreiface/options/namesys/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
217 changes: 55 additions & 162 deletions coreiface/tests/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}

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())
}
require.NoError(t, err)
require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).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)
}

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")
}
require.NoError(t, err)
require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String())

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)
}

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")
}
require.NoError(t, err)
require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String())

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)
}

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")
}
require.NoError(t, err)
require.Equal(t, name.String(), ipns.NameFromPeer(self.ID()).String())

resPath, err := api.Name().Resolve(ctx, name.String()+"/b", ropts...)
require.NoError(t, err)
require.Equal(t, p.String()+"/a/b", resPath.String())
})
}

Expand All @@ -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)
}

e, err := api.Name().Publish(ctx, p, opt.Name.Key(k.Name()))
if err != nil {
t.Fatal(err)
}

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())
}
require.NoError(t, err)

resPath, err := api.Name().Resolve(ctx, e.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 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) {
Expand All @@ -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
Loading