Add RoleRegistry native contract for hierarchical RBAC

Implements RoleRegistry as a native contract for role-based access control
that integrates with PersonToken for democratic governance.

Key features:
- Built-in roles: COMMITTEE, REGISTRAR, ATTESTOR, OPERATOR
- Hierarchical roles with parent inheritance
- Permission system (resource/action/scope tuples)
- CheckCommittee() method for admin authorization
- TutusCommittee config for initial committee members

PersonToken integration:
- Added RoleRegistry field for cross-contract calls
- checkCommittee helper delegates to RoleRegistry with NEO fallback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Tutus Development 2025-12-20 01:35:26 +00:00
parent 99ba041a85
commit de34f66286
9 changed files with 1759 additions and 13 deletions

View File

@ -56,6 +56,11 @@ type (
SeedList []string `yaml:"SeedList"`
StandbyCommittee []string `yaml:"StandbyCommittee"`
// TutusCommittee is the list of initial Tutus committee member addresses.
// These are granted the COMMITTEE role in RoleRegistry at initialization.
// Unlike StandbyCommittee (for consensus), TutusCommittee represents
// designated officials for democratic governance (1 person = 1 vote).
TutusCommittee []string `yaml:"TutusCommittee"`
// StateRootInHeader enables storing state root in block header.
StateRootInHeader bool `yaml:"StateRootInHeader"`
// StateSyncInterval is the number of blocks between state heights available for MPT state data synchronization.

View File

@ -120,6 +120,20 @@ type (
TokenExists(d *dao.Simple, owner util.Uint160) bool
GetAttribute(d *dao.Simple, tokenID uint64, key string) (*state.Attribute, error)
}
// IRoleRegistry is an interface required from native RoleRegistry contract
// for interaction with Blockchain and other native contracts.
// RoleRegistry provides democratic governance for Tutus, replacing NEO.CheckCommittee().
IRoleRegistry interface {
interop.Contract
// CheckCommittee returns true if caller has COMMITTEE role.
// This replaces NEO.CheckCommittee() for Tutus democratic governance.
CheckCommittee(ic *interop.Context) bool
// HasRoleInternal checks if address has role (includes hierarchy).
HasRoleInternal(d *dao.Simple, address util.Uint160, roleID uint64, blockHeight uint32) bool
// HasPermissionInternal checks if address has permission via roles.
HasPermissionInternal(d *dao.Simple, address util.Uint160, resource, action string, scope state.Scope, blockHeight uint32) bool
}
)
// Contracts is a convenient wrapper around an arbitrary set of native contracts
@ -241,6 +255,12 @@ func (cs *Contracts) PersonToken() IPersonToken {
return cs.ByName(nativenames.PersonToken).(IPersonToken)
}
// RoleRegistry returns native IRoleRegistry contract implementation. It panics if
// there's no contract with proper name in cs.
func (cs *Contracts) RoleRegistry() IRoleRegistry {
return cs.ByName(nativenames.RoleRegistry).(IRoleRegistry)
}
// NewDefaultContracts returns a new set of default native contracts.
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
mgmt := NewManagement()
@ -280,6 +300,25 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
personToken := newPersonToken()
personToken.NEO = neo
// Parse TutusCommittee addresses from config
var tutusCommittee []util.Uint160
for _, addrStr := range cfg.TutusCommittee {
addr, err := util.Uint160DecodeStringLE(addrStr)
if err != nil {
// Try parsing as hex (BE format)
addr, err = util.Uint160DecodeStringBE(addrStr)
if err != nil {
continue // Skip invalid addresses
}
}
tutusCommittee = append(tutusCommittee, addr)
}
roleRegistry := newRoleRegistry(tutusCommittee)
roleRegistry.NEO = neo
// Set RoleRegistry on PersonToken for cross-contract integration
personToken.RoleRegistry = roleRegistry
return []interop.Contract{
mgmt,
s,
@ -293,5 +332,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
notary,
treasury,
personToken,
roleRegistry,
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,296 @@
package native_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
"github.com/tutus-one/tutus-chain/pkg/neotest"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
func newRoleRegistryClient(t *testing.T) *neotest.ContractInvoker {
return newNativeClient(t, nativenames.RoleRegistry)
}
// TestRoleRegistry_BuiltinRoles tests that built-in roles are created at initialization.
func TestRoleRegistry_BuiltinRoles(t *testing.T) {
c := newRoleRegistryClient(t)
// Check totalRoles returns at least 4 (the built-in roles)
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
count, err := stack[0].TryInteger()
require.NoError(t, err)
require.GreaterOrEqual(t, count.Int64(), int64(4))
}, "totalRoles")
// Check COMMITTEE role exists (ID=1)
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
// Should be an array (role struct)
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array for role")
require.Equal(t, 7, len(arr)) // Role has 7 fields
// Check role ID is 1
id, err := arr[0].TryInteger()
require.NoError(t, err)
require.Equal(t, int64(1), id.Int64())
// Check name is COMMITTEE
name, err := arr[1].TryBytes()
require.NoError(t, err)
require.Equal(t, "COMMITTEE", string(name))
// Check role is active
active, err := arr[6].TryBool()
require.NoError(t, err)
require.True(t, active)
}, "getRole", 1)
// Check REGISTRAR role exists (ID=2)
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array for role")
name, err := arr[1].TryBytes()
require.NoError(t, err)
require.Equal(t, "REGISTRAR", string(name))
}, "getRole", 2)
// Check ATTESTOR role exists (ID=3)
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array for role")
name, err := arr[1].TryBytes()
require.NoError(t, err)
require.Equal(t, "ATTESTOR", string(name))
}, "getRole", 3)
// Check OPERATOR role exists (ID=4)
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array for role")
name, err := arr[1].TryBytes()
require.NoError(t, err)
require.Equal(t, "OPERATOR", string(name))
}, "getRole", 4)
}
// TestRoleRegistry_GetRoleByName tests looking up roles by name.
func TestRoleRegistry_GetRoleByName(t *testing.T) {
c := newRoleRegistryClient(t)
// Look up COMMITTEE by name
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array for role")
id, err := arr[0].TryInteger()
require.NoError(t, err)
require.Equal(t, int64(1), id.Int64())
}, "getRoleByName", "COMMITTEE")
// Look up non-existent role by name
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
require.Nil(t, stack[0].Value())
}, "getRoleByName", "NONEXISTENT")
}
// TestRoleRegistry_HasRole tests checking if an address has a role.
func TestRoleRegistry_HasRole(t *testing.T) {
c := newRoleRegistryClient(t)
e := c.Executor
acc := e.NewAccount(t)
// Account should not have COMMITTEE role initially
c.Invoke(t, false, "hasRole", acc.ScriptHash(), 1)
// Grant COMMITTEE role (committee only)
committeeInvoker := c.WithSigners(c.Committee)
committeeInvoker.Invoke(t, true, "grantRole", acc.ScriptHash(), 1, 0)
// Now account should have COMMITTEE role
c.Invoke(t, true, "hasRole", acc.ScriptHash(), 1)
}
// TestRoleRegistry_GrantRevokeRole tests granting and revoking roles.
func TestRoleRegistry_GrantRevokeRole(t *testing.T) {
c := newRoleRegistryClient(t)
e := c.Executor
acc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
// Non-committee cannot grant roles
userInvoker := c.WithSigners(acc)
userInvoker.InvokeFail(t, "caller is not a committee member", "grantRole", acc.ScriptHash(), 2, 0)
// Committee can grant role
committeeInvoker.Invoke(t, true, "grantRole", acc.ScriptHash(), 2, 0)
// Check role is granted
c.Invoke(t, true, "hasRole", acc.ScriptHash(), 2)
// Get roles for address
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array")
require.GreaterOrEqual(t, len(arr), 1)
}, "getRolesForAddress", acc.ScriptHash())
// Non-committee cannot revoke roles
userInvoker.InvokeFail(t, "caller is not a committee member", "revokeRole", acc.ScriptHash(), 2)
// Committee can revoke role
committeeInvoker.Invoke(t, true, "revokeRole", acc.ScriptHash(), 2)
// Check role is revoked
c.Invoke(t, false, "hasRole", acc.ScriptHash(), 2)
}
// TestRoleRegistry_CreateRole tests creating custom roles.
func TestRoleRegistry_CreateRole(t *testing.T) {
c := newRoleRegistryClient(t)
e := c.Executor
acc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
// Non-committee cannot create roles
userInvoker := c.WithSigners(acc)
userInvoker.InvokeFail(t, "caller is not a committee member", "createRole", "CUSTOM_ROLE", "A custom role", 0)
// Committee can create role
committeeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
roleID, err := stack[0].TryInteger()
require.NoError(t, err)
require.GreaterOrEqual(t, roleID.Int64(), int64(5)) // Custom roles start at 5
}, "createRole", "CUSTOM_ROLE", "A custom role", 0)
// Verify role exists
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array for role")
name, err := arr[1].TryBytes()
require.NoError(t, err)
require.Equal(t, "CUSTOM_ROLE", string(name))
}, "getRoleByName", "CUSTOM_ROLE")
// Cannot create duplicate role name
committeeInvoker.InvokeFail(t, "role name already exists", "createRole", "CUSTOM_ROLE", "Duplicate", 0)
}
// TestRoleRegistry_DeleteRole tests deleting (deactivating) roles.
func TestRoleRegistry_DeleteRole(t *testing.T) {
c := newRoleRegistryClient(t)
committeeInvoker := c.WithSigners(c.Committee)
// Create a custom role first
var customRoleID int64
committeeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
roleID, _ := stack[0].TryInteger()
customRoleID = roleID.Int64()
}, "createRole", "DELETE_TEST", "Role to delete", 0)
// Cannot delete built-in roles
committeeInvoker.InvokeFail(t, "cannot modify built-in role", "deleteRole", 1)
committeeInvoker.InvokeFail(t, "cannot modify built-in role", "deleteRole", 2)
committeeInvoker.InvokeFail(t, "cannot modify built-in role", "deleteRole", 3)
committeeInvoker.InvokeFail(t, "cannot modify built-in role", "deleteRole", 4)
// Can delete custom role
committeeInvoker.Invoke(t, true, "deleteRole", customRoleID)
// Verify role is deactivated (still exists but not active)
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array for role")
active, err := arr[6].TryBool()
require.NoError(t, err)
require.False(t, active)
}, "getRole", customRoleID)
}
// TestRoleRegistry_Permissions tests assigning and checking permissions.
func TestRoleRegistry_Permissions(t *testing.T) {
c := newRoleRegistryClient(t)
e := c.Executor
acc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
// Create a custom role
var customRoleID int64
committeeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
roleID, _ := stack[0].TryInteger()
customRoleID = roleID.Int64()
}, "createRole", "PERM_TEST", "Permission test role", 0)
// Assign permission to role
committeeInvoker.Invoke(t, true, "assignPermission", customRoleID, "documents", "read", 0)
// Grant role to account
committeeInvoker.Invoke(t, true, "grantRole", acc.ScriptHash(), customRoleID, 0)
// Check account has permission
c.Invoke(t, true, "hasPermission", acc.ScriptHash(), "documents", "read", 0)
// Check account does NOT have other permission
c.Invoke(t, false, "hasPermission", acc.ScriptHash(), "documents", "write", 0)
// Get permissions for role
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array")
require.Equal(t, 1, len(arr))
}, "getPermissions", customRoleID)
// Remove permission
committeeInvoker.Invoke(t, true, "removePermission", customRoleID, "documents", "read")
// Check permission is gone
c.Invoke(t, false, "hasPermission", acc.ScriptHash(), "documents", "read", 0)
}
// TestRoleRegistry_RoleHierarchy tests role hierarchy (parent roles).
func TestRoleRegistry_RoleHierarchy(t *testing.T) {
c := newRoleRegistryClient(t)
e := c.Executor
acc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
// Create parent role
var parentRoleID int64
committeeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
roleID, _ := stack[0].TryInteger()
parentRoleID = roleID.Int64()
}, "createRole", "PARENT_ROLE", "Parent role", 0)
// Create child role with parent
var childRoleID int64
committeeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
roleID, _ := stack[0].TryInteger()
childRoleID = roleID.Int64()
}, "createRole", "CHILD_ROLE", "Child role", parentRoleID)
// Grant child role to account
committeeInvoker.Invoke(t, true, "grantRole", acc.ScriptHash(), childRoleID, 0)
// Check account has child role
c.Invoke(t, true, "hasRole", acc.ScriptHash(), childRoleID)
// Check account ALSO has parent role through hierarchy
c.Invoke(t, true, "hasRole", acc.ScriptHash(), parentRoleID)
}

View File

@ -31,4 +31,6 @@ const (
Treasury int32 = -11
// PersonToken is an ID of native PersonToken contract.
PersonToken int32 = -12
// RoleRegistry is an ID of native RoleRegistry contract.
RoleRegistry int32 = -13
)

View File

@ -12,8 +12,9 @@ const (
Notary = "Notary"
CryptoLib = "CryptoLib"
StdLib = "StdLib"
Treasury = "Treasury"
PersonToken = "PersonToken"
Treasury = "Treasury"
PersonToken = "PersonToken"
RoleRegistry = "RoleRegistry"
)
// All contains the list of all native contract names ordered by the contract ID.
@ -30,6 +31,7 @@ var All = []string{
Notary,
Treasury,
PersonToken,
RoleRegistry,
}
// IsValid checks if the name is a valid native contract's name.
@ -45,5 +47,6 @@ func IsValid(name string) bool {
name == CryptoLib ||
name == StdLib ||
name == Treasury ||
name == PersonToken
name == PersonToken ||
name == RoleRegistry
}

View File

@ -25,7 +25,8 @@ import (
// PersonToken represents a soul-bound identity native contract.
type PersonToken struct {
interop.ContractMD
NEO INEO
NEO INEO
RoleRegistry IRoleRegistry
}
// PersonTokenCache represents the cached state for PersonToken contract.
@ -129,6 +130,16 @@ func (c *PersonTokenCache) Copy() dao.NativeContractCache {
}
}
// checkCommittee checks if the caller has committee authority.
// Uses RoleRegistry if available, falls back to NEO.CheckCommittee().
func (p *PersonToken) checkCommittee(ic *interop.Context) bool {
if p.RoleRegistry != nil {
return p.RoleRegistry.CheckCommittee(ic)
}
// Fallback to NEO for backwards compatibility
return p.NEO.CheckCommittee(ic)
}
// newPersonToken creates a new PersonToken native contract.
func newPersonToken() *PersonToken {
p := &PersonToken{
@ -709,7 +720,7 @@ func (p *PersonToken) suspend(ic *interop.Context, args []stackitem.Item) stacki
reason := toString(args[1])
// Check committee
if !p.NEO.CheckCommittee(ic) {
if !p.checkCommittee(ic) {
panic(ErrNotCommittee)
}
@ -762,7 +773,7 @@ func (p *PersonToken) reinstate(ic *interop.Context, args []stackitem.Item) stac
owner := toUint160(args[0])
// Check committee
if !p.NEO.CheckCommittee(ic) {
if !p.checkCommittee(ic) {
panic(ErrNotCommittee)
}
@ -815,7 +826,7 @@ func (p *PersonToken) revoke(ic *interop.Context, args []stackitem.Item) stackit
reason := toString(args[1])
// Check committee
if !p.NEO.CheckCommittee(ic) {
if !p.checkCommittee(ic) {
panic(ErrNotCommittee)
}
@ -1044,7 +1055,7 @@ func (p *PersonToken) revokeAttribute(ic *interop.Context, args []stackitem.Item
caller := ic.VM.GetCallingScriptHash()
isOwner := caller.Equals(token.Owner)
isAttestor := caller.Equals(attr.Attestor)
isCommittee := p.NEO.CheckCommittee(ic)
isCommittee := p.checkCommittee(ic)
if isOwner {
ok, err := runtime.CheckHashedWitness(ic, token.Owner)
@ -1532,7 +1543,7 @@ func (p *PersonToken) approveRecovery(ic *interop.Context, args []stackitem.Item
}
// Check committee
if !p.NEO.CheckCommittee(ic) {
if !p.checkCommittee(ic) {
panic(ErrNotCommittee)
}
@ -1714,7 +1725,7 @@ func (p *PersonToken) cancelRecovery(ic *interop.Context, args []stackitem.Item)
caller := ic.VM.GetCallingScriptHash()
isOwner := caller.Equals(token.Owner)
isRequester := caller.Equals(req.Requester)
isCommittee := p.NEO.CheckCommittee(ic)
isCommittee := p.checkCommittee(ic)
if isOwner {
ok, err := runtime.CheckHashedWitness(ic, token.Owner)
@ -1845,7 +1856,7 @@ func (p *PersonToken) getCoreRoles(ic *interop.Context, token *state.PersonToken
}
// Check if user is a committee member
if p.NEO.CheckCommittee(ic) {
if p.checkCommittee(ic) {
roles |= 1 << uint64(CoreRoleCommittee)
roles |= 1 << uint64(CoreRoleAttestor) // Committee members can attest
roles |= 1 << uint64(CoreRoleRecovery) // Committee members participate in recovery
@ -1980,7 +1991,7 @@ func (p *PersonToken) requirePermission(ic *interop.Context, args []stackitem.It
// Stub: In future, this will check against RoleRegistry RBAC
// For now, committee members have all permissions
if p.NEO.CheckCommittee(ic) {
if p.checkCommittee(ic) {
return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,327 @@
package state
import (
"errors"
"fmt"
"math/big"
"github.com/tutus-one/tutus-chain/pkg/util"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
// Scope represents permission scope level.
type Scope uint8
const (
// ScopeGlobal applies the permission globally.
ScopeGlobal Scope = 0
// ScopePersonal applies only to the owner's own resources.
ScopePersonal Scope = 1
// ScopeDelegated is delegated by another token holder.
ScopeDelegated Scope = 2
)
// Role represents a custom role definition in the RoleRegistry.
type Role struct {
ID uint64 // Unique sequential identifier
Name string // Human-readable name (max 64 chars)
Description string // Description (max 256 chars)
ParentID uint64 // Parent role ID (0 = no parent, enables hierarchy)
CreatedAt uint32 // Block height when created
CreatedBy util.Uint160 // Creator's script hash
Active bool // Whether role is active
}
// ToStackItem implements stackitem.Convertible interface.
func (r *Role) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(r.ID))),
stackitem.NewByteArray([]byte(r.Name)),
stackitem.NewByteArray([]byte(r.Description)),
stackitem.NewBigInteger(big.NewInt(int64(r.ParentID))),
stackitem.NewBigInteger(big.NewInt(int64(r.CreatedAt))),
stackitem.NewByteArray(r.CreatedBy.BytesBE()),
stackitem.NewBool(r.Active),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (r *Role) FromStackItem(item stackitem.Item) error {
items, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(items) != 7 {
return fmt.Errorf("wrong number of elements: expected 7, got %d", len(items))
}
id, err := items[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid id: %w", err)
}
r.ID = id.Uint64()
nameBytes, err := items[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid name: %w", err)
}
r.Name = string(nameBytes)
descBytes, err := items[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid description: %w", err)
}
r.Description = string(descBytes)
parentID, err := items[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid parentID: %w", err)
}
r.ParentID = parentID.Uint64()
createdAt, err := items[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid createdAt: %w", err)
}
r.CreatedAt = uint32(createdAt.Int64())
createdByBytes, err := items[5].TryBytes()
if err != nil {
return fmt.Errorf("invalid createdBy: %w", err)
}
r.CreatedBy, err = util.Uint160DecodeBytesBE(createdByBytes)
if err != nil {
return fmt.Errorf("invalid createdBy hash: %w", err)
}
r.Active, err = items[6].TryBool()
if err != nil {
return fmt.Errorf("invalid active: %w", err)
}
return nil
}
// RoleAssignment represents a role granted to a PersonToken holder.
type RoleAssignment struct {
TokenID uint64 // PersonToken ID (or script hash as uint64 for address-based)
RoleID uint64 // Role ID
GrantedAt uint32 // Block height when granted
GrantedBy util.Uint160 // Granter's script hash
ExpiresAt uint32 // Expiration block (0 = never)
Active bool // Whether assignment is active
}
// ToStackItem implements stackitem.Convertible interface.
func (a *RoleAssignment) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(a.TokenID))),
stackitem.NewBigInteger(big.NewInt(int64(a.RoleID))),
stackitem.NewBigInteger(big.NewInt(int64(a.GrantedAt))),
stackitem.NewByteArray(a.GrantedBy.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(a.ExpiresAt))),
stackitem.NewBool(a.Active),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (a *RoleAssignment) FromStackItem(item stackitem.Item) error {
items, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(items) != 6 {
return fmt.Errorf("wrong number of elements: expected 6, got %d", len(items))
}
tokenID, err := items[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid tokenID: %w", err)
}
a.TokenID = tokenID.Uint64()
roleID, err := items[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid roleID: %w", err)
}
a.RoleID = roleID.Uint64()
grantedAt, err := items[2].TryInteger()
if err != nil {
return fmt.Errorf("invalid grantedAt: %w", err)
}
a.GrantedAt = uint32(grantedAt.Int64())
grantedByBytes, err := items[3].TryBytes()
if err != nil {
return fmt.Errorf("invalid grantedBy: %w", err)
}
a.GrantedBy, err = util.Uint160DecodeBytesBE(grantedByBytes)
if err != nil {
return fmt.Errorf("invalid grantedBy hash: %w", err)
}
expiresAt, err := items[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid expiresAt: %w", err)
}
a.ExpiresAt = uint32(expiresAt.Int64())
a.Active, err = items[5].TryBool()
if err != nil {
return fmt.Errorf("invalid active: %w", err)
}
return nil
}
// PermissionGrant represents a permission assigned to a role.
type PermissionGrant struct {
RoleID uint64 // Role ID
Resource string // Resource identifier (e.g., "documents")
Action string // Action identifier (e.g., "read", "write")
Scope Scope // Scope level
GrantedAt uint32 // Block height when granted
GrantedBy util.Uint160 // Granter's script hash
}
// ToStackItem implements stackitem.Convertible interface.
func (p *PermissionGrant) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(p.RoleID))),
stackitem.NewByteArray([]byte(p.Resource)),
stackitem.NewByteArray([]byte(p.Action)),
stackitem.NewBigInteger(big.NewInt(int64(p.Scope))),
stackitem.NewBigInteger(big.NewInt(int64(p.GrantedAt))),
stackitem.NewByteArray(p.GrantedBy.BytesBE()),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (p *PermissionGrant) FromStackItem(item stackitem.Item) error {
items, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(items) != 6 {
return fmt.Errorf("wrong number of elements: expected 6, got %d", len(items))
}
roleID, err := items[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid roleID: %w", err)
}
p.RoleID = roleID.Uint64()
resourceBytes, err := items[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid resource: %w", err)
}
p.Resource = string(resourceBytes)
actionBytes, err := items[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid action: %w", err)
}
p.Action = string(actionBytes)
scope, err := items[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid scope: %w", err)
}
p.Scope = Scope(scope.Int64())
grantedAt, err := items[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid grantedAt: %w", err)
}
p.GrantedAt = uint32(grantedAt.Int64())
grantedByBytes, err := items[5].TryBytes()
if err != nil {
return fmt.Errorf("invalid grantedBy: %w", err)
}
p.GrantedBy, err = util.Uint160DecodeBytesBE(grantedByBytes)
if err != nil {
return fmt.Errorf("invalid grantedBy hash: %w", err)
}
return nil
}
// AddressRoleAssignment represents a role granted directly to an address (script hash).
// This is used for bootstrapping when PersonTokens may not exist yet.
type AddressRoleAssignment struct {
Address util.Uint160 // Script hash of the address
RoleID uint64 // Role ID
GrantedAt uint32 // Block height when granted
GrantedBy util.Uint160 // Granter's script hash
ExpiresAt uint32 // Expiration block (0 = never)
Active bool // Whether assignment is active
}
// ToStackItem implements stackitem.Convertible interface.
func (a *AddressRoleAssignment) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray(a.Address.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(a.RoleID))),
stackitem.NewBigInteger(big.NewInt(int64(a.GrantedAt))),
stackitem.NewByteArray(a.GrantedBy.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(a.ExpiresAt))),
stackitem.NewBool(a.Active),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (a *AddressRoleAssignment) FromStackItem(item stackitem.Item) error {
items, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(items) != 6 {
return fmt.Errorf("wrong number of elements: expected 6, got %d", len(items))
}
addressBytes, err := items[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid address: %w", err)
}
a.Address, err = util.Uint160DecodeBytesBE(addressBytes)
if err != nil {
return fmt.Errorf("invalid address hash: %w", err)
}
roleID, err := items[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid roleID: %w", err)
}
a.RoleID = roleID.Uint64()
grantedAt, err := items[2].TryInteger()
if err != nil {
return fmt.Errorf("invalid grantedAt: %w", err)
}
a.GrantedAt = uint32(grantedAt.Int64())
grantedByBytes, err := items[3].TryBytes()
if err != nil {
return fmt.Errorf("invalid grantedBy: %w", err)
}
a.GrantedBy, err = util.Uint160DecodeBytesBE(grantedByBytes)
if err != nil {
return fmt.Errorf("invalid grantedBy hash: %w", err)
}
expiresAt, err := items[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid expiresAt: %w", err)
}
a.ExpiresAt = uint32(expiresAt.Int64())
a.Active, err = items[5].TryBool()
if err != nil {
return fmt.Errorf("invalid active: %w", err)
}
return nil
}