package native import ( "encoding/binary" "errors" "fmt" "math/big" "github.com/tutus-one/tutus-chain/pkg/config" "github.com/tutus-one/tutus-chain/pkg/core/dao" "github.com/tutus-one/tutus-chain/pkg/core/interop" "github.com/tutus-one/tutus-chain/pkg/core/interop/runtime" "github.com/tutus-one/tutus-chain/pkg/core/native/nativeids" "github.com/tutus-one/tutus-chain/pkg/core/native/nativenames" "github.com/tutus-one/tutus-chain/pkg/core/state" "github.com/tutus-one/tutus-chain/pkg/core/storage" "github.com/tutus-one/tutus-chain/pkg/smartcontract" "github.com/tutus-one/tutus-chain/pkg/smartcontract/callflag" "github.com/tutus-one/tutus-chain/pkg/smartcontract/manifest" "github.com/tutus-one/tutus-chain/pkg/util" "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" ) // RoleRegistry is a native contract for hierarchical role-based access control. // It replaces NEO.CheckCommittee() with democratic governance based on Vita. type RoleRegistry struct { interop.ContractMD // Annos is used for fallback committee checks when TutusCommittee is not set. Annos IAnnos // tutusCommittee contains initial committee member addresses from config. tutusCommittee []util.Uint160 } // RoleRegistryCache contains cached role registry data. type RoleRegistryCache struct { roleCount uint64 } // Built-in role IDs. const ( // RoleCommittee is the Tutus Committee role (designated officials). RoleCommittee uint64 = 1 // RoleRegistrar can register new Vitas. RoleRegistrar uint64 = 2 // RoleAttestor can attest identity attributes. RoleAttestor uint64 = 3 // RoleOperator is a system operator (node management). RoleOperator uint64 = 4 ) // Storage key prefixes for RoleRegistry. const ( rrPrefixRole byte = 0x01 // roleID -> Role rrPrefixRoleName byte = 0x02 // name -> roleID rrPrefixRoleAssignment byte = 0x03 // tokenID + roleID -> RoleAssignment rrPrefixTokenRoles byte = 0x04 // tokenID -> []roleID rrPrefixRoleMembers byte = 0x05 // roleID -> []tokenID rrPrefixPermission byte = 0x06 // roleID + resource + action -> PermissionGrant rrPrefixRolePermissions byte = 0x07 // roleID -> []permission keys rrPrefixCounter byte = 0x10 // "counter" -> next roleID rrPrefixConfig byte = 0x11 // "config" -> config rrPrefixAddressAssignment byte = 0x12 // address + roleID -> AddressRoleAssignment rrPrefixAddressRoles byte = 0x13 // address -> []roleID ) // Max lengths for validation. const ( maxRoleNameLength = 64 maxRoleDescriptionLength = 256 maxResourceLength = 128 maxActionLength = 64 ) // Errors. var ( ErrRoleNotFound = errors.New("role not found") ErrRoleNameExists = errors.New("role name already exists") ErrRoleNameTooLong = errors.New("role name too long") ErrRoleDescTooLong = errors.New("role description too long") ErrRoleNotActive = errors.New("role is not active") ErrAssignmentNotFound = errors.New("role assignment not found") ErrAssignmentExists = errors.New("role assignment already exists") ErrAssignmentExpired = errors.New("role assignment has expired") ErrPermissionNotFound = errors.New("permission not found") ErrPermissionExists = errors.New("permission already exists") ErrResourceTooLong = errors.New("resource identifier too long") ErrActionTooLong = errors.New("action identifier too long") ErrInvalidScope = errors.New("invalid scope") ErrRoleRegistryNotCommittee = errors.New("caller is not a committee member") ErrBuiltinRole = errors.New("cannot modify built-in role") ErrInvalidParentRole = errors.New("invalid parent role") ErrCircularHierarchy = errors.New("circular role hierarchy detected") ) // Event names. const ( RoleCreatedEvent = "RoleCreated" RoleDeletedEvent = "RoleDeleted" RoleGrantedEvent = "RoleGranted" RoleRevokedEvent = "RoleRevoked" PermissionAssignedEvent = "PermissionAssigned" PermissionRemovedEvent = "PermissionRemoved" ) var ( _ interop.Contract = (*RoleRegistry)(nil) _ dao.NativeContractCache = (*RoleRegistryCache)(nil) ) // Copy implements NativeContractCache interface. func (c *RoleRegistryCache) Copy() dao.NativeContractCache { return &RoleRegistryCache{roleCount: c.roleCount} } // newRoleRegistry creates a new RoleRegistry contract. func newRoleRegistry(tutusCommittee []util.Uint160) *RoleRegistry { r := &RoleRegistry{ ContractMD: *interop.NewContractMD(nativenames.RoleRegistry, nativeids.RoleRegistry), tutusCommittee: tutusCommittee, } defer r.BuildHFSpecificMD(r.ActiveIn()) // Query methods desc := NewDescriptor("totalRoles", smartcontract.IntegerType) md := NewMethodAndPrice(r.totalRoles, 1<<15, callflag.ReadStates) r.AddMethod(md, desc) desc = NewDescriptor("getRole", smartcontract.ArrayType, manifest.NewParameter("roleID", smartcontract.IntegerType)) md = NewMethodAndPrice(r.getRole, 1<<15, callflag.ReadStates) r.AddMethod(md, desc) desc = NewDescriptor("getRoleByName", smartcontract.ArrayType, manifest.NewParameter("name", smartcontract.StringType)) md = NewMethodAndPrice(r.getRoleByName, 1<<15, callflag.ReadStates) r.AddMethod(md, desc) desc = NewDescriptor("hasRole", smartcontract.BoolType, manifest.NewParameter("address", smartcontract.Hash160Type), manifest.NewParameter("roleID", smartcontract.IntegerType)) md = NewMethodAndPrice(r.hasRole, 1<<15, callflag.ReadStates) r.AddMethod(md, desc) desc = NewDescriptor("getRolesForAddress", smartcontract.ArrayType, manifest.NewParameter("address", smartcontract.Hash160Type)) md = NewMethodAndPrice(r.getRolesForAddress, 1<<15, callflag.ReadStates) r.AddMethod(md, desc) desc = NewDescriptor("getPermissions", smartcontract.ArrayType, manifest.NewParameter("roleID", smartcontract.IntegerType)) md = NewMethodAndPrice(r.getPermissions, 1<<15, callflag.ReadStates) r.AddMethod(md, desc) desc = NewDescriptor("hasPermission", smartcontract.BoolType, manifest.NewParameter("address", smartcontract.Hash160Type), manifest.NewParameter("resource", smartcontract.StringType), manifest.NewParameter("action", smartcontract.StringType), manifest.NewParameter("scope", smartcontract.IntegerType)) md = NewMethodAndPrice(r.hasPermission, 1<<15, callflag.ReadStates) r.AddMethod(md, desc) // Admin methods (committee-only) desc = NewDescriptor("createRole", smartcontract.IntegerType, manifest.NewParameter("name", smartcontract.StringType), manifest.NewParameter("description", smartcontract.StringType), manifest.NewParameter("parentID", smartcontract.IntegerType)) md = NewMethodAndPrice(r.createRole, 1<<15, callflag.States|callflag.AllowNotify) r.AddMethod(md, desc) desc = NewDescriptor("deleteRole", smartcontract.BoolType, manifest.NewParameter("roleID", smartcontract.IntegerType)) md = NewMethodAndPrice(r.deleteRole, 1<<15, callflag.States|callflag.AllowNotify) r.AddMethod(md, desc) desc = NewDescriptor("grantRole", smartcontract.BoolType, manifest.NewParameter("address", smartcontract.Hash160Type), manifest.NewParameter("roleID", smartcontract.IntegerType), manifest.NewParameter("expiresAt", smartcontract.IntegerType)) md = NewMethodAndPrice(r.grantRole, 1<<15, callflag.States|callflag.AllowNotify) r.AddMethod(md, desc) desc = NewDescriptor("revokeRole", smartcontract.BoolType, manifest.NewParameter("address", smartcontract.Hash160Type), manifest.NewParameter("roleID", smartcontract.IntegerType)) md = NewMethodAndPrice(r.revokeRole, 1<<15, callflag.States|callflag.AllowNotify) r.AddMethod(md, desc) desc = NewDescriptor("assignPermission", smartcontract.BoolType, manifest.NewParameter("roleID", smartcontract.IntegerType), manifest.NewParameter("resource", smartcontract.StringType), manifest.NewParameter("action", smartcontract.StringType), manifest.NewParameter("scope", smartcontract.IntegerType)) md = NewMethodAndPrice(r.assignPermission, 1<<15, callflag.States|callflag.AllowNotify) r.AddMethod(md, desc) desc = NewDescriptor("removePermission", smartcontract.BoolType, manifest.NewParameter("roleID", smartcontract.IntegerType), manifest.NewParameter("resource", smartcontract.StringType), manifest.NewParameter("action", smartcontract.StringType)) md = NewMethodAndPrice(r.removePermission, 1<<15, callflag.States|callflag.AllowNotify) r.AddMethod(md, desc) // Events eDesc := NewEventDescriptor(RoleCreatedEvent, manifest.NewParameter("roleID", smartcontract.IntegerType), manifest.NewParameter("name", smartcontract.StringType), manifest.NewParameter("parentID", smartcontract.IntegerType), manifest.NewParameter("createdBy", smartcontract.Hash160Type)) eMD := NewEvent(eDesc) r.AddEvent(eMD) eDesc = NewEventDescriptor(RoleDeletedEvent, manifest.NewParameter("roleID", smartcontract.IntegerType), manifest.NewParameter("deletedBy", smartcontract.Hash160Type)) eMD = NewEvent(eDesc) r.AddEvent(eMD) eDesc = NewEventDescriptor(RoleGrantedEvent, manifest.NewParameter("address", smartcontract.Hash160Type), manifest.NewParameter("roleID", smartcontract.IntegerType), manifest.NewParameter("expiresAt", smartcontract.IntegerType), manifest.NewParameter("grantedBy", smartcontract.Hash160Type)) eMD = NewEvent(eDesc) r.AddEvent(eMD) eDesc = NewEventDescriptor(RoleRevokedEvent, manifest.NewParameter("address", smartcontract.Hash160Type), manifest.NewParameter("roleID", smartcontract.IntegerType), manifest.NewParameter("revokedBy", smartcontract.Hash160Type)) eMD = NewEvent(eDesc) r.AddEvent(eMD) eDesc = NewEventDescriptor(PermissionAssignedEvent, manifest.NewParameter("roleID", smartcontract.IntegerType), manifest.NewParameter("resource", smartcontract.StringType), manifest.NewParameter("action", smartcontract.StringType), manifest.NewParameter("scope", smartcontract.IntegerType)) eMD = NewEvent(eDesc) r.AddEvent(eMD) eDesc = NewEventDescriptor(PermissionRemovedEvent, manifest.NewParameter("roleID", smartcontract.IntegerType), manifest.NewParameter("resource", smartcontract.StringType), manifest.NewParameter("action", smartcontract.StringType)) eMD = NewEvent(eDesc) r.AddEvent(eMD) return r } // Initialize initializes RoleRegistry contract at genesis. func (r *RoleRegistry) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { if hf != r.ActiveIn() { return nil } cache := &RoleRegistryCache{roleCount: 0} ic.DAO.SetCache(r.ID, cache) // Initialize role counter starting at the first custom role ID (after built-ins) r.putRoleCounter(ic.DAO, 5) // Built-in roles are 1-4, custom roles start at 5 // Create built-in roles builtinRoles := []struct { id uint64 name string description string }{ {RoleCommittee, "COMMITTEE", "Tutus Committee member (designated officials)"}, {RoleRegistrar, "REGISTRAR", "Can register new Vitas"}, {RoleAttestor, "ATTESTOR", "Can attest identity attributes"}, {RoleOperator, "OPERATOR", "System operator (node management)"}, } for _, br := range builtinRoles { role := &state.Role{ ID: br.id, Name: br.name, Description: br.description, ParentID: 0, CreatedAt: 0, CreatedBy: util.Uint160{}, Active: true, } err := r.putRole(ic.DAO, role) if err != nil { return fmt.Errorf("failed to create built-in role %s: %w", br.name, err) } r.putRoleNameIndex(ic.DAO, br.name, br.id) cache.roleCount++ } // Grant COMMITTEE role to initial committee members from config for _, addr := range r.tutusCommittee { assignment := &state.AddressRoleAssignment{ Address: addr, RoleID: RoleCommittee, GrantedAt: 0, GrantedBy: util.Uint160{}, ExpiresAt: 0, // Never expires Active: true, } err := r.putAddressRoleAssignment(ic.DAO, assignment) if err != nil { return fmt.Errorf("failed to grant COMMITTEE role to %s: %w", addr.StringLE(), err) } } return nil } // InitializeCache initializes RoleRegistry cache from DAO. func (r *RoleRegistry) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { cache := &RoleRegistryCache{} cache.roleCount = r.getRoleCounter(d) d.SetCache(r.ID, cache) return nil } // OnPersist implements the Contract interface. func (r *RoleRegistry) OnPersist(ic *interop.Context) error { return nil } // PostPersist implements the Contract interface. func (r *RoleRegistry) PostPersist(ic *interop.Context) error { return nil } // Metadata returns contract metadata. func (r *RoleRegistry) Metadata() *interop.ContractMD { return &r.ContractMD } // ActiveIn implements the Contract interface. func (r *RoleRegistry) ActiveIn() *config.Hardfork { return nil } // CheckCommittee returns true if the caller has the COMMITTEE role. // This replaces NEO.CheckCommittee() throughout Tutus for democratic governance. // Falls back to NEO.CheckCommittee() for backwards compatibility with StandbyCommittee. func (r *RoleRegistry) CheckCommittee(ic *interop.Context) bool { caller := ic.VM.GetCallingScriptHash() // Check if caller has COMMITTEE role in RoleRegistry if r.HasRoleInternal(ic.DAO, caller, RoleCommittee, ic.Block.Index) { return true } // Check if caller is in the tutusCommittee config for _, addr := range r.tutusCommittee { if addr.Equals(caller) { return true } } // Fallback to Annos.CheckCommittee for backwards compatibility // This allows StandbyCommittee to work when TutusCommittee is not configured if r.Annos != nil && r.Annos.CheckCommittee(ic) { return true } return false } // CheckCommitteeWitness checks if the caller has committee signature. func (r *RoleRegistry) checkCommitteeWitness(ic *interop.Context) error { if !r.CheckCommittee(ic) { // Fall back to checking if any TutusCommittee member signed the transaction for _, addr := range r.tutusCommittee { if ok, _ := runtime.CheckHashedWitness(ic, addr); ok { return nil } } // Check addresses that have been granted COMMITTEE role committeeAddrs := r.getAddressesWithRole(ic.DAO, RoleCommittee) for _, addr := range committeeAddrs { if ok, _ := runtime.CheckHashedWitness(ic, addr); ok { return nil } } // Fallback to Annos.CheckCommittee for backwards compatibility // This allows StandbyCommittee to work when TutusCommittee is not configured if r.Annos != nil && r.Annos.CheckCommittee(ic) { return nil } return ErrRoleRegistryNotCommittee } return nil } // HasRoleInternal checks if an address has a specific role (including hierarchy). // A user has a role if they have it directly, OR if they have a child role that inherits from it. func (r *RoleRegistry) HasRoleInternal(d *dao.Simple, address util.Uint160, roleID uint64, blockHeight uint32) bool { // Get all roles directly assigned to this address assignedRoleIDs := r.getAddressRoleIDs(d, address) for _, assignedRoleID := range assignedRoleIDs { // Check if assignment is valid assignment := r.getAddressRoleAssignment(d, address, assignedRoleID) if assignment == nil || !assignment.Active { continue } if assignment.ExpiresAt != 0 && assignment.ExpiresAt <= blockHeight { continue } // Check if this assigned role equals the queried role, or inherits from it if r.roleInheritsFrom(d, assignedRoleID, roleID) { return true } } return false } // roleInheritsFrom checks if roleID equals targetRoleID, or if roleID has targetRoleID as an ancestor. func (r *RoleRegistry) roleInheritsFrom(d *dao.Simple, roleID, targetRoleID uint64) bool { // Direct match if roleID == targetRoleID { return true } // Traverse up the hierarchy role := r.getRoleInternal(d, roleID) if role != nil && role.ParentID != 0 { return r.roleInheritsFrom(d, role.ParentID, targetRoleID) } return false } // HasPermissionInternal checks if an address has a specific permission via roles. func (r *RoleRegistry) HasPermissionInternal(d *dao.Simple, address util.Uint160, resource, action string, scope state.Scope, blockHeight uint32) bool { // Get all roles for this address roleIDs := r.getAddressRoleIDs(d, address) for _, roleID := range roleIDs { // Check if assignment is valid assignment := r.getAddressRoleAssignment(d, address, roleID) if assignment == nil || !assignment.Active { continue } if assignment.ExpiresAt != 0 && assignment.ExpiresAt <= blockHeight { continue } // Check if role has the permission if r.roleHasPermission(d, roleID, resource, action, scope) { return true } } return false } // roleHasPermission checks if a role (or its parents) has a specific permission. func (r *RoleRegistry) roleHasPermission(d *dao.Simple, roleID uint64, resource, action string, scope state.Scope) bool { // COMMITTEE role has all permissions if roleID == RoleCommittee { return true } // Check direct permission perm := r.getPermission(d, roleID, resource, action) if perm != nil && perm.Scope <= scope { return true } // Check parent roles role := r.getRoleInternal(d, roleID) if role != nil && role.ParentID != 0 { return r.roleHasPermission(d, role.ParentID, resource, action, scope) } return false } // Storage helpers func (r *RoleRegistry) makeRoleKey(roleID uint64) []byte { key := make([]byte, 9) key[0] = rrPrefixRole binary.BigEndian.PutUint64(key[1:], roleID) return key } func (r *RoleRegistry) makeRoleNameKey(name string) []byte { key := make([]byte, 1+len(name)) key[0] = rrPrefixRoleName copy(key[1:], []byte(name)) return key } func (r *RoleRegistry) makeAddressRoleKey(address util.Uint160, roleID uint64) []byte { key := make([]byte, 1+20+8) key[0] = rrPrefixAddressAssignment copy(key[1:21], address.BytesBE()) binary.BigEndian.PutUint64(key[21:], roleID) return key } func (r *RoleRegistry) makeAddressRolesKey(address util.Uint160) []byte { key := make([]byte, 1+20) key[0] = rrPrefixAddressRoles copy(key[1:], address.BytesBE()) return key } func (r *RoleRegistry) makePermissionKey(roleID uint64, resource, action string) []byte { key := make([]byte, 1+8+len(resource)+1+len(action)) key[0] = rrPrefixPermission binary.BigEndian.PutUint64(key[1:9], roleID) copy(key[9:9+len(resource)], resource) key[9+len(resource)] = 0 // separator copy(key[10+len(resource):], action) return key } func (r *RoleRegistry) putRole(d *dao.Simple, role *state.Role) error { key := r.makeRoleKey(role.ID) return putConvertibleToDAO(r.ID, d, key, role) } func (r *RoleRegistry) getRoleInternal(d *dao.Simple, roleID uint64) *state.Role { key := r.makeRoleKey(roleID) si := d.GetStorageItem(r.ID, key) if si == nil { return nil } role := new(state.Role) err := stackitem.DeserializeConvertible(si, role) if err != nil { return nil } return role } func (r *RoleRegistry) putRoleNameIndex(d *dao.Simple, name string, roleID uint64) { key := r.makeRoleNameKey(name) data := make([]byte, 8) binary.BigEndian.PutUint64(data, roleID) d.PutStorageItem(r.ID, key, data) } func (r *RoleRegistry) getRoleIDByName(d *dao.Simple, name string) (uint64, bool) { key := r.makeRoleNameKey(name) si := d.GetStorageItem(r.ID, key) if si == nil { return 0, false } return binary.BigEndian.Uint64(si), true } func (r *RoleRegistry) putAddressRoleAssignment(d *dao.Simple, assignment *state.AddressRoleAssignment) error { key := r.makeAddressRoleKey(assignment.Address, assignment.RoleID) err := putConvertibleToDAO(r.ID, d, key, assignment) if err != nil { return err } // Update address roles index r.addAddressRoleIndex(d, assignment.Address, assignment.RoleID) return nil } func (r *RoleRegistry) getAddressRoleAssignment(d *dao.Simple, address util.Uint160, roleID uint64) *state.AddressRoleAssignment { key := r.makeAddressRoleKey(address, roleID) si := d.GetStorageItem(r.ID, key) if si == nil { return nil } assignment := new(state.AddressRoleAssignment) err := stackitem.DeserializeConvertible(si, assignment) if err != nil { return nil } return assignment } func (r *RoleRegistry) deleteAddressRoleAssignment(d *dao.Simple, address util.Uint160, roleID uint64) { key := r.makeAddressRoleKey(address, roleID) d.DeleteStorageItem(r.ID, key) r.removeAddressRoleIndex(d, address, roleID) } func (r *RoleRegistry) addAddressRoleIndex(d *dao.Simple, address util.Uint160, roleID uint64) { key := r.makeAddressRolesKey(address) si := d.GetStorageItem(r.ID, key) var roleIDs []uint64 if si != nil { roleIDs = r.decodeRoleIDs(si) } // Check if already exists for _, id := range roleIDs { if id == roleID { return } } roleIDs = append(roleIDs, roleID) d.PutStorageItem(r.ID, key, r.encodeRoleIDs(roleIDs)) } func (r *RoleRegistry) removeAddressRoleIndex(d *dao.Simple, address util.Uint160, roleID uint64) { key := r.makeAddressRolesKey(address) si := d.GetStorageItem(r.ID, key) if si == nil { return } roleIDs := r.decodeRoleIDs(si) newIDs := make([]uint64, 0, len(roleIDs)) for _, id := range roleIDs { if id != roleID { newIDs = append(newIDs, id) } } if len(newIDs) == 0 { d.DeleteStorageItem(r.ID, key) } else { d.PutStorageItem(r.ID, key, r.encodeRoleIDs(newIDs)) } } func (r *RoleRegistry) getAddressRoleIDs(d *dao.Simple, address util.Uint160) []uint64 { key := r.makeAddressRolesKey(address) si := d.GetStorageItem(r.ID, key) if si == nil { return nil } return r.decodeRoleIDs(si) } func (r *RoleRegistry) getAddressesWithRole(d *dao.Simple, roleID uint64) []util.Uint160 { var addresses []util.Uint160 prefix := []byte{rrPrefixAddressAssignment} d.Seek(r.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) >= 28 { storedRoleID := binary.BigEndian.Uint64(k[20:28]) if storedRoleID == roleID { addr, err := util.Uint160DecodeBytesBE(k[:20]) if err == nil { assignment := new(state.AddressRoleAssignment) if err := stackitem.DeserializeConvertible(v, assignment); err == nil && assignment.Active { addresses = append(addresses, addr) } } } } return true }) return addresses } func (r *RoleRegistry) encodeRoleIDs(ids []uint64) []byte { data := make([]byte, 8*len(ids)) for i, id := range ids { binary.BigEndian.PutUint64(data[i*8:], id) } return data } func (r *RoleRegistry) decodeRoleIDs(data []byte) []uint64 { count := len(data) / 8 ids := make([]uint64, count) for i := 0; i < count; i++ { ids[i] = binary.BigEndian.Uint64(data[i*8:]) } return ids } func (r *RoleRegistry) putPermission(d *dao.Simple, perm *state.PermissionGrant) error { key := r.makePermissionKey(perm.RoleID, perm.Resource, perm.Action) return putConvertibleToDAO(r.ID, d, key, perm) } func (r *RoleRegistry) getPermission(d *dao.Simple, roleID uint64, resource, action string) *state.PermissionGrant { key := r.makePermissionKey(roleID, resource, action) si := d.GetStorageItem(r.ID, key) if si == nil { return nil } perm := new(state.PermissionGrant) err := stackitem.DeserializeConvertible(si, perm) if err != nil { return nil } return perm } func (r *RoleRegistry) deletePermission(d *dao.Simple, roleID uint64, resource, action string) { key := r.makePermissionKey(roleID, resource, action) d.DeleteStorageItem(r.ID, key) } func (r *RoleRegistry) getRoleCounter(d *dao.Simple) uint64 { key := []byte{rrPrefixCounter} si := d.GetStorageItem(r.ID, key) if si == nil { return 0 } return binary.BigEndian.Uint64(si) } func (r *RoleRegistry) putRoleCounter(d *dao.Simple, count uint64) { key := []byte{rrPrefixCounter} data := make([]byte, 8) binary.BigEndian.PutUint64(data, count) d.PutStorageItem(r.ID, key, data) } func (r *RoleRegistry) nextRoleID(d *dao.Simple, cache *RoleRegistryCache) uint64 { id := r.getRoleCounter(d) r.putRoleCounter(d, id+1) cache.roleCount++ return id } // Contract methods func (r *RoleRegistry) totalRoles(ic *interop.Context, args []stackitem.Item) stackitem.Item { cache := ic.DAO.GetROCache(r.ID).(*RoleRegistryCache) return stackitem.NewBigInteger(big.NewInt(int64(cache.roleCount))) } func (r *RoleRegistry) getRole(ic *interop.Context, args []stackitem.Item) stackitem.Item { roleID := toBigInt(args[0]).Uint64() role := r.getRoleInternal(ic.DAO, roleID) if role == nil { return stackitem.Null{} } item, _ := role.ToStackItem() return item } func (r *RoleRegistry) getRoleByName(ic *interop.Context, args []stackitem.Item) stackitem.Item { name := toString(args[0]) roleID, found := r.getRoleIDByName(ic.DAO, name) if !found { return stackitem.Null{} } role := r.getRoleInternal(ic.DAO, roleID) if role == nil { return stackitem.Null{} } item, _ := role.ToStackItem() return item } func (r *RoleRegistry) hasRole(ic *interop.Context, args []stackitem.Item) stackitem.Item { address := toUint160(args[0]) roleID := toBigInt(args[1]).Uint64() return stackitem.NewBool(r.HasRoleInternal(ic.DAO, address, roleID, ic.Block.Index)) } func (r *RoleRegistry) getRolesForAddress(ic *interop.Context, args []stackitem.Item) stackitem.Item { address := toUint160(args[0]) roleIDs := r.getAddressRoleIDs(ic.DAO, address) items := make([]stackitem.Item, len(roleIDs)) for i, id := range roleIDs { items[i] = stackitem.NewBigInteger(big.NewInt(int64(id))) } return stackitem.NewArray(items) } func (r *RoleRegistry) getPermissions(ic *interop.Context, args []stackitem.Item) stackitem.Item { roleID := toBigInt(args[0]).Uint64() var perms []stackitem.Item prefix := make([]byte, 9) prefix[0] = rrPrefixPermission binary.BigEndian.PutUint64(prefix[1:], roleID) ic.DAO.Seek(r.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { perm := new(state.PermissionGrant) if err := stackitem.DeserializeConvertible(v, perm); err == nil { item, _ := perm.ToStackItem() perms = append(perms, item) } return true }) return stackitem.NewArray(perms) } func (r *RoleRegistry) hasPermission(ic *interop.Context, args []stackitem.Item) stackitem.Item { address := toUint160(args[0]) resource := toString(args[1]) action := toString(args[2]) scopeInt := toBigInt(args[3]).Int64() scope := state.Scope(scopeInt) return stackitem.NewBool(r.HasPermissionInternal(ic.DAO, address, resource, action, scope, ic.Block.Index)) } func (r *RoleRegistry) createRole(ic *interop.Context, args []stackitem.Item) stackitem.Item { if err := r.checkCommitteeWitness(ic); err != nil { panic(err) } name := toString(args[0]) description := toString(args[1]) parentID := toBigInt(args[2]).Uint64() // Validation if len(name) > maxRoleNameLength { panic(ErrRoleNameTooLong) } if len(description) > maxRoleDescriptionLength { panic(ErrRoleDescTooLong) } if _, exists := r.getRoleIDByName(ic.DAO, name); exists { panic(ErrRoleNameExists) } if parentID != 0 { parent := r.getRoleInternal(ic.DAO, parentID) if parent == nil { panic(ErrInvalidParentRole) } if !parent.Active { panic(ErrRoleNotActive) } } cache := ic.DAO.GetRWCache(r.ID).(*RoleRegistryCache) roleID := r.nextRoleID(ic.DAO, cache) caller := ic.VM.GetCallingScriptHash() role := &state.Role{ ID: roleID, Name: name, Description: description, ParentID: parentID, CreatedAt: ic.Block.Index, CreatedBy: caller, Active: true, } if err := r.putRole(ic.DAO, role); err != nil { panic(err) } r.putRoleNameIndex(ic.DAO, name, roleID) err := ic.AddNotification(r.Hash, RoleCreatedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(roleID))), stackitem.NewByteArray([]byte(name)), stackitem.NewBigInteger(big.NewInt(int64(parentID))), stackitem.NewByteArray(caller.BytesBE()), })) if err != nil { panic(err) } return stackitem.NewBigInteger(big.NewInt(int64(roleID))) } func (r *RoleRegistry) deleteRole(ic *interop.Context, args []stackitem.Item) stackitem.Item { if err := r.checkCommitteeWitness(ic); err != nil { panic(err) } roleID := toBigInt(args[0]).Uint64() // Cannot delete built-in roles if roleID <= RoleOperator { panic(ErrBuiltinRole) } role := r.getRoleInternal(ic.DAO, roleID) if role == nil { panic(ErrRoleNotFound) } role.Active = false if err := r.putRole(ic.DAO, role); err != nil { panic(err) } caller := ic.VM.GetCallingScriptHash() err := ic.AddNotification(r.Hash, RoleDeletedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(roleID))), stackitem.NewByteArray(caller.BytesBE()), })) if err != nil { panic(err) } return stackitem.NewBool(true) } func (r *RoleRegistry) grantRole(ic *interop.Context, args []stackitem.Item) stackitem.Item { if err := r.checkCommitteeWitness(ic); err != nil { panic(err) } address := toUint160(args[0]) roleID := toBigInt(args[1]).Uint64() expiresAt := uint32(toBigInt(args[2]).Uint64()) // Verify role exists and is active role := r.getRoleInternal(ic.DAO, roleID) if role == nil { panic(ErrRoleNotFound) } if !role.Active { panic(ErrRoleNotActive) } // Check if already assigned existing := r.getAddressRoleAssignment(ic.DAO, address, roleID) if existing != nil && existing.Active { panic(ErrAssignmentExists) } caller := ic.VM.GetCallingScriptHash() assignment := &state.AddressRoleAssignment{ Address: address, RoleID: roleID, GrantedAt: ic.Block.Index, GrantedBy: caller, ExpiresAt: expiresAt, Active: true, } if err := r.putAddressRoleAssignment(ic.DAO, assignment); err != nil { panic(err) } err := ic.AddNotification(r.Hash, RoleGrantedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(address.BytesBE()), stackitem.NewBigInteger(big.NewInt(int64(roleID))), stackitem.NewBigInteger(big.NewInt(int64(expiresAt))), stackitem.NewByteArray(caller.BytesBE()), })) if err != nil { panic(err) } return stackitem.NewBool(true) } func (r *RoleRegistry) revokeRole(ic *interop.Context, args []stackitem.Item) stackitem.Item { if err := r.checkCommitteeWitness(ic); err != nil { panic(err) } address := toUint160(args[0]) roleID := toBigInt(args[1]).Uint64() assignment := r.getAddressRoleAssignment(ic.DAO, address, roleID) if assignment == nil || !assignment.Active { panic(ErrAssignmentNotFound) } assignment.Active = false if err := r.putAddressRoleAssignment(ic.DAO, assignment); err != nil { panic(err) } caller := ic.VM.GetCallingScriptHash() err := ic.AddNotification(r.Hash, RoleRevokedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(address.BytesBE()), stackitem.NewBigInteger(big.NewInt(int64(roleID))), stackitem.NewByteArray(caller.BytesBE()), })) if err != nil { panic(err) } return stackitem.NewBool(true) } func (r *RoleRegistry) assignPermission(ic *interop.Context, args []stackitem.Item) stackitem.Item { if err := r.checkCommitteeWitness(ic); err != nil { panic(err) } roleID := toBigInt(args[0]).Uint64() resource := toString(args[1]) action := toString(args[2]) scopeInt := toBigInt(args[3]).Int64() scope := state.Scope(scopeInt) // Validation if len(resource) > maxResourceLength { panic(ErrResourceTooLong) } if len(action) > maxActionLength { panic(ErrActionTooLong) } if scope > state.ScopeDelegated { panic(ErrInvalidScope) } // Verify role exists and is active role := r.getRoleInternal(ic.DAO, roleID) if role == nil { panic(ErrRoleNotFound) } if !role.Active { panic(ErrRoleNotActive) } // Check if already exists existing := r.getPermission(ic.DAO, roleID, resource, action) if existing != nil { panic(ErrPermissionExists) } caller := ic.VM.GetCallingScriptHash() perm := &state.PermissionGrant{ RoleID: roleID, Resource: resource, Action: action, Scope: scope, GrantedAt: ic.Block.Index, GrantedBy: caller, } if err := r.putPermission(ic.DAO, perm); err != nil { panic(err) } err := ic.AddNotification(r.Hash, PermissionAssignedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(roleID))), stackitem.NewByteArray([]byte(resource)), stackitem.NewByteArray([]byte(action)), stackitem.NewBigInteger(big.NewInt(int64(scope))), })) if err != nil { panic(err) } return stackitem.NewBool(true) } func (r *RoleRegistry) removePermission(ic *interop.Context, args []stackitem.Item) stackitem.Item { if err := r.checkCommitteeWitness(ic); err != nil { panic(err) } roleID := toBigInt(args[0]).Uint64() resource := toString(args[1]) action := toString(args[2]) // Verify permission exists existing := r.getPermission(ic.DAO, roleID, resource, action) if existing == nil { panic(ErrPermissionNotFound) } r.deletePermission(ic.DAO, roleID, resource, action) err := ic.AddNotification(r.Hash, PermissionRemovedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(roleID))), stackitem.NewByteArray([]byte(resource)), stackitem.NewByteArray([]byte(action)), })) if err != nil { panic(err) } return stackitem.NewBool(true) }