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:
parent
99ba041a85
commit
de34f66286
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue