tutus-chain/pkg/core/native/role_registry.go

1062 lines
33 KiB
Go

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
// Tutus is used for fallback committee checks when TutusCommittee is not set.
Tutus ITutus
// 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 Tutus.CheckCommittee for backwards compatibility
// This allows StandbyCommittee to work when TutusCommittee is not configured
if r.Tutus != nil && r.Tutus.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 Tutus.CheckCommittee for backwards compatibility
// This allows StandbyCommittee to work when TutusCommittee is not configured
if r.Tutus != nil && r.Tutus.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)
}