user: Refactor user.ID structure #323

Merged
fyrchik merged 2 commits from elebedeva/frostfs-sdk-go:fix/user-id-scripthash into master 2025-01-29 10:36:03 +00:00
2 changed files with 37 additions and 43 deletions

View file

@ -12,9 +12,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/util"
)
const idSize = 25
var zeroSlice = bytes.Repeat([]byte{0}, idSize)
// idFullSize is the size of ID in bytes, including prefix and checksum.
const idFullSize = util.Uint160Size + 5
// ID identifies users of the FrostFS system.
//
@ -25,7 +24,7 @@ var zeroSlice = bytes.Repeat([]byte{0}, idSize)
// so it MUST be initialized using some modifying function (e.g. SetScriptHash,
// IDFromKey, etc.).
type ID struct {
w []byte
w util.Uint160
}
// ReadFromV2 reads ID from the refs.OwnerID message. Returns an error if
@ -33,22 +32,7 @@ type ID struct {
//
// See also WriteToV2.
func (x *ID) ReadFromV2(m refs.OwnerID) error {
w := m.GetValue()
if len(w) != idSize {
return fmt.Errorf("invalid length %d, expected %d", len(w), idSize)
}
if w[0] != address.NEO3Prefix {
return fmt.Errorf("invalid prefix byte 0x%X, expected 0x%X", w[0], address.NEO3Prefix)
}
if !bytes.Equal(w[21:], hash.Checksum(w[:21])) {
return errors.New("checksum mismatch")
}
x.w = w
return nil
return x.setUserID(m.GetValue())
}
// WriteToV2 writes ID to the refs.OwnerID message.
@ -56,25 +40,17 @@ func (x *ID) ReadFromV2(m refs.OwnerID) error {
//
// See also ReadFromV2.
func (x ID) WriteToV2(m *refs.OwnerID) {
m.SetValue(x.w)
m.SetValue(x.WalletBytes())
}
// SetScriptHash forms user ID from wallet address scripthash.
func (x *ID) SetScriptHash(scriptHash util.Uint160) {
if cap(x.w) < idSize {
x.w = make([]byte, idSize)
} else if len(x.w) < idSize {
x.w = x.w[:idSize]
}
x.w[0] = address.Prefix
copy(x.w[1:], scriptHash.BytesBE())
copy(x.w[21:], hash.Checksum(x.w[:21]))
x.w = scriptHash
}
// ScriptHash calculates and returns script hash of ID.
func (x *ID) ScriptHash() (util.Uint160, error) {
return util.Uint160DecodeBytesBE(x.w[1:21])
func (x *ID) ScriptHash() util.Uint160 {
return x.w
}
// WalletBytes returns FrostFS user ID as Neo3 wallet address in a binary format.
@ -83,14 +59,18 @@ func (x *ID) ScriptHash() (util.Uint160, error) {
//
// See also Neo3 wallet docs.
func (x ID) WalletBytes() []byte {
return x.w
v := make([]byte, idFullSize)
Review

The comment about MUST NOT be mutated can be removed now.

The comment about `MUST NOT be mutated` can be removed now.
v[0] = address.Prefix
copy(v[1:], x.w[:])
copy(v[21:], hash.Checksum(v[:21]))
return v
}
// EncodeToString encodes ID into FrostFS API V2 protocol string.
//
// See also DecodeString.
func (x ID) EncodeToString() string {
return base58.Encode(x.w)
return base58.Encode(x.WalletBytes())
Review

EncodeToString() makes 3 allocations. One of them (inside WalletBytes()) can be avoided with little cost.

$ go build -gcflags "-m -l" ./user
...
user/id.go:62:11: make([]byte, 25) escapes to heap

One way to achieve this is to move encoding to a function (ID).encode([]byte).
Both WalletBytes() and EncodeToString() methods will create new slice and encode to it.

`EncodeToString()` makes 3 allocations. One of them (inside `WalletBytes()`) can be avoided with little cost. ``` $ go build -gcflags "-m -l" ./user ... user/id.go:62:11: make([]byte, 25) escapes to heap ``` One way to achieve this is to move encoding to a function `(ID).encode([]byte)`. Both `WalletBytes()` and `EncodeToString()` methods will create new slice and `encode` to it.
Review

We can do this in another PR, though.

We can do this in another PR, though.
}
// DecodeString decodes FrostFS API V2 protocol string. Returns an error
@ -100,14 +80,11 @@ func (x ID) EncodeToString() string {
//
// See also EncodeToString.
func (x *ID) DecodeString(s string) error {
var err error
x.w, err = base58.Decode(s)
w, err := base58.Decode(s)
if err != nil {
return fmt.Errorf("decode base58: %w", err)
}
return nil
return x.setUserID(w)
}
// String implements fmt.Stringer.
@ -121,10 +98,28 @@ func (x ID) String() string {
// Equals defines a comparison relation between two ID instances.
func (x ID) Equals(x2 ID) bool {
return bytes.Equal(x.w, x2.w)
return x.w == x2.w
}
// IsEmpty returns True, if ID is empty value.
func (x ID) IsEmpty() bool {
return bytes.Equal(zeroSlice, x.w)
return x.w == util.Uint160{}
}
func (x *ID) setUserID(w []byte) error {
if len(w) != idFullSize {
return fmt.Errorf("invalid length %d, expected %d", len(w), idFullSize)
}
if w[0] != address.NEO3Prefix {
return fmt.Errorf("invalid prefix byte 0x%X, expected 0x%X", w[0], address.NEO3Prefix)
}
if !bytes.Equal(w[21:], hash.Checksum(w[:21])) {
return errors.New("checksum mismatch")
}
copy(x.w[:], w[1:21])
return nil
}

View file

@ -51,8 +51,7 @@ func TestID_SetScriptHash(t *testing.T) {
func TestID_ScriptHash(t *testing.T) {
userID := usertest.ID()
scriptHash, err := userID.ScriptHash()
require.NoError(t, err)
scriptHash := userID.ScriptHash()
ownerAddress := userID.EncodeToString()
decodedScriptHash, err := address.StringToUint160(ownerAddress)