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

2078 lines
63 KiB
Go

package native
import (
"encoding/binary"
"errors"
"fmt"
"math/big"
"github.com/tutus-one/tutus-chain/pkg/config"
"github.com/tutus-one/tutus-chain/pkg/core/dao"
"github.com/tutus-one/tutus-chain/pkg/core/interop"
"github.com/tutus-one/tutus-chain/pkg/core/interop/runtime"
"github.com/tutus-one/tutus-chain/pkg/core/native/nativeids"
"github.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
"github.com/tutus-one/tutus-chain/pkg/core/state"
"github.com/tutus-one/tutus-chain/pkg/core/storage"
"github.com/tutus-one/tutus-chain/pkg/crypto/hash"
"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"
)
// Vita represents a soul-bound identity native contract.
type Vita struct {
interop.ContractMD
Annos IAnnos
RoleRegistry IRoleRegistry
Lex ILex
}
// VitaCache represents the cached state for Vita contract.
type VitaCache struct {
tokenCount uint64
}
// Storage key prefixes for Vita.
const (
prefixTokenByOwner = 0x01 // owner (Uint160) -> tokenID (uint64)
prefixTokenByID = 0x02 // tokenID (uint64) -> Vita token
prefixPersonHash = 0x03 // personHash -> tokenID (uniqueness)
prefixAttribute = 0x04 // tokenID + attrKey -> Attribute
prefixChallenge = 0x05 // challengeID -> AuthChallenge
prefixRecovery = 0x06 // requestID -> RecoveryRequest
prefixActiveRecovery = 0x07 // tokenID + "recovery" -> requestID
prefixTokenCounter = 0x10 // -> uint64
prefixConfig = 0x11 // -> VitaConfig
)
// Event names for Vita.
const (
VitaCreatedEvent = "VitaCreated"
VitaSuspendedEvent = "VitaSuspended"
VitaReinstatedEvent = "VitaReinstated"
VitaRevokedEvent = "VitaRevoked"
AttributeSetEvent = "AttributeSet"
AttributeRevokedEvent = "AttributeRevoked"
AuthenticationSuccessEvent = "AuthenticationSuccess"
AuthChallengeCreatedEvent = "AuthChallengeCreated"
RecoveryInitiatedEvent = "RecoveryInitiated"
RecoveryApprovalEvent = "RecoveryApproval"
RecoveryExecutedEvent = "RecoveryExecuted"
RecoveryCancelledEvent = "RecoveryCancelled"
)
// Various errors.
var (
ErrTokenAlreadyExists = errors.New("token already exists for this owner")
ErrPersonHashExists = errors.New("person hash already linked to another token")
ErrTokenNotFound = errors.New("token not found")
ErrTokenSuspended = errors.New("token is suspended")
ErrTokenRevoked = errors.New("token is revoked")
ErrTokenNotSuspended = errors.New("token is not suspended")
ErrInvalidOwner = errors.New("invalid owner")
ErrInvalidPersonHash = errors.New("invalid person hash")
ErrNotCommittee = errors.New("invalid committee signature")
ErrVitaInvalidWitness = errors.New("invalid witness")
ErrAttributeNotFound = errors.New("attribute not found")
ErrAttributeRevoked = errors.New("attribute is already revoked")
ErrAttributeExpired = errors.New("attribute has expired")
ErrInvalidAttributeKey = errors.New("invalid attribute key")
ErrInvalidValueHash = errors.New("invalid value hash")
ErrInvalidDisclosureLevel = errors.New("invalid disclosure level")
ErrTokenNotActive = errors.New("token is not active")
ErrChallengeNotFound = errors.New("challenge not found")
ErrChallengeExpired = errors.New("challenge has expired")
ErrChallengeAlreadyFulfilled = errors.New("challenge already fulfilled")
ErrInvalidSignature = errors.New("invalid signature")
ErrInvalidPurpose = errors.New("invalid purpose")
ErrNoRecentAuth = errors.New("no recent authentication found")
ErrRecoveryNotFound = errors.New("recovery request not found")
ErrRecoveryAlreadyActive = errors.New("recovery already active for this token")
ErrRecoveryNotPending = errors.New("recovery is not in pending state")
ErrRecoveryDelayNotPassed = errors.New("recovery delay period has not passed")
ErrRecoveryExpired = errors.New("recovery request has expired")
ErrAlreadyApproved = errors.New("already approved this recovery")
ErrInvalidNewOwner = errors.New("invalid new owner")
ErrTokenInRecovery = errors.New("token is already in recovery")
ErrNotRecoveryRequester = errors.New("not the recovery requester")
ErrCallerHasNoVita = errors.New("caller does not have a Vita")
ErrRoleNotAssigned = errors.New("required role not assigned")
ErrPermissionDenied = errors.New("permission denied")
ErrVitaInvalidRole = errors.New("invalid core role")
ErrVitaInvalidResource = errors.New("invalid resource")
ErrVitaInvalidAction = errors.New("invalid action")
)
// CoreRole represents built-in roles for cross-contract access control.
type CoreRole uint8
// Core roles for Vita holders.
const (
CoreRoleNone CoreRole = 0
CoreRoleUser CoreRole = 1 // Basic authenticated user
CoreRoleVerified CoreRole = 2 // User with verified attributes
CoreRoleCommittee CoreRole = 3 // Committee member
CoreRoleAttestor CoreRole = 4 // Can attest attributes for others
CoreRoleRecovery CoreRole = 5 // Can participate in recovery
)
var (
_ interop.Contract = (*Vita)(nil)
_ dao.NativeContractCache = (*VitaCache)(nil)
)
// Copy implements NativeContractCache interface.
func (c *VitaCache) Copy() dao.NativeContractCache {
return &VitaCache{
tokenCount: c.tokenCount,
}
}
// checkCommittee checks if the caller has committee authority.
// Uses RoleRegistry if available, falls back to NEO.CheckCommittee().
func (v *Vita) checkCommittee(ic *interop.Context) bool {
if v.RoleRegistry != nil {
return v.RoleRegistry.CheckCommittee(ic)
}
// Fallback to Annos for backwards compatibility
return v.Annos.CheckCommittee(ic)
}
// newVita creates a new Vita native contract.
func newVita() *Vita {
v := &Vita{
ContractMD: *interop.NewContractMD(nativenames.Vita, nativeids.Vita),
}
defer v.BuildHFSpecificMD(v.ActiveIn())
// Register method
desc := NewDescriptor("register", smartcontract.ByteArrayType,
manifest.NewParameter("owner", smartcontract.Hash160Type),
manifest.NewParameter("personHash", smartcontract.ByteArrayType),
manifest.NewParameter("isEntity", smartcontract.BoolType),
manifest.NewParameter("recoveryHash", smartcontract.ByteArrayType))
md := NewMethodAndPrice(v.register, 1<<17, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// GetToken method
desc = NewDescriptor("getToken", smartcontract.ArrayType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(v.getToken, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// GetTokenByID method
desc = NewDescriptor("getTokenByID", smartcontract.ArrayType,
manifest.NewParameter("tokenId", smartcontract.IntegerType))
md = NewMethodAndPrice(v.getTokenByID, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// Exists method
desc = NewDescriptor("exists", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(v.exists, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// TotalSupply method
desc = NewDescriptor("totalSupply", smartcontract.IntegerType)
md = NewMethodAndPrice(v.totalSupply, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// Suspend method
desc = NewDescriptor("suspend", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type),
manifest.NewParameter("reason", smartcontract.StringType))
md = NewMethodAndPrice(v.suspend, 1<<16, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// Reinstate method
desc = NewDescriptor("reinstate", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(v.reinstate, 1<<16, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// Revoke method
desc = NewDescriptor("revoke", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type),
manifest.NewParameter("reason", smartcontract.StringType))
md = NewMethodAndPrice(v.revoke, 1<<17, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// SetAttribute method
desc = NewDescriptor("setAttribute", smartcontract.BoolType,
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("key", smartcontract.StringType),
manifest.NewParameter("valueHash", smartcontract.ByteArrayType),
manifest.NewParameter("valueEnc", smartcontract.ByteArrayType),
manifest.NewParameter("expiresAt", smartcontract.IntegerType),
manifest.NewParameter("disclosureLevel", smartcontract.IntegerType))
md = NewMethodAndPrice(v.setAttribute, 1<<17, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// GetAttribute method
desc = NewDescriptor("getAttribute", smartcontract.ArrayType,
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("key", smartcontract.StringType))
md = NewMethodAndPrice(v.getAttribute, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// RevokeAttribute method
desc = NewDescriptor("revokeAttribute", smartcontract.BoolType,
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("key", smartcontract.StringType),
manifest.NewParameter("reason", smartcontract.StringType))
md = NewMethodAndPrice(v.revokeAttribute, 1<<16, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// VerifyAttribute method
desc = NewDescriptor("verifyAttribute", smartcontract.BoolType,
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("key", smartcontract.StringType),
manifest.NewParameter("expectedHash", smartcontract.ByteArrayType))
md = NewMethodAndPrice(v.verifyAttribute, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// CreateChallenge method
desc = NewDescriptor("createChallenge", smartcontract.ArrayType,
manifest.NewParameter("owner", smartcontract.Hash160Type),
manifest.NewParameter("purpose", smartcontract.StringType))
md = NewMethodAndPrice(v.createChallenge, 1<<16, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// GetChallenge method
desc = NewDescriptor("getChallenge", smartcontract.ArrayType,
manifest.NewParameter("challengeId", smartcontract.ByteArrayType))
md = NewMethodAndPrice(v.getChallenge, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// FulfillChallenge method
desc = NewDescriptor("fulfillChallenge", smartcontract.BoolType,
manifest.NewParameter("challengeId", smartcontract.ByteArrayType),
manifest.NewParameter("signature", smartcontract.ByteArrayType))
md = NewMethodAndPrice(v.fulfillChallenge, 1<<17, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// VerifyAuth method
desc = NewDescriptor("verifyAuth", smartcontract.BoolType,
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("purpose", smartcontract.StringType),
manifest.NewParameter("maxAge", smartcontract.IntegerType))
md = NewMethodAndPrice(v.verifyAuth, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// InitiateRecovery method
desc = NewDescriptor("initiateRecovery", smartcontract.ByteArrayType,
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("newOwner", smartcontract.Hash160Type),
manifest.NewParameter("evidence", smartcontract.ByteArrayType))
md = NewMethodAndPrice(v.initiateRecovery, 1<<17, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// ApproveRecovery method
desc = NewDescriptor("approveRecovery", smartcontract.BoolType,
manifest.NewParameter("requestId", smartcontract.ByteArrayType))
md = NewMethodAndPrice(v.approveRecovery, 1<<16, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// ExecuteRecovery method
desc = NewDescriptor("executeRecovery", smartcontract.BoolType,
manifest.NewParameter("requestId", smartcontract.ByteArrayType))
md = NewMethodAndPrice(v.executeRecovery, 1<<17, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// CancelRecovery method
desc = NewDescriptor("cancelRecovery", smartcontract.BoolType,
manifest.NewParameter("requestId", smartcontract.ByteArrayType))
md = NewMethodAndPrice(v.cancelRecovery, 1<<16, callflag.States|callflag.AllowNotify)
v.AddMethod(md, desc)
// GetRecoveryRequest method
desc = NewDescriptor("getRecoveryRequest", smartcontract.ArrayType,
manifest.NewParameter("requestId", smartcontract.ByteArrayType))
md = NewMethodAndPrice(v.getRecoveryRequest, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// ValidateCaller method
desc = NewDescriptor("validateCaller", smartcontract.ArrayType)
md = NewMethodAndPrice(v.validateCaller, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// RequireRole method
desc = NewDescriptor("requireRole", smartcontract.IntegerType,
manifest.NewParameter("roleId", smartcontract.IntegerType))
md = NewMethodAndPrice(v.requireRole, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// RequireCoreRole method
desc = NewDescriptor("requireCoreRole", smartcontract.IntegerType,
manifest.NewParameter("coreRole", smartcontract.IntegerType))
md = NewMethodAndPrice(v.requireCoreRole, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// RequirePermission method
desc = NewDescriptor("requirePermission", smartcontract.IntegerType,
manifest.NewParameter("resource", smartcontract.StringType),
manifest.NewParameter("action", smartcontract.StringType),
manifest.NewParameter("scope", smartcontract.StringType))
md = NewMethodAndPrice(v.requirePermission, 1<<15, callflag.ReadStates)
v.AddMethod(md, desc)
// Events
eDesc := NewEventDescriptor(VitaCreatedEvent,
manifest.NewParameter("tokenId", smartcontract.ByteArrayType),
manifest.NewParameter("owner", smartcontract.Hash160Type),
manifest.NewParameter("createdAt", smartcontract.IntegerType))
v.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(VitaSuspendedEvent,
manifest.NewParameter("tokenId", smartcontract.ByteArrayType),
manifest.NewParameter("reason", smartcontract.StringType),
manifest.NewParameter("suspendedBy", smartcontract.Hash160Type))
v.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(VitaReinstatedEvent,
manifest.NewParameter("tokenId", smartcontract.ByteArrayType),
manifest.NewParameter("reinstatedBy", smartcontract.Hash160Type))
v.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(VitaRevokedEvent,
manifest.NewParameter("tokenId", smartcontract.ByteArrayType),
manifest.NewParameter("reason", smartcontract.StringType),
manifest.NewParameter("revokedBy", smartcontract.Hash160Type))
v.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(AttributeSetEvent,
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("key", smartcontract.StringType),
manifest.NewParameter("attestor", smartcontract.Hash160Type),
manifest.NewParameter("expiresAt", smartcontract.IntegerType))
v.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(AttributeRevokedEvent,
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("key", smartcontract.StringType),
manifest.NewParameter("revokedBy", smartcontract.Hash160Type),
manifest.NewParameter("reason", smartcontract.StringType))
v.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(AuthChallengeCreatedEvent,
manifest.NewParameter("challengeId", smartcontract.ByteArrayType),
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("purpose", smartcontract.StringType),
manifest.NewParameter("expiresAt", smartcontract.IntegerType))
v.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(AuthenticationSuccessEvent,
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("purpose", smartcontract.StringType),
manifest.NewParameter("timestamp", smartcontract.IntegerType))
v.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(RecoveryInitiatedEvent,
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("requestId", smartcontract.ByteArrayType),
manifest.NewParameter("delayUntil", smartcontract.IntegerType))
v.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(RecoveryApprovalEvent,
manifest.NewParameter("requestId", smartcontract.ByteArrayType),
manifest.NewParameter("approver", smartcontract.Hash160Type),
manifest.NewParameter("approvalCount", smartcontract.IntegerType),
manifest.NewParameter("required", smartcontract.IntegerType))
v.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(RecoveryExecutedEvent,
manifest.NewParameter("tokenId", smartcontract.IntegerType),
manifest.NewParameter("oldOwner", smartcontract.Hash160Type),
manifest.NewParameter("newOwner", smartcontract.Hash160Type))
v.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(RecoveryCancelledEvent,
manifest.NewParameter("requestId", smartcontract.ByteArrayType),
manifest.NewParameter("cancelledBy", smartcontract.Hash160Type))
v.AddEvent(NewEvent(eDesc))
return v
}
// Metadata returns contract metadata.
func (v *Vita) Metadata() *interop.ContractMD {
return &v.ContractMD
}
// Initialize initializes Vita contract at the specified hardfork.
func (v *Vita) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
if hf != v.ActiveIn() {
return nil
}
cache := &VitaCache{
tokenCount: 0,
}
ic.DAO.SetCache(v.ID, cache)
// Initialize token counter to 0
v.setTokenCounter(ic.DAO, 0)
return nil
}
// InitializeCache fills native Vita cache from DAO on node restart.
func (v *Vita) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
cache := &VitaCache{}
// Load token counter from storage
counter := v.getTokenCounter(d)
cache.tokenCount = counter
d.SetCache(v.ID, cache)
return nil
}
// OnPersist implements the Contract interface.
func (v *Vita) OnPersist(ic *interop.Context) error {
return nil
}
// PostPersist implements the Contract interface.
func (v *Vita) PostPersist(ic *interop.Context) error {
return nil
}
// ActiveIn returns the hardfork this contract activates in.
// Vita is always active (returns nil).
func (v *Vita) ActiveIn() *config.Hardfork {
return nil
}
// Storage key helpers
func makeTokenByOwnerKey(owner util.Uint160) []byte {
key := make([]byte, 1+util.Uint160Size)
key[0] = prefixTokenByOwner
copy(key[1:], owner.BytesBE())
return key
}
func makeTokenByIDKey(tokenID uint64) []byte {
key := make([]byte, 9)
key[0] = prefixTokenByID
binary.BigEndian.PutUint64(key[1:], tokenID)
return key
}
func makePersonHashKey(personHash []byte) []byte {
key := make([]byte, 1+len(personHash))
key[0] = prefixPersonHash
copy(key[1:], personHash)
return key
}
func makeAttributeKey(tokenID uint64, attrKey string) []byte {
keyBytes := []byte(attrKey)
key := make([]byte, 9+len(keyBytes))
key[0] = prefixAttribute
binary.BigEndian.PutUint64(key[1:9], tokenID)
copy(key[9:], keyBytes)
return key
}
func makeChallengeKey(challengeID util.Uint256) []byte {
key := make([]byte, 1+util.Uint256Size)
key[0] = prefixChallenge
copy(key[1:], challengeID.BytesBE())
return key
}
func makeLastAuthKey(tokenID uint64, purpose string) []byte {
purposeBytes := []byte(purpose)
key := make([]byte, 9+len(purposeBytes))
key[0] = prefixConfig // Using config prefix with tokenID+purpose for last auth tracking
binary.BigEndian.PutUint64(key[1:9], tokenID)
copy(key[9:], purposeBytes)
return key
}
func makeRecoveryKey(requestID util.Uint256) []byte {
key := make([]byte, 1+util.Uint256Size)
key[0] = prefixRecovery
copy(key[1:], requestID.BytesBE())
return key
}
func makeActiveRecoveryKey(tokenID uint64) []byte {
key := make([]byte, 9)
key[0] = prefixActiveRecovery
binary.BigEndian.PutUint64(key[1:], tokenID)
return key
}
// Token counter methods
func (v *Vita) getTokenCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(v.ID, []byte{prefixTokenCounter})
if si == nil {
return 0
}
return binary.BigEndian.Uint64(si)
}
func (v *Vita) setTokenCounter(d *dao.Simple, count uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, count)
d.PutStorageItem(v.ID, []byte{prefixTokenCounter}, buf)
}
func (v *Vita) getAndIncrementTokenCounter(d *dao.Simple) uint64 {
cache := d.GetRWCache(v.ID).(*VitaCache)
tokenID := cache.tokenCount
cache.tokenCount++
v.setTokenCounter(d, cache.tokenCount)
return tokenID
}
// Token storage methods
func (v *Vita) putToken(d *dao.Simple, token *state.Vita) error {
item, err := token.ToStackItem()
if err != nil {
return err
}
data, err := stackitem.Serialize(item)
if err != nil {
return err
}
// Store by ID
d.PutStorageItem(v.ID, makeTokenByIDKey(token.TokenID), data)
// Store owner -> tokenID mapping
tokenIDBytes := make([]byte, 8)
binary.BigEndian.PutUint64(tokenIDBytes, token.TokenID)
d.PutStorageItem(v.ID, makeTokenByOwnerKey(token.Owner), tokenIDBytes)
// Store personHash -> tokenID mapping for uniqueness
if len(token.PersonHash) > 0 {
d.PutStorageItem(v.ID, makePersonHashKey(token.PersonHash), tokenIDBytes)
}
return nil
}
func (v *Vita) getTokenByOwnerInternal(d *dao.Simple, owner util.Uint160) (*state.Vita, error) {
si := d.GetStorageItem(v.ID, makeTokenByOwnerKey(owner))
if si == nil {
return nil, nil
}
tokenID := binary.BigEndian.Uint64(si)
return v.getTokenByIDInternal(d, tokenID)
}
func (v *Vita) getTokenByIDInternal(d *dao.Simple, tokenID uint64) (*state.Vita, error) {
si := d.GetStorageItem(v.ID, makeTokenByIDKey(tokenID))
if si == nil {
return nil, nil
}
item, err := stackitem.Deserialize(si)
if err != nil {
return nil, err
}
token := new(state.Vita)
if err := token.FromStackItem(item); err != nil {
return nil, err
}
return token, nil
}
func (v *Vita) tokenExistsForOwner(d *dao.Simple, owner util.Uint160) bool {
si := d.GetStorageItem(v.ID, makeTokenByOwnerKey(owner))
return si != nil
}
func (v *Vita) personHashExists(d *dao.Simple, personHash []byte) bool {
if len(personHash) == 0 {
return false
}
si := d.GetStorageItem(v.ID, makePersonHashKey(personHash))
return si != nil
}
// Contract methods
// register creates a new Vita.
func (v *Vita) register(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
personHash := toBytes(args[1])
isEntity, err := args[2].TryBool()
if err != nil {
panic(fmt.Errorf("invalid isEntity: %w", err))
}
recoveryHash := toBytes(args[3])
// Validate owner
if owner.Equals(util.Uint160{}) {
panic(ErrInvalidOwner)
}
// Check witness for the owner
ok, err := runtime.CheckHashedWitness(ic, owner)
if err != nil || !ok {
panic(ErrVitaInvalidWitness)
}
// Check if token already exists for this owner
if v.tokenExistsForOwner(ic.DAO, owner) {
panic(ErrTokenAlreadyExists)
}
// Check if person hash is already linked to another token
if v.personHashExists(ic.DAO, personHash) {
panic(ErrPersonHashExists)
}
// Get next token ID
tokenID := v.getAndIncrementTokenCounter(ic.DAO)
// Create token
token := &state.Vita{
TokenID: tokenID,
Owner: owner,
PersonHash: personHash,
IsEntity: isEntity,
CreatedAt: ic.Block.Index,
UpdatedAt: ic.Block.Index,
Status: state.TokenStatusActive,
StatusReason: "",
RecoveryHash: recoveryHash,
}
// Store token
if err := v.putToken(ic.DAO, token); err != nil {
panic(err)
}
// Generate token ID bytes for return and event
tokenIDBytes := hash.Sha256(append(owner.BytesBE(), personHash...)).BytesBE()
// Emit event
err = ic.AddNotification(v.Hash, VitaCreatedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(tokenIDBytes),
stackitem.NewByteArray(owner.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))),
}))
if err != nil {
panic(err)
}
return stackitem.NewByteArray(tokenIDBytes)
}
// getToken returns the token for the given owner.
func (v *Vita) getToken(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
token, err := v.getTokenByOwnerInternal(ic.DAO, owner)
if err != nil {
panic(err)
}
if token == nil {
return stackitem.Null{}
}
item, err := token.ToStackItem()
if err != nil {
panic(err)
}
return item
}
// getTokenByID returns the token for the given token ID.
func (v *Vita) getTokenByID(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tokenID := toBigInt(args[0]).Uint64()
token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
if err != nil {
panic(err)
}
if token == nil {
return stackitem.Null{}
}
item, err := token.ToStackItem()
if err != nil {
panic(err)
}
return item
}
// exists checks if a token exists for the given owner.
func (v *Vita) exists(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
return stackitem.NewBool(v.tokenExistsForOwner(ic.DAO, owner))
}
// totalSupply returns the total number of tokens.
func (v *Vita) totalSupply(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
cache := ic.DAO.GetROCache(v.ID).(*VitaCache)
return stackitem.NewBigInteger(big.NewInt(int64(cache.tokenCount)))
}
// suspend temporarily suspends a token (committee only).
func (v *Vita) suspend(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
reason := toString(args[1])
// Check committee
if !v.checkCommittee(ic) {
panic(ErrNotCommittee)
}
// Require liberty restriction order from Lex (due process protection)
// Suspending a Vita is a restriction of liberty, which requires judicial authority
if v.Lex != nil && !v.Lex.IsRestrictedInternal(ic.DAO, owner, state.RightLiberty, ic.Block.Index) {
panic("liberty restriction order required via Lex (due process)")
}
// Get token
token, err := v.getTokenByOwnerInternal(ic.DAO, owner)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrTokenNotFound)
}
// Check if already revoked
if token.Status == state.TokenStatusRevoked {
panic(ErrTokenRevoked)
}
// Update status
token.Status = state.TokenStatusSuspended
token.StatusReason = reason
token.UpdatedAt = ic.Block.Index
// Store updated token
if err := v.putToken(ic.DAO, token); err != nil {
panic(err)
}
// Get caller for event
caller := ic.VM.GetCallingScriptHash()
// Generate token ID bytes
tokenIDBytes := make([]byte, 8)
binary.BigEndian.PutUint64(tokenIDBytes, token.TokenID)
// Emit event
err = ic.AddNotification(v.Hash, VitaSuspendedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(tokenIDBytes),
stackitem.NewByteArray([]byte(reason)),
stackitem.NewByteArray(caller.BytesBE()),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
// reinstate reinstates a suspended token (committee only).
func (v *Vita) reinstate(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
// Check committee
if !v.checkCommittee(ic) {
panic(ErrNotCommittee)
}
// Get token
token, err := v.getTokenByOwnerInternal(ic.DAO, owner)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrTokenNotFound)
}
// Check if suspended
if token.Status != state.TokenStatusSuspended {
panic(ErrTokenNotSuspended)
}
// Update status
token.Status = state.TokenStatusActive
token.StatusReason = ""
token.UpdatedAt = ic.Block.Index
// Store updated token
if err := v.putToken(ic.DAO, token); err != nil {
panic(err)
}
// Get caller for event
caller := ic.VM.GetCallingScriptHash()
// Generate token ID bytes
tokenIDBytes := make([]byte, 8)
binary.BigEndian.PutUint64(tokenIDBytes, token.TokenID)
// Emit event
err = ic.AddNotification(v.Hash, VitaReinstatedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(tokenIDBytes),
stackitem.NewByteArray(caller.BytesBE()),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
// revoke permanently revokes a token (committee only).
func (v *Vita) revoke(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
reason := toString(args[1])
// Check committee
if !v.checkCommittee(ic) {
panic(ErrNotCommittee)
}
// Require liberty restriction order from Lex (due process protection)
// Revoking a Vita is a permanent restriction of liberty, requiring judicial authority
if v.Lex != nil && !v.Lex.IsRestrictedInternal(ic.DAO, owner, state.RightLiberty, ic.Block.Index) {
panic("liberty restriction order required via Lex (due process)")
}
// Get token
token, err := v.getTokenByOwnerInternal(ic.DAO, owner)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrTokenNotFound)
}
// Check if already revoked
if token.Status == state.TokenStatusRevoked {
panic(ErrTokenRevoked)
}
// Update status
token.Status = state.TokenStatusRevoked
token.StatusReason = reason
token.UpdatedAt = ic.Block.Index
// Store updated token
if err := v.putToken(ic.DAO, token); err != nil {
panic(err)
}
// Get caller for event
caller := ic.VM.GetCallingScriptHash()
// Generate token ID bytes
tokenIDBytes := make([]byte, 8)
binary.BigEndian.PutUint64(tokenIDBytes, token.TokenID)
// Emit event
err = ic.AddNotification(v.Hash, VitaRevokedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(tokenIDBytes),
stackitem.NewByteArray([]byte(reason)),
stackitem.NewByteArray(caller.BytesBE()),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
// Public methods for cross-native access
// GetTokenByOwner returns the token for the given owner (for cross-native access).
func (v *Vita) GetTokenByOwner(d *dao.Simple, owner util.Uint160) (*state.Vita, error) {
return v.getTokenByOwnerInternal(d, owner)
}
// GetTokenByIDPublic returns the token for the given ID (for cross-native access).
func (v *Vita) GetTokenByIDPublic(d *dao.Simple, tokenID uint64) (*state.Vita, error) {
return v.getTokenByIDInternal(d, tokenID)
}
// TokenExists returns true if a token exists for the given owner.
func (v *Vita) TokenExists(d *dao.Simple, owner util.Uint160) bool {
return v.tokenExistsForOwner(d, owner)
}
// GetTotalTokenCount returns the total number of tokens issued (for quorum calculations).
func (v *Vita) GetTotalTokenCount(d *dao.Simple) uint64 {
return v.getTokenCounter(d)
}
// IsAdultVerified checks if the owner has a verified "age_verified" attribute
// indicating they are 18+ years old. Used for age-restricted purchases.
// The attribute must be non-revoked and not expired.
func (v *Vita) IsAdultVerified(d *dao.Simple, owner util.Uint160) bool {
token, err := v.getTokenByOwnerInternal(d, owner)
if err != nil || token == nil {
return false
}
// Check for "age_verified" attribute
attr, err := v.getAttributeInternal(d, token.TokenID, "age_verified")
if err != nil || attr == nil {
return false
}
// Check attribute is not revoked
if attr.Revoked {
return false
}
// Note: Expiration check would require current block height
// For now, we check if ExpiresAt is set and > 0 means it could expire
// In production, pass block height and compare
return true
}
// Attribute storage methods
func (v *Vita) putAttribute(d *dao.Simple, tokenID uint64, attr *state.Attribute) error {
item, err := attr.ToStackItem()
if err != nil {
return err
}
data, err := stackitem.Serialize(item)
if err != nil {
return err
}
d.PutStorageItem(v.ID, makeAttributeKey(tokenID, attr.Key), data)
return nil
}
func (v *Vita) getAttributeInternal(d *dao.Simple, tokenID uint64, key string) (*state.Attribute, error) {
si := d.GetStorageItem(v.ID, makeAttributeKey(tokenID, key))
if si == nil {
return nil, nil
}
item, err := stackitem.Deserialize(si)
if err != nil {
return nil, err
}
attr := new(state.Attribute)
if err := attr.FromStackItem(item); err != nil {
return nil, err
}
return attr, nil
}
// setAttribute sets or updates an attribute on a token.
// The caller must be either the token owner (self-attestation) or any other account (third-party attestation).
func (v *Vita) setAttribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tokenID := toBigInt(args[0]).Uint64()
key := toString(args[1])
valueHash := toBytes(args[2])
valueEnc := toBytes(args[3])
expiresAt := uint32(toBigInt(args[4]).Int64())
disclosureLevel := state.DisclosureLevel(toBigInt(args[5]).Int64())
// Validate inputs
if len(key) == 0 || len(key) > 64 {
panic(ErrInvalidAttributeKey)
}
if len(valueHash) == 0 || len(valueHash) > 64 {
panic(ErrInvalidValueHash)
}
if disclosureLevel > state.DisclosurePublic {
panic(ErrInvalidDisclosureLevel)
}
// Get token
token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrTokenNotFound)
}
// Check token is active
if token.Status != state.TokenStatusActive {
panic(ErrTokenNotActive)
}
// Get the attestor (caller)
attestor := ic.VM.GetCallingScriptHash()
// If attestor is the owner, check witness
// Otherwise, any contract/account can attest (third-party attestation)
if attestor.Equals(token.Owner) {
ok, err := runtime.CheckHashedWitness(ic, token.Owner)
if err != nil || !ok {
panic(ErrVitaInvalidWitness)
}
}
// Create attribute
attr := &state.Attribute{
Key: key,
ValueHash: valueHash,
ValueEnc: valueEnc,
Attestor: attestor,
AttestedAt: ic.Block.Index,
ExpiresAt: expiresAt,
Revoked: false,
RevokedAt: 0,
DisclosureLevel: disclosureLevel,
}
// Store attribute
if err := v.putAttribute(ic.DAO, tokenID, attr); err != nil {
panic(err)
}
// Emit event
err = ic.AddNotification(v.Hash, AttributeSetEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(tokenID))),
stackitem.NewByteArray([]byte(key)),
stackitem.NewByteArray(attestor.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(expiresAt))),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
// getAttribute returns an attribute for a token.
func (v *Vita) getAttribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tokenID := toBigInt(args[0]).Uint64()
key := toString(args[1])
attr, err := v.getAttributeInternal(ic.DAO, tokenID, key)
if err != nil {
panic(err)
}
if attr == nil {
return stackitem.Null{}
}
item, err := attr.ToStackItem()
if err != nil {
panic(err)
}
return item
}
// revokeAttribute revokes an attribute.
// Can be called by the token owner, the original attestor, or committee.
func (v *Vita) revokeAttribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tokenID := toBigInt(args[0]).Uint64()
key := toString(args[1])
reason := toString(args[2])
// Get token
token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrTokenNotFound)
}
// Get attribute
attr, err := v.getAttributeInternal(ic.DAO, tokenID, key)
if err != nil {
panic(err)
}
if attr == nil {
panic(ErrAttributeNotFound)
}
// Check if already revoked
if attr.Revoked {
panic(ErrAttributeRevoked)
}
// Check authorization: owner, attestor, or committee
caller := ic.VM.GetCallingScriptHash()
isOwner := caller.Equals(token.Owner)
isAttestor := caller.Equals(attr.Attestor)
isCommittee := v.checkCommittee(ic)
if isOwner {
ok, err := runtime.CheckHashedWitness(ic, token.Owner)
if err != nil || !ok {
panic(ErrVitaInvalidWitness)
}
} else if !isAttestor && !isCommittee {
panic(ErrVitaInvalidWitness)
}
// Revoke attribute
attr.Revoked = true
attr.RevokedAt = ic.Block.Index
// Store updated attribute
if err := v.putAttribute(ic.DAO, tokenID, attr); err != nil {
panic(err)
}
// Emit event
err = ic.AddNotification(v.Hash, AttributeRevokedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(tokenID))),
stackitem.NewByteArray([]byte(key)),
stackitem.NewByteArray(caller.BytesBE()),
stackitem.NewByteArray([]byte(reason)),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
// verifyAttribute verifies that an attribute exists and its hash matches.
// Returns true if the attribute exists, is not revoked, not expired, and hash matches.
func (v *Vita) verifyAttribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tokenID := toBigInt(args[0]).Uint64()
key := toString(args[1])
expectedHash := toBytes(args[2])
// Get attribute
attr, err := v.getAttributeInternal(ic.DAO, tokenID, key)
if err != nil {
panic(err)
}
if attr == nil {
return stackitem.NewBool(false)
}
// Check if revoked
if attr.Revoked {
return stackitem.NewBool(false)
}
// Check if expired (0 means never expires)
if attr.ExpiresAt > 0 && ic.Block.Index > attr.ExpiresAt {
return stackitem.NewBool(false)
}
// Compare hashes
if len(attr.ValueHash) != len(expectedHash) {
return stackitem.NewBool(false)
}
for i := range attr.ValueHash {
if attr.ValueHash[i] != expectedHash[i] {
return stackitem.NewBool(false)
}
}
return stackitem.NewBool(true)
}
// Public methods for cross-native attribute access
// GetAttribute returns an attribute for the given token and key (for cross-native access).
func (v *Vita) GetAttribute(d *dao.Simple, tokenID uint64, key string) (*state.Attribute, error) {
return v.getAttributeInternal(d, tokenID, key)
}
// Challenge storage methods
func (v *Vita) putChallenge(d *dao.Simple, challenge *state.AuthChallenge) error {
item, err := challenge.ToStackItem()
if err != nil {
return err
}
data, err := stackitem.Serialize(item)
if err != nil {
return err
}
d.PutStorageItem(v.ID, makeChallengeKey(challenge.ChallengeID), data)
return nil
}
func (v *Vita) getChallengeInternal(d *dao.Simple, challengeID util.Uint256) (*state.AuthChallenge, error) {
si := d.GetStorageItem(v.ID, makeChallengeKey(challengeID))
if si == nil {
return nil, nil
}
item, err := stackitem.Deserialize(si)
if err != nil {
return nil, err
}
challenge := new(state.AuthChallenge)
if err := challenge.FromStackItem(item); err != nil {
return nil, err
}
return challenge, nil
}
func (v *Vita) setLastAuth(d *dao.Simple, tokenID uint64, purpose string, blockHeight uint32) {
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, blockHeight)
d.PutStorageItem(v.ID, makeLastAuthKey(tokenID, purpose), buf)
}
func (v *Vita) getLastAuth(d *dao.Simple, tokenID uint64, purpose string) uint32 {
si := d.GetStorageItem(v.ID, makeLastAuthKey(tokenID, purpose))
if si == nil {
return 0
}
return binary.BigEndian.Uint32(si)
}
// Default challenge expiry in blocks (approximately 5 minutes at 15 seconds per block)
const defaultChallengeExpiry uint32 = 20
// createChallenge creates a new authentication challenge for a token owner.
// Anyone can create a challenge for any token owner.
func (v *Vita) createChallenge(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
purpose := toString(args[1])
// Validate purpose
if len(purpose) == 0 || len(purpose) > 32 {
panic(ErrInvalidPurpose)
}
// Get token
token, err := v.getTokenByOwnerInternal(ic.DAO, owner)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrTokenNotFound)
}
// Check token is active
if token.Status != state.TokenStatusActive {
panic(ErrTokenNotActive)
}
// Generate nonce (use transaction hash + block index + token ID for uniqueness)
nonceData := append(ic.Tx.Hash().BytesBE(), make([]byte, 8)...)
binary.BigEndian.PutUint64(nonceData[util.Uint256Size:], token.TokenID)
nonce := hash.Sha256(nonceData).BytesBE()
// Generate challenge ID
challengeIDData := append(nonce, owner.BytesBE()...)
challengeIDData = append(challengeIDData, []byte(purpose)...)
challengeID := hash.Sha256(challengeIDData)
// Create challenge
challenge := &state.AuthChallenge{
ChallengeID: challengeID,
TokenID: token.TokenID,
Nonce: nonce,
CreatedAt: ic.Block.Index,
ExpiresAt: ic.Block.Index + defaultChallengeExpiry,
Purpose: purpose,
Fulfilled: false,
FulfilledAt: 0,
}
// Store challenge
if err := v.putChallenge(ic.DAO, challenge); err != nil {
panic(err)
}
// Emit event
err = ic.AddNotification(v.Hash, AuthChallengeCreatedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(challengeID.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(token.TokenID))),
stackitem.NewByteArray([]byte(purpose)),
stackitem.NewBigInteger(big.NewInt(int64(challenge.ExpiresAt))),
}))
if err != nil {
panic(err)
}
// Return challenge as struct
item, err := challenge.ToStackItem()
if err != nil {
panic(err)
}
return item
}
// getChallenge returns a challenge by ID.
func (v *Vita) getChallenge(ic *interop.Context, args []stackitem.Item) stackitem.Item {
challengeIDBytes := toBytes(args[0])
if len(challengeIDBytes) != util.Uint256Size {
panic("invalid challenge ID length")
}
challengeID, err := util.Uint256DecodeBytesBE(challengeIDBytes)
if err != nil {
panic(err)
}
challenge, err := v.getChallengeInternal(ic.DAO, challengeID)
if err != nil {
panic(err)
}
if challenge == nil {
return stackitem.Null{}
}
item, err := challenge.ToStackItem()
if err != nil {
panic(err)
}
return item
}
// fulfillChallenge fulfills an authentication challenge by providing a valid signature.
// The signature must be from the token owner over the challenge nonce.
func (v *Vita) fulfillChallenge(ic *interop.Context, args []stackitem.Item) stackitem.Item {
challengeIDBytes := toBytes(args[0])
if len(challengeIDBytes) != util.Uint256Size {
panic("invalid challenge ID length")
}
challengeID, err := util.Uint256DecodeBytesBE(challengeIDBytes)
if err != nil {
panic(err)
}
_ = toBytes(args[1]) // signature - validation happens via witness
// Get challenge
challenge, err := v.getChallengeInternal(ic.DAO, challengeID)
if err != nil {
panic(err)
}
if challenge == nil {
panic(ErrChallengeNotFound)
}
// Check if already fulfilled
if challenge.Fulfilled {
panic(ErrChallengeAlreadyFulfilled)
}
// Check if expired
if ic.Block.Index > challenge.ExpiresAt {
panic(ErrChallengeExpired)
}
// Get token
token, err := v.getTokenByIDInternal(ic.DAO, challenge.TokenID)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrTokenNotFound)
}
// Verify the caller has witness for the token owner
// This ensures the owner is actually signing this transaction
ok, err := runtime.CheckHashedWitness(ic, token.Owner)
if err != nil || !ok {
panic(ErrInvalidSignature)
}
// Mark challenge as fulfilled
challenge.Fulfilled = true
challenge.FulfilledAt = ic.Block.Index
// Store updated challenge
if err := v.putChallenge(ic.DAO, challenge); err != nil {
panic(err)
}
// Record last auth time for this token and purpose
v.setLastAuth(ic.DAO, challenge.TokenID, challenge.Purpose, ic.Block.Index)
// Emit success event
err = ic.AddNotification(v.Hash, AuthenticationSuccessEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(challenge.TokenID))),
stackitem.NewByteArray([]byte(challenge.Purpose)),
stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
// verifyAuth checks if a token has authenticated recently for a given purpose.
// Returns true if the last successful authentication for the purpose was within maxAge blocks.
func (v *Vita) verifyAuth(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tokenID := toBigInt(args[0]).Uint64()
purpose := toString(args[1])
maxAge := uint32(toBigInt(args[2]).Int64())
// Get last auth time
lastAuth := v.getLastAuth(ic.DAO, tokenID, purpose)
if lastAuth == 0 {
return stackitem.NewBool(false)
}
// Check if within maxAge
if ic.Block.Index > lastAuth+maxAge {
return stackitem.NewBool(false)
}
return stackitem.NewBool(true)
}
// Public methods for cross-native authentication access
// GetLastAuth returns the last authentication block height for a token and purpose.
func (v *Vita) GetLastAuth(d *dao.Simple, tokenID uint64, purpose string) uint32 {
return v.getLastAuth(d, tokenID, purpose)
}
// Recovery configuration constants
const (
// defaultRecoveryDelay is the number of blocks before recovery can be executed (approximately 24 hours at 15s blocks)
defaultRecoveryDelay uint32 = 5760
// defaultRecoveryExpiry is the number of blocks until a recovery request expires (approximately 7 days)
defaultRecoveryExpiry uint32 = 40320
// defaultRequiredApprovals is the number of committee approvals needed for recovery
defaultRequiredApprovals int = 2
)
// Recovery storage methods
func (v *Vita) putRecoveryRequest(d *dao.Simple, req *state.RecoveryRequest) error {
item, err := req.ToStackItem()
if err != nil {
return err
}
data, err := stackitem.Serialize(item)
if err != nil {
return err
}
d.PutStorageItem(v.ID, makeRecoveryKey(req.RequestID), data)
return nil
}
func (v *Vita) getRecoveryRequestInternal(d *dao.Simple, requestID util.Uint256) (*state.RecoveryRequest, error) {
si := d.GetStorageItem(v.ID, makeRecoveryKey(requestID))
if si == nil {
return nil, nil
}
item, err := stackitem.Deserialize(si)
if err != nil {
return nil, err
}
req := new(state.RecoveryRequest)
if err := req.FromStackItem(item); err != nil {
return nil, err
}
return req, nil
}
func (v *Vita) setActiveRecovery(d *dao.Simple, tokenID uint64, requestID util.Uint256) {
d.PutStorageItem(v.ID, makeActiveRecoveryKey(tokenID), requestID.BytesBE())
}
func (v *Vita) getActiveRecovery(d *dao.Simple, tokenID uint64) *util.Uint256 {
si := d.GetStorageItem(v.ID, makeActiveRecoveryKey(tokenID))
if si == nil {
return nil
}
requestID, err := util.Uint256DecodeBytesBE(si)
if err != nil {
return nil
}
return &requestID
}
func (v *Vita) clearActiveRecovery(d *dao.Simple, tokenID uint64) {
d.DeleteStorageItem(v.ID, makeActiveRecoveryKey(tokenID))
}
// initiateRecovery starts a key recovery process for a token.
// Anyone can initiate recovery for any token.
func (v *Vita) initiateRecovery(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tokenID := toBigInt(args[0]).Uint64()
newOwner := toUint160(args[1])
evidence := toBytes(args[2])
// Validate new owner
if newOwner.Equals(util.Uint160{}) {
panic(ErrInvalidNewOwner)
}
// Get token
token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrTokenNotFound)
}
// Check token is not revoked
if token.Status == state.TokenStatusRevoked {
panic(ErrTokenRevoked)
}
// Check token is not already in recovery
if token.Status == state.TokenStatusRecovering {
panic(ErrTokenInRecovery)
}
// Check no active recovery exists
if v.getActiveRecovery(ic.DAO, tokenID) != nil {
panic(ErrRecoveryAlreadyActive)
}
// Get requester (caller)
requester := ic.VM.GetCallingScriptHash()
// Generate request ID
requestIDData := append(ic.Tx.Hash().BytesBE(), make([]byte, 8)...)
binary.BigEndian.PutUint64(requestIDData[util.Uint256Size:], tokenID)
requestIDData = append(requestIDData, newOwner.BytesBE()...)
requestID := hash.Sha256(requestIDData)
// Create recovery request
req := &state.RecoveryRequest{
RequestID: requestID,
TokenID: tokenID,
NewOwner: newOwner,
Requester: requester,
Evidence: evidence,
Approvals: []util.Uint160{},
RequiredApprovals: defaultRequiredApprovals,
CreatedAt: ic.Block.Index,
DelayUntil: ic.Block.Index + defaultRecoveryDelay,
ExpiresAt: ic.Block.Index + defaultRecoveryExpiry,
Status: state.RecoveryStatusPending,
}
// Store recovery request
if err := v.putRecoveryRequest(ic.DAO, req); err != nil {
panic(err)
}
// Mark active recovery for token
v.setActiveRecovery(ic.DAO, tokenID, requestID)
// Update token status to recovering
token.Status = state.TokenStatusRecovering
token.StatusReason = "recovery initiated"
token.UpdatedAt = ic.Block.Index
if err := v.putToken(ic.DAO, token); err != nil {
panic(err)
}
// Emit event
err = ic.AddNotification(v.Hash, RecoveryInitiatedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(tokenID))),
stackitem.NewByteArray(requestID.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(req.DelayUntil))),
}))
if err != nil {
panic(err)
}
return stackitem.NewByteArray(requestID.BytesBE())
}
// approveRecovery approves a recovery request (committee member only).
func (v *Vita) approveRecovery(ic *interop.Context, args []stackitem.Item) stackitem.Item {
requestIDBytes := toBytes(args[0])
if len(requestIDBytes) != util.Uint256Size {
panic("invalid request ID length")
}
requestID, err := util.Uint256DecodeBytesBE(requestIDBytes)
if err != nil {
panic(err)
}
// Check committee
if !v.checkCommittee(ic) {
panic(ErrNotCommittee)
}
// Get recovery request
req, err := v.getRecoveryRequestInternal(ic.DAO, requestID)
if err != nil {
panic(err)
}
if req == nil {
panic(ErrRecoveryNotFound)
}
// Check status is pending
if req.Status != state.RecoveryStatusPending {
panic(ErrRecoveryNotPending)
}
// Check not expired
if ic.Block.Index > req.ExpiresAt {
panic(ErrRecoveryExpired)
}
// Get approver (caller)
approver := ic.VM.GetCallingScriptHash()
// Check not already approved by this approver
for _, a := range req.Approvals {
if a.Equals(approver) {
panic(ErrAlreadyApproved)
}
}
// Add approval
req.Approvals = append(req.Approvals, approver)
// Check if sufficient approvals
if len(req.Approvals) >= req.RequiredApprovals {
req.Status = state.RecoveryStatusApproved
}
// Store updated request
if err := v.putRecoveryRequest(ic.DAO, req); err != nil {
panic(err)
}
// Emit event
err = ic.AddNotification(v.Hash, RecoveryApprovalEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(requestID.BytesBE()),
stackitem.NewByteArray(approver.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(len(req.Approvals)))),
stackitem.NewBigInteger(big.NewInt(int64(req.RequiredApprovals))),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
// executeRecovery executes an approved recovery request after the delay period.
func (v *Vita) executeRecovery(ic *interop.Context, args []stackitem.Item) stackitem.Item {
requestIDBytes := toBytes(args[0])
if len(requestIDBytes) != util.Uint256Size {
panic("invalid request ID length")
}
requestID, err := util.Uint256DecodeBytesBE(requestIDBytes)
if err != nil {
panic(err)
}
// Get recovery request
req, err := v.getRecoveryRequestInternal(ic.DAO, requestID)
if err != nil {
panic(err)
}
if req == nil {
panic(ErrRecoveryNotFound)
}
// Check status is approved
if req.Status != state.RecoveryStatusApproved {
panic(ErrRecoveryNotPending)
}
// Check delay period has passed
if ic.Block.Index < req.DelayUntil {
panic(ErrRecoveryDelayNotPassed)
}
// Check not expired
if ic.Block.Index > req.ExpiresAt {
panic(ErrRecoveryExpired)
}
// Get token
token, err := v.getTokenByIDInternal(ic.DAO, req.TokenID)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrTokenNotFound)
}
// Store old owner for event
oldOwner := token.Owner
// Delete old owner -> tokenID mapping
ic.DAO.DeleteStorageItem(v.ID, makeTokenByOwnerKey(oldOwner))
// Update token owner
token.Owner = req.NewOwner
token.Status = state.TokenStatusActive
token.StatusReason = ""
token.UpdatedAt = ic.Block.Index
// Store updated token (this will create new owner -> tokenID mapping)
if err := v.putToken(ic.DAO, token); err != nil {
panic(err)
}
// Update request status
req.Status = state.RecoveryStatusExecuted
if err := v.putRecoveryRequest(ic.DAO, req); err != nil {
panic(err)
}
// Clear active recovery
v.clearActiveRecovery(ic.DAO, req.TokenID)
// Emit event
err = ic.AddNotification(v.Hash, RecoveryExecutedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(req.TokenID))),
stackitem.NewByteArray(oldOwner.BytesBE()),
stackitem.NewByteArray(req.NewOwner.BytesBE()),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
// cancelRecovery cancels an active recovery request.
// Can be called by the original token owner or the requester.
func (v *Vita) cancelRecovery(ic *interop.Context, args []stackitem.Item) stackitem.Item {
requestIDBytes := toBytes(args[0])
if len(requestIDBytes) != util.Uint256Size {
panic("invalid request ID length")
}
requestID, err := util.Uint256DecodeBytesBE(requestIDBytes)
if err != nil {
panic(err)
}
// Get recovery request
req, err := v.getRecoveryRequestInternal(ic.DAO, requestID)
if err != nil {
panic(err)
}
if req == nil {
panic(ErrRecoveryNotFound)
}
// Check status is pending or approved (not executed or denied)
if req.Status == state.RecoveryStatusExecuted || req.Status == state.RecoveryStatusDenied {
panic(ErrRecoveryNotPending)
}
// Get token
token, err := v.getTokenByIDInternal(ic.DAO, req.TokenID)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrTokenNotFound)
}
// Check authorization: owner, requester, or committee
caller := ic.VM.GetCallingScriptHash()
isOwner := caller.Equals(token.Owner)
isRequester := caller.Equals(req.Requester)
isCommittee := v.checkCommittee(ic)
if isOwner {
ok, err := runtime.CheckHashedWitness(ic, token.Owner)
if err != nil || !ok {
panic(ErrVitaInvalidWitness)
}
} else if !isRequester && !isCommittee {
panic(ErrVitaInvalidWitness)
}
// Update request status
req.Status = state.RecoveryStatusDenied
if err := v.putRecoveryRequest(ic.DAO, req); err != nil {
panic(err)
}
// Restore token status
token.Status = state.TokenStatusActive
token.StatusReason = ""
token.UpdatedAt = ic.Block.Index
if err := v.putToken(ic.DAO, token); err != nil {
panic(err)
}
// Clear active recovery
v.clearActiveRecovery(ic.DAO, req.TokenID)
// Emit event
err = ic.AddNotification(v.Hash, RecoveryCancelledEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(requestID.BytesBE()),
stackitem.NewByteArray(caller.BytesBE()),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
// getRecoveryRequest returns a recovery request by ID.
func (v *Vita) getRecoveryRequest(ic *interop.Context, args []stackitem.Item) stackitem.Item {
requestIDBytes := toBytes(args[0])
if len(requestIDBytes) != util.Uint256Size {
panic("invalid request ID length")
}
requestID, err := util.Uint256DecodeBytesBE(requestIDBytes)
if err != nil {
panic(err)
}
req, err := v.getRecoveryRequestInternal(ic.DAO, requestID)
if err != nil {
panic(err)
}
if req == nil {
return stackitem.Null{}
}
item, err := req.ToStackItem()
if err != nil {
panic(err)
}
return item
}
// Public methods for cross-native recovery access
// GetRecoveryRequest returns a recovery request by ID (for cross-native access).
func (v *Vita) GetRecoveryRequest(d *dao.Simple, requestID util.Uint256) (*state.RecoveryRequest, error) {
return v.getRecoveryRequestInternal(d, requestID)
}
// GetActiveRecoveryForToken returns the active recovery request ID for a token (for cross-native access).
func (v *Vita) GetActiveRecoveryForToken(d *dao.Simple, tokenID uint64) *util.Uint256 {
return v.getActiveRecovery(d, tokenID)
}
// Cross-contract integration methods
// validateCaller validates that the calling contract's owner has a valid Vita.
// Returns the tokenID and core roles for the caller.
func (v *Vita) validateCaller(ic *interop.Context, args []stackitem.Item) stackitem.Item {
caller := ic.VM.GetCallingScriptHash()
// Get token by caller address
token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrCallerHasNoVita)
}
// Check token is active
if token.Status != state.TokenStatusActive {
if token.Status == state.TokenStatusSuspended {
panic(ErrTokenSuspended)
}
if token.Status == state.TokenStatusRevoked {
panic(ErrTokenRevoked)
}
if token.Status == state.TokenStatusRecovering {
panic(ErrTokenInRecovery)
}
panic(ErrTokenNotActive)
}
// Determine core roles
roles := v.getCoreRoles(ic, token)
// Return struct with tokenID and roles
return stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(token.TokenID))),
stackitem.NewBigInteger(big.NewInt(int64(roles))),
})
}
// getCoreRoles determines the core roles for a token holder.
func (v *Vita) getCoreRoles(ic *interop.Context, token *state.Vita) uint64 {
var roles uint64
// All active token holders have User role
roles |= 1 << uint64(CoreRoleUser)
// Check if user is verified (has non-expired, non-revoked attributes)
if v.hasVerifiedAttributes(ic.DAO, token.TokenID) {
roles |= 1 << uint64(CoreRoleVerified)
}
// Check if user is a committee member
if v.checkCommittee(ic) {
roles |= 1 << uint64(CoreRoleCommittee)
roles |= 1 << uint64(CoreRoleAttestor) // Committee members can attest
roles |= 1 << uint64(CoreRoleRecovery) // Committee members participate in recovery
}
return roles
}
// hasVerifiedAttributes checks if a token has any valid (non-expired, non-revoked) attributes.
func (v *Vita) hasVerifiedAttributes(d *dao.Simple, tokenID uint64) bool {
// For now, just check if any attribute exists
// In a full implementation, this would scan attributes and check expiry/revocation
prefix := make([]byte, 9)
prefix[0] = prefixAttribute
binary.BigEndian.PutUint64(prefix[1:], tokenID)
// Check if any attribute storage items exist for this token
var found bool
d.Seek(v.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
found = true
return false // Stop after finding first
})
return found
}
// requireRole checks if the caller has a specific role ID (for RoleRegistry integration).
// This is a stub that will be extended when RoleRegistry is implemented.
func (v *Vita) requireRole(ic *interop.Context, args []stackitem.Item) stackitem.Item {
roleID := toBigInt(args[0]).Uint64()
caller := ic.VM.GetCallingScriptHash()
// Get token by caller address
token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrCallerHasNoVita)
}
// Check token is active
if token.Status != state.TokenStatusActive {
panic(ErrTokenNotActive)
}
// Stub: In the future, this will check against RoleRegistry
// For now, just validate the token exists and is active
// Role 0 means "any authenticated user"
if roleID == 0 {
return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
}
// For non-zero roles, check if it maps to a core role
if roleID <= uint64(CoreRoleRecovery) {
roles := v.getCoreRoles(ic, token)
if roles&(1<<roleID) != 0 {
return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
}
}
panic(ErrRoleNotAssigned)
}
// requireCoreRole checks if the caller has a specific core role.
func (v *Vita) requireCoreRole(ic *interop.Context, args []stackitem.Item) stackitem.Item {
coreRole := CoreRole(toBigInt(args[0]).Uint64())
if coreRole > CoreRoleRecovery {
panic(ErrVitaInvalidRole)
}
caller := ic.VM.GetCallingScriptHash()
// Get token by caller address
token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrCallerHasNoVita)
}
// Check token is active
if token.Status != state.TokenStatusActive {
panic(ErrTokenNotActive)
}
// CoreRoleNone means any authenticated user
if coreRole == CoreRoleNone {
return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
}
// Check if user has the required core role
roles := v.getCoreRoles(ic, token)
if roles&(1<<uint64(coreRole)) != 0 {
return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
}
panic(ErrRoleNotAssigned)
}
// requirePermission checks if the caller has permission for a resource/action.
// This is a stub for future RoleRegistry integration with RBAC.
func (v *Vita) requirePermission(ic *interop.Context, args []stackitem.Item) stackitem.Item {
resource := toString(args[0])
action := toString(args[1])
scope := toString(args[2])
if len(resource) == 0 {
panic(ErrVitaInvalidResource)
}
if len(action) == 0 {
panic(ErrVitaInvalidAction)
}
caller := ic.VM.GetCallingScriptHash()
// Get token by caller address
token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
if err != nil {
panic(err)
}
if token == nil {
panic(ErrCallerHasNoVita)
}
// Check token is active
if token.Status != state.TokenStatusActive {
panic(ErrTokenNotActive)
}
// Stub: In future, this will check against RoleRegistry RBAC
// For now, committee members have all permissions
if v.checkCommittee(ic) {
return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
}
// For now, allow any authenticated user for basic resources
// This is a permissive stub - real implementation will check RBAC
_ = scope // scope will be used in future implementation
return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
}
// Public methods for cross-native access control
// ValidateCaller validates a caller has a Vita and returns their roles.
func (v *Vita) ValidateCaller(ic *interop.Context, caller util.Uint160) (*state.Vita, uint64, error) {
token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
if err != nil {
return nil, 0, err
}
if token == nil {
return nil, 0, ErrCallerHasNoVita
}
if token.Status != state.TokenStatusActive {
return nil, 0, ErrTokenNotActive
}
roles := v.getCoreRoles(ic, token)
return token, roles, nil
}
// HasCoreRole checks if a token holder has a specific core role.
func (v *Vita) HasCoreRole(ic *interop.Context, tokenID uint64, role CoreRole) bool {
token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
if err != nil || token == nil {
return false
}
if token.Status != state.TokenStatusActive {
return false
}
roles := v.getCoreRoles(ic, token)
return roles&(1<<uint64(role)) != 0
}