1062 lines
33 KiB
Go
Executable File
1062 lines
33 KiB
Go
Executable File
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)
|
|
}
|