1308 lines
44 KiB
Go
1308 lines
44 KiB
Go
package native
|
|
|
|
import (
|
|
"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"
|
|
)
|
|
|
|
// Ancora represents the Ancora native contract for anchoring Merkle roots of off-chain data.
|
|
// Latin: "ancora" = anchor - anchors off-chain data to on-chain verification.
|
|
type Ancora struct {
|
|
interop.ContractMD
|
|
Vita IVita
|
|
Tutus ITutus
|
|
Audit *AuditLogger // ARCH-005: Comprehensive audit logging
|
|
}
|
|
|
|
// AncoraCache holds cached configuration for the Ancora contract.
|
|
type AncoraCache struct {
|
|
config state.StateAnchorsConfig
|
|
}
|
|
|
|
// Storage prefixes for Ancora contract.
|
|
const (
|
|
ancoraConfigPrefix = 0x01
|
|
ancoraRootPrefix = 0x10 // vitaID + dataType -> RootInfo
|
|
ancoraHistoryPrefix = 0x11 // vitaID + dataType + version -> RootInfo
|
|
ancoraProviderPrefix = 0x20 // dataType + provider -> ProviderConfig
|
|
ancoraErasurePrefix = 0x30 // vitaID + dataType -> ErasureInfo
|
|
ancoraUpdateCountPrefix = 0x40 // blockHeight + provider -> count
|
|
ancoraLastUpdatePrefix = 0x41 // vitaID + dataType + provider -> blockHeight
|
|
ancoraAttestationPrefix = 0x50 // attestationHash -> AttestationInfo
|
|
)
|
|
|
|
// Errors for Ancora contract.
|
|
var (
|
|
ErrAncoraInvalidDataType = errors.New("invalid data type")
|
|
ErrAncoraInvalidRoot = errors.New("invalid Merkle root: must be 32 bytes")
|
|
ErrAncoraProviderNotFound = errors.New("provider not found")
|
|
ErrAncoraProviderInactive = errors.New("provider is inactive")
|
|
ErrAncoraProviderExists = errors.New("provider already registered")
|
|
ErrAncoraUnauthorized = errors.New("unauthorized: caller is not authorized provider")
|
|
ErrAncoraRateLimited = errors.New("rate limit exceeded")
|
|
ErrAncoraUpdateCooldown = errors.New("update cooldown not elapsed")
|
|
ErrAncoraNoRoot = errors.New("no root found for vitaID and dataType")
|
|
ErrAncoraInvalidProof = errors.New("invalid Merkle proof")
|
|
ErrAncoraProofTooDeep = errors.New("proof exceeds maximum depth")
|
|
ErrAncoraVersionNotFound = errors.New("version not found in history")
|
|
ErrAncoraErasurePending = errors.New("erasure already pending")
|
|
ErrAncoraErasureNotPending = errors.New("no pending erasure request")
|
|
ErrAncoraErasureGracePeriod = errors.New("erasure grace period not elapsed")
|
|
ErrAncoraInvalidAttestation = errors.New("invalid attestation")
|
|
ErrAncoraAttestationExpired = errors.New("attestation has expired")
|
|
ErrAncoraVitaNotFound = errors.New("vita not found")
|
|
)
|
|
|
|
var (
|
|
_ interop.Contract = (*Ancora)(nil)
|
|
_ dao.NativeContractCache = (*AncoraCache)(nil)
|
|
)
|
|
|
|
// Copy implements NativeContractCache interface.
|
|
func (c *AncoraCache) Copy() dao.NativeContractCache {
|
|
cp := &AncoraCache{config: c.config}
|
|
return cp
|
|
}
|
|
|
|
// newAncora returns a new Ancora native contract.
|
|
func newAncora() *Ancora {
|
|
a := &Ancora{
|
|
ContractMD: *interop.NewContractMD(nativenames.Ancora, nativeids.Ancora, nil),
|
|
Audit: NewAuditLogger(nativeids.Ancora),
|
|
}
|
|
defer a.BuildHFSpecificMD(a.ActiveIn())
|
|
|
|
// Configuration methods
|
|
desc := NewDescriptor("getConfig", smartcontract.ArrayType)
|
|
md := NewMethodAndPrice(a.getConfig, 1<<15, callflag.ReadStates)
|
|
a.AddMethod(md, desc)
|
|
|
|
desc = NewDescriptor("setConfig", smartcontract.VoidType,
|
|
manifest.NewParameter("config", smartcontract.ArrayType))
|
|
md = NewMethodAndPrice(a.setConfig, 1<<15, callflag.States)
|
|
a.AddMethod(md, desc)
|
|
|
|
// Provider management
|
|
desc = NewDescriptor("registerProvider", smartcontract.BoolType,
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType),
|
|
manifest.NewParameter("provider", smartcontract.Hash160Type),
|
|
manifest.NewParameter("description", smartcontract.StringType),
|
|
manifest.NewParameter("maxUpdatesPerBlock", smartcontract.IntegerType),
|
|
manifest.NewParameter("updateCooldown", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(a.registerProvider, 1<<15, callflag.States)
|
|
a.AddMethod(md, desc)
|
|
|
|
desc = NewDescriptor("revokeProvider", smartcontract.BoolType,
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType),
|
|
manifest.NewParameter("provider", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(a.revokeProvider, 1<<15, callflag.States)
|
|
a.AddMethod(md, desc)
|
|
|
|
desc = NewDescriptor("getProvider", smartcontract.ArrayType,
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType),
|
|
manifest.NewParameter("provider", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(a.getProvider, 1<<15, callflag.ReadStates)
|
|
a.AddMethod(md, desc)
|
|
|
|
desc = NewDescriptor("isProviderActive", smartcontract.BoolType,
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType),
|
|
manifest.NewParameter("provider", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(a.isProviderActive, 1<<15, callflag.ReadStates)
|
|
a.AddMethod(md, desc)
|
|
|
|
// Root management
|
|
desc = NewDescriptor("updateDataRoot", smartcontract.BoolType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType),
|
|
manifest.NewParameter("root", smartcontract.ByteArrayType),
|
|
manifest.NewParameter("leafCount", smartcontract.IntegerType),
|
|
manifest.NewParameter("treeAlgorithm", smartcontract.IntegerType),
|
|
manifest.NewParameter("schemaVersion", smartcontract.StringType),
|
|
manifest.NewParameter("contentHash", smartcontract.ByteArrayType))
|
|
md = NewMethodAndPrice(a.updateDataRoot, 1<<16, callflag.States)
|
|
a.AddMethod(md, desc)
|
|
|
|
desc = NewDescriptor("getDataRoot", smartcontract.ArrayType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(a.getDataRoot, 1<<15, callflag.ReadStates)
|
|
a.AddMethod(md, desc)
|
|
|
|
desc = NewDescriptor("getDataRootAtVersion", smartcontract.ArrayType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType),
|
|
manifest.NewParameter("version", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(a.getDataRootAtVersion, 1<<15, callflag.ReadStates)
|
|
a.AddMethod(md, desc)
|
|
|
|
// Proof verification
|
|
desc = NewDescriptor("verifyProof", smartcontract.BoolType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType),
|
|
manifest.NewParameter("leaf", smartcontract.ByteArrayType),
|
|
manifest.NewParameter("proof", smartcontract.ArrayType),
|
|
manifest.NewParameter("index", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(a.verifyProof, 1<<16, callflag.ReadStates)
|
|
a.AddMethod(md, desc)
|
|
|
|
desc = NewDescriptor("verifyProofAtVersion", smartcontract.BoolType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType),
|
|
manifest.NewParameter("version", smartcontract.IntegerType),
|
|
manifest.NewParameter("leaf", smartcontract.ByteArrayType),
|
|
manifest.NewParameter("proof", smartcontract.ArrayType),
|
|
manifest.NewParameter("index", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(a.verifyProofAtVersion, 1<<16, callflag.ReadStates)
|
|
a.AddMethod(md, desc)
|
|
|
|
// GDPR erasure
|
|
desc = NewDescriptor("requestErasure", smartcontract.BoolType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
md = NewMethodAndPrice(a.requestErasure, 1<<15, callflag.States)
|
|
a.AddMethod(md, desc)
|
|
|
|
desc = NewDescriptor("confirmErasure", smartcontract.BoolType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(a.confirmErasure, 1<<15, callflag.States)
|
|
a.AddMethod(md, desc)
|
|
|
|
desc = NewDescriptor("denyErasure", smartcontract.BoolType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
md = NewMethodAndPrice(a.denyErasure, 1<<15, callflag.States)
|
|
a.AddMethod(md, desc)
|
|
|
|
desc = NewDescriptor("getErasureInfo", smartcontract.ArrayType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("dataType", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(a.getErasureInfo, 1<<15, callflag.ReadStates)
|
|
a.AddMethod(md, desc)
|
|
|
|
// Data portability
|
|
desc = NewDescriptor("generatePortabilityAttestation", smartcontract.ByteArrayType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("dataTypes", smartcontract.ArrayType))
|
|
md = NewMethodAndPrice(a.generatePortabilityAttestation, 1<<16, callflag.States)
|
|
a.AddMethod(md, desc)
|
|
|
|
desc = NewDescriptor("verifyPortabilityAttestation", smartcontract.BoolType,
|
|
manifest.NewParameter("attestation", smartcontract.ByteArrayType))
|
|
md = NewMethodAndPrice(a.verifyPortabilityAttestation, 1<<15, callflag.ReadStates)
|
|
a.AddMethod(md, desc)
|
|
|
|
// Audit log query (ARCH-005)
|
|
desc = NewDescriptor("getAuditLog", smartcontract.ArrayType,
|
|
manifest.NewParameter("vitaID", smartcontract.IntegerType),
|
|
manifest.NewParameter("startBlock", smartcontract.IntegerType),
|
|
manifest.NewParameter("endBlock", smartcontract.IntegerType),
|
|
manifest.NewParameter("limit", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(a.getAuditLog, 1<<16, callflag.ReadStates)
|
|
a.AddMethod(md, desc)
|
|
|
|
return a
|
|
}
|
|
|
|
// Metadata implements the Contract interface.
|
|
func (a *Ancora) Metadata() *interop.ContractMD {
|
|
return &a.ContractMD
|
|
}
|
|
|
|
// Initialize initializes Ancora native contract and implements the Contract interface.
|
|
func (a *Ancora) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
|
|
if hf != a.ActiveIn() {
|
|
return nil
|
|
}
|
|
|
|
cfg := state.DefaultStateAnchorsConfig()
|
|
if err := a.putConfig(ic.DAO, &cfg); err != nil {
|
|
return fmt.Errorf("failed to initialize config: %w", err)
|
|
}
|
|
|
|
cache := &AncoraCache{config: cfg}
|
|
ic.DAO.SetCache(a.ID, cache)
|
|
return nil
|
|
}
|
|
|
|
// InitializeCache implements the Contract interface.
|
|
func (a *Ancora) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
|
|
cfg, err := a.loadConfig(d)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load config: %w", err)
|
|
}
|
|
|
|
cache := &AncoraCache{config: *cfg}
|
|
d.SetCache(a.ID, cache)
|
|
return nil
|
|
}
|
|
|
|
// OnPersist implements the Contract interface.
|
|
func (a *Ancora) OnPersist(ic *interop.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// PostPersist implements the Contract interface.
|
|
func (a *Ancora) PostPersist(ic *interop.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// ActiveIn implements the Contract interface.
|
|
func (a *Ancora) ActiveIn() *config.Hardfork {
|
|
return nil // Active from genesis
|
|
}
|
|
|
|
// Address returns the contract's script hash.
|
|
func (a *Ancora) Address() util.Uint160 {
|
|
return a.Hash
|
|
}
|
|
|
|
// ========== Configuration Methods ==========
|
|
|
|
func (a *Ancora) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
|
cache := ic.DAO.GetROCache(a.ID).(*AncoraCache)
|
|
cfg := cache.config
|
|
return stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(cfg.DefaultTreeAlgorithm))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(cfg.MaxProofDepth))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(cfg.DefaultMaxUpdatesPerBlock))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(cfg.DefaultUpdateCooldown))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(cfg.MaxHistoryVersions))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(cfg.ErasureGracePeriod))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(cfg.AttestationValidBlocks))),
|
|
})
|
|
}
|
|
|
|
func (a *Ancora) setConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
if !a.Tutus.CheckCommittee(ic) {
|
|
panic("invalid committee signature")
|
|
}
|
|
|
|
arr, ok := args[0].Value().([]stackitem.Item)
|
|
if !ok || len(arr) != 7 {
|
|
panic("invalid config array")
|
|
}
|
|
|
|
cfg := state.StateAnchorsConfig{
|
|
DefaultTreeAlgorithm: state.TreeAlgorithm(toUint32(arr[0])),
|
|
MaxProofDepth: toUint32(arr[1]),
|
|
DefaultMaxUpdatesPerBlock: toUint32(arr[2]),
|
|
DefaultUpdateCooldown: toUint32(arr[3]),
|
|
MaxHistoryVersions: toUint32(arr[4]),
|
|
ErasureGracePeriod: toUint32(arr[5]),
|
|
AttestationValidBlocks: toUint32(arr[6]),
|
|
}
|
|
|
|
if err := a.putConfig(ic.DAO, &cfg); err != nil {
|
|
panic(fmt.Errorf("failed to save config: %w", err))
|
|
}
|
|
|
|
cache := ic.DAO.GetRWCache(a.ID).(*AncoraCache)
|
|
cache.config = cfg
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
func (a *Ancora) putConfig(d *dao.Simple, cfg *state.StateAnchorsConfig) error {
|
|
key := []byte{ancoraConfigPrefix}
|
|
data := make([]byte, 28) // 7 * 4 bytes
|
|
// Simple serialization
|
|
putUint32(data[0:4], uint32(cfg.DefaultTreeAlgorithm))
|
|
putUint32(data[4:8], cfg.MaxProofDepth)
|
|
putUint32(data[8:12], cfg.DefaultMaxUpdatesPerBlock)
|
|
putUint32(data[12:16], cfg.DefaultUpdateCooldown)
|
|
putUint32(data[16:20], cfg.MaxHistoryVersions)
|
|
putUint32(data[20:24], cfg.ErasureGracePeriod)
|
|
putUint32(data[24:28], cfg.AttestationValidBlocks)
|
|
d.PutStorageItem(a.ID, key, data)
|
|
return nil
|
|
}
|
|
|
|
func (a *Ancora) loadConfig(d *dao.Simple) (*state.StateAnchorsConfig, error) {
|
|
key := []byte{ancoraConfigPrefix}
|
|
data := d.GetStorageItem(a.ID, key)
|
|
if data == nil {
|
|
return nil, errors.New("config not found")
|
|
}
|
|
if len(data) != 28 {
|
|
return nil, errors.New("invalid config data length")
|
|
}
|
|
cfg := &state.StateAnchorsConfig{
|
|
DefaultTreeAlgorithm: state.TreeAlgorithm(getUint32(data[0:4])),
|
|
MaxProofDepth: getUint32(data[4:8]),
|
|
DefaultMaxUpdatesPerBlock: getUint32(data[8:12]),
|
|
DefaultUpdateCooldown: getUint32(data[12:16]),
|
|
MaxHistoryVersions: getUint32(data[16:20]),
|
|
ErasureGracePeriod: getUint32(data[20:24]),
|
|
AttestationValidBlocks: getUint32(data[24:28]),
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
// ========== Provider Management ==========
|
|
|
|
func (a *Ancora) registerProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
if !a.Tutus.CheckCommittee(ic) {
|
|
panic("invalid committee signature")
|
|
}
|
|
|
|
dataType := state.DataType(toUint32(args[0]))
|
|
if dataType > state.DataTypeCustom {
|
|
panic(ErrAncoraInvalidDataType)
|
|
}
|
|
// Note: Valid types are 0-5 (Medical, Education, Investment, Documents, Presence, Custom)
|
|
|
|
provider := toUint160(args[1])
|
|
description := toString(args[2])
|
|
maxUpdatesPerBlock := toUint32(args[3])
|
|
updateCooldown := toUint32(args[4])
|
|
|
|
// Check if provider already exists
|
|
existing := a.getProviderConfig(ic.DAO, dataType, provider)
|
|
if existing != nil {
|
|
panic(ErrAncoraProviderExists)
|
|
}
|
|
|
|
cfg := &state.ProviderConfig{
|
|
DataType: dataType,
|
|
Provider: provider,
|
|
Description: description,
|
|
RegisteredAt: ic.BlockHeight(),
|
|
Active: true,
|
|
MaxUpdatesPerBlock: maxUpdatesPerBlock,
|
|
UpdateCooldown: updateCooldown,
|
|
}
|
|
|
|
if err := a.putProviderConfig(ic.DAO, cfg); err != nil {
|
|
panic(fmt.Errorf("failed to register provider: %w", err))
|
|
}
|
|
|
|
// ARCH-005: Audit log provider registration
|
|
a.Audit.LogGovernance(ic.DAO, ic.VM.GetCallingScriptHash(), "ancora.provider_registered",
|
|
fmt.Sprintf("dataType=%d provider=%s desc=%s", dataType, provider.StringLE(), description),
|
|
AuditOutcomeSuccess, ic.BlockHeight())
|
|
|
|
ic.AddNotification(a.Hash, "ProviderRegistered", stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(dataType))),
|
|
stackitem.NewByteArray(provider.BytesBE()),
|
|
stackitem.NewByteArray([]byte(description)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (a *Ancora) revokeProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
if !a.Tutus.CheckCommittee(ic) {
|
|
panic("invalid committee signature")
|
|
}
|
|
|
|
dataType := state.DataType(toUint32(args[0]))
|
|
provider := toUint160(args[1])
|
|
|
|
cfg := a.getProviderConfig(ic.DAO, dataType, provider)
|
|
if cfg == nil {
|
|
panic(ErrAncoraProviderNotFound)
|
|
}
|
|
|
|
cfg.Active = false
|
|
if err := a.putProviderConfig(ic.DAO, cfg); err != nil {
|
|
panic(fmt.Errorf("failed to revoke provider: %w", err))
|
|
}
|
|
|
|
// ARCH-005: Audit log provider revocation
|
|
a.Audit.LogGovernance(ic.DAO, ic.VM.GetCallingScriptHash(), "ancora.provider_revoked",
|
|
fmt.Sprintf("dataType=%d provider=%s", dataType, provider.StringLE()),
|
|
AuditOutcomeSuccess, ic.BlockHeight())
|
|
|
|
ic.AddNotification(a.Hash, "ProviderRevoked", stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(dataType))),
|
|
stackitem.NewByteArray(provider.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (a *Ancora) getProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
dataType := state.DataType(toUint32(args[0]))
|
|
provider := toUint160(args[1])
|
|
|
|
cfg := a.getProviderConfig(ic.DAO, dataType, provider)
|
|
if cfg == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
item, err := cfg.ToStackItem()
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to convert provider config to stack item: %w", err))
|
|
}
|
|
return item
|
|
}
|
|
|
|
func (a *Ancora) isProviderActive(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
dataType := state.DataType(toUint32(args[0]))
|
|
provider := toUint160(args[1])
|
|
|
|
cfg := a.getProviderConfig(ic.DAO, dataType, provider)
|
|
return stackitem.NewBool(cfg != nil && cfg.Active)
|
|
}
|
|
|
|
func (a *Ancora) getProviderConfig(d *dao.Simple, dataType state.DataType, provider util.Uint160) *state.ProviderConfig {
|
|
key := append([]byte{ancoraProviderPrefix, byte(dataType)}, provider.BytesBE()...)
|
|
cfg := new(state.ProviderConfig)
|
|
err := getConvertibleFromDAO(a.ID, d, key, cfg)
|
|
if err != nil {
|
|
if errors.Is(err, storage.ErrKeyNotFound) {
|
|
return nil
|
|
}
|
|
panic(fmt.Errorf("failed to get provider config: %w", err))
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func (a *Ancora) putProviderConfig(d *dao.Simple, cfg *state.ProviderConfig) error {
|
|
key := append([]byte{ancoraProviderPrefix, byte(cfg.DataType)}, cfg.Provider.BytesBE()...)
|
|
return putConvertibleToDAO(a.ID, d, key, cfg)
|
|
}
|
|
|
|
// ========== Root Management ==========
|
|
|
|
func (a *Ancora) updateDataRoot(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
dataType := state.DataType(toUint32(args[1]))
|
|
root, err := args[2].TryBytes()
|
|
if err != nil {
|
|
panic(ErrAncoraInvalidRoot)
|
|
}
|
|
if len(root) != 32 {
|
|
panic(ErrAncoraInvalidRoot)
|
|
}
|
|
leafCount := toUint64(args[3])
|
|
treeAlgorithm := state.TreeAlgorithm(toUint32(args[4]))
|
|
schemaVersion := toString(args[5])
|
|
contentHash, _ := args[6].TryBytes()
|
|
|
|
// Verify Vita exists
|
|
if !a.Vita.ExistsInternal(ic.DAO, vitaID) {
|
|
panic(ErrAncoraVitaNotFound)
|
|
}
|
|
|
|
// Verify caller is authorized provider or Vita owner
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
providerCfg := a.getProviderConfig(ic.DAO, dataType, caller)
|
|
|
|
if providerCfg == nil || !providerCfg.Active {
|
|
// Check if caller is Vita owner
|
|
owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
|
|
ok, err := runtime.CheckHashedWitness(ic, owner)
|
|
if err != nil || !ok {
|
|
panic(ErrAncoraUnauthorized)
|
|
}
|
|
} else {
|
|
// Rate limiting for providers
|
|
if err := a.checkRateLimit(ic, providerCfg, vitaID, dataType); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Get current root to determine version
|
|
currentRoot := a.getRootInfo(ic.DAO, vitaID, dataType)
|
|
var version uint64 = 1
|
|
if currentRoot != nil {
|
|
version = currentRoot.Version + 1
|
|
// Archive current root to history
|
|
if err := a.archiveRoot(ic.DAO, vitaID, dataType, currentRoot); err != nil {
|
|
panic(fmt.Errorf("failed to archive previous root: %w", err))
|
|
}
|
|
}
|
|
|
|
newRoot := &state.RootInfo{
|
|
Root: root,
|
|
LeafCount: leafCount,
|
|
UpdatedAt: ic.BlockHeight(),
|
|
UpdatedBy: caller,
|
|
Version: version,
|
|
TreeAlgorithm: treeAlgorithm,
|
|
SchemaVersion: schemaVersion,
|
|
ContentHash: contentHash,
|
|
}
|
|
|
|
if err := a.putRootInfo(ic.DAO, vitaID, dataType, newRoot); err != nil {
|
|
panic(fmt.Errorf("failed to update root: %w", err))
|
|
}
|
|
|
|
// Update rate limit tracking
|
|
a.recordUpdate(ic.DAO, ic.BlockHeight(), caller, vitaID, dataType)
|
|
|
|
// ARCH-005: Audit log data root update
|
|
// For GDPR compliance: the on-chain audit entry serves as immutable proof
|
|
// that an update occurred, even if off-chain data is later deleted
|
|
prevStateHash := util.Uint256{}
|
|
newStateHash := hash.Sha256(root)
|
|
if currentRoot != nil {
|
|
prevStateHash = hash.Sha256(currentRoot.Root)
|
|
}
|
|
resourceID := make([]byte, 9)
|
|
putUint64(resourceID[0:8], vitaID)
|
|
resourceID[8] = byte(dataType)
|
|
a.Audit.LogDataChange(ic.DAO, caller, nativeids.Ancora, "ancora.data_root_updated",
|
|
resourceID, prevStateHash, newStateHash, ic.BlockHeight())
|
|
|
|
ic.AddNotification(a.Hash, "DataRootUpdated", stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(dataType))),
|
|
stackitem.NewByteArray(root),
|
|
stackitem.NewBigInteger(big.NewInt(int64(version))),
|
|
stackitem.NewByteArray(caller.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (a *Ancora) getDataRoot(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
dataType := state.DataType(toUint32(args[1]))
|
|
|
|
root := a.getRootInfo(ic.DAO, vitaID, dataType)
|
|
if root == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
item, err := root.ToStackItem()
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to convert root to stack item: %w", err))
|
|
}
|
|
return item
|
|
}
|
|
|
|
func (a *Ancora) getDataRootAtVersion(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
dataType := state.DataType(toUint32(args[1]))
|
|
version := toUint64(args[2])
|
|
|
|
// First check current version
|
|
current := a.getRootInfo(ic.DAO, vitaID, dataType)
|
|
if current != nil && current.Version == version {
|
|
item, err := current.ToStackItem()
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to convert root to stack item: %w", err))
|
|
}
|
|
return item
|
|
}
|
|
|
|
// Check history
|
|
root := a.getHistoricalRoot(ic.DAO, vitaID, dataType, version)
|
|
if root == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
item, err := root.ToStackItem()
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to convert root to stack item: %w", err))
|
|
}
|
|
return item
|
|
}
|
|
|
|
func (a *Ancora) getRootInfo(d *dao.Simple, vitaID uint64, dataType state.DataType) *state.RootInfo {
|
|
key := a.makeRootKey(vitaID, dataType)
|
|
root := new(state.RootInfo)
|
|
err := getConvertibleFromDAO(a.ID, d, key, root)
|
|
if err != nil {
|
|
if errors.Is(err, storage.ErrKeyNotFound) {
|
|
return nil
|
|
}
|
|
panic(fmt.Errorf("failed to get root info: %w", err))
|
|
}
|
|
return root
|
|
}
|
|
|
|
func (a *Ancora) putRootInfo(d *dao.Simple, vitaID uint64, dataType state.DataType, root *state.RootInfo) error {
|
|
key := a.makeRootKey(vitaID, dataType)
|
|
return putConvertibleToDAO(a.ID, d, key, root)
|
|
}
|
|
|
|
func (a *Ancora) getHistoricalRoot(d *dao.Simple, vitaID uint64, dataType state.DataType, version uint64) *state.RootInfo {
|
|
key := a.makeHistoryKey(vitaID, dataType, version)
|
|
root := new(state.RootInfo)
|
|
err := getConvertibleFromDAO(a.ID, d, key, root)
|
|
if err != nil {
|
|
if errors.Is(err, storage.ErrKeyNotFound) {
|
|
return nil
|
|
}
|
|
panic(fmt.Errorf("failed to get historical root: %w", err))
|
|
}
|
|
return root
|
|
}
|
|
|
|
func (a *Ancora) archiveRoot(d *dao.Simple, vitaID uint64, dataType state.DataType, root *state.RootInfo) error {
|
|
key := a.makeHistoryKey(vitaID, dataType, root.Version)
|
|
return putConvertibleToDAO(a.ID, d, key, root)
|
|
}
|
|
|
|
func (a *Ancora) makeRootKey(vitaID uint64, dataType state.DataType) []byte {
|
|
key := make([]byte, 1+8+1)
|
|
key[0] = ancoraRootPrefix
|
|
putUint64(key[1:9], vitaID)
|
|
key[9] = byte(dataType)
|
|
return key
|
|
}
|
|
|
|
func (a *Ancora) makeHistoryKey(vitaID uint64, dataType state.DataType, version uint64) []byte {
|
|
key := make([]byte, 1+8+1+8)
|
|
key[0] = ancoraHistoryPrefix
|
|
putUint64(key[1:9], vitaID)
|
|
key[9] = byte(dataType)
|
|
putUint64(key[10:18], version)
|
|
return key
|
|
}
|
|
|
|
// ========== Proof Verification ==========
|
|
|
|
func (a *Ancora) verifyProof(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
dataType := state.DataType(toUint32(args[1]))
|
|
leaf, err := args[2].TryBytes()
|
|
if err != nil {
|
|
panic(ErrAncoraInvalidProof)
|
|
}
|
|
|
|
proofArr, ok := args[3].Value().([]stackitem.Item)
|
|
if !ok {
|
|
panic(ErrAncoraInvalidProof)
|
|
}
|
|
|
|
proof := make([][]byte, len(proofArr))
|
|
for i, item := range proofArr {
|
|
proof[i], err = item.TryBytes()
|
|
if err != nil {
|
|
panic(ErrAncoraInvalidProof)
|
|
}
|
|
}
|
|
|
|
index := toUint64(args[4])
|
|
|
|
rootInfo := a.getRootInfo(ic.DAO, vitaID, dataType)
|
|
if rootInfo == nil {
|
|
return stackitem.NewBool(false)
|
|
}
|
|
|
|
valid := a.verifyMerkleProofInternal(rootInfo.Root, leaf, proof, index, rootInfo.TreeAlgorithm)
|
|
|
|
// ARCH-005: Audit log proof verification (access tracking)
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
|
|
resourceID := make([]byte, 9)
|
|
putUint64(resourceID[0:8], vitaID)
|
|
resourceID[8] = byte(dataType)
|
|
outcome := AuditOutcomeSuccess
|
|
if !valid {
|
|
outcome = AuditOutcomeFailure
|
|
}
|
|
a.Audit.LogAccess(ic.DAO, caller, owner, "ancora.proof_verified", resourceID, outcome, ic.BlockHeight())
|
|
|
|
return stackitem.NewBool(valid)
|
|
}
|
|
|
|
func (a *Ancora) verifyProofAtVersion(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
dataType := state.DataType(toUint32(args[1]))
|
|
version := toUint64(args[2])
|
|
leaf, err := args[3].TryBytes()
|
|
if err != nil {
|
|
panic(ErrAncoraInvalidProof)
|
|
}
|
|
|
|
proofArr, ok := args[4].Value().([]stackitem.Item)
|
|
if !ok {
|
|
panic(ErrAncoraInvalidProof)
|
|
}
|
|
|
|
proof := make([][]byte, len(proofArr))
|
|
for i, item := range proofArr {
|
|
proof[i], err = item.TryBytes()
|
|
if err != nil {
|
|
panic(ErrAncoraInvalidProof)
|
|
}
|
|
}
|
|
|
|
index := toUint64(args[5])
|
|
|
|
// Check current version first
|
|
rootInfo := a.getRootInfo(ic.DAO, vitaID, dataType)
|
|
if rootInfo != nil && rootInfo.Version == version {
|
|
valid := a.verifyMerkleProofInternal(rootInfo.Root, leaf, proof, index, rootInfo.TreeAlgorithm)
|
|
a.logProofVerification(ic, vitaID, dataType, version, valid)
|
|
return stackitem.NewBool(valid)
|
|
}
|
|
|
|
// Check historical version
|
|
rootInfo = a.getHistoricalRoot(ic.DAO, vitaID, dataType, version)
|
|
if rootInfo == nil {
|
|
return stackitem.NewBool(false)
|
|
}
|
|
|
|
valid := a.verifyMerkleProofInternal(rootInfo.Root, leaf, proof, index, rootInfo.TreeAlgorithm)
|
|
a.logProofVerification(ic, vitaID, dataType, version, valid)
|
|
return stackitem.NewBool(valid)
|
|
}
|
|
|
|
func (a *Ancora) verifyMerkleProofInternal(root, leaf []byte, proof [][]byte, index uint64, algorithm state.TreeAlgorithm) bool {
|
|
if len(root) != 32 {
|
|
return false
|
|
}
|
|
|
|
// Check proof depth
|
|
cache := a.getCache(nil)
|
|
if cache != nil && uint32(len(proof)) > cache.config.MaxProofDepth {
|
|
return false
|
|
}
|
|
|
|
// Compute hash based on algorithm
|
|
// Note: Currently only SHA256 is implemented. Keccak256 and Poseidon are TODO.
|
|
var computed []byte
|
|
switch algorithm {
|
|
case state.TreeAlgorithmSHA256:
|
|
computed = hash.Sha256(leaf).BytesBE()
|
|
case state.TreeAlgorithmKeccak256:
|
|
// TODO: Implement Keccak256 when needed for EVM compatibility
|
|
computed = hash.Sha256(leaf).BytesBE()
|
|
default:
|
|
// Poseidon not yet implemented, fallback to SHA256
|
|
computed = hash.Sha256(leaf).BytesBE()
|
|
}
|
|
|
|
// Traverse the proof
|
|
for _, sibling := range proof {
|
|
if len(sibling) != 32 {
|
|
return false
|
|
}
|
|
|
|
var combined []byte
|
|
if index%2 == 0 {
|
|
combined = append(computed, sibling...)
|
|
} else {
|
|
combined = append(sibling, computed...)
|
|
}
|
|
|
|
switch algorithm {
|
|
case state.TreeAlgorithmSHA256:
|
|
computed = hash.Sha256(combined).BytesBE()
|
|
case state.TreeAlgorithmKeccak256:
|
|
// TODO: Implement Keccak256 when needed for EVM compatibility
|
|
computed = hash.Sha256(combined).BytesBE()
|
|
default:
|
|
computed = hash.Sha256(combined).BytesBE()
|
|
}
|
|
|
|
index /= 2
|
|
}
|
|
|
|
// Compare computed root with stored root
|
|
if len(computed) != len(root) {
|
|
return false
|
|
}
|
|
for i := range computed {
|
|
if computed[i] != root[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// VerifyProofInternal is a cross-contract method for other native contracts.
|
|
func (a *Ancora) VerifyProofInternal(d *dao.Simple, vitaID uint64, dataType state.DataType, leaf []byte, proof [][]byte, index uint64) bool {
|
|
rootInfo := a.getRootInfo(d, vitaID, dataType)
|
|
if rootInfo == nil {
|
|
return false
|
|
}
|
|
return a.verifyMerkleProofInternal(rootInfo.Root, leaf, proof, index, rootInfo.TreeAlgorithm)
|
|
}
|
|
|
|
// RequireValidRoot is a cross-contract method that panics if no valid root exists.
|
|
func (a *Ancora) RequireValidRoot(d *dao.Simple, vitaID uint64, dataType state.DataType) {
|
|
root := a.getRootInfo(d, vitaID, dataType)
|
|
if root == nil {
|
|
panic(ErrAncoraNoRoot)
|
|
}
|
|
}
|
|
|
|
// logProofVerification logs a proof verification to the audit trail.
|
|
func (a *Ancora) logProofVerification(ic *interop.Context, vitaID uint64, dataType state.DataType, version uint64, valid bool) {
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
|
|
resourceID := make([]byte, 17)
|
|
putUint64(resourceID[0:8], vitaID)
|
|
resourceID[8] = byte(dataType)
|
|
putUint64(resourceID[9:17], version)
|
|
outcome := AuditOutcomeSuccess
|
|
if !valid {
|
|
outcome = AuditOutcomeFailure
|
|
}
|
|
a.Audit.LogAccess(ic.DAO, caller, owner, "ancora.proof_verified_at_version", resourceID, outcome, ic.BlockHeight())
|
|
}
|
|
|
|
// ========== GDPR Erasure ==========
|
|
|
|
func (a *Ancora) requestErasure(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
dataType := state.DataType(toUint32(args[1]))
|
|
reason := toString(args[2])
|
|
|
|
// Verify caller is Vita owner
|
|
owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
|
|
ok, err := runtime.CheckHashedWitness(ic, owner)
|
|
if err != nil || !ok {
|
|
panic(ErrAncoraUnauthorized)
|
|
}
|
|
|
|
// Check for existing erasure request
|
|
existing := a.getErasureInfoInternal(ic.DAO, vitaID, dataType)
|
|
if existing != nil && existing.Status == state.ErasurePending {
|
|
panic(ErrAncoraErasurePending)
|
|
}
|
|
|
|
erasure := &state.ErasureInfo{
|
|
RequestedAt: ic.BlockHeight(),
|
|
RequestedBy: owner,
|
|
Reason: reason,
|
|
Status: state.ErasurePending,
|
|
}
|
|
|
|
if err := a.putErasureInfo(ic.DAO, vitaID, dataType, erasure); err != nil {
|
|
panic(fmt.Errorf("failed to save erasure request: %w", err))
|
|
}
|
|
|
|
// ARCH-005: Audit log erasure request (GDPR Article 17 - Right to be forgotten)
|
|
// This audit entry is CRITICAL for compliance - proves user exercised their right
|
|
resourceID := make([]byte, 9)
|
|
putUint64(resourceID[0:8], vitaID)
|
|
resourceID[8] = byte(dataType)
|
|
a.Audit.LogCompliance(ic.DAO, owner, util.Uint160{}, "ancora.erasure_requested",
|
|
fmt.Sprintf("vitaID=%d dataType=%d reason=%s", vitaID, dataType, reason), ic.BlockHeight())
|
|
|
|
ic.AddNotification(a.Hash, "ErasureRequested", stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(dataType))),
|
|
stackitem.NewByteArray([]byte(reason)),
|
|
stackitem.NewBigInteger(big.NewInt(int64(ic.BlockHeight()))),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (a *Ancora) confirmErasure(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
dataType := state.DataType(toUint32(args[1]))
|
|
|
|
// Verify caller is authorized provider
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
providerCfg := a.getProviderConfig(ic.DAO, dataType, caller)
|
|
if providerCfg == nil || !providerCfg.Active {
|
|
panic(ErrAncoraUnauthorized)
|
|
}
|
|
|
|
erasure := a.getErasureInfoInternal(ic.DAO, vitaID, dataType)
|
|
if erasure == nil || erasure.Status != state.ErasurePending {
|
|
panic(ErrAncoraErasureNotPending)
|
|
}
|
|
|
|
erasure.Status = state.ErasureConfirmed
|
|
erasure.ProcessedAt = ic.BlockHeight()
|
|
erasure.ConfirmedBy = caller
|
|
|
|
if err := a.putErasureInfo(ic.DAO, vitaID, dataType, erasure); err != nil {
|
|
panic(fmt.Errorf("failed to confirm erasure: %w", err))
|
|
}
|
|
|
|
// Clear the data root
|
|
rootKey := a.makeRootKey(vitaID, dataType)
|
|
ic.DAO.DeleteStorageItem(a.ID, rootKey)
|
|
|
|
// ARCH-005: Audit log erasure confirmation (GDPR Article 17 fulfilled)
|
|
// This audit entry proves we complied with the erasure request
|
|
// Note: The on-chain hash of this audit entry remains as proof of compliance
|
|
a.Audit.LogCompliance(ic.DAO, caller, erasure.RequestedBy, "ancora.erasure_confirmed",
|
|
fmt.Sprintf("vitaID=%d dataType=%d requestedAt=%d", vitaID, dataType, erasure.RequestedAt),
|
|
ic.BlockHeight())
|
|
|
|
ic.AddNotification(a.Hash, "ErasureConfirmed", stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(dataType))),
|
|
stackitem.NewByteArray(caller.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (a *Ancora) denyErasure(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
dataType := state.DataType(toUint32(args[1]))
|
|
reason := toString(args[2])
|
|
|
|
// Check grace period
|
|
erasure := a.getErasureInfoInternal(ic.DAO, vitaID, dataType)
|
|
if erasure == nil || erasure.Status != state.ErasurePending {
|
|
panic(ErrAncoraErasureNotPending)
|
|
}
|
|
|
|
cache := ic.DAO.GetROCache(a.ID).(*AncoraCache)
|
|
if ic.BlockHeight() < erasure.RequestedAt+cache.config.ErasureGracePeriod {
|
|
panic(ErrAncoraErasureGracePeriod)
|
|
}
|
|
|
|
// Verify caller is authorized provider or committee
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
providerCfg := a.getProviderConfig(ic.DAO, dataType, caller)
|
|
if (providerCfg == nil || !providerCfg.Active) && !a.Tutus.CheckCommittee(ic) {
|
|
panic(ErrAncoraUnauthorized)
|
|
}
|
|
|
|
erasure.Status = state.ErasureDenied
|
|
erasure.ProcessedAt = ic.BlockHeight()
|
|
erasure.ConfirmedBy = caller
|
|
erasure.DeniedReason = reason
|
|
|
|
if err := a.putErasureInfo(ic.DAO, vitaID, dataType, erasure); err != nil {
|
|
panic(fmt.Errorf("failed to deny erasure: %w", err))
|
|
}
|
|
|
|
// ARCH-005: Audit log erasure denial (GDPR Article 17 exception invoked)
|
|
// Critical for legal compliance - documents the legal basis for denial
|
|
a.Audit.LogCompliance(ic.DAO, caller, erasure.RequestedBy, "ancora.erasure_denied",
|
|
fmt.Sprintf("vitaID=%d dataType=%d reason=%s", vitaID, dataType, reason),
|
|
ic.BlockHeight())
|
|
|
|
ic.AddNotification(a.Hash, "ErasureDenied", stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(dataType))),
|
|
stackitem.NewByteArray([]byte(reason)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (a *Ancora) getErasureInfo(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
dataType := state.DataType(toUint32(args[1]))
|
|
|
|
erasure := a.getErasureInfoFromDAO(ic.DAO, vitaID, dataType)
|
|
if erasure == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
item, err := erasure.ToStackItem()
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to convert erasure to stack item: %w", err))
|
|
}
|
|
return item
|
|
}
|
|
|
|
func (a *Ancora) getErasureInfoFromDAO(d *dao.Simple, vitaID uint64, dataType state.DataType) *state.ErasureInfo {
|
|
return a.getErasureInfoInternal(d, vitaID, dataType)
|
|
}
|
|
|
|
func (a *Ancora) getErasureInfoInternal(d *dao.Simple, vitaID uint64, dataType state.DataType) *state.ErasureInfo {
|
|
key := a.makeErasureKey(vitaID, dataType)
|
|
erasure := new(state.ErasureInfo)
|
|
err := getConvertibleFromDAO(a.ID, d, key, erasure)
|
|
if err != nil {
|
|
if errors.Is(err, storage.ErrKeyNotFound) {
|
|
return nil
|
|
}
|
|
panic(fmt.Errorf("failed to get erasure info: %w", err))
|
|
}
|
|
return erasure
|
|
}
|
|
|
|
func (a *Ancora) putErasureInfo(d *dao.Simple, vitaID uint64, dataType state.DataType, erasure *state.ErasureInfo) error {
|
|
key := a.makeErasureKey(vitaID, dataType)
|
|
return putConvertibleToDAO(a.ID, d, key, erasure)
|
|
}
|
|
|
|
func (a *Ancora) makeErasureKey(vitaID uint64, dataType state.DataType) []byte {
|
|
key := make([]byte, 1+8+1)
|
|
key[0] = ancoraErasurePrefix
|
|
putUint64(key[1:9], vitaID)
|
|
key[9] = byte(dataType)
|
|
return key
|
|
}
|
|
|
|
// ========== Data Portability ==========
|
|
|
|
func (a *Ancora) generatePortabilityAttestation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
dataTypesArr, ok := args[1].Value().([]stackitem.Item)
|
|
if !ok {
|
|
panic("invalid dataTypes array")
|
|
}
|
|
|
|
// Verify caller is Vita owner
|
|
owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
|
|
ok2, err := runtime.CheckHashedWitness(ic, owner)
|
|
if err != nil || !ok2 {
|
|
panic(ErrAncoraUnauthorized)
|
|
}
|
|
|
|
// Build attestation data
|
|
attestData := make([]byte, 0, 8+4+32*len(dataTypesArr))
|
|
attestData = appendUint64(attestData, vitaID)
|
|
attestData = appendUint32(attestData, ic.BlockHeight())
|
|
|
|
for _, item := range dataTypesArr {
|
|
dataType := state.DataType(toUint32(item))
|
|
rootInfo := a.getRootInfo(ic.DAO, vitaID, dataType)
|
|
if rootInfo != nil {
|
|
attestData = append(attestData, byte(dataType))
|
|
attestData = append(attestData, rootInfo.Root...)
|
|
}
|
|
}
|
|
|
|
// Hash the attestation
|
|
attestHash := hash.Sha256(attestData).BytesBE()
|
|
|
|
// Store attestation with expiry
|
|
cache := ic.DAO.GetROCache(a.ID).(*AncoraCache)
|
|
expiryBlock := ic.BlockHeight() + cache.config.AttestationValidBlocks
|
|
|
|
key := append([]byte{ancoraAttestationPrefix}, attestHash...)
|
|
value := make([]byte, 4)
|
|
putUint32(value, expiryBlock)
|
|
ic.DAO.PutStorageItem(a.ID, key, value)
|
|
|
|
ic.AddNotification(a.Hash, "PortabilityAttestationGenerated", stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
|
|
stackitem.NewByteArray(attestHash),
|
|
stackitem.NewBigInteger(big.NewInt(int64(expiryBlock))),
|
|
}))
|
|
|
|
return stackitem.NewByteArray(attestData)
|
|
}
|
|
|
|
func (a *Ancora) verifyPortabilityAttestation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
attestation, err := args[0].TryBytes()
|
|
if err != nil {
|
|
panic(ErrAncoraInvalidAttestation)
|
|
}
|
|
|
|
attestHash := hash.Sha256(attestation).BytesBE()
|
|
key := append([]byte{ancoraAttestationPrefix}, attestHash...)
|
|
|
|
value := ic.DAO.GetStorageItem(a.ID, key)
|
|
if value == nil {
|
|
return stackitem.NewBool(false)
|
|
}
|
|
|
|
expiryBlock := getUint32(value)
|
|
if ic.BlockHeight() > expiryBlock {
|
|
return stackitem.NewBool(false)
|
|
}
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// ========== Audit Log Query ==========
|
|
|
|
// getAuditLog retrieves audit log entries for a specific Vita or globally.
|
|
// This provides GDPR-compliant transparency: citizens can see who accessed their data.
|
|
func (a *Ancora) getAuditLog(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
vitaID := toUint64(args[0])
|
|
startBlock := toUint32(args[1])
|
|
endBlock := toUint32(args[2])
|
|
limit := int(toUint32(args[3]))
|
|
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 100 // Cap at 100 entries per query
|
|
}
|
|
|
|
// If vitaID is specified, verify caller is the owner or committee
|
|
if vitaID > 0 {
|
|
owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
// Check if caller is the Vita owner
|
|
ok, err := runtime.CheckHashedWitness(ic, owner)
|
|
if err != nil || !ok {
|
|
// Not the owner - check if committee
|
|
if !a.Tutus.CheckCommittee(ic) {
|
|
// Check if caller is an authorized provider for this data
|
|
isProvider := false
|
|
for dt := state.DataType(0); dt <= state.DataTypeCustom; dt++ {
|
|
if cfg := a.getProviderConfig(ic.DAO, dt, caller); cfg != nil && cfg.Active {
|
|
isProvider = true
|
|
break
|
|
}
|
|
}
|
|
if !isProvider {
|
|
panic(ErrAncoraUnauthorized)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Global audit query - requires committee
|
|
if !a.Tutus.CheckCommittee(ic) {
|
|
panic(ErrAncoraUnauthorized)
|
|
}
|
|
}
|
|
|
|
// Build query
|
|
query := &AuditQuery{
|
|
StartBlock: startBlock,
|
|
EndBlock: endBlock,
|
|
Limit: limit,
|
|
}
|
|
|
|
// If vitaID specified, query by target (owner of Vita)
|
|
if vitaID > 0 {
|
|
owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
|
|
query.Target = &owner
|
|
}
|
|
|
|
// Query the audit log
|
|
entries := a.Audit.Query(ic.DAO, query)
|
|
|
|
// Convert entries to stack items
|
|
items := make([]stackitem.Item, 0, len(entries))
|
|
for _, entry := range entries {
|
|
items = append(items, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(entry.EntryID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(entry.Timestamp))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(entry.Category))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(entry.Severity))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(entry.Outcome))),
|
|
stackitem.NewByteArray(entry.Actor.BytesBE()),
|
|
stackitem.NewByteArray(entry.Target.BytesBE()),
|
|
stackitem.NewByteArray([]byte(entry.Action)),
|
|
stackitem.NewByteArray(entry.ResourceID),
|
|
stackitem.NewByteArray([]byte(entry.Details)),
|
|
}))
|
|
}
|
|
|
|
return stackitem.NewArray(items)
|
|
}
|
|
|
|
// ========== Rate Limiting Helpers ==========
|
|
|
|
func (a *Ancora) checkRateLimit(ic *interop.Context, cfg *state.ProviderConfig, vitaID uint64, dataType state.DataType) error {
|
|
// Check per-block rate limit
|
|
blockHeight := ic.BlockHeight()
|
|
updateCount := a.getUpdateCount(ic.DAO, blockHeight, cfg.Provider)
|
|
if updateCount >= cfg.MaxUpdatesPerBlock {
|
|
return ErrAncoraRateLimited
|
|
}
|
|
|
|
// Check per-vitaID cooldown
|
|
lastUpdate := a.getLastUpdate(ic.DAO, vitaID, dataType, cfg.Provider)
|
|
if lastUpdate > 0 && blockHeight < lastUpdate+cfg.UpdateCooldown {
|
|
return ErrAncoraUpdateCooldown
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *Ancora) recordUpdate(d *dao.Simple, blockHeight uint32, provider util.Uint160, vitaID uint64, dataType state.DataType) {
|
|
// Increment block update count
|
|
countKey := append([]byte{ancoraUpdateCountPrefix}, provider.BytesBE()...)
|
|
countKey = appendUint32(countKey, blockHeight)
|
|
|
|
var count uint32
|
|
data := d.GetStorageItem(a.ID, countKey)
|
|
if data != nil && len(data) >= 4 {
|
|
count = getUint32(data)
|
|
}
|
|
count++
|
|
countData := make([]byte, 4)
|
|
putUint32(countData, count)
|
|
d.PutStorageItem(a.ID, countKey, countData)
|
|
|
|
// Record last update for vitaID+dataType+provider
|
|
lastKey := a.makeLastUpdateKey(vitaID, dataType, provider)
|
|
lastData := make([]byte, 4)
|
|
putUint32(lastData, blockHeight)
|
|
d.PutStorageItem(a.ID, lastKey, lastData)
|
|
}
|
|
|
|
func (a *Ancora) getUpdateCount(d *dao.Simple, blockHeight uint32, provider util.Uint160) uint32 {
|
|
countKey := append([]byte{ancoraUpdateCountPrefix}, provider.BytesBE()...)
|
|
countKey = appendUint32(countKey, blockHeight)
|
|
|
|
data := d.GetStorageItem(a.ID, countKey)
|
|
if data == nil || len(data) < 4 {
|
|
return 0
|
|
}
|
|
return getUint32(data)
|
|
}
|
|
|
|
func (a *Ancora) getLastUpdate(d *dao.Simple, vitaID uint64, dataType state.DataType, provider util.Uint160) uint32 {
|
|
key := a.makeLastUpdateKey(vitaID, dataType, provider)
|
|
data := d.GetStorageItem(a.ID, key)
|
|
if data == nil || len(data) < 4 {
|
|
return 0
|
|
}
|
|
return getUint32(data)
|
|
}
|
|
|
|
func (a *Ancora) makeLastUpdateKey(vitaID uint64, dataType state.DataType, provider util.Uint160) []byte {
|
|
key := make([]byte, 1+8+1+20)
|
|
key[0] = ancoraLastUpdatePrefix
|
|
putUint64(key[1:9], vitaID)
|
|
key[9] = byte(dataType)
|
|
copy(key[10:30], provider.BytesBE())
|
|
return key
|
|
}
|
|
|
|
// ========== Utility Helpers ==========
|
|
|
|
func (a *Ancora) getCache(d *dao.Simple) *AncoraCache {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
cache := d.GetROCache(a.ID)
|
|
if cache == nil {
|
|
return nil
|
|
}
|
|
return cache.(*AncoraCache)
|
|
}
|
|
|
|
func putUint32(b []byte, v uint32) {
|
|
b[0] = byte(v)
|
|
b[1] = byte(v >> 8)
|
|
b[2] = byte(v >> 16)
|
|
b[3] = byte(v >> 24)
|
|
}
|
|
|
|
func getUint32(b []byte) uint32 {
|
|
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
|
|
}
|
|
|
|
func putUint64(b []byte, v uint64) {
|
|
b[0] = byte(v)
|
|
b[1] = byte(v >> 8)
|
|
b[2] = byte(v >> 16)
|
|
b[3] = byte(v >> 24)
|
|
b[4] = byte(v >> 32)
|
|
b[5] = byte(v >> 40)
|
|
b[6] = byte(v >> 48)
|
|
b[7] = byte(v >> 56)
|
|
}
|
|
|
|
func appendUint32(b []byte, v uint32) []byte {
|
|
return append(b, byte(v), byte(v>>8), byte(v>>16), byte(v>>24))
|
|
}
|
|
|
|
func appendUint64(b []byte, v uint64) []byte {
|
|
return append(b, byte(v), byte(v>>8), byte(v>>16), byte(v>>24),
|
|
byte(v>>32), byte(v>>40), byte(v>>48), byte(v>>56))
|
|
}
|