forked from TrueCloudLab/frostfs-contract
Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
fe7a767e8f | |||
60b81c4bf6 | |||
a2c2791146 | |||
7a8c64b966 |
7 changed files with 141 additions and 44 deletions
|
@ -20,6 +20,7 @@ FrostFSID contract does not produce notifications to process.
|
|||
| `G` + [ RIPEMD160 of namespace ] + [ 8 byte group id ] + [ subject address ] | []byte{1} | subject that belongs to the group |
|
||||
| `c` | Int | group id counter |
|
||||
| `m` + [ RIPEMD160 of namespace ] + [ RIPEMD160 of subject name ] | Serialized group id int | group name to group id index |
|
||||
| `A` + [ subject address ] | bool | means that the wallet has been used |
|
||||
|
||||
|
||||
*/
|
||||
|
|
|
@ -96,6 +96,7 @@ const (
|
|||
groupSubjectsKeysPrefix = 'G'
|
||||
groupCounterKey = 'c'
|
||||
namespaceGroupsNamesPrefix = 'm'
|
||||
addressPrefix = 'A'
|
||||
)
|
||||
|
||||
func _deploy(data any, isUpdate bool) {
|
||||
|
@ -112,6 +113,27 @@ func _deploy(data any, isUpdate bool) {
|
|||
storage.Put(ctx, adminKey, args.admin)
|
||||
}
|
||||
|
||||
if isUpdate {
|
||||
it := storage.Find(ctx, subjectKeysPrefix, storage.ValuesOnly)
|
||||
for iterator.Next(it) {
|
||||
subjectRaw := iterator.Value(it)
|
||||
subject := std.Deserialize(subjectRaw.([]byte)).(Subject)
|
||||
address := addressKey(contract.CreateStandardAccount(subject.PrimaryKey))
|
||||
if storage.Get(ctx, address) != nil {
|
||||
panic("frostfsid contract contains duplicate keys")
|
||||
}
|
||||
storage.Put(ctx, address, true)
|
||||
|
||||
for i := 0; i < len(subject.AdditionalKeys); i++ {
|
||||
address = addressKey(contract.CreateStandardAccount(subject.AdditionalKeys[i]))
|
||||
if storage.Get(ctx, address) != nil {
|
||||
panic("frostfsid contract contains duplicate keys")
|
||||
}
|
||||
storage.Put(ctx, address, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storage.Put(ctx, groupCounterKey, 0)
|
||||
storage.Put(ctx, namespaceKey(""), std.Serialize(Namespace{}))
|
||||
|
||||
|
@ -182,6 +204,11 @@ func CreateSubject(ns string, key interop.PublicKey) {
|
|||
panic("key is occupied")
|
||||
}
|
||||
|
||||
allAddressKey := addressKey(addr)
|
||||
if storage.Get(ctx, allAddressKey) != nil {
|
||||
panic("key is occupied by another additional key")
|
||||
}
|
||||
|
||||
nsKey := namespaceKey(ns)
|
||||
data = storage.Get(ctx, nsKey).([]byte)
|
||||
if data == nil {
|
||||
|
@ -197,6 +224,7 @@ func CreateSubject(ns string, key interop.PublicKey) {
|
|||
|
||||
nsSubjKey := namespaceSubjectKey(ns, addr)
|
||||
storage.Put(ctx, nsSubjKey, []byte{1})
|
||||
storage.Put(ctx, allAddressKey, true)
|
||||
|
||||
runtime.Notify("CreateSubject", interop.Hash160(addr))
|
||||
}
|
||||
|
@ -213,6 +241,11 @@ func AddSubjectKey(addr interop.Hash160, key interop.PublicKey) {
|
|||
panic("incorrect public key length")
|
||||
}
|
||||
|
||||
addressKey := addressKey(contract.CreateStandardAccount(key))
|
||||
if storage.Get(ctx, addressKey) != nil {
|
||||
panic("key is occupied")
|
||||
}
|
||||
|
||||
saKey := subjectAdditionalKey(key, addr)
|
||||
data := storage.Get(ctx, saKey).([]byte)
|
||||
if data != nil {
|
||||
|
@ -230,6 +263,7 @@ func AddSubjectKey(addr interop.Hash160, key interop.PublicKey) {
|
|||
subject.AdditionalKeys = append(subject.AdditionalKeys, key)
|
||||
|
||||
storage.Put(ctx, sKey, std.Serialize(subject))
|
||||
storage.Put(ctx, addressKey, true)
|
||||
runtime.Notify("AddSubjectKey", addr, key)
|
||||
}
|
||||
|
||||
|
@ -269,6 +303,7 @@ func RemoveSubjectKey(addr interop.Hash160, key interop.PublicKey) {
|
|||
subject.AdditionalKeys = additionalKeys
|
||||
|
||||
storage.Put(ctx, sKey, std.Serialize(subject))
|
||||
storage.Delete(ctx, addressKey(contract.CreateStandardAccount(key)))
|
||||
runtime.Notify("RemoveSubjectKey", addr, key)
|
||||
}
|
||||
|
||||
|
@ -362,6 +397,7 @@ func DeleteSubject(addr interop.Hash160) {
|
|||
|
||||
for i := 0; i < len(subj.AdditionalKeys); i++ {
|
||||
storage.Delete(ctx, subjectAdditionalKey(subj.AdditionalKeys[i], addr))
|
||||
storage.Delete(ctx, addressKey(contract.CreateStandardAccount(subj.AdditionalKeys[i])))
|
||||
}
|
||||
storage.Delete(ctx, sKey)
|
||||
|
||||
|
@ -381,7 +417,12 @@ func GetSubject(addr interop.Hash160) Subject {
|
|||
sKey := subjectKeyFromAddr(addr)
|
||||
data := storage.Get(ctx, sKey).([]byte)
|
||||
if data == nil {
|
||||
panic("subject not found")
|
||||
a := getPrimaryAddr(ctx, addr)
|
||||
sKey = subjectKeyFromAddr(a)
|
||||
data = storage.Get(ctx, sKey).([]byte)
|
||||
if data == nil {
|
||||
panic("subject not found")
|
||||
}
|
||||
}
|
||||
|
||||
return std.Deserialize(data).(Subject)
|
||||
|
@ -433,21 +474,25 @@ func GetSubjectByKey(key interop.PublicKey) Subject {
|
|||
return std.Deserialize(data).(Subject)
|
||||
}
|
||||
|
||||
saPrefix := subjectAdditionalPrefix(key)
|
||||
it := storage.Find(ctx, saPrefix, storage.KeysOnly|storage.RemovePrefix)
|
||||
for iterator.Next(it) {
|
||||
addr := iterator.Value(it).([]byte)
|
||||
sKey = subjectKeyFromAddr(addr)
|
||||
data = storage.Get(ctx, sKey).([]byte)
|
||||
if data != nil {
|
||||
return std.Deserialize(data).(Subject)
|
||||
}
|
||||
break
|
||||
addr := getPrimaryAddr(ctx, contract.CreateStandardAccount(key))
|
||||
sKey = subjectKeyFromAddr(addr)
|
||||
data = storage.Get(ctx, sKey).([]byte)
|
||||
if data != nil {
|
||||
return std.Deserialize(data).(Subject)
|
||||
}
|
||||
|
||||
panic("subject not found")
|
||||
}
|
||||
|
||||
func getPrimaryAddr(ctx storage.Context, addr interop.Hash160) interop.Hash160 {
|
||||
saPrefix := append([]byte{additionalKeysPrefix}, addr...)
|
||||
it := storage.Find(ctx, saPrefix, storage.KeysOnly|storage.RemovePrefix)
|
||||
if iterator.Next(it) {
|
||||
return iterator.Value(it).([]byte)
|
||||
}
|
||||
panic("subject not found")
|
||||
}
|
||||
|
||||
// GetSubjectByName retrieves the subject with the specified name within the given namespace.
|
||||
func GetSubjectByName(ns, name string) Subject {
|
||||
key := GetSubjectKeyByName(ns, name)
|
||||
|
@ -1029,3 +1074,7 @@ func idToBytes(itemID int) []byte {
|
|||
zeros := make([]byte, 8-ln)
|
||||
return append(b, zeros...)
|
||||
}
|
||||
|
||||
func addressKey(address []byte) []byte {
|
||||
return append([]byte{addressPrefix}, address...)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ name: "NameService"
|
|||
supportedstandards: ["NEP-11"]
|
||||
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf",
|
||||
"tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords",
|
||||
"getAllRecords",
|
||||
"resolve", "version"]
|
||||
events:
|
||||
- name: RegisterDomain
|
||||
|
|
|
@ -41,8 +41,8 @@ const (
|
|||
// prefixRecord contains map from (token key + hash160(token name) + record type)
|
||||
// to record.
|
||||
prefixRecord byte = 0x22
|
||||
//prefixGlobalDomain contains a flag indicating that this domain was created using GlobalDomain.
|
||||
//This is necessary to distinguish it from regular CNAME records.
|
||||
// prefixGlobalDomain contains a flag indicating that this domain was created using GlobalDomain.
|
||||
// This is necessary to distinguish it from regular CNAME records.
|
||||
prefixGlobalDomain byte = 0x23
|
||||
// prefixCountSubDomains contains information about the number of domains in the zone.
|
||||
// If it is nil, it will definitely be calculated on the first removal.
|
||||
|
@ -79,7 +79,7 @@ const (
|
|||
|
||||
const (
|
||||
// Cnametgt is a special TXT record ensuring all created subdomains point to the global domain - the value of this variable.
|
||||
//It is guaranteed that two domains cannot point to the same global domain.
|
||||
// It is guaranteed that two domains cannot point to the same global domain.
|
||||
Cnametgt = "cnametgt"
|
||||
)
|
||||
|
||||
|
|
|
@ -91,6 +91,20 @@ func New(actor Actor, hash util.Uint160) *Contract {
|
|||
return &Contract{ContractReader{nep11ndt.NonDivisibleReader, actor, hash}, nep11ndt.BaseWriter, actor, hash}
|
||||
}
|
||||
|
||||
// GetAllRecords invokes `getAllRecords` method of contract.
|
||||
func (c *ContractReader) GetAllRecords(name string) (uuid.UUID, result.Iterator, error) {
|
||||
return unwrap.SessionIterator(c.invoker.Call(c.hash, "getAllRecords", name))
|
||||
}
|
||||
|
||||
// GetAllRecordsExpanded is similar to GetAllRecords (uses the same contract
|
||||
// method), but can be useful if the server used doesn't support sessions and
|
||||
// doesn't expand iterators. It creates a script that will get the specified
|
||||
// number of result items from the iterator right in the VM and return them to
|
||||
// you. It's only limited by VM stack and GAS available for RPC invocations.
|
||||
func (c *ContractReader) GetAllRecordsExpanded(name string, _numOfIteratorItems int) ([]stackitem.Item, error) {
|
||||
return unwrap.Array(c.invoker.CallAndExpandIterator(c.hash, "getAllRecords", _numOfIteratorItems, name))
|
||||
}
|
||||
|
||||
// GetPrice invokes `getPrice` method of contract.
|
||||
func (c *ContractReader) GetPrice() (*big.Int, error) {
|
||||
return unwrap.BigInt(c.invoker.Call(c.hash, "getPrice"))
|
||||
|
@ -234,28 +248,6 @@ func (c *Contract) DeleteRecordsUnsigned(name string, typ *big.Int) (*transactio
|
|||
return c.actor.MakeUnsignedCall(c.hash, "deleteRecords", nil, name, typ)
|
||||
}
|
||||
|
||||
// GetAllRecords creates a transaction invoking `getAllRecords` 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) GetAllRecords(name string) (util.Uint256, uint32, error) {
|
||||
return c.actor.SendCall(c.hash, "getAllRecords", name)
|
||||
}
|
||||
|
||||
// GetAllRecordsTransaction creates a transaction invoking `getAllRecords` method of the contract.
|
||||
// This transaction is signed, but not sent to the network, instead it's
|
||||
// returned to the caller.
|
||||
func (c *Contract) GetAllRecordsTransaction(name string) (*transaction.Transaction, error) {
|
||||
return c.actor.MakeCall(c.hash, "getAllRecords", name)
|
||||
}
|
||||
|
||||
// GetAllRecordsUnsigned creates a transaction invoking `getAllRecords` 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) GetAllRecordsUnsigned(name string) (*transaction.Transaction, error) {
|
||||
return c.actor.MakeUnsignedCall(c.hash, "getAllRecords", nil, name)
|
||||
}
|
||||
|
||||
func (c *Contract) scriptForRegister(name string, owner util.Uint160, email string, refresh *big.Int, retry *big.Int, expire *big.Int, ttl *big.Int) ([]byte, error) {
|
||||
return smartcontract.CreateCallWithAssertScript(c.hash, "register", name, owner, email, refresh, retry, expire, ttl)
|
||||
}
|
||||
|
|
|
@ -238,6 +238,13 @@ func TestFrostFSID_SubjectManagement(t *testing.T) {
|
|||
subj = parseSubject(t, s)
|
||||
require.True(t, subjKey.PublicKey().Equal(&subj.PrimaryKey), "keys must be the same")
|
||||
|
||||
t.Run("with GetSubject", func(t *testing.T) {
|
||||
s, err = anonInvoker.TestInvoke(t, getSubjectMethod, subjKey.PublicKey().GetScriptHash())
|
||||
require.NoError(t, err)
|
||||
subj = parseSubject(t, s)
|
||||
require.True(t, subjKey.PublicKey().Equal(&subj.PrimaryKey), "keys must be the same")
|
||||
})
|
||||
|
||||
t.Run("remove subject key", func(t *testing.T) {
|
||||
anonInvoker.InvokeFail(t, notWitnessedError, removeSubjectKeyMethod, subjKeyAddr, subjNewKey.PublicKey().Bytes())
|
||||
invoker.Invoke(t, stackitem.Null{}, removeSubjectKeyMethod, subjKeyAddr, subjNewKey.PublicKey().Bytes())
|
||||
|
@ -604,6 +611,44 @@ func TestFrostFSID_GroupManagement(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAdditionalKeyFromPrimarySubject(t *testing.T) {
|
||||
f := newFrostFSIDInvoker(t)
|
||||
invoker := f.OwnerInvoker()
|
||||
|
||||
subjAPrimaryKey, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
subjAKeyAddr := subjAPrimaryKey.PublicKey().GetScriptHash()
|
||||
|
||||
subjBPrimaryKey, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
subjBKeyAddr := subjBPrimaryKey.PublicKey().GetScriptHash()
|
||||
|
||||
subjCPrimaryKey, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
subjDPrimaryKey, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, defaultNamespace, subjAPrimaryKey.PublicKey().Bytes())
|
||||
|
||||
invoker.Invoke(t, stackitem.Null{}, createSubjectMethod, defaultNamespace, subjBPrimaryKey.PublicKey().Bytes())
|
||||
|
||||
invoker.InvokeFail(t, "key is occupied", addSubjectKeyMethod, subjBKeyAddr, subjAPrimaryKey.PublicKey().Bytes())
|
||||
|
||||
invoker.Invoke(t, stackitem.Null{}, addSubjectKeyMethod, subjAKeyAddr, subjCPrimaryKey.PublicKey().Bytes())
|
||||
invoker.InvokeFail(t, "key is occupied", addSubjectKeyMethod, subjBKeyAddr, subjCPrimaryKey.PublicKey().Bytes())
|
||||
|
||||
invoker.Invoke(t, stackitem.Null{}, addSubjectKeyMethod, subjAKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
|
||||
|
||||
invoker.InvokeFail(t, "key is occupied", addSubjectKeyMethod, subjBKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
|
||||
invoker.Invoke(t, stackitem.Null{}, removeSubjectKeyMethod, subjAKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
|
||||
invoker.Invoke(t, stackitem.Null{}, addSubjectKeyMethod, subjBKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
|
||||
|
||||
invoker.InvokeFail(t, "key is occupied", addSubjectKeyMethod, subjAKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
|
||||
invoker.Invoke(t, stackitem.Null{}, deleteSubjectMethod, subjBKeyAddr)
|
||||
invoker.Invoke(t, stackitem.Null{}, addSubjectKeyMethod, subjAKeyAddr, subjDPrimaryKey.PublicKey().Bytes())
|
||||
}
|
||||
|
||||
func checkPublicKeyResult(t *testing.T, s *vm.Stack, err error, key *keys.PrivateKey) {
|
||||
if key == nil {
|
||||
require.ErrorContains(t, err, "not found")
|
||||
|
|
|
@ -177,8 +177,10 @@ func TestNNSRegister(t *testing.T) {
|
|||
|
||||
cAcc := c.WithSigners(acc)
|
||||
|
||||
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")),
|
||||
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))})
|
||||
expected = stackitem.NewArray([]stackitem.Item{
|
||||
stackitem.NewByteArray([]byte("testdomain.com")),
|
||||
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT))),
|
||||
})
|
||||
tx = cAcc.Invoke(t, stackitem.Null{}, "addRecord",
|
||||
"testdomain.com", int64(nns.TXT), "first TXT record")
|
||||
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "AddRecord", Item: expected})
|
||||
|
@ -194,8 +196,10 @@ func TestNNSRegister(t *testing.T) {
|
|||
})
|
||||
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
|
||||
|
||||
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")),
|
||||
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))})
|
||||
expected = stackitem.NewArray([]stackitem.Item{
|
||||
stackitem.NewByteArray([]byte("testdomain.com")),
|
||||
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT))),
|
||||
})
|
||||
tx = cAcc.Invoke(t, stackitem.Null{}, "setRecord",
|
||||
"testdomain.com", int64(nns.TXT), int64(0), "replaced first")
|
||||
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "AddRecord", Item: expected})
|
||||
|
@ -207,8 +211,10 @@ func TestNNSRegister(t *testing.T) {
|
|||
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
|
||||
|
||||
tx = cAcc.Invoke(t, stackitem.Null{}, "deleteRecords", "testdomain.com", int64(nns.TXT))
|
||||
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")),
|
||||
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))})
|
||||
expected = stackitem.NewArray([]stackitem.Item{
|
||||
stackitem.NewByteArray([]byte("testdomain.com")),
|
||||
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT))),
|
||||
})
|
||||
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteRecords", Item: expected})
|
||||
|
||||
c.Invoke(t, stackitem.Null{}, "getRecords", "testdomain.com", int64(nns.TXT))
|
||||
|
@ -255,8 +261,10 @@ func TestNNSRegister(t *testing.T) {
|
|||
c.Invoke(t, stackitem.Null{}, "getRecords", "testdomain.com", int64(nns.TXT))
|
||||
|
||||
tx = cAcc.Invoke(t, stackitem.Null{}, "deleteDomain", "testdomain.com")
|
||||
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")),
|
||||
stackitem.NewBigInteger(big.NewInt(int64(nns.CNAME)))})
|
||||
expected = stackitem.NewArray([]stackitem.Item{
|
||||
stackitem.NewByteArray([]byte("testdomain.com")),
|
||||
stackitem.NewBigInteger(big.NewInt(int64(nns.CNAME))),
|
||||
})
|
||||
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteRecords", Item: expected})
|
||||
|
||||
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com"))})
|
||||
|
@ -371,6 +379,7 @@ func TestGlobalDomain(t *testing.T) {
|
|||
|
||||
c.InvokeFail(t, "global domain is already taken", "isAvailable", "dom.testdomain.com")
|
||||
}
|
||||
|
||||
func TestTLDRecord(t *testing.T) {
|
||||
c := newNNSInvoker(t, true)
|
||||
c.Invoke(t, stackitem.Null{}, "addRecord",
|
||||
|
|
Loading…
Reference in a new issue