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)) }