diff --git a/go.mod b/go.mod index c40aa5c..e000c68 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521073959-f0d4d129b7f1 github.com/golang/mock v1.6.0 github.com/google/uuid v1.2.0 + github.com/mr-tron/base58 v1.2.0 github.com/nspcc-dev/hrw v1.0.9 github.com/nspcc-dev/neo-go v0.96.1 github.com/nspcc-dev/neofs-api-go v1.30.0 diff --git a/owner/id.go b/owner/id.go new file mode 100644 index 0000000..b454286 --- /dev/null +++ b/owner/id.go @@ -0,0 +1,101 @@ +package owner + +import ( + "bytes" + "errors" + "fmt" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neofs-api-go/v2/refs" +) + +// ID represents v2-compatible owner identifier. +type ID refs.OwnerID + +var errInvalidIDString = errors.New("incorrect format of the string owner ID") + +// NewIDFromV2 wraps v2 OwnerID message to ID. +// +// Nil refs.OwnerID converts to nil. +func NewIDFromV2(idV2 *refs.OwnerID) *ID { + return (*ID)(idV2) +} + +// NewID creates and initializes blank ID. +// +// Works similar as NewIDFromV2(new(OwnerID)). +// +// Defaults: +// - value: nil. +func NewID() *ID { + return NewIDFromV2(new(refs.OwnerID)) +} + +// SetNeo3Wallet sets owner identifier value to NEO3 wallet address. +func (id *ID) SetNeo3Wallet(v *NEO3Wallet) { + (*refs.OwnerID)(id).SetValue(v.Bytes()) +} + +// ToV2 returns the v2 owner ID message. +// +// Nil ID converts to nil. +func (id *ID) ToV2() *refs.OwnerID { + return (*refs.OwnerID)(id) +} + +// String implements fmt.Stringer. +func (id *ID) String() string { + return base58.Encode((*refs.OwnerID)(id).GetValue()) +} + +// Equal defines a comparison relation on ID's. +// +// ID's are equal if they have the same binary representation. +func (id *ID) Equal(id2 *ID) bool { + return bytes.Equal( + (*refs.ObjectID)(id).GetValue(), + (*refs.ObjectID)(id2).GetValue(), + ) +} + +// NewIDFromNeo3Wallet creates new owner identity from 25-byte neo wallet. +func NewIDFromNeo3Wallet(v *NEO3Wallet) *ID { + id := NewID() + id.SetNeo3Wallet(v) + + return id +} + +// Parse converts base58 string representation into ID. +func (id *ID) Parse(s string) error { + data, err := base58.Decode(s) + if err != nil { + return fmt.Errorf("could not parse owner.ID from string: %w", err) + } else if len(data) != NEO3WalletSize { + return errInvalidIDString + } + + (*refs.OwnerID)(id).SetValue(data) + + return nil +} + +// Marshal marshals ID into a protobuf binary form. +func (id *ID) Marshal() ([]byte, error) { + return (*refs.OwnerID)(id).StableMarshal(nil) +} + +// Unmarshal unmarshals protobuf binary representation of ID. +func (id *ID) Unmarshal(data []byte) error { + return (*refs.OwnerID)(id).Unmarshal(data) +} + +// MarshalJSON encodes ID to protobuf JSON format. +func (id *ID) MarshalJSON() ([]byte, error) { + return (*refs.OwnerID)(id).MarshalJSON() +} + +// UnmarshalJSON decodes ID from protobuf JSON format. +func (id *ID) UnmarshalJSON(data []byte) error { + return (*refs.OwnerID)(id).UnmarshalJSON(data) +} diff --git a/owner/id_test.go b/owner/id_test.go new file mode 100644 index 0000000..877a9bb --- /dev/null +++ b/owner/id_test.go @@ -0,0 +1,133 @@ +package owner_test + +import ( + "crypto/ecdsa" + "testing" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-api-go/v2/refs" + . "github.com/nspcc-dev/neofs-sdk-go/owner" + ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test" + "github.com/stretchr/testify/require" +) + +func TestIDV2(t *testing.T) { + id := ownertest.GenerateID() + + idV2 := id.ToV2() + + require.Equal(t, id, NewIDFromV2(idV2)) +} + +func TestNewIDFromNeo3Wallet(t *testing.T) { + p, err := keys.NewPrivateKey() + require.NoError(t, err) + + wallet, err := NEO3WalletFromPublicKey((*ecdsa.PublicKey)(p.PublicKey())) + require.NoError(t, err) + + id := NewIDFromNeo3Wallet(wallet) + require.Equal(t, id.ToV2().GetValue(), wallet.Bytes()) +} + +func TestID_Parse(t *testing.T) { + t.Run("should parse successful", func(t *testing.T) { + p, err := keys.NewPrivateKey() + require.NoError(t, err) + + wallet, err := NEO3WalletFromPublicKey((*ecdsa.PublicKey)(p.PublicKey())) + require.NoError(t, err) + + eid := NewIDFromNeo3Wallet(wallet) + aid := NewID() + + require.NoError(t, aid.Parse(eid.String())) + require.Equal(t, eid, aid) + }) + + t.Run("should failure on parse", func(t *testing.T) { + cs := []byte{1, 2, 3, 4, 5, 6} + str := base58.Encode(cs) + cid := NewID() + + require.Error(t, cid.Parse(str)) + }) +} + +func TestIDEncoding(t *testing.T) { + id := ownertest.GenerateID() + + t.Run("binary", func(t *testing.T) { + data, err := id.Marshal() + require.NoError(t, err) + + id2 := NewID() + require.NoError(t, id2.Unmarshal(data)) + + require.Equal(t, id, id2) + }) + + t.Run("json", func(t *testing.T) { + data, err := id.MarshalJSON() + require.NoError(t, err) + + a2 := NewID() + require.NoError(t, a2.UnmarshalJSON(data)) + + require.Equal(t, id, a2) + }) +} + +func TestID_Equal(t *testing.T) { + var ( + data1 = []byte{1, 2, 3} + data2 = data1 + data3 = append(data1, 255) + ) + + id1 := ownertest.GenerateIDFromBytes(data1) + + require.True(t, id1.Equal( + ownertest.GenerateIDFromBytes(data2), + )) + + require.False(t, id1.Equal( + ownertest.GenerateIDFromBytes(data3), + )) +} + +func TestNewIDFromV2(t *testing.T) { + t.Run("from nil", func(t *testing.T) { + var x *refs.OwnerID + + require.Nil(t, NewIDFromV2(x)) + }) +} + +func TestID_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *ID + + require.Nil(t, x.ToV2()) + }) +} + +func TestID_String(t *testing.T) { + t.Run("nil", func(t *testing.T) { + id := NewID() + + require.Empty(t, id.String()) + }) +} + +func TestNewID(t *testing.T) { + t.Run("default values", func(t *testing.T) { + id := NewID() + + // convert to v2 message + idV2 := id.ToV2() + + require.Nil(t, idV2.GetValue()) + }) +} diff --git a/owner/test/id.go b/owner/test/id.go new file mode 100644 index 0000000..fe360c9 --- /dev/null +++ b/owner/test/id.go @@ -0,0 +1,27 @@ +package ownertest + +import ( + "math/rand" + + "github.com/nspcc-dev/neofs-api-go/v2/refs" + "github.com/nspcc-dev/neofs-sdk-go/owner" +) + +// GenerateID returns owner.ID calculated +// from a random owner.NEO3Wallet. +func GenerateID() *owner.ID { + data := make([]byte, owner.NEO3WalletSize) + + rand.Read(data) + + return GenerateIDFromBytes(data) +} + +// GenerateIDFromBytes returns owner.ID generated +// from a passed byte slice. +func GenerateIDFromBytes(val []byte) *owner.ID { + idV2 := new(refs.OwnerID) + idV2.SetValue(val) + + return owner.NewIDFromV2(idV2) +} diff --git a/owner/wallet.go b/owner/wallet.go new file mode 100644 index 0000000..3dfcb00 --- /dev/null +++ b/owner/wallet.go @@ -0,0 +1,57 @@ +package owner + +import ( + "crypto/ecdsa" + "errors" + "fmt" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" +) + +// NEO3Wallet represents NEO3 wallet address. +type NEO3Wallet [NEO3WalletSize]byte + +// NEO3WalletSize contains size of neo3 wallet. +const NEO3WalletSize = 25 + +// ErrEmptyPublicKey when PK passed to Verify method is nil. +var ErrEmptyPublicKey = errors.New("empty public key") + +// NEO3WalletFromPublicKey converts public key to NEO3 wallet address. +func NEO3WalletFromPublicKey(key *ecdsa.PublicKey) (*NEO3Wallet, error) { + if key == nil { + return nil, ErrEmptyPublicKey + } + + neoPublicKey := (*keys.PublicKey)(key) + + d, err := base58.Decode(neoPublicKey.Address()) + if err != nil { + return nil, fmt.Errorf("can't decode neo3 address from key: %w", err) + } + + w := new(NEO3Wallet) + + copy(w.Bytes(), d) + + return w, nil +} + +// String implements fmt.Stringer. +func (w *NEO3Wallet) String() string { + if w != nil { + return base58.Encode(w[:]) + } + + return "" +} + +// Bytes returns slice of NEO3 wallet address bytes. +func (w *NEO3Wallet) Bytes() []byte { + if w != nil { + return w[:] + } + + return nil +}