[#55] frostfsid: Use single admin instead of many

Autorization can be dedicated to a separate contract, iterating over
multiple keys can be costly. Also add committee as "default" admin:
everything is allowed for it.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
This commit is contained in:
Evgenii Stratonikov 2023-11-28 11:47:12 +03:00
parent 03d0c10852
commit 43097d2152
6 changed files with 149 additions and 134 deletions

View file

@ -82,9 +82,9 @@ const (
const iteratorBatchSize = 100 const iteratorBatchSize = 100
const ( const (
addOwnerMethod = "addOwner" getAdminMethod = "getAdmin"
deleteOwnerMethod = "deleteOwner" setAdminMethod = "setAdmin"
listOwnersMethod = "listOwners" clearAdminMethod = "clearAdmin"
versionMethod = "version" versionMethod = "version"
@ -157,33 +157,46 @@ func (c Client) Version() (int64, error) {
return unwrap.Int64(c.act.Call(c.contract, versionMethod)) return unwrap.Int64(c.act.Call(c.contract, versionMethod))
} }
// AddOwner adds new address that can perform write operations on contract. // SetAdmin sets address that can perform write operations on contract.
// Must be invoked by committee. // Must be invoked by committee.
func (c Client) AddOwner(owner util.Uint160) (tx util.Uint256, vub uint32, err error) { func (c Client) SetAdmin(owner util.Uint160) (tx util.Uint256, vub uint32, err error) {
method, args := c.AddOwnerCall(owner) method, args := c.SetAdminCall(owner)
return c.act.SendCall(c.contract, method, args...) return c.act.SendCall(c.contract, method, args...)
} }
// AddOwnerCall provides args for AddOwner to use in commonclient.Transaction. // SetAdminCall provides args for SetAdmin to use in commonclient.Transaction.
func (c Client) AddOwnerCall(owner util.Uint160) (method string, args []any) { func (c Client) SetAdminCall(owner util.Uint160) (method string, args []any) {
return addOwnerMethod, []any{owner} return setAdminMethod, []any{owner}
} }
// DeleteOwner removes address from list of that can perform write operations on contract. // ClearAdmin removes address that can perform write operations on contract.
// Must be invoked by committee. // Must be invoked by committee.
func (c Client) DeleteOwner(owner util.Uint160) (tx util.Uint256, vub uint32, err error) { func (c Client) ClearAdmin() (tx util.Uint256, vub uint32, err error) {
method, args := c.DeleteOwnerCall(owner) method, args := c.ClearAdminCall()
return c.act.SendCall(c.contract, method, args...) return c.act.SendCall(c.contract, method, args...)
} }
// DeleteOwnerCall provides args for DeleteOwner to use in commonclient.Transaction. // ClearAdminCall provides args for ClearAdmin to use in commonclient.Transaction.
func (c Client) DeleteOwnerCall(owner util.Uint160) (method string, args []any) { func (c Client) ClearAdminCall() (method string, args []any) {
return deleteOwnerMethod, []any{owner} return clearAdminMethod, nil
} }
// ListOwners returns list of address that can perform write operations on contract. // GetAdmin returns address that can perform write operations on contract.
func (c Client) ListOwners() ([]util.Uint160, error) { // Second return values is true iff admin is set.
return unwrapArrayOfUint160(commonclient.ReadIteratorItems(c.act, iteratorBatchSize, c.contract, listOwnersMethod)) func (c Client) GetAdmin() (util.Uint160, bool, error) {
item, err := unwrap.Item(c.act.Call(c.contract, getAdminMethod))
if err != nil {
return util.Uint160{}, false, err
}
if item.Value() == nil {
return util.Uint160{}, false, nil
}
bs, err := item.TryBytes()
if err != nil {
return util.Uint160{}, true, err
}
u, err := util.Uint160DecodeBytesBE(bs)
return u, true, err
} }
// CreateSubject creates new subject using public key. // CreateSubject creates new subject using public key.

View file

@ -1,5 +1,7 @@
name: "Identity" name: "Identity"
safemethods: ["version"] safemethods:
- "getAdmin"
- "version"
permissions: permissions:
- methods: ["update"] - methods: ["update"]
events: events:

View file

@ -58,7 +58,7 @@ type (
) )
const ( const (
ownerKeysPrefix = 'o' adminKey = 'o'
subjectKeysPrefix = 's' subjectKeysPrefix = 's'
additionalKeysPrefix = 'a' additionalKeysPrefix = 'a'
namespaceKeysPrefix = 'n' namespaceKeysPrefix = 'n'
@ -74,14 +74,14 @@ func _deploy(data any, isUpdate bool) {
ctx := storage.GetContext() ctx := storage.GetContext()
args := data.(struct { args := data.(struct {
owners []interop.Hash160 admin interop.Hash160
}) })
for _, owner := range args.owners { if args.admin != nil {
if len(owner) != interop.Hash160Len { if len(args.admin) != interop.Hash160Len {
panic("incorrect length of owner addresses") panic("incorrect length of owner address")
} }
storage.Put(ctx, ownerKey(owner), []byte{1}) storage.Put(ctx, adminKey, args.admin)
} }
storage.Put(ctx, groupCounterKey, 0) storage.Put(ctx, groupCounterKey, 0)
@ -89,27 +89,27 @@ func _deploy(data any, isUpdate bool) {
runtime.Log("frostfsid contract initialized") runtime.Log("frostfsid contract initialized")
} }
func AddOwner(addr interop.Hash160) { func SetAdmin(addr interop.Hash160) {
ctx := storage.GetContext() ctx := storage.GetContext()
if !common.HasUpdateAccess() { if !common.HasUpdateAccess() {
panic("not witnessed") panic("not witnessed")
} }
storage.Put(ctx, ownerKey(addr), []byte{1}) storage.Put(ctx, adminKey, addr)
} }
func DeleteOwner(addr interop.Hash160) { func ClearAdmin() {
ctx := storage.GetContext() ctx := storage.GetContext()
if !common.HasUpdateAccess() { if !common.HasUpdateAccess() {
panic("not witnessed") panic("not witnessed")
} }
storage.Delete(ctx, ownerKey(addr)) storage.Delete(ctx, adminKey)
} }
func ListOwners() iterator.Iterator { func GetAdmin() interop.Hash160 {
ctx := storage.GetReadOnlyContext() ctx := storage.GetReadOnlyContext()
return storage.Find(ctx, []byte{ownerKeysPrefix}, storage.KeysOnly|storage.RemovePrefix) return storage.Get(ctx, adminKey).(interop.Hash160)
} }
// Update method updates contract source code and manifest. It can be invoked // Update method updates contract source code and manifest. It can be invoked
@ -815,12 +815,12 @@ func DeleteGroup(ns string, groupID int) {
} }
func checkContractOwner(ctx storage.Context) { func checkContractOwner(ctx storage.Context) {
it := storage.Find(ctx, []byte{ownerKeysPrefix}, storage.KeysOnly|storage.RemovePrefix) addr := storage.Get(ctx, adminKey)
for iterator.Next(it) { if addr != nil && runtime.CheckWitness(addr.(interop.Hash160)) {
owner := iterator.Value(it).([]byte)
if runtime.CheckWitness(owner) {
return return
} }
if common.HasUpdateAccess() {
return
} }
panic("not witnessed") panic("not witnessed")
} }
@ -905,10 +905,6 @@ func setNamespaceGroupName(ctx storage.Context, gr Group) {
} }
} }
func ownerKey(owner interop.Hash160) []byte {
return append([]byte{ownerKeysPrefix}, owner...)
}
func subjectKey(key interop.PublicKey) []byte { func subjectKey(key interop.PublicKey) []byte {
addr := contract.CreateStandardAccount(key) addr := contract.CreateStandardAccount(key)
return subjectKeyFromAddr(addr) return subjectKeyFromAddr(addr)

View file

@ -163,33 +163,16 @@ func New(actor Actor, hash util.Uint160) *Contract {
return &Contract{ContractReader{actor, hash}, actor, hash} return &Contract{ContractReader{actor, hash}, actor, hash}
} }
// GetAdmin invokes `getAdmin` method of contract.
func (c *ContractReader) GetAdmin() (util.Uint160, error) {
return unwrap.Uint160(c.invoker.Call(c.hash, "getAdmin"))
}
// Version invokes `version` method of contract. // Version invokes `version` method of contract.
func (c *ContractReader) Version() (*big.Int, error) { func (c *ContractReader) Version() (*big.Int, error) {
return unwrap.BigInt(c.invoker.Call(c.hash, "version")) return unwrap.BigInt(c.invoker.Call(c.hash, "version"))
} }
// AddOwner creates a transaction invoking `addOwner` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) AddOwner(addr util.Uint160) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "addOwner", addr)
}
// AddOwnerTransaction creates a transaction invoking `addOwner` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) AddOwnerTransaction(addr util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "addOwner", addr)
}
// AddOwnerUnsigned creates a transaction invoking `addOwner` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) AddOwnerUnsigned(addr util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "addOwner", nil, addr)
}
// AddSubjectKey creates a transaction invoking `addSubjectKey` method of the contract. // AddSubjectKey creates a transaction invoking `addSubjectKey` method of the contract.
// This transaction is signed and immediately sent to the network. // This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any. // The values returned are its hash, ValidUntilBlock value and error if any.
@ -256,6 +239,28 @@ func (c *Contract) AddSubjectToNamespaceUnsigned(addr util.Uint160, ns string) (
return c.actor.MakeUnsignedCall(c.hash, "addSubjectToNamespace", nil, addr, ns) return c.actor.MakeUnsignedCall(c.hash, "addSubjectToNamespace", nil, addr, ns)
} }
// ClearAdmin creates a transaction invoking `clearAdmin` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) ClearAdmin() (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "clearAdmin")
}
// ClearAdminTransaction creates a transaction invoking `clearAdmin` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) ClearAdminTransaction() (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "clearAdmin")
}
// ClearAdminUnsigned creates a transaction invoking `clearAdmin` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) ClearAdminUnsigned() (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "clearAdmin", nil)
}
// CreateGroup creates a transaction invoking `createGroup` method of the contract. // CreateGroup creates a transaction invoking `createGroup` method of the contract.
// This transaction is signed and immediately sent to the network. // This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any. // The values returned are its hash, ValidUntilBlock value and error if any.
@ -366,28 +371,6 @@ func (c *Contract) DeleteGroupKVUnsigned(ns string, groupID *big.Int, key string
return c.actor.MakeUnsignedCall(c.hash, "deleteGroupKV", nil, ns, groupID, key) return c.actor.MakeUnsignedCall(c.hash, "deleteGroupKV", nil, ns, groupID, key)
} }
// DeleteOwner creates a transaction invoking `deleteOwner` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) DeleteOwner(addr util.Uint160) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "deleteOwner", addr)
}
// DeleteOwnerTransaction creates a transaction invoking `deleteOwner` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) DeleteOwnerTransaction(addr util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "deleteOwner", addr)
}
// DeleteOwnerUnsigned creates a transaction invoking `deleteOwner` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) DeleteOwnerUnsigned(addr util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "deleteOwner", nil, addr)
}
// DeleteSubject creates a transaction invoking `deleteSubject` method of the contract. // DeleteSubject creates a transaction invoking `deleteSubject` method of the contract.
// This transaction is signed and immediately sent to the network. // This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any. // The values returned are its hash, ValidUntilBlock value and error if any.
@ -718,28 +701,6 @@ func (c *Contract) ListNamespacesUnsigned() (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "listNamespaces", nil) return c.actor.MakeUnsignedCall(c.hash, "listNamespaces", nil)
} }
// ListOwners creates a transaction invoking `listOwners` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) ListOwners() (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "listOwners")
}
// ListOwnersTransaction creates a transaction invoking `listOwners` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) ListOwnersTransaction() (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "listOwners")
}
// ListOwnersUnsigned creates a transaction invoking `listOwners` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) ListOwnersUnsigned() (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "listOwners", nil)
}
// ListSubjects creates a transaction invoking `listSubjects` method of the contract. // ListSubjects creates a transaction invoking `listSubjects` method of the contract.
// This transaction is signed and immediately sent to the network. // This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any. // The values returned are its hash, ValidUntilBlock value and error if any.
@ -828,6 +789,28 @@ func (c *Contract) RemoveSubjectKeyUnsigned(addr util.Uint160, key *keys.PublicK
return c.actor.MakeUnsignedCall(c.hash, "removeSubjectKey", nil, addr, key) return c.actor.MakeUnsignedCall(c.hash, "removeSubjectKey", nil, addr, key)
} }
// SetAdmin creates a transaction invoking `setAdmin` method of the contract.
// This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any.
func (c *Contract) SetAdmin(addr util.Uint160) (util.Uint256, uint32, error) {
return c.actor.SendCall(c.hash, "setAdmin", addr)
}
// SetAdminTransaction creates a transaction invoking `setAdmin` method of the contract.
// This transaction is signed, but not sent to the network, instead it's
// returned to the caller.
func (c *Contract) SetAdminTransaction(addr util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeCall(c.hash, "setAdmin", addr)
}
// SetAdminUnsigned creates a transaction invoking `setAdmin` method of the contract.
// This transaction is not signed, it's simply returned to the caller.
// Any fields of it that do not affect fees can be changed (ValidUntilBlock,
// Nonce), fee values (NetworkFee, SystemFee) can be increased as well.
func (c *Contract) SetAdminUnsigned(addr util.Uint160) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(c.hash, "setAdmin", nil, addr)
}
// SetGroupKV creates a transaction invoking `setGroupKV` method of the contract. // SetGroupKV creates a transaction invoking `setGroupKV` method of the contract.
// This transaction is signed and immediately sent to the network. // This transaction is signed and immediately sent to the network.
// The values returned are its hash, ValidUntilBlock value and error if any. // The values returned are its hash, ValidUntilBlock value and error if any.

View file

@ -76,19 +76,19 @@ func TestFrostFSID_Client_ContractOwnersManagement(t *testing.T) {
defaultOwnerAddress := ffsid.base.owner.ScriptHash() defaultOwnerAddress := ffsid.base.owner.ScriptHash()
_, newOwnerAddress := newKey(t) _, newOwnerAddress := newKey(t)
checkListOwnersClient(t, ffsid.cli, defaultOwnerAddress) checkAdminClient(t, ffsid.cli, defaultOwnerAddress)
_, _, err := ffsid.cli.AddOwner(newOwnerAddress) _, _, err := ffsid.cli.SetAdmin(newOwnerAddress)
require.ErrorContains(t, err, "not witnessed") require.ErrorContains(t, err, "not witnessed")
committeeInvoker.Invoke(t, stackitem.Null{}, addOwnerMethod, newOwnerAddress) committeeInvoker.Invoke(t, stackitem.Null{}, setAdminMethod, newOwnerAddress)
checkListOwnersClient(t, ffsid.cli, defaultOwnerAddress, newOwnerAddress) checkAdminClient(t, ffsid.cli, newOwnerAddress)
_, _, err = ffsid.cli.DeleteOwner(newOwnerAddress) _, _, err = ffsid.cli.ClearAdmin()
require.ErrorContains(t, err, "not witnessed") require.ErrorContains(t, err, "not witnessed")
committeeInvoker.Invoke(t, stackitem.Null{}, deleteOwnerMethod, newOwnerAddress) committeeInvoker.Invoke(t, stackitem.Null{}, clearAdminMethod)
checkListOwnersClient(t, ffsid.cli, defaultOwnerAddress) checkAdminClient(t, ffsid.cli)
} }
func newKey(t *testing.T) (*keys.PrivateKey, util.Uint160) { func newKey(t *testing.T) (*keys.PrivateKey, util.Uint160) {
@ -97,10 +97,13 @@ func newKey(t *testing.T) (*keys.PrivateKey, util.Uint160) {
return key, key.PublicKey().GetScriptHash() return key, key.PublicKey().GetScriptHash()
} }
func checkListOwnersClient(t *testing.T, cli *client.Client, owners ...util.Uint160) { func checkAdminClient(t *testing.T, cli *client.Client, owners ...util.Uint160) {
addresses, err := cli.ListOwners() address, isSet, err := cli.GetAdmin()
require.NoError(t, err) require.NoError(t, err)
require.ElementsMatch(t, addresses, owners) require.Equal(t, len(owners) > 0, isSet)
if isSet {
require.Equal(t, owners[0], address)
}
} }
func TestFrostFSID_Client_SubjectManagement(t *testing.T) { func TestFrostFSID_Client_SubjectManagement(t *testing.T) {

View file

@ -23,9 +23,9 @@ import (
const frostfsidPath = "../frostfsid" const frostfsidPath = "../frostfsid"
const ( const (
addOwnerMethod = "addOwner" setAdminMethod = "setAdmin"
deleteOwnerMethod = "deleteOwner" getAdminMethod = "getAdmin"
listOwnersMethod = "listOwners" clearAdminMethod = "clearAdmin"
createSubjectMethod = "createSubject" createSubjectMethod = "createSubject"
getSubjectMethod = "getSubject" getSubjectMethod = "getSubject"
@ -97,7 +97,7 @@ func newSigner(t *testing.T, c *neotest.ContractInvoker, acc *wallet.Account) ne
func deployFrostFSIDContract(t *testing.T, e *neotest.Executor, contractOwner util.Uint160) util.Uint160 { func deployFrostFSIDContract(t *testing.T, e *neotest.Executor, contractOwner util.Uint160) util.Uint160 {
args := make([]any, 5) args := make([]any, 5)
args[0] = []any{contractOwner} args[0] = contractOwner
c := neotest.CompileFile(t, e.CommitteeHash, frostfsidPath, path.Join(frostfsidPath, "config.yml")) c := neotest.CompileFile(t, e.CommitteeHash, frostfsidPath, path.Join(frostfsidPath, "config.yml"))
e.DeployContract(t, c, args) e.DeployContract(t, c, args)
@ -130,30 +130,48 @@ func TestFrostFSID_ContractOwnersManagement(t *testing.T) {
invokerHash := invoker.Signers[0].ScriptHash() invokerHash := invoker.Signers[0].ScriptHash()
committeeInvoker := f.CommitteeInvoker() committeeInvoker := f.CommitteeInvoker()
checkListOwners(t, anonInvoker, invokerHash) checkOwner(t, anonInvoker, invokerHash)
anonInvoker.InvokeFail(t, notWitnessedError, createNamespaceMethod, "namespace") anonInvoker.InvokeFail(t, notWitnessedError, createNamespaceMethod, "namespace")
invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, "namespace") invoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, "namespace")
invoker.InvokeFail(t, notWitnessedError, addOwnerMethod, anonInvokerHash) t.Run("setAdmin is only allowed for committee", func(t *testing.T) {
committeeInvoker.Invoke(t, stackitem.Null{}, addOwnerMethod, anonInvokerHash) invoker.InvokeFail(t, notWitnessedError, setAdminMethod, anonInvokerHash)
})
t.Run("replace owner", func(t *testing.T) {
committeeInvoker.Invoke(t, stackitem.Null{}, setAdminMethod, anonInvokerHash)
checkOwner(t, anonInvoker, anonInvokerHash)
invoker.InvokeFail(t, notWitnessedError, createNamespaceMethod, "namespace2")
anonInvoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, "namespace2") anonInvoker.Invoke(t, stackitem.Null{}, createNamespaceMethod, "namespace2")
})
t.Run("remove owner", func(t *testing.T) {
committeeInvoker.Invoke(t, stackitem.Null{}, clearAdminMethod)
checkOwner(t, anonInvoker)
checkListOwners(t, anonInvoker, invokerHash, anonInvokerHash) invoker.InvokeFail(t, notWitnessedError, createNamespaceMethod, "namespace3")
anonInvoker.InvokeFail(t, notWitnessedError, deleteOwnerMethod, anonInvokerHash)
committeeInvoker.Invoke(t, stackitem.Null{}, deleteOwnerMethod, anonInvokerHash)
anonInvoker.InvokeFail(t, notWitnessedError, createNamespaceMethod, "namespace3") anonInvoker.InvokeFail(t, notWitnessedError, createNamespaceMethod, "namespace3")
})
checkListOwners(t, anonInvoker, invokerHash)
} }
func checkListOwners(t *testing.T, invoker *neotest.ContractInvoker, expectedAddresses ...util.Uint160) { func checkOwner(t *testing.T, invoker *neotest.ContractInvoker, owner ...util.Uint160) {
s, err := invoker.TestInvoke(t, listOwnersMethod) if len(owner) > 1 {
require.Fail(t, "invalid testcase")
}
s, err := invoker.TestInvoke(t, getAdminMethod)
require.NoError(t, err) require.NoError(t, err)
addresses, err := unwrap.ArrayOfUint160(makeValidRes(stackitem.NewArray(readIteratorAll(s))), nil) require.Equal(t, 1, s.Len(), "unexpected number items on stack")
if len(owner) == 0 {
_, isMissing := s.Pop().Item().(stackitem.Null)
require.True(t, isMissing)
return
}
bs, err := s.Pop().Item().TryBytes()
require.NoError(t, err) require.NoError(t, err)
require.ElementsMatch(t, addresses, expectedAddresses) require.Equal(t, bs, owner[0].BytesBE())
} }
func TestFrostFSID_SubjectManagement(t *testing.T) { func TestFrostFSID_SubjectManagement(t *testing.T) {