diff --git a/pkg/core/native/ancora.go b/pkg/core/native/ancora.go new file mode 100644 index 0000000..b263e89 --- /dev/null +++ b/pkg/core/native/ancora.go @@ -0,0 +1,1138 @@ +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 +} + +// 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)} + 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) + + 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) + } + + 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)) + } + + 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)) + } + + 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) + + 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) + 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) + 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) + 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) + } +} + +// ========== 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)) + } + + 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) + + 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)) + } + + 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) +} + +// ========== 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)) +} diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go old mode 100644 new mode 100755 index fc25283..22a99fb --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -124,6 +124,10 @@ type ( IsAdultVerified(d *dao.Simple, owner util.Uint160) bool // GetTotalTokenCount returns total number of Vita tokens (for quorum calculation). GetTotalTokenCount(d *dao.Simple) uint64 + // ExistsInternal checks if a Vita token with the given ID exists. + ExistsInternal(d *dao.Simple, vitaID uint64) bool + // OwnerOfInternal returns the owner of a Vita token by ID. + OwnerOfInternal(d *dao.Simple, vitaID uint64) util.Uint160 } // IRoleRegistry is an interface required from native RoleRegistry contract @@ -331,6 +335,19 @@ type ( // Address returns the contract's script hash. Address() util.Uint160 } + + // IAncora is an interface required from native Ancora contract for + // interaction with Blockchain and other native contracts. + // Ancora anchors Merkle roots of off-chain data for privacy-preserving verification. + IAncora interface { + interop.Contract + // VerifyProofInternal verifies a Merkle proof against the stored root. + VerifyProofInternal(d *dao.Simple, vitaID uint64, dataType state.DataType, leaf []byte, proof [][]byte, index uint64) bool + // RequireValidRoot panics if no valid root exists for the vitaID and dataType. + RequireValidRoot(d *dao.Simple, vitaID uint64, dataType state.DataType) + // Address returns the contract's script hash. + Address() util.Uint160 + } ) // Contracts is a convenient wrapper around an arbitrary set of native contracts @@ -530,6 +547,12 @@ func (cs *Contracts) Annos() IAnnos { return cs.ByName(nativenames.Annos).(IAnnos) } +// Ancora returns native IAncora contract implementation. It panics if +// there's no contract with proper name in cs. +func (cs *Contracts) Ancora() IAncora { + return cs.ByName(nativenames.Ancora).(IAncora) +} + // NewDefaultContracts returns a new set of default native contracts. func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract { mgmt := NewManagement() @@ -699,6 +722,11 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract { // Wire Annos into Eligere for voting age verification eligere.Annos = annos + // Create Ancora (Merkle Root Anchoring) contract + ancora := newAncora() + ancora.Vita = vita + ancora.Tutus = tutus + return []interop.Contract{ mgmt, s, @@ -726,5 +754,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract { pons, collocatio, annos, + ancora, } } diff --git a/pkg/core/native/native_test/ancora_test.go b/pkg/core/native/native_test/ancora_test.go new file mode 100644 index 0000000..93af6d4 --- /dev/null +++ b/pkg/core/native/native_test/ancora_test.go @@ -0,0 +1,281 @@ +package native_test + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/stretchr/testify/require" + "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/tutustest" + "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" +) + +func newAncoraClient(t *testing.T) *tutustest.ContractInvoker { + return newNativeClient(t, nativenames.Ancora) +} + +// TestAncora_GetConfig tests the getConfig method. +func TestAncora_GetConfig(t *testing.T) { + c := newAncoraClient(t) + + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + arr, ok := stack[0].Value().([]stackitem.Item) + require.True(t, ok, "expected array result") + require.Equal(t, 7, len(arr)) // StateAnchorsConfig has 7 fields + + // Check default values + defaultAlgo, _ := arr[0].TryInteger() + require.Equal(t, int64(state.TreeAlgorithmSHA256), defaultAlgo.Int64()) + + maxProofDepth, _ := arr[1].TryInteger() + require.Equal(t, int64(32), maxProofDepth.Int64()) + + defaultMaxUpdates, _ := arr[2].TryInteger() + require.Equal(t, int64(10), defaultMaxUpdates.Int64()) + + defaultCooldown, _ := arr[3].TryInteger() + require.Equal(t, int64(1), defaultCooldown.Int64()) + + maxHistory, _ := arr[4].TryInteger() + require.Equal(t, int64(100), maxHistory.Int64()) + + erasureGrace, _ := arr[5].TryInteger() + require.Equal(t, int64(1000), erasureGrace.Int64()) + + attestValid, _ := arr[6].TryInteger() + require.Equal(t, int64(86400), attestValid.Int64()) + }, "getConfig") +} + +// TestAncora_GetDataRoot_NonExistent tests getting a non-existent data root. +func TestAncora_GetDataRoot_NonExistent(t *testing.T) { + c := newAncoraClient(t) + + // Non-existent root should return null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent root") + }, "getDataRoot", uint64(999), uint32(state.DataTypeMedical)) +} + +// TestAncora_GetProvider_NonExistent tests getting a non-existent provider config. +func TestAncora_GetProvider_NonExistent(t *testing.T) { + c := newAncoraClient(t) + e := c.Executor + + acc := e.NewAccount(t) + + // Non-existent provider should return null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent provider") + }, "getProvider", uint32(state.DataTypeMedical), acc.ScriptHash()) +} + +// TestAncora_GetErasureInfo_NonExistent tests getting non-existent erasure info. +func TestAncora_GetErasureInfo_NonExistent(t *testing.T) { + c := newAncoraClient(t) + + // Non-existent erasure info should return null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent erasure info") + }, "getErasureInfo", uint64(999), uint32(state.DataTypeMedical)) +} + +// TestAncora_IsProviderActive_NonExistent tests checking non-existent provider. +func TestAncora_IsProviderActive_NonExistent(t *testing.T) { + c := newAncoraClient(t) + e := c.Executor + + acc := e.NewAccount(t) + + // Non-existent provider should return false + c.Invoke(t, false, "isProviderActive", uint32(state.DataTypeMedical), acc.ScriptHash()) +} + +// TestAncora_RegisterProvider_NoCommittee tests that non-committee cannot register providers. +func TestAncora_RegisterProvider_NoCommittee(t *testing.T) { + c := newAncoraClient(t) + e := c.Executor + + acc := e.NewAccount(t) + randomInvoker := c.WithSigners(acc) + + // registerProvider takes: dataType, provider, description, maxUpdatesPerBlock, updateCooldown + randomInvoker.InvokeFail(t, "invalid committee signature", + "registerProvider", + uint32(state.DataTypeMedical), + acc.ScriptHash(), + "Test Healthcare Provider", + uint32(10), + uint32(1), + ) +} + +// TestAncora_RegisterProvider_CommitteeSuccess tests committee can register providers. +func TestAncora_RegisterProvider_CommitteeSuccess(t *testing.T) { + c := newAncoraClient(t) + e := c.Executor + + providerAcc := e.NewAccount(t) + + // Register a healthcare provider (c is already a CommitteeInvoker) + c.Invoke(t, true, + "registerProvider", + uint32(state.DataTypeMedical), + providerAcc.ScriptHash(), + "Test Healthcare Provider", + uint32(10), + uint32(1), + ) + + // Verify provider is active + c.Invoke(t, true, "isProviderActive", uint32(state.DataTypeMedical), providerAcc.ScriptHash()) + + // Verify provider info + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + arr, ok := stack[0].Value().([]stackitem.Item) + require.True(t, ok, "expected array result") + require.Equal(t, 7, len(arr)) // ProviderConfig has 7 fields + + // Check dataType + dataType, _ := arr[0].TryInteger() + require.Equal(t, int64(state.DataTypeMedical), dataType.Int64()) + + // Check active status + active, _ := arr[4].TryBool() + require.True(t, active) + }, "getProvider", uint32(state.DataTypeMedical), providerAcc.ScriptHash()) +} + +// TestAncora_RevokeProvider_NoCommittee tests that non-committee cannot revoke providers. +func TestAncora_RevokeProvider_NoCommittee(t *testing.T) { + c := newAncoraClient(t) + e := c.Executor + + acc := e.NewAccount(t) + randomInvoker := c.WithSigners(acc) + + randomInvoker.InvokeFail(t, "invalid committee signature", + "revokeProvider", + uint32(state.DataTypeMedical), + acc.ScriptHash(), + ) +} + +// TestAncora_UpdateDataRoot_VitaNotFound tests that updating root for non-existent Vita fails. +// The contract should fail when the Vita doesn't exist. +func TestAncora_UpdateDataRoot_VitaNotFound(t *testing.T) { + c := newAncoraClient(t) + e := c.Executor + + acc := e.NewAccount(t) + randomInvoker := c.WithSigners(acc) + + root := hash.Sha256([]byte("test data")).BytesBE() + + // Should fail - when calling updateDataRoot with invalid Vita ID + // The error could be "vita not found" or "unauthorized" depending on check order + randomInvoker.InvokeFail(t, "", + "updateDataRoot", + uint64(999), + uint32(state.DataTypeMedical), + root, + uint64(100), + uint32(state.TreeAlgorithmSHA256), + "1.0.0", + []byte{}, + ) +} + +// Note: TestAncora_VerifyProof tests would require complex array serialization +// that the test framework doesn't support directly. Proof verification is tested +// via cross-contract integration tests. + +// TestAncora_RequestErasure_Unauthorized tests that erasure request requires Vita ownership. +// When Vita doesn't exist, OwnerOfInternal returns empty Uint160 and the caller +// fails the witness check, resulting in "unauthorized" error. +func TestAncora_RequestErasure_Unauthorized(t *testing.T) { + c := newAncoraClient(t) + e := c.Executor + + acc := e.NewAccount(t) + randomInvoker := c.WithSigners(acc) + + // Should fail - caller is not the Vita owner (Vita doesn't exist) + randomInvoker.InvokeFail(t, "unauthorized", + "requestErasure", + uint64(999), + uint32(state.DataTypeMedical), + "GDPR Art. 17", + ) +} + +// TestAncora_ConfirmErasure_Unauthorized tests that unauthorized providers cannot confirm erasure. +func TestAncora_ConfirmErasure_Unauthorized(t *testing.T) { + c := newAncoraClient(t) + e := c.Executor + + acc := e.NewAccount(t) + randomInvoker := c.WithSigners(acc) + + randomInvoker.InvokeFail(t, "unauthorized: caller is not authorized provider", + "confirmErasure", + uint64(1), + uint32(state.DataTypeMedical), + ) +} + +// TestAncora_DenyErasure_NoPending tests that denying non-existent erasure fails. +func TestAncora_DenyErasure_NoPending(t *testing.T) { + c := newAncoraClient(t) + e := c.Executor + + acc := e.NewAccount(t) + randomInvoker := c.WithSigners(acc) + + randomInvoker.InvokeFail(t, "no pending erasure request", + "denyErasure", + uint64(1), + uint32(state.DataTypeMedical), + "Legal hold", + ) +} + +// TestAncora_VerifyPortabilityAttestation_Invalid tests verification of invalid attestation. +func TestAncora_VerifyPortabilityAttestation_Invalid(t *testing.T) { + c := newAncoraClient(t) + + // Invalid attestation should return false + c.Invoke(t, false, "verifyPortabilityAttestation", []byte("invalid attestation")) +} + +// TestAncora_DataTypes tests that all data types are valid. +func TestAncora_DataTypes(t *testing.T) { + // Verify data type constants + require.Equal(t, state.DataType(0), state.DataTypeMedical) + require.Equal(t, state.DataType(1), state.DataTypeEducation) + require.Equal(t, state.DataType(2), state.DataTypeInvestment) + require.Equal(t, state.DataType(3), state.DataTypeDocuments) + require.Equal(t, state.DataType(4), state.DataTypeCustom) +} + +// TestAncora_TreeAlgorithms tests that all tree algorithms are valid. +func TestAncora_TreeAlgorithms(t *testing.T) { + // Verify tree algorithm constants + require.Equal(t, state.TreeAlgorithm(0), state.TreeAlgorithmSHA256) + require.Equal(t, state.TreeAlgorithm(1), state.TreeAlgorithmKeccak256) + require.Equal(t, state.TreeAlgorithm(2), state.TreeAlgorithmPoseidon) +} + +// TestAncora_ErasureStatuses tests that all erasure statuses are valid. +func TestAncora_ErasureStatuses(t *testing.T) { + // Verify erasure status constants + require.Equal(t, state.ErasureStatus(0), state.ErasurePending) + require.Equal(t, state.ErasureStatus(1), state.ErasureConfirmed) + require.Equal(t, state.ErasureStatus(2), state.ErasureDenied) +} diff --git a/pkg/core/native/nativehashes/hashes.go b/pkg/core/native/nativehashes/hashes.go index b4a724c..ed6d620 100644 --- a/pkg/core/native/nativehashes/hashes.go +++ b/pkg/core/native/nativehashes/hashes.go @@ -61,4 +61,6 @@ var ( Collocatio = util.Uint160{0xf9, 0x9c, 0x85, 0xeb, 0xea, 0x3, 0xa0, 0xd0, 0x69, 0x29, 0x13, 0x95, 0xdd, 0x33, 0xbc, 0x55, 0x53, 0xc6, 0x28, 0xf5} // Annos is a hash of native Annos contract. Annos = util.Uint160{0xaa, 0xad, 0x31, 0x3a, 0x1a, 0x53, 0x92, 0xd9, 0x98, 0x51, 0xee, 0xa7, 0xe3, 0x14, 0x36, 0xaa, 0x7e, 0xc8, 0xca, 0xf8} + // Ancora is a hash of native Ancora contract. + Ancora = util.Uint160{0x30, 0x5f, 0x26, 0x1b, 0x64, 0xdb, 0xfe, 0x5a, 0x2a, 0x37, 0x54, 0x52, 0xc6, 0x98, 0x5c, 0xd3, 0x3, 0x2d, 0xc1, 0x92} ) diff --git a/pkg/core/native/nativeids/ids.go b/pkg/core/native/nativeids/ids.go index 030139d..70c1307 100644 --- a/pkg/core/native/nativeids/ids.go +++ b/pkg/core/native/nativeids/ids.go @@ -59,4 +59,6 @@ const ( Collocatio int32 = -25 // Annos is an ID of native Annos contract (lifespan/years tracking). Annos int32 = -26 + // Ancora is an ID of native Ancora contract (Merkle root anchoring for off-chain data). + Ancora int32 = -27 ) diff --git a/pkg/core/native/nativenames/names.go b/pkg/core/native/nativenames/names.go old mode 100644 new mode 100755 index 935cde1..4b68145 --- a/pkg/core/native/nativenames/names.go +++ b/pkg/core/native/nativenames/names.go @@ -26,8 +26,9 @@ const ( Opus = "Opus" Palam = "Palam" Pons = "Pons" - Collocatio = "Collocatio" - Annos = "Annos" + Collocatio = "Collocatio" + Annos = "Annos" + Ancora = "Ancora" ) // All contains the list of all native contract names ordered by the contract ID. @@ -58,6 +59,7 @@ var All = []string{ Pons, Collocatio, Annos, + Ancora, } // IsValid checks if the name is a valid native contract's name. @@ -87,5 +89,6 @@ func IsValid(name string) bool { name == Palam || name == Pons || name == Collocatio || - name == Annos + name == Annos || + name == Ancora } diff --git a/pkg/core/native/vita.go b/pkg/core/native/vita.go old mode 100644 new mode 100755 index d9b7c40..cc116f5 --- a/pkg/core/native/vita.go +++ b/pkg/core/native/vita.go @@ -922,6 +922,22 @@ func (v *Vita) GetTotalTokenCount(d *dao.Simple) uint64 { return v.getTokenCounter(d) } +// ExistsInternal checks if a Vita token with the given ID exists. +func (v *Vita) ExistsInternal(d *dao.Simple, vitaID uint64) bool { + token, err := v.getTokenByIDInternal(d, vitaID) + return err == nil && token != nil +} + +// OwnerOfInternal returns the owner of a Vita token by ID. +// Returns empty Uint160 if token doesn't exist. +func (v *Vita) OwnerOfInternal(d *dao.Simple, vitaID uint64) util.Uint160 { + token, err := v.getTokenByIDInternal(d, vitaID) + if err != nil || token == nil { + return util.Uint160{} + } + return token.Owner +} + // 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. diff --git a/pkg/core/state/state_anchors.go b/pkg/core/state/state_anchors.go new file mode 100644 index 0000000..a1ffe6c --- /dev/null +++ b/pkg/core/state/state_anchors.go @@ -0,0 +1,421 @@ +package state + +import ( + "errors" + "math/big" + + "github.com/tutus-one/tutus-chain/pkg/io" + "github.com/tutus-one/tutus-chain/pkg/util" + "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" +) + +var errAncoraInvalidStackItem = errors.New("invalid stack item") + +// DataType represents the type of off-chain data being anchored. +type DataType uint8 + +const ( + // DataTypeMedical is for Salus healthcare records (HIPAA protected). + DataTypeMedical DataType = 0 + // DataTypeEducation is for Scire education records (certifications, transcripts). + DataTypeEducation DataType = 1 + // DataTypeInvestment is for Collocatio investment records (portfolios, transactions). + DataTypeInvestment DataType = 2 + // DataTypeDocuments is for personal documents (IPFS CIDs). + DataTypeDocuments DataType = 3 + // DataTypeCustom is for government-defined extensions. + DataTypeCustom DataType = 4 +) + +// TreeAlgorithm represents the Merkle tree hash algorithm. +type TreeAlgorithm uint8 + +const ( + // TreeAlgorithmSHA256 is the default SHA256 algorithm. + TreeAlgorithmSHA256 TreeAlgorithm = 0 + // TreeAlgorithmKeccak256 is Keccak256 for EVM compatibility. + TreeAlgorithmKeccak256 TreeAlgorithm = 1 + // TreeAlgorithmPoseidon is Poseidon for ZK-proof friendliness. + TreeAlgorithmPoseidon TreeAlgorithm = 2 +) + +// ErasureStatus represents the status of a GDPR erasure request. +type ErasureStatus uint8 + +const ( + // ErasurePending means the request is awaiting off-chain deletion. + ErasurePending ErasureStatus = 0 + // ErasureConfirmed means off-chain deletion has been confirmed. + ErasureConfirmed ErasureStatus = 1 + // ErasureDenied means the erasure was denied (legal hold, etc.). + ErasureDenied ErasureStatus = 2 +) + +// RootInfo contains metadata about a Merkle root anchored on-chain. +type RootInfo struct { + // Root is the 32-byte Merkle root. + Root []byte + // LeafCount is the number of leaves in the tree. + LeafCount uint64 + // UpdatedAt is the block height of the last update. + UpdatedAt uint32 + // UpdatedBy is the provider script hash that updated this root. + UpdatedBy util.Uint160 + // Version is the incrementing version number. + Version uint64 + // TreeAlgorithm is the hash algorithm used (0=SHA256, 1=Keccak256, 2=Poseidon). + TreeAlgorithm TreeAlgorithm + // SchemaVersion is the data schema version (e.g., "1.0.0"). + SchemaVersion string + // ContentHash is an optional hash of the serialized tree. + ContentHash []byte +} + +// EncodeBinary implements io.Serializable. +func (r *RootInfo) EncodeBinary(w *io.BinWriter) { + w.WriteVarBytes(r.Root) + w.WriteU64LE(r.LeafCount) + w.WriteU32LE(r.UpdatedAt) + r.UpdatedBy.EncodeBinary(w) + w.WriteU64LE(r.Version) + w.WriteB(byte(r.TreeAlgorithm)) + w.WriteString(r.SchemaVersion) + w.WriteVarBytes(r.ContentHash) +} + +// DecodeBinary implements io.Serializable. +func (r *RootInfo) DecodeBinary(br *io.BinReader) { + r.Root = br.ReadVarBytes() + r.LeafCount = br.ReadU64LE() + r.UpdatedAt = br.ReadU32LE() + r.UpdatedBy.DecodeBinary(br) + r.Version = br.ReadU64LE() + r.TreeAlgorithm = TreeAlgorithm(br.ReadB()) + r.SchemaVersion = br.ReadString() + r.ContentHash = br.ReadVarBytes() +} + +// ToStackItem converts RootInfo to a stack item for VM. +func (r *RootInfo) ToStackItem() (stackitem.Item, error) { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(r.Root), + stackitem.NewBigInteger(big.NewInt(int64(r.LeafCount))), + stackitem.NewBigInteger(big.NewInt(int64(r.UpdatedAt))), + stackitem.NewByteArray(r.UpdatedBy.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(r.Version))), + stackitem.NewBigInteger(big.NewInt(int64(r.TreeAlgorithm))), + stackitem.NewByteArray([]byte(r.SchemaVersion)), + stackitem.NewByteArray(r.ContentHash), + }), nil +} + +// FromStackItem populates RootInfo from a stack item. +func (r *RootInfo) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 8 { + return errAncoraInvalidStackItem + } + + root, err := arr[0].TryBytes() + if err != nil { + return err + } + r.Root = root + + leafCount, err := arr[1].TryInteger() + if err != nil { + return err + } + r.LeafCount = leafCount.Uint64() + + updatedAt, err := arr[2].TryInteger() + if err != nil { + return err + } + r.UpdatedAt = uint32(updatedAt.Uint64()) + + updatedBy, err := arr[3].TryBytes() + if err != nil { + return err + } + r.UpdatedBy, err = util.Uint160DecodeBytesBE(updatedBy) + if err != nil { + return err + } + + version, err := arr[4].TryInteger() + if err != nil { + return err + } + r.Version = version.Uint64() + + algo, err := arr[5].TryInteger() + if err != nil { + return err + } + r.TreeAlgorithm = TreeAlgorithm(algo.Uint64()) + + schema, err := arr[6].TryBytes() + if err != nil { + return err + } + r.SchemaVersion = string(schema) + + contentHash, err := arr[7].TryBytes() + if err != nil { + return err + } + r.ContentHash = contentHash + + return nil +} + +// ErasureInfo contains metadata about a GDPR erasure request. +type ErasureInfo struct { + // RequestedAt is the block height when erasure was requested. + RequestedAt uint32 + // RequestedBy is the script hash of the requester. + RequestedBy util.Uint160 + // Reason is the GDPR reason code. + Reason string + // Status is the current erasure status. + Status ErasureStatus + // ProcessedAt is when off-chain deletion was confirmed. + ProcessedAt uint32 + // ConfirmedBy is the provider that confirmed deletion. + ConfirmedBy util.Uint160 + // DeniedReason is the reason for denial (if denied). + DeniedReason string +} + +// EncodeBinary implements io.Serializable. +func (e *ErasureInfo) EncodeBinary(w *io.BinWriter) { + w.WriteU32LE(e.RequestedAt) + e.RequestedBy.EncodeBinary(w) + w.WriteString(e.Reason) + w.WriteB(byte(e.Status)) + w.WriteU32LE(e.ProcessedAt) + e.ConfirmedBy.EncodeBinary(w) + w.WriteString(e.DeniedReason) +} + +// DecodeBinary implements io.Serializable. +func (e *ErasureInfo) DecodeBinary(br *io.BinReader) { + e.RequestedAt = br.ReadU32LE() + e.RequestedBy.DecodeBinary(br) + e.Reason = br.ReadString() + e.Status = ErasureStatus(br.ReadB()) + e.ProcessedAt = br.ReadU32LE() + e.ConfirmedBy.DecodeBinary(br) + e.DeniedReason = br.ReadString() +} + +// ToStackItem converts ErasureInfo to a stack item for VM. +func (e *ErasureInfo) ToStackItem() (stackitem.Item, error) { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(e.RequestedAt))), + stackitem.NewByteArray(e.RequestedBy.BytesBE()), + stackitem.NewByteArray([]byte(e.Reason)), + stackitem.NewBigInteger(big.NewInt(int64(e.Status))), + stackitem.NewBigInteger(big.NewInt(int64(e.ProcessedAt))), + stackitem.NewByteArray(e.ConfirmedBy.BytesBE()), + stackitem.NewByteArray([]byte(e.DeniedReason)), + }), nil +} + +// FromStackItem populates ErasureInfo from a stack item. +func (e *ErasureInfo) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 7 { + return errAncoraInvalidStackItem + } + + requestedAt, err := arr[0].TryInteger() + if err != nil { + return err + } + e.RequestedAt = uint32(requestedAt.Uint64()) + + requestedBy, err := arr[1].TryBytes() + if err != nil { + return err + } + e.RequestedBy, err = util.Uint160DecodeBytesBE(requestedBy) + if err != nil { + return err + } + + reason, err := arr[2].TryBytes() + if err != nil { + return err + } + e.Reason = string(reason) + + status, err := arr[3].TryInteger() + if err != nil { + return err + } + e.Status = ErasureStatus(status.Uint64()) + + processedAt, err := arr[4].TryInteger() + if err != nil { + return err + } + e.ProcessedAt = uint32(processedAt.Uint64()) + + confirmedBy, err := arr[5].TryBytes() + if err != nil { + return err + } + e.ConfirmedBy, err = util.Uint160DecodeBytesBE(confirmedBy) + if err != nil { + return err + } + + deniedReason, err := arr[6].TryBytes() + if err != nil { + return err + } + e.DeniedReason = string(deniedReason) + + return nil +} + +// ProviderConfig contains configuration for an authorized data provider. +type ProviderConfig struct { + // DataType is the data type this provider is authorized for. + DataType DataType + // Provider is the script hash of the authorized provider. + Provider util.Uint160 + // Description is a human-readable description. + Description string + // RegisteredAt is the block height when registered. + RegisteredAt uint32 + // Active indicates if the provider is currently active. + Active bool + // MaxUpdatesPerBlock is the anti-spam rate limit. + MaxUpdatesPerBlock uint32 + // UpdateCooldown is the blocks between updates per VitaID. + UpdateCooldown uint32 +} + +// EncodeBinary implements io.Serializable. +func (p *ProviderConfig) EncodeBinary(w *io.BinWriter) { + w.WriteB(byte(p.DataType)) + p.Provider.EncodeBinary(w) + w.WriteString(p.Description) + w.WriteU32LE(p.RegisteredAt) + w.WriteBool(p.Active) + w.WriteU32LE(p.MaxUpdatesPerBlock) + w.WriteU32LE(p.UpdateCooldown) +} + +// DecodeBinary implements io.Serializable. +func (p *ProviderConfig) DecodeBinary(br *io.BinReader) { + p.DataType = DataType(br.ReadB()) + p.Provider.DecodeBinary(br) + p.Description = br.ReadString() + p.RegisteredAt = br.ReadU32LE() + p.Active = br.ReadBool() + p.MaxUpdatesPerBlock = br.ReadU32LE() + p.UpdateCooldown = br.ReadU32LE() +} + +// ToStackItem converts ProviderConfig to a stack item for VM. +func (p *ProviderConfig) ToStackItem() (stackitem.Item, error) { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(p.DataType))), + stackitem.NewByteArray(p.Provider.BytesBE()), + stackitem.NewByteArray([]byte(p.Description)), + stackitem.NewBigInteger(big.NewInt(int64(p.RegisteredAt))), + stackitem.NewBool(p.Active), + stackitem.NewBigInteger(big.NewInt(int64(p.MaxUpdatesPerBlock))), + stackitem.NewBigInteger(big.NewInt(int64(p.UpdateCooldown))), + }), nil +} + +// FromStackItem populates ProviderConfig from a stack item. +func (p *ProviderConfig) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 7 { + return errAncoraInvalidStackItem + } + + dataType, err := arr[0].TryInteger() + if err != nil { + return err + } + p.DataType = DataType(dataType.Uint64()) + + provider, err := arr[1].TryBytes() + if err != nil { + return err + } + p.Provider, err = util.Uint160DecodeBytesBE(provider) + if err != nil { + return err + } + + description, err := arr[2].TryBytes() + if err != nil { + return err + } + p.Description = string(description) + + registeredAt, err := arr[3].TryInteger() + if err != nil { + return err + } + p.RegisteredAt = uint32(registeredAt.Uint64()) + + active, err := arr[4].TryBool() + if err != nil { + return err + } + p.Active = active + + maxUpdates, err := arr[5].TryInteger() + if err != nil { + return err + } + p.MaxUpdatesPerBlock = uint32(maxUpdates.Uint64()) + + cooldown, err := arr[6].TryInteger() + if err != nil { + return err + } + p.UpdateCooldown = uint32(cooldown.Uint64()) + + return nil +} + +// StateAnchorsConfig contains configuration for the StateAnchors contract. +type StateAnchorsConfig struct { + // DefaultTreeAlgorithm is the default hash algorithm (0=SHA256). + DefaultTreeAlgorithm TreeAlgorithm + // MaxProofDepth is the maximum depth of Merkle proofs (default: 32). + MaxProofDepth uint32 + // DefaultMaxUpdatesPerBlock is the default rate limit. + DefaultMaxUpdatesPerBlock uint32 + // DefaultUpdateCooldown is the default cooldown in blocks. + DefaultUpdateCooldown uint32 + // MaxHistoryVersions is the max history to retain per vitaID+dataType. + MaxHistoryVersions uint32 + // ErasureGracePeriod is blocks before erasure can be denied. + ErasureGracePeriod uint32 + // AttestationValidBlocks is how long attestations are valid. + AttestationValidBlocks uint32 +} + +// DefaultStateAnchorsConfig returns the default configuration. +func DefaultStateAnchorsConfig() StateAnchorsConfig { + return StateAnchorsConfig{ + DefaultTreeAlgorithm: TreeAlgorithmSHA256, + MaxProofDepth: 32, + DefaultMaxUpdatesPerBlock: 10, + DefaultUpdateCooldown: 1, + MaxHistoryVersions: 100, + ErasureGracePeriod: 1000, // ~16 minutes at 1s blocks + AttestationValidBlocks: 86400, // ~24 hours + } +}