481 lines
15 KiB
Go
481 lines
15 KiB
Go
package state
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
|
|
"github.com/tutus-one/tutus-chain/pkg/util"
|
|
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
|
|
)
|
|
|
|
// TokenStatus represents the status of a Vita token.
|
|
type TokenStatus uint8
|
|
|
|
const (
|
|
// TokenStatusActive indicates an active token.
|
|
TokenStatusActive TokenStatus = 0
|
|
// TokenStatusSuspended indicates a temporarily suspended token.
|
|
TokenStatusSuspended TokenStatus = 1
|
|
// TokenStatusRevoked indicates a permanently revoked token.
|
|
TokenStatusRevoked TokenStatus = 2
|
|
// TokenStatusRecovering indicates recovery is in progress.
|
|
TokenStatusRecovering TokenStatus = 3
|
|
)
|
|
|
|
// DisclosureLevel represents the visibility level of an attribute.
|
|
type DisclosureLevel uint8
|
|
|
|
const (
|
|
// DisclosurePrivate means only the owner can see the attribute.
|
|
DisclosurePrivate DisclosureLevel = 0
|
|
// DisclosureVerifier means owner and designated verifiers can see it.
|
|
DisclosureVerifier DisclosureLevel = 1
|
|
// DisclosureRole means owner and callers with specified roles can see it.
|
|
DisclosureRole DisclosureLevel = 2
|
|
// DisclosurePublic means anyone can see the attribute.
|
|
DisclosurePublic DisclosureLevel = 3
|
|
)
|
|
|
|
// RecoveryStatus represents the status of a recovery request.
|
|
type RecoveryStatus uint8
|
|
|
|
const (
|
|
// RecoveryStatusPending indicates recovery is pending approval.
|
|
RecoveryStatusPending RecoveryStatus = 0
|
|
// RecoveryStatusApproved indicates recovery has been approved.
|
|
RecoveryStatusApproved RecoveryStatus = 1
|
|
// RecoveryStatusExecuted indicates recovery has been executed.
|
|
RecoveryStatusExecuted RecoveryStatus = 2
|
|
// RecoveryStatusDenied indicates recovery has been denied.
|
|
RecoveryStatusDenied RecoveryStatus = 3
|
|
// RecoveryStatusExpired indicates recovery request has expired.
|
|
RecoveryStatusExpired RecoveryStatus = 4
|
|
)
|
|
|
|
// Vita represents a soul-bound identity token.
|
|
type Vita struct {
|
|
TokenID uint64 // Unique sequential identifier
|
|
Owner util.Uint160 // Owner's script hash
|
|
PersonHash []byte // Hash of biometric/identity proof
|
|
IsEntity bool // True if organization, false if natural person
|
|
CreatedAt uint32 // Block height when created
|
|
UpdatedAt uint32 // Block height of last modification
|
|
Status TokenStatus // Current status
|
|
StatusReason string // Reason for status change
|
|
RecoveryHash []byte // Hash of recovery mechanism
|
|
VestedUntil uint32 // Block height until which the Vita is vesting (Sybil resistance)
|
|
}
|
|
|
|
// ToStackItem implements stackitem.Convertible interface.
|
|
func (t *Vita) ToStackItem() (stackitem.Item, error) {
|
|
return stackitem.NewStruct([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(t.TokenID))),
|
|
stackitem.NewByteArray(t.Owner.BytesBE()),
|
|
stackitem.NewByteArray(t.PersonHash),
|
|
stackitem.NewBool(t.IsEntity),
|
|
stackitem.NewBigInteger(big.NewInt(int64(t.CreatedAt))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(t.UpdatedAt))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(t.Status))),
|
|
stackitem.NewByteArray([]byte(t.StatusReason)),
|
|
stackitem.NewByteArray(t.RecoveryHash),
|
|
stackitem.NewBigInteger(big.NewInt(int64(t.VestedUntil))),
|
|
}), nil
|
|
}
|
|
|
|
// FromStackItem implements stackitem.Convertible interface.
|
|
func (t *Vita) FromStackItem(item stackitem.Item) error {
|
|
items, ok := item.Value().([]stackitem.Item)
|
|
if !ok {
|
|
return errors.New("not a struct")
|
|
}
|
|
if len(items) != 10 {
|
|
return fmt.Errorf("wrong number of elements: expected 10, got %d", len(items))
|
|
}
|
|
|
|
tokenID, err := items[0].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid tokenID: %w", err)
|
|
}
|
|
t.TokenID = tokenID.Uint64()
|
|
|
|
ownerBytes, err := items[1].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid owner: %w", err)
|
|
}
|
|
t.Owner, err = util.Uint160DecodeBytesBE(ownerBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid owner hash: %w", err)
|
|
}
|
|
|
|
t.PersonHash, err = items[2].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid personHash: %w", err)
|
|
}
|
|
|
|
isEntity, err := items[3].TryBool()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid isEntity: %w", err)
|
|
}
|
|
t.IsEntity = isEntity
|
|
|
|
createdAt, err := items[4].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid createdAt: %w", err)
|
|
}
|
|
t.CreatedAt = uint32(createdAt.Int64())
|
|
|
|
updatedAt, err := items[5].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid updatedAt: %w", err)
|
|
}
|
|
t.UpdatedAt = uint32(updatedAt.Int64())
|
|
|
|
status, err := items[6].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid status: %w", err)
|
|
}
|
|
t.Status = TokenStatus(status.Int64())
|
|
|
|
statusReasonBytes, err := items[7].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid statusReason: %w", err)
|
|
}
|
|
t.StatusReason = string(statusReasonBytes)
|
|
|
|
t.RecoveryHash, err = items[8].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid recoveryHash: %w", err)
|
|
}
|
|
|
|
vestedUntil, err := items[9].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid vestedUntil: %w", err)
|
|
}
|
|
t.VestedUntil = uint32(vestedUntil.Int64())
|
|
|
|
return nil
|
|
}
|
|
|
|
// Attribute represents an identity attribute with disclosure control.
|
|
type Attribute struct {
|
|
Key string // Attribute name
|
|
ValueHash []byte // Hash of value (privacy)
|
|
ValueEnc []byte // Encrypted value (optional)
|
|
Attestor util.Uint160 // Who attested this (script hash)
|
|
AttestedAt uint32 // Block height when attested
|
|
ExpiresAt uint32 // Optional expiration (0 = never)
|
|
Revoked bool // Whether attribute has been revoked
|
|
RevokedAt uint32 // Block height when revoked
|
|
DisclosureLevel DisclosureLevel // Visibility level
|
|
}
|
|
|
|
// ToStackItem implements stackitem.Convertible interface.
|
|
func (a *Attribute) ToStackItem() (stackitem.Item, error) {
|
|
return stackitem.NewStruct([]stackitem.Item{
|
|
stackitem.NewByteArray([]byte(a.Key)),
|
|
stackitem.NewByteArray(a.ValueHash),
|
|
stackitem.NewByteArray(a.ValueEnc),
|
|
stackitem.NewByteArray(a.Attestor.BytesBE()),
|
|
stackitem.NewBigInteger(big.NewInt(int64(a.AttestedAt))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(a.ExpiresAt))),
|
|
stackitem.NewBool(a.Revoked),
|
|
stackitem.NewBigInteger(big.NewInt(int64(a.RevokedAt))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(a.DisclosureLevel))),
|
|
}), nil
|
|
}
|
|
|
|
// FromStackItem implements stackitem.Convertible interface.
|
|
func (a *Attribute) FromStackItem(item stackitem.Item) error {
|
|
items, ok := item.Value().([]stackitem.Item)
|
|
if !ok {
|
|
return errors.New("not a struct")
|
|
}
|
|
if len(items) != 9 {
|
|
return fmt.Errorf("wrong number of elements: expected 9, got %d", len(items))
|
|
}
|
|
|
|
keyBytes, err := items[0].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid key: %w", err)
|
|
}
|
|
a.Key = string(keyBytes)
|
|
|
|
a.ValueHash, err = items[1].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid valueHash: %w", err)
|
|
}
|
|
|
|
a.ValueEnc, err = items[2].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid valueEnc: %w", err)
|
|
}
|
|
|
|
attestorBytes, err := items[3].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid attestor: %w", err)
|
|
}
|
|
a.Attestor, err = util.Uint160DecodeBytesBE(attestorBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid attestor hash: %w", err)
|
|
}
|
|
|
|
attestedAt, err := items[4].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid attestedAt: %w", err)
|
|
}
|
|
a.AttestedAt = uint32(attestedAt.Int64())
|
|
|
|
expiresAt, err := items[5].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid expiresAt: %w", err)
|
|
}
|
|
a.ExpiresAt = uint32(expiresAt.Int64())
|
|
|
|
a.Revoked, err = items[6].TryBool()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid revoked: %w", err)
|
|
}
|
|
|
|
revokedAt, err := items[7].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid revokedAt: %w", err)
|
|
}
|
|
a.RevokedAt = uint32(revokedAt.Int64())
|
|
|
|
disclosureLevel, err := items[8].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid disclosureLevel: %w", err)
|
|
}
|
|
a.DisclosureLevel = DisclosureLevel(disclosureLevel.Int64())
|
|
|
|
return nil
|
|
}
|
|
|
|
// AuthChallenge represents a passwordless authentication challenge.
|
|
type AuthChallenge struct {
|
|
ChallengeID util.Uint256 // Unique challenge identifier
|
|
TokenID uint64 // Associated Vita token
|
|
Nonce []byte // Random bytes to sign
|
|
CreatedAt uint32 // Block height when created
|
|
ExpiresAt uint32 // Block height when expires
|
|
Purpose string // "login", "sign", "recover"
|
|
Fulfilled bool // Whether challenge has been fulfilled
|
|
FulfilledAt uint32 // Block height when fulfilled
|
|
}
|
|
|
|
// ToStackItem implements stackitem.Convertible interface.
|
|
func (c *AuthChallenge) ToStackItem() (stackitem.Item, error) {
|
|
return stackitem.NewStruct([]stackitem.Item{
|
|
stackitem.NewByteArray(c.ChallengeID.BytesBE()),
|
|
stackitem.NewBigInteger(big.NewInt(int64(c.TokenID))),
|
|
stackitem.NewByteArray(c.Nonce),
|
|
stackitem.NewBigInteger(big.NewInt(int64(c.CreatedAt))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(c.ExpiresAt))),
|
|
stackitem.NewByteArray([]byte(c.Purpose)),
|
|
stackitem.NewBool(c.Fulfilled),
|
|
stackitem.NewBigInteger(big.NewInt(int64(c.FulfilledAt))),
|
|
}), nil
|
|
}
|
|
|
|
// FromStackItem implements stackitem.Convertible interface.
|
|
func (c *AuthChallenge) FromStackItem(item stackitem.Item) error {
|
|
items, ok := item.Value().([]stackitem.Item)
|
|
if !ok {
|
|
return errors.New("not a struct")
|
|
}
|
|
if len(items) != 8 {
|
|
return fmt.Errorf("wrong number of elements: expected 8, got %d", len(items))
|
|
}
|
|
|
|
challengeIDBytes, err := items[0].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid challengeID: %w", err)
|
|
}
|
|
c.ChallengeID, err = util.Uint256DecodeBytesBE(challengeIDBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid challengeID hash: %w", err)
|
|
}
|
|
|
|
tokenID, err := items[1].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid tokenID: %w", err)
|
|
}
|
|
c.TokenID = tokenID.Uint64()
|
|
|
|
c.Nonce, err = items[2].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid nonce: %w", err)
|
|
}
|
|
|
|
createdAt, err := items[3].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid createdAt: %w", err)
|
|
}
|
|
c.CreatedAt = uint32(createdAt.Int64())
|
|
|
|
expiresAt, err := items[4].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid expiresAt: %w", err)
|
|
}
|
|
c.ExpiresAt = uint32(expiresAt.Int64())
|
|
|
|
purposeBytes, err := items[5].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid purpose: %w", err)
|
|
}
|
|
c.Purpose = string(purposeBytes)
|
|
|
|
c.Fulfilled, err = items[6].TryBool()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid fulfilled: %w", err)
|
|
}
|
|
|
|
fulfilledAt, err := items[7].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid fulfilledAt: %w", err)
|
|
}
|
|
c.FulfilledAt = uint32(fulfilledAt.Int64())
|
|
|
|
return nil
|
|
}
|
|
|
|
// RecoveryRequest represents a key recovery request.
|
|
type RecoveryRequest struct {
|
|
RequestID util.Uint256 // Unique request identifier
|
|
TokenID uint64 // Token being recovered
|
|
NewOwner util.Uint160 // Proposed new owner
|
|
Requester util.Uint160 // Who initiated recovery
|
|
Evidence []byte // Encrypted evidence hash
|
|
Approvals []util.Uint160 // Approvers who have approved
|
|
RequiredApprovals int // Required number of approvals
|
|
CreatedAt uint32 // Block height when created
|
|
DelayUntil uint32 // Block height when executable
|
|
ExpiresAt uint32 // Block height when expires
|
|
Status RecoveryStatus // Current status
|
|
}
|
|
|
|
// ToStackItem implements stackitem.Convertible interface.
|
|
func (r *RecoveryRequest) ToStackItem() (stackitem.Item, error) {
|
|
approvals := make([]stackitem.Item, len(r.Approvals))
|
|
for i, a := range r.Approvals {
|
|
approvals[i] = stackitem.NewByteArray(a.BytesBE())
|
|
}
|
|
|
|
return stackitem.NewStruct([]stackitem.Item{
|
|
stackitem.NewByteArray(r.RequestID.BytesBE()),
|
|
stackitem.NewBigInteger(big.NewInt(int64(r.TokenID))),
|
|
stackitem.NewByteArray(r.NewOwner.BytesBE()),
|
|
stackitem.NewByteArray(r.Requester.BytesBE()),
|
|
stackitem.NewByteArray(r.Evidence),
|
|
stackitem.NewArray(approvals),
|
|
stackitem.NewBigInteger(big.NewInt(int64(r.RequiredApprovals))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(r.CreatedAt))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(r.DelayUntil))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(r.ExpiresAt))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(r.Status))),
|
|
}), nil
|
|
}
|
|
|
|
// FromStackItem implements stackitem.Convertible interface.
|
|
func (r *RecoveryRequest) FromStackItem(item stackitem.Item) error {
|
|
items, ok := item.Value().([]stackitem.Item)
|
|
if !ok {
|
|
return errors.New("not a struct")
|
|
}
|
|
if len(items) != 11 {
|
|
return fmt.Errorf("wrong number of elements: expected 11, got %d", len(items))
|
|
}
|
|
|
|
requestIDBytes, err := items[0].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid requestID: %w", err)
|
|
}
|
|
r.RequestID, err = util.Uint256DecodeBytesBE(requestIDBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid requestID hash: %w", err)
|
|
}
|
|
|
|
tokenID, err := items[1].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid tokenID: %w", err)
|
|
}
|
|
r.TokenID = tokenID.Uint64()
|
|
|
|
newOwnerBytes, err := items[2].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid newOwner: %w", err)
|
|
}
|
|
r.NewOwner, err = util.Uint160DecodeBytesBE(newOwnerBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid newOwner hash: %w", err)
|
|
}
|
|
|
|
requesterBytes, err := items[3].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid requester: %w", err)
|
|
}
|
|
r.Requester, err = util.Uint160DecodeBytesBE(requesterBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid requester hash: %w", err)
|
|
}
|
|
|
|
r.Evidence, err = items[4].TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid evidence: %w", err)
|
|
}
|
|
|
|
approvalsArray, ok := items[5].Value().([]stackitem.Item)
|
|
if !ok {
|
|
return errors.New("approvals is not an array")
|
|
}
|
|
r.Approvals = make([]util.Uint160, len(approvalsArray))
|
|
for i, a := range approvalsArray {
|
|
approvalBytes, err := a.TryBytes()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid approval %d: %w", i, err)
|
|
}
|
|
r.Approvals[i], err = util.Uint160DecodeBytesBE(approvalBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid approval %d hash: %w", i, err)
|
|
}
|
|
}
|
|
|
|
requiredApprovals, err := items[6].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid requiredApprovals: %w", err)
|
|
}
|
|
ra := requiredApprovals.Int64()
|
|
if ra < 0 || ra > math.MaxInt {
|
|
return errors.New("requiredApprovals out of range")
|
|
}
|
|
r.RequiredApprovals = int(ra)
|
|
|
|
createdAt, err := items[7].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid createdAt: %w", err)
|
|
}
|
|
r.CreatedAt = uint32(createdAt.Int64())
|
|
|
|
delayUntil, err := items[8].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid delayUntil: %w", err)
|
|
}
|
|
r.DelayUntil = uint32(delayUntil.Int64())
|
|
|
|
expiresAt, err := items[9].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid expiresAt: %w", err)
|
|
}
|
|
r.ExpiresAt = uint32(expiresAt.Int64())
|
|
|
|
status, err := items[10].TryInteger()
|
|
if err != nil {
|
|
return fmt.Errorf("invalid status: %w", err)
|
|
}
|
|
r.Status = RecoveryStatus(status.Int64())
|
|
|
|
return nil
|
|
}
|