2091 lines
64 KiB
Go
2091 lines
64 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
|
|
Tutus ITutus
|
|
RoleRegistry IRoleRegistry
|
|
Lex ILex
|
|
Annos IAnnos
|
|
}
|
|
|
|
// 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 Tutus for backwards compatibility
|
|
return v.Tutus.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),
|
|
manifest.NewParameter("birthTimestamp", smartcontract.IntegerType))
|
|
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])
|
|
birthTimestamp := toUint64(args[4])
|
|
|
|
// 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)
|
|
}
|
|
|
|
// Register birth in Annos contract for lifespan tracking
|
|
// birthTimestamp is the actual birth date (Unix timestamp in seconds)
|
|
// This allows existing adults to register with their real birth date
|
|
// For newborns being registered at birth, use current block timestamp
|
|
if v.Annos != nil {
|
|
if err := v.Annos.RegisterBirthInternal(ic.DAO, ic, tokenID, owner, birthTimestamp); err != nil {
|
|
panic(fmt.Errorf("failed to register birth in Annos: %w", 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
|
|
}
|