package native import ( "encoding/binary" "errors" "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/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/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" ) // Annos represents the lifespan/years native contract for tracking Vita holder ages. type Annos struct { interop.ContractMD Tutus ITutus Vita IVita } // AnnosCache represents the cached state for Annos contract. type AnnosCache struct { recordCount uint64 } // Storage key prefixes for Annos. const ( annosPrefixLifespan byte = 0x01 // vitaID -> LifespanRecord annosPrefixByOwner byte = 0x02 // owner -> vitaID annosPrefixRecordCounter byte = 0xF0 // -> uint64 annosPrefixConfig byte = 0xFF // -> AnnosConfig ) // Event names for Annos. const ( BirthRegisteredEvent = "BirthRegistered" DeathRecordedEvent = "DeathRecorded" LifeStageChangedEvent = "LifeStageChanged" ) // Seconds per year (approximately, for age calculation). const secondsPerYear = 365 * 24 * 60 * 60 // Various errors for Annos. var ( ErrAnnosRecordNotFound = errors.New("lifespan record not found") ErrAnnosRecordExists = errors.New("lifespan record already exists") ErrAnnosNoVita = errors.New("owner must have an active Vita") ErrAnnosNotCommittee = errors.New("invalid committee signature") ErrAnnosAlreadyDeceased = errors.New("person is already marked as deceased") ErrAnnosInvalidTimestamp = errors.New("invalid birth timestamp") ErrAnnosNotVita = errors.New("only Vita contract can call this method") ) var ( _ interop.Contract = (*Annos)(nil) _ dao.NativeContractCache = (*AnnosCache)(nil) ) // Copy implements NativeContractCache interface. func (c *AnnosCache) Copy() dao.NativeContractCache { return &AnnosCache{ recordCount: c.recordCount, } } // checkCommittee checks if the caller has committee authority. func (a *Annos) checkCommittee(ic *interop.Context) bool { return a.Tutus.CheckCommittee(ic) } // newAnnos creates a new Annos native contract. func newAnnos() *Annos { ann := &Annos{ ContractMD: *interop.NewContractMD(nativenames.Annos, nativeids.Annos), } defer ann.BuildHFSpecificMD(ann.ActiveIn()) // ===== Age Queries ===== // getAge - Get current age in years desc := NewDescriptor("getAge", smartcontract.IntegerType, manifest.NewParameter("owner", smartcontract.Hash160Type)) md := NewMethodAndPrice(ann.getAge, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // getAgeAtTime - Get age at a specific timestamp desc = NewDescriptor("getAgeAtTime", smartcontract.IntegerType, manifest.NewParameter("owner", smartcontract.Hash160Type), manifest.NewParameter("timestamp", smartcontract.IntegerType)) md = NewMethodAndPrice(ann.getAgeAtTime, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // getLifeStage - Get life stage (Child, Youth, Adult, Elder) desc = NewDescriptor("getLifeStage", smartcontract.IntegerType, manifest.NewParameter("owner", smartcontract.Hash160Type)) md = NewMethodAndPrice(ann.getLifeStage, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // getBirthTimestamp - Get actual birth timestamp desc = NewDescriptor("getBirthTimestamp", smartcontract.IntegerType, manifest.NewParameter("owner", smartcontract.Hash160Type)) md = NewMethodAndPrice(ann.getBirthTimestamp, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // getRecord - Get full lifespan record desc = NewDescriptor("getRecord", smartcontract.ArrayType, manifest.NewParameter("owner", smartcontract.Hash160Type)) md = NewMethodAndPrice(ann.getRecord, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // getRecordByVitaID - Get record by Vita ID desc = NewDescriptor("getRecordByVitaID", smartcontract.ArrayType, manifest.NewParameter("vitaID", smartcontract.IntegerType)) md = NewMethodAndPrice(ann.getRecordByVitaID, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // ===== Entitlement Checks ===== // isVotingAge - Check if at voting age desc = NewDescriptor("isVotingAge", smartcontract.BoolType, manifest.NewParameter("owner", smartcontract.Hash160Type)) md = NewMethodAndPrice(ann.isVotingAge, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // isAdult - Check if adult desc = NewDescriptor("isAdult", smartcontract.BoolType, manifest.NewParameter("owner", smartcontract.Hash160Type)) md = NewMethodAndPrice(ann.isAdult, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // isRetirementAge - Check if at retirement age desc = NewDescriptor("isRetirementAge", smartcontract.BoolType, manifest.NewParameter("owner", smartcontract.Hash160Type)) md = NewMethodAndPrice(ann.isRetirementAge, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // isAlive - Check if person is alive desc = NewDescriptor("isAlive", smartcontract.BoolType, manifest.NewParameter("owner", smartcontract.Hash160Type)) md = NewMethodAndPrice(ann.isAlive, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // ===== Life Events (Committee Only) ===== // recordDeath - Record death (committee only) desc = NewDescriptor("recordDeath", smartcontract.BoolType, manifest.NewParameter("vitaID", smartcontract.IntegerType), manifest.NewParameter("deathTimestamp", smartcontract.IntegerType)) md = NewMethodAndPrice(ann.recordDeath, 1<<17, callflag.States|callflag.AllowNotify) ann.AddMethod(md, desc) // ===== Query Methods ===== // getConfig - Get Annos configuration desc = NewDescriptor("getConfig", smartcontract.ArrayType) md = NewMethodAndPrice(ann.getConfig, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // getTotalRecords - Get total lifespan records desc = NewDescriptor("getTotalRecords", smartcontract.IntegerType) md = NewMethodAndPrice(ann.getTotalRecords, 1<<15, callflag.ReadStates) ann.AddMethod(md, desc) // ===== Events ===== // BirthRegistered event eDesc := NewEventDescriptor(BirthRegisteredEvent, manifest.NewParameter("vitaID", smartcontract.IntegerType), manifest.NewParameter("owner", smartcontract.Hash160Type), manifest.NewParameter("birthTimestamp", smartcontract.IntegerType)) ann.AddEvent(NewEvent(eDesc)) // DeathRecorded event eDesc = NewEventDescriptor(DeathRecordedEvent, manifest.NewParameter("vitaID", smartcontract.IntegerType), manifest.NewParameter("deathTimestamp", smartcontract.IntegerType)) ann.AddEvent(NewEvent(eDesc)) // LifeStageChanged event eDesc = NewEventDescriptor(LifeStageChangedEvent, manifest.NewParameter("vitaID", smartcontract.IntegerType), manifest.NewParameter("previousStage", smartcontract.IntegerType), manifest.NewParameter("newStage", smartcontract.IntegerType)) ann.AddEvent(NewEvent(eDesc)) return ann } // Metadata returns contract metadata. func (a *Annos) Metadata() *interop.ContractMD { return &a.ContractMD } // Initialize initializes the Annos contract. func (a *Annos) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { if hf != a.ActiveIn() { return nil } // Initialize counter a.setRecordCounter(ic.DAO, 0) // Initialize config with defaults cfg := state.DefaultAnnosConfig() a.setConfig(ic.DAO, &cfg) // Initialize cache cache := &AnnosCache{ recordCount: 0, } ic.DAO.SetCache(a.ID, cache) return nil } // InitializeCache initializes the cache from storage. func (a *Annos) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { cache := &AnnosCache{ recordCount: a.getRecordCounter(d), } d.SetCache(a.ID, cache) return nil } // OnPersist is called before block is committed. func (a *Annos) OnPersist(ic *interop.Context) error { return nil } // PostPersist is called after block is committed. func (a *Annos) PostPersist(ic *interop.Context) error { return nil } // ActiveIn returns the hardfork at which this contract is activated. func (a *Annos) ActiveIn() *config.Hardfork { return nil // Always active } // ===== Storage Helpers ===== func (a *Annos) makeLifespanKey(vitaID uint64) []byte { key := make([]byte, 9) key[0] = annosPrefixLifespan binary.BigEndian.PutUint64(key[1:], vitaID) return key } func (a *Annos) makeByOwnerKey(owner util.Uint160) []byte { key := make([]byte, 21) key[0] = annosPrefixByOwner copy(key[1:], owner.BytesBE()) return key } func (a *Annos) getRecordCounter(d *dao.Simple) uint64 { si := d.GetStorageItem(a.ID, []byte{annosPrefixRecordCounter}) if si == nil { return 0 } return binary.BigEndian.Uint64(si) } func (a *Annos) setRecordCounter(d *dao.Simple, count uint64) { buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, count) d.PutStorageItem(a.ID, []byte{annosPrefixRecordCounter}, buf) } func (a *Annos) getConfigInternal(d *dao.Simple) *state.AnnosConfig { si := d.GetStorageItem(a.ID, []byte{annosPrefixConfig}) if si == nil { cfg := state.DefaultAnnosConfig() return &cfg } cfg := new(state.AnnosConfig) item, _ := stackitem.Deserialize(si) cfg.FromStackItem(item) return cfg } func (a *Annos) setConfig(d *dao.Simple, cfg *state.AnnosConfig) { item, _ := cfg.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(a.ID, []byte{annosPrefixConfig}, data) } func (a *Annos) getRecordInternal(d *dao.Simple, vitaID uint64) *state.LifespanRecord { si := d.GetStorageItem(a.ID, a.makeLifespanKey(vitaID)) if si == nil { return nil } rec := new(state.LifespanRecord) item, _ := stackitem.Deserialize(si) rec.FromStackItem(item) return rec } func (a *Annos) putRecord(d *dao.Simple, rec *state.LifespanRecord) { item, _ := rec.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(a.ID, a.makeLifespanKey(rec.VitaID), data) } func (a *Annos) getVitaIDByOwner(d *dao.Simple, owner util.Uint160) (uint64, bool) { si := d.GetStorageItem(a.ID, a.makeByOwnerKey(owner)) if si == nil { return 0, false } return binary.BigEndian.Uint64(si), true } func (a *Annos) setOwnerToVitaID(d *dao.Simple, owner util.Uint160, vitaID uint64) { buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, vitaID) d.PutStorageItem(a.ID, a.makeByOwnerKey(owner), buf) } // calculateAge calculates age in years from birth timestamp to current timestamp. func calculateAge(birthTimestamp, currentTimestamp uint64) uint8 { if currentTimestamp <= birthTimestamp { return 0 } ageSeconds := currentTimestamp - birthTimestamp ageYears := ageSeconds / secondsPerYear if ageYears > 255 { return 255 // Cap at max uint8 } return uint8(ageYears) } // calculateLifeStage determines the life stage based on age. func (a *Annos) calculateLifeStage(d *dao.Simple, age uint8) state.LifeStage { cfg := a.getConfigInternal(d) if age < cfg.AdultAge { return state.LifeStageChild } else if age <= cfg.YouthMaxAge { return state.LifeStageYouth } else if age < cfg.ElderMinAge { return state.LifeStageAdult } return state.LifeStageElder } // ===== Contract Methods ===== // getAge returns the current age in years. func (a *Annos) getAge(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) vitaID, found := a.getVitaIDByOwner(ic.DAO, owner) if !found { return stackitem.NewBigInteger(big.NewInt(0)) } rec := a.getRecordInternal(ic.DAO, vitaID) if rec == nil { return stackitem.NewBigInteger(big.NewInt(0)) } // If deceased, return age at death currentTime := ic.Block.Timestamp / 1000 // Block timestamp is in ms if rec.Status == state.LifespanDeceased && rec.DeathTimestamp > 0 { currentTime = rec.DeathTimestamp } age := calculateAge(rec.BirthTimestamp, currentTime) return stackitem.NewBigInteger(big.NewInt(int64(age))) } // getAgeAtTime returns the age at a specific timestamp. func (a *Annos) getAgeAtTime(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) timestamp := toUint64(args[1]) vitaID, found := a.getVitaIDByOwner(ic.DAO, owner) if !found { return stackitem.NewBigInteger(big.NewInt(0)) } rec := a.getRecordInternal(ic.DAO, vitaID) if rec == nil { return stackitem.NewBigInteger(big.NewInt(0)) } // If deceased and query time is after death, use death time queryTime := timestamp if rec.Status == state.LifespanDeceased && rec.DeathTimestamp > 0 && timestamp > rec.DeathTimestamp { queryTime = rec.DeathTimestamp } age := calculateAge(rec.BirthTimestamp, queryTime) return stackitem.NewBigInteger(big.NewInt(int64(age))) } // getLifeStage returns the current life stage. func (a *Annos) getLifeStage(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) vitaID, found := a.getVitaIDByOwner(ic.DAO, owner) if !found { return stackitem.NewBigInteger(big.NewInt(0)) } rec := a.getRecordInternal(ic.DAO, vitaID) if rec == nil { return stackitem.NewBigInteger(big.NewInt(0)) } currentTime := ic.Block.Timestamp / 1000 if rec.Status == state.LifespanDeceased && rec.DeathTimestamp > 0 { currentTime = rec.DeathTimestamp } age := calculateAge(rec.BirthTimestamp, currentTime) stage := a.calculateLifeStage(ic.DAO, age) return stackitem.NewBigInteger(big.NewInt(int64(stage))) } // getBirthTimestamp returns the birth timestamp. func (a *Annos) getBirthTimestamp(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) vitaID, found := a.getVitaIDByOwner(ic.DAO, owner) if !found { return stackitem.NewBigInteger(big.NewInt(0)) } rec := a.getRecordInternal(ic.DAO, vitaID) if rec == nil { return stackitem.NewBigInteger(big.NewInt(0)) } return stackitem.NewBigInteger(new(big.Int).SetUint64(rec.BirthTimestamp)) } // getRecord returns the full lifespan record. func (a *Annos) getRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) vitaID, found := a.getVitaIDByOwner(ic.DAO, owner) if !found { return stackitem.Null{} } rec := a.getRecordInternal(ic.DAO, vitaID) if rec == nil { return stackitem.Null{} } item, _ := rec.ToStackItem() return item } // getRecordByVitaID returns the record by Vita ID. func (a *Annos) getRecordByVitaID(ic *interop.Context, args []stackitem.Item) stackitem.Item { vitaID := toUint64(args[0]) rec := a.getRecordInternal(ic.DAO, vitaID) if rec == nil { return stackitem.Null{} } item, _ := rec.ToStackItem() return item } // isVotingAge checks if the owner is at voting age. func (a *Annos) isVotingAge(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) return stackitem.NewBool(a.IsVotingAgeInternal(ic.DAO, owner, ic.Block.Timestamp/1000)) } // isAdult checks if the owner is an adult. func (a *Annos) isAdult(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) return stackitem.NewBool(a.IsAdultInternal(ic.DAO, owner, ic.Block.Timestamp/1000)) } // isRetirementAge checks if the owner is at retirement age. func (a *Annos) isRetirementAge(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) return stackitem.NewBool(a.IsRetirementAgeInternal(ic.DAO, owner, ic.Block.Timestamp/1000)) } // isAlive checks if the owner is alive. func (a *Annos) isAlive(ic *interop.Context, args []stackitem.Item) stackitem.Item { owner := toUint160(args[0]) vitaID, found := a.getVitaIDByOwner(ic.DAO, owner) if !found { return stackitem.NewBool(false) } rec := a.getRecordInternal(ic.DAO, vitaID) if rec == nil { return stackitem.NewBool(false) } return stackitem.NewBool(rec.Status == state.LifespanActive) } // recordDeath records a death (committee only). func (a *Annos) recordDeath(ic *interop.Context, args []stackitem.Item) stackitem.Item { vitaID := toUint64(args[0]) deathTimestamp := toUint64(args[1]) // Committee only if !a.checkCommittee(ic) { panic(ErrAnnosNotCommittee) } rec := a.getRecordInternal(ic.DAO, vitaID) if rec == nil { panic(ErrAnnosRecordNotFound) } if rec.Status == state.LifespanDeceased { panic(ErrAnnosAlreadyDeceased) } // Validate death timestamp if deathTimestamp == 0 { deathTimestamp = ic.Block.Timestamp / 1000 } if deathTimestamp < rec.BirthTimestamp { panic(ErrAnnosInvalidTimestamp) } // Update record rec.DeathTimestamp = deathTimestamp rec.DeathBlock = ic.Block.Index rec.Status = state.LifespanDeceased a.putRecord(ic.DAO, rec) // Emit event ic.AddNotification(a.Hash, DeathRecordedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(vitaID))), stackitem.NewBigInteger(new(big.Int).SetUint64(deathTimestamp)), })) return stackitem.NewBool(true) } // getConfig returns the Annos configuration. func (a *Annos) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item { cfg := a.getConfigInternal(ic.DAO) item, _ := cfg.ToStackItem() return item } // getTotalRecords returns the total number of lifespan records. func (a *Annos) getTotalRecords(ic *interop.Context, args []stackitem.Item) stackitem.Item { cache := ic.DAO.GetROCache(a.ID).(*AnnosCache) return stackitem.NewBigInteger(big.NewInt(int64(cache.recordCount))) } // ===== Public Interface Methods for Cross-Contract Access ===== // RegisterBirthInternal is called by Vita contract to register a birth. // This should only be called when minting a new Vita token. func (a *Annos) RegisterBirthInternal(d *dao.Simple, ic *interop.Context, vitaID uint64, owner util.Uint160, birthTimestamp uint64) error { // Check if record already exists existing := a.getRecordInternal(d, vitaID) if existing != nil { return ErrAnnosRecordExists } // Validate birth timestamp (must not be in the future) currentTime := ic.Block.Timestamp / 1000 if birthTimestamp > currentTime { birthTimestamp = currentTime // Cap at current time } // Increment counter cache := d.GetRWCache(a.ID).(*AnnosCache) cache.recordCount++ a.setRecordCounter(d, cache.recordCount) // Create lifespan record rec := &state.LifespanRecord{ VitaID: vitaID, Owner: owner, BirthTimestamp: birthTimestamp, RegistrationBlock: ic.Block.Index, RegistrationTime: currentTime, DeathTimestamp: 0, DeathBlock: 0, Status: state.LifespanActive, } a.putRecord(d, rec) a.setOwnerToVitaID(d, owner, vitaID) // Emit event ic.AddNotification(a.Hash, BirthRegisteredEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(vitaID))), stackitem.NewByteArray(owner.BytesBE()), stackitem.NewBigInteger(new(big.Int).SetUint64(birthTimestamp)), })) return nil } // GetAgeInternal returns the age of an owner at the current timestamp. func (a *Annos) GetAgeInternal(d *dao.Simple, owner util.Uint160, currentTimestamp uint64) (uint8, error) { vitaID, found := a.getVitaIDByOwner(d, owner) if !found { return 0, ErrAnnosRecordNotFound } rec := a.getRecordInternal(d, vitaID) if rec == nil { return 0, ErrAnnosRecordNotFound } // If deceased, use death time queryTime := currentTimestamp if rec.Status == state.LifespanDeceased && rec.DeathTimestamp > 0 { queryTime = rec.DeathTimestamp } return calculateAge(rec.BirthTimestamp, queryTime), nil } // IsVotingAgeInternal checks if an owner is at voting age. func (a *Annos) IsVotingAgeInternal(d *dao.Simple, owner util.Uint160, currentTimestamp uint64) bool { age, err := a.GetAgeInternal(d, owner, currentTimestamp) if err != nil { return false } cfg := a.getConfigInternal(d) return age >= cfg.VotingAge } // IsAdultInternal checks if an owner is an adult. func (a *Annos) IsAdultInternal(d *dao.Simple, owner util.Uint160, currentTimestamp uint64) bool { age, err := a.GetAgeInternal(d, owner, currentTimestamp) if err != nil { return false } cfg := a.getConfigInternal(d) return age >= cfg.AdultAge } // IsRetirementAgeInternal checks if an owner is at retirement age. func (a *Annos) IsRetirementAgeInternal(d *dao.Simple, owner util.Uint160, currentTimestamp uint64) bool { age, err := a.GetAgeInternal(d, owner, currentTimestamp) if err != nil { return false } cfg := a.getConfigInternal(d) return age >= cfg.RetirementAge } // GetLifeStageInternal returns the life stage of an owner. func (a *Annos) GetLifeStageInternal(d *dao.Simple, owner util.Uint160, currentTimestamp uint64) state.LifeStage { age, err := a.GetAgeInternal(d, owner, currentTimestamp) if err != nil { return state.LifeStageChild // Default } return a.calculateLifeStage(d, age) } // IsAliveInternal checks if an owner is alive. func (a *Annos) IsAliveInternal(d *dao.Simple, owner util.Uint160) bool { vitaID, found := a.getVitaIDByOwner(d, owner) if !found { return false } rec := a.getRecordInternal(d, vitaID) if rec == nil { return false } return rec.Status == state.LifespanActive } // GetRecordByOwner returns the lifespan record for an owner. func (a *Annos) GetRecordByOwner(d *dao.Simple, owner util.Uint160) (*state.LifespanRecord, error) { vitaID, found := a.getVitaIDByOwner(d, owner) if !found { return nil, ErrAnnosRecordNotFound } rec := a.getRecordInternal(d, vitaID) if rec == nil { return nil, ErrAnnosRecordNotFound } return rec, nil } // Address returns the contract's script hash. func (a *Annos) Address() util.Uint160 { return a.Hash }