mirror of
https://github.com/luxfi/proto.git
synced 2026-10-30 15:21:09 +00:00
Wire-level renames in the P/X-Chain proto:
Codec registrations (CRITICAL — these register block + tx type IDs):
- block.RegisterEtnaTypes → block.RegisterQuasarTypes
- txs.RegisterEtnaTypes → txs.RegisterQuasarTypes
Block builder:
- packEtnaBlockTxs → packQuasarBlockTxs
X-Chain (XVM) config struct + JSON tag:
- x/config.Config.EtnaTime → QuasarTime
- json tag "etnaTime" → "quasarTime"
- x/config.Config.IsEtnaActivated → IsQuasarActivated
Errors:
- errEtnaUpgradeNotActive → errQuasarUpgradeNotActive
- errTransformChainTxPostEtna → errTransformChainTxPostQuasar
Test names:
- TestEtnaCreateChainTxInvalidWithManagedNet → TestQuasarCreateChainTxInvalidWithManagedNet
- TestEtnaDisabledTransactions → TestQuasarDisabledTransactions
- TestEtnaStandardTxExecutorAddNetValidator → TestQuasarStandardTxExecutorAddNetValidator
- TestGetValidatorSet_AfterEtna → TestGetValidatorSet_AfterQuasar
All luxfi/upgrade.Config.IsEtnaActivated callsites updated to IsQuasarActivated.
Bumps luxfi/upgrade@main + luxfi/constants@main.
Pre-existing test build failures in p/block/builder (helpers_test.go
validator interface mismatch) are unrelated to this rename.
No backcompat aliases — forwards-only.
376 lines
12 KiB
Go
376 lines
12 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package executor
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/math"
|
|
"github.com/luxfi/proto/p/reward"
|
|
"github.com/luxfi/proto/p/state"
|
|
"github.com/luxfi/proto/p/txs"
|
|
"github.com/luxfi/proto/p/validators/fee"
|
|
"github.com/luxfi/vm/components/gas"
|
|
)
|
|
|
|
var (
|
|
ErrChildBlockEarlierThanParent = errors.New("proposed timestamp before current chain time")
|
|
ErrChildBlockAfterStakerChangeTime = errors.New("proposed timestamp later than next staker change time")
|
|
ErrChildBlockBeyondSyncBound = errors.New("proposed timestamp is too far in the future relative to local time")
|
|
)
|
|
|
|
// VerifyNewChainTime returns nil if the [newChainTime] is a valid chain time.
|
|
// Requires:
|
|
// - [newChainTime] >= [currentChainTime]: to ensure chain time advances
|
|
// monotonically.
|
|
// - [newChainTime] <= [now] + [SyncBound]: to ensure chain time approximates
|
|
// "real" time.
|
|
// - [newChainTime] <= [nextStakerChangeTime]: so that no staking set changes
|
|
// are skipped.
|
|
func VerifyNewChainTime(
|
|
config fee.Config,
|
|
newChainTime time.Time,
|
|
now time.Time,
|
|
currentState state.Chain,
|
|
) error {
|
|
currentChainTime := currentState.GetTimestamp()
|
|
if newChainTime.Before(currentChainTime) {
|
|
return fmt.Errorf(
|
|
"%w: proposed timestamp (%s), chain time (%s)",
|
|
ErrChildBlockEarlierThanParent,
|
|
newChainTime,
|
|
currentChainTime,
|
|
)
|
|
}
|
|
|
|
// Only allow timestamp to be reasonably far forward
|
|
maxNewChainTime := now.Add(SyncBound)
|
|
if newChainTime.After(maxNewChainTime) {
|
|
return fmt.Errorf(
|
|
"%w, proposed time (%s), local time (%s)",
|
|
ErrChildBlockBeyondSyncBound,
|
|
newChainTime,
|
|
now,
|
|
)
|
|
}
|
|
|
|
// nextStakerChangeTime is calculated last to ensure that the function is
|
|
// able to be calculated efficiently.
|
|
nextStakerChangeTime, err := state.GetNextStakerChangeTime(
|
|
config,
|
|
currentState,
|
|
newChainTime,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not verify block timestamp: %w", err)
|
|
}
|
|
|
|
// Only allow timestamp to move as far forward as the time of the next
|
|
// staker set change
|
|
if newChainTime.After(nextStakerChangeTime) {
|
|
return fmt.Errorf(
|
|
"%w, proposed timestamp (%s), next staker change time (%s)",
|
|
ErrChildBlockAfterStakerChangeTime,
|
|
newChainTime,
|
|
nextStakerChangeTime,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AdvanceTimeTo applies all state changes to [parentState] resulting from
|
|
// advancing the chain time to [newChainTime].
|
|
//
|
|
// Returns true iff the validator set changed.
|
|
func AdvanceTimeTo(
|
|
backend *Backend,
|
|
parentState state.Chain,
|
|
newChainTime time.Time,
|
|
) (bool, error) {
|
|
diff, changed, err := advanceTimeTo(backend, parentState, newChainTime)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return changed, diff.Apply(parentState)
|
|
}
|
|
|
|
// advanceTimeTo returns the state diff on top of parentState resulting from
|
|
// advancing the chain time to newChainTime. It also returns a boolean
|
|
// indicating if the validator set changed.
|
|
//
|
|
// parentState is not modified.
|
|
func advanceTimeTo(
|
|
backend *Backend,
|
|
parentState state.Chain,
|
|
newChainTime time.Time,
|
|
) (state.Diff, bool, error) {
|
|
// We promote pending stakers to current stakers first and remove
|
|
// completed stakers from the current staker set. We assume that any
|
|
// promoted staker will not immediately be removed from the current staker
|
|
// set. This is guaranteed by the following invariants.
|
|
//
|
|
// Invariant: MinStakeDuration > 0 => guarantees [StartTime] != [EndTime]
|
|
// Invariant: [newChainTime] <= nextStakerChangeTime.
|
|
|
|
changes, err := state.NewDiffOn(parentState)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Promote any pending stakers to current if [StartTime] <= [newChainTime].
|
|
//
|
|
// Invariant: It is not safe to modify the state while iterating over it,
|
|
// so we use the parentState's iterator rather than the changes iterator.
|
|
// ParentState must not be modified before this iterator is released.
|
|
pendingStakerIterator, err := parentState.GetPendingStakerIterator()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer pendingStakerIterator.Release()
|
|
|
|
var changed bool
|
|
for pendingStakerIterator.Next() {
|
|
stakerToRemove := pendingStakerIterator.Value()
|
|
if stakerToRemove.StartTime.After(newChainTime) {
|
|
break
|
|
}
|
|
|
|
stakerToAdd := *stakerToRemove
|
|
stakerToAdd.NextTime = stakerToRemove.EndTime
|
|
stakerToAdd.Priority = txs.PendingToCurrentPriorities[stakerToRemove.Priority]
|
|
|
|
if stakerToRemove.Priority == txs.NetPermissionedValidatorPendingPriority {
|
|
if err := changes.PutCurrentValidator(&stakerToAdd); err != nil {
|
|
return nil, false, err
|
|
}
|
|
changes.DeletePendingValidator(stakerToRemove)
|
|
changed = true
|
|
continue
|
|
}
|
|
|
|
supply, err := changes.GetCurrentSupply(stakerToRemove.ChainID)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
rewards, err := GetRewardsCalculator(backend, parentState, stakerToRemove.ChainID)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
potentialReward := rewards.Calculate(
|
|
stakerToRemove.EndTime.Sub(stakerToRemove.StartTime),
|
|
stakerToRemove.Weight,
|
|
supply,
|
|
)
|
|
stakerToAdd.PotentialReward = potentialReward
|
|
|
|
// Invariant: [rewards.Calculate] can never return a [potentialReward]
|
|
// such that [supply + potentialReward > maximumSupply].
|
|
changes.SetCurrentSupply(stakerToRemove.ChainID, supply+potentialReward)
|
|
|
|
switch stakerToRemove.Priority {
|
|
case txs.PrimaryNetworkValidatorPendingPriority, txs.NetPermissionlessValidatorPendingPriority:
|
|
if err := changes.PutCurrentValidator(&stakerToAdd); err != nil {
|
|
return nil, false, err
|
|
}
|
|
changes.DeletePendingValidator(stakerToRemove)
|
|
|
|
case txs.PrimaryNetworkDelegatorApricotPendingPriority, txs.PrimaryNetworkDelegatorBanffPendingPriority, txs.NetPermissionlessDelegatorPendingPriority:
|
|
changes.PutCurrentDelegator(&stakerToAdd)
|
|
changes.DeletePendingDelegator(stakerToRemove)
|
|
|
|
default:
|
|
return nil, false, fmt.Errorf("expected staker priority got %d", stakerToRemove.Priority)
|
|
}
|
|
|
|
changed = true
|
|
}
|
|
|
|
// Remove any current stakers whose [EndTime] <= [newChainTime].
|
|
//
|
|
// Invariant: It is not safe to modify the state while iterating over it,
|
|
// so we use the parentState's iterator rather than the changes iterator.
|
|
// ParentState must not be modified before this iterator is released.
|
|
currentStakerIterator, err := parentState.GetCurrentStakerIterator()
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer currentStakerIterator.Release()
|
|
|
|
for currentStakerIterator.Next() {
|
|
stakerToRemove := currentStakerIterator.Value()
|
|
if stakerToRemove.EndTime.After(newChainTime) {
|
|
break
|
|
}
|
|
|
|
// Invariant: Permissioned stakers are encountered first for a given
|
|
// timestamp because their priority is the smallest.
|
|
if stakerToRemove.Priority != txs.NetPermissionedValidatorCurrentPriority {
|
|
// Permissionless stakers are removed by the RewardValidatorTx, not
|
|
// an AdvanceTimeTx.
|
|
break
|
|
}
|
|
|
|
changes.DeleteCurrentValidator(stakerToRemove)
|
|
changed = true
|
|
}
|
|
|
|
if !backend.Config.UpgradeConfig.IsQuasarActivated(newChainTime) {
|
|
changes.SetTimestamp(newChainTime)
|
|
return changes, changed, nil
|
|
}
|
|
|
|
newChainTimeUnix := uint64(newChainTime.Unix())
|
|
if err := removeStaleExpiries(parentState, changes, newChainTimeUnix); err != nil {
|
|
return nil, false, fmt.Errorf("failed to remove stale expiries: %w", err)
|
|
}
|
|
|
|
// Calculate number of seconds the time is advancing
|
|
previousChainTime := changes.GetTimestamp()
|
|
seconds := uint64(newChainTime.Sub(previousChainTime) / time.Second)
|
|
|
|
advanceDynamicFeeState(backend.Config.DynamicFeeConfig, changes, seconds)
|
|
l1ValidatorsChanged, err := advanceValidatorFeeState(
|
|
backend.Config.ValidatorFeeConfig,
|
|
parentState,
|
|
changes,
|
|
seconds,
|
|
)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("failed to advance validator fee state: %w", err)
|
|
}
|
|
changed = changed || l1ValidatorsChanged
|
|
|
|
changes.SetTimestamp(newChainTime)
|
|
return changes, changed, nil
|
|
}
|
|
|
|
// Remove all expiries whose timestamp now implies they can never be re-issued.
|
|
//
|
|
// The expiry timestamp is the time at which it is no longer valid, so any
|
|
// expiry with a timestamp less than or equal to the new chain time can be
|
|
// removed.
|
|
//
|
|
// Ref: https://github.com/luxfi/LPs/tree/e333b335c34c8692d84259d21bd07b2bb849dc2c/LPs/77-reinventing-chains#registerl1validatortx
|
|
func removeStaleExpiries(
|
|
parentState state.Chain,
|
|
changes state.Diff,
|
|
newChainTimeUnix uint64,
|
|
) error {
|
|
// Invariant: It is not safe to modify the state while iterating over it, so
|
|
// we use the parentState's iterator rather than the changes iterator.
|
|
// ParentState must not be modified before this iterator is released.
|
|
expiryIterator, err := parentState.GetExpiryIterator()
|
|
if err != nil {
|
|
return fmt.Errorf("could not iterate over expiries: %w", err)
|
|
}
|
|
defer expiryIterator.Release()
|
|
|
|
for expiryIterator.Next() {
|
|
expiry := expiryIterator.Value()
|
|
// The expiry iterator is sorted in order of increasing timestamp. Once
|
|
// we find a non-expired expiry, we can break.
|
|
if expiry.Timestamp > newChainTimeUnix {
|
|
break
|
|
}
|
|
|
|
changes.DeleteExpiry(expiry)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func advanceDynamicFeeState(
|
|
config gas.Config,
|
|
changes state.Diff,
|
|
seconds uint64,
|
|
) {
|
|
dynamicFeeState := changes.GetFeeState()
|
|
dynamicFeeState = dynamicFeeState.AdvanceTime(
|
|
config.MaxCapacity,
|
|
config.MaxPerSecond,
|
|
config.TargetPerSecond,
|
|
seconds,
|
|
)
|
|
changes.SetFeeState(dynamicFeeState)
|
|
}
|
|
|
|
// advanceValidatorFeeState advances the validator fee state by [seconds]. L1
|
|
// validators are read from [parentState] and written to [changes] to avoid
|
|
// modifying state while an iterator is held.
|
|
func advanceValidatorFeeState(
|
|
config fee.Config,
|
|
parentState state.Chain,
|
|
changes state.Diff,
|
|
seconds uint64,
|
|
) (bool, error) {
|
|
validatorFeeState := fee.State{
|
|
Current: gas.Gas(changes.NumActiveL1Validators()),
|
|
Excess: changes.GetL1ValidatorExcess(),
|
|
}
|
|
validatorCost := validatorFeeState.CostOf(config, seconds)
|
|
|
|
accruedFees := changes.GetAccruedFees()
|
|
accruedFees, err := math.Add(accruedFees, validatorCost)
|
|
if err != nil {
|
|
return false, fmt.Errorf("could not calculate accrued fees: %w", err)
|
|
}
|
|
|
|
// Invariant: It is not safe to modify the state while iterating over it,
|
|
// so we use the parentState's iterator rather than the changes iterator.
|
|
// ParentState must not be modified before this iterator is released.
|
|
l1ValidatorIterator, err := parentState.GetActiveL1ValidatorsIterator()
|
|
if err != nil {
|
|
return false, fmt.Errorf("could not iterate over active L1 validators: %w", err)
|
|
}
|
|
defer l1ValidatorIterator.Release()
|
|
|
|
var changed bool
|
|
for l1ValidatorIterator.Next() {
|
|
l1Validator := l1ValidatorIterator.Value()
|
|
// GetActiveL1ValidatorsIterator iterates in order of increasing
|
|
// EndAccumulatedFee, so we can break early.
|
|
if l1Validator.EndAccumulatedFee > accruedFees {
|
|
break
|
|
}
|
|
|
|
l1Validator.EndAccumulatedFee = 0 // Deactivate the validator
|
|
if err := changes.PutL1Validator(l1Validator); err != nil {
|
|
return false, fmt.Errorf("could not deactivate L1 validator %s: %w", l1Validator.ValidationID, err)
|
|
}
|
|
changed = true
|
|
}
|
|
|
|
validatorFeeState = validatorFeeState.AdvanceTime(config.Target, seconds)
|
|
changes.SetL1ValidatorExcess(validatorFeeState.Excess)
|
|
changes.SetAccruedFees(accruedFees)
|
|
return changed, nil
|
|
}
|
|
|
|
func GetRewardsCalculator(
|
|
backend *Backend,
|
|
parentState state.Chain,
|
|
netID ids.ID,
|
|
) (reward.Calculator, error) {
|
|
if netID == constants.PrimaryNetworkID {
|
|
return backend.Rewards, nil
|
|
}
|
|
|
|
transformNet, err := GetTransformChainTx(parentState, netID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return reward.NewCalculator(reward.Config{
|
|
MaxConsumptionRate: transformNet.MaxConsumptionRate,
|
|
MinConsumptionRate: transformNet.MinConsumptionRate,
|
|
MintingPeriod: backend.Config.RewardConfig.MintingPeriod,
|
|
SupplyCap: transformNet.MaximumSupply,
|
|
}), nil
|
|
}
|