Files
proto/p/txs/executor/proposal_tx_executor.go

651 lines
18 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/database"
"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/txs/fee"
lux "github.com/luxfi/utxo"
"github.com/luxfi/vm/components/verify"
)
const (
// Maximum future start time for staking/delegating
MaxFutureStartTime = 24 * 7 * 2 * time.Hour
// SyncBound is the synchrony bound used for safe decision making
SyncBound = 10 * time.Second
MaxValidatorWeightFactor = 5
)
var (
_ txs.Visitor = (*proposalTxExecutor)(nil)
ErrRemoveStakerTooEarly = errors.New("attempting to remove staker before their end time")
ErrRemoveWrongStaker = errors.New("attempting to remove wrong staker")
ErrInvalidState = errors.New("generated output isn't valid state")
ErrShouldBePermissionlessStaker = errors.New("expected permissionless staker")
ErrWrongTxType = errors.New("wrong transaction type")
ErrInvalidID = errors.New("invalid ID")
ErrProposedAddStakerTxAfterBanff = errors.New("staker transaction proposed after Banff")
ErrAdvanceTimeTxIssuedAfterBanff = errors.New("AdvanceTimeTx issued after Banff")
)
// ProposalTx executes the proposal transaction [tx].
//
// [onCommitState] will be modified to reflect the changes made to the state if
// the proposal is committed.
//
// [onAbortState] will be modified to reflect the changes made to the state if
// the proposal is aborted.
//
// Invariant: It is assumed that [onCommitState] and [onAbortState] represent
// the same state when passed into this function.
func ProposalTx(
backend *Backend,
feeCalculator fee.Calculator,
tx *txs.Tx,
onCommitState state.Diff,
onAbortState state.Diff,
) error {
proposalExecutor := proposalTxExecutor{
backend: backend,
feeCalculator: feeCalculator,
tx: tx,
onCommitState: onCommitState,
onAbortState: onAbortState,
}
if err := tx.Unsigned.Visit(&proposalExecutor); err != nil {
txID := tx.ID()
return fmt.Errorf("proposal tx %s failed execution: %w", txID, err)
}
return nil
}
type proposalTxExecutor struct {
// inputs, to be filled before visitor methods are called
backend *Backend
feeCalculator fee.Calculator
tx *txs.Tx
// [onCommitState] is the state used for validation.
// [onCommitState] is modified by this struct's methods to
// reflect changes made to the state if the proposal is committed.
onCommitState state.Diff
// [onAbortState] is modified by this struct's methods to
// reflect changes made to the state if the proposal is aborted.
onAbortState state.Diff
}
func (*proposalTxExecutor) CreateChainTx(*txs.CreateChainTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) CreateNetworkTx(*txs.CreateNetworkTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) ImportTx(*txs.ImportTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) ExportTx(*txs.ExportTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) RemoveChainValidatorTx(*txs.RemoveChainValidatorTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) TransformChainTx(*txs.TransformChainTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) TransferChainOwnershipTx(*txs.TransferChainOwnershipTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) BaseTx(*txs.BaseTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) ConvertNetworkToL1Tx(*txs.ConvertNetworkToL1Tx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) RegisterL1ValidatorTx(*txs.RegisterL1ValidatorTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) SetL1ValidatorWeightTx(*txs.SetL1ValidatorWeightTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) IncreaseL1ValidatorBalanceTx(*txs.IncreaseL1ValidatorBalanceTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) DisableL1ValidatorTx(*txs.DisableL1ValidatorTx) error {
return ErrWrongTxType
}
func (e *proposalTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error {
// AddValidatorTx is a proposal transaction until the Banff fork
// activation. Following the activation, AddValidatorTxs must be issued into
// StandardBlocks.
currentTimestamp := e.onCommitState.GetTimestamp()
if e.backend.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) {
return fmt.Errorf(
"%w: timestamp (%s) >= Banff fork time (%s)",
ErrProposedAddStakerTxAfterBanff,
currentTimestamp,
e.backend.Config.UpgradeConfig.BanffTime,
)
}
onAbortOuts, err := verifyAddValidatorTx(
e.backend,
e.feeCalculator,
e.onCommitState,
e.tx,
tx,
)
if err != nil {
return err
}
txID := e.tx.ID()
// Set up the state if this tx is committed
// Consume the UTXOs
lux.Consume(e.onCommitState, tx.Ins)
// Produce the UTXOs
lux.Produce(e.onCommitState, txID, tx.Outs)
newStaker, err := state.NewPendingStaker(txID, tx)
if err != nil {
return err
}
if err := e.onCommitState.PutPendingValidator(newStaker); err != nil {
return err
}
// Set up the state if this tx is aborted
// Consume the UTXOs
lux.Consume(e.onAbortState, tx.Ins)
// Produce the UTXOs
lux.Produce(e.onAbortState, txID, onAbortOuts)
return nil
}
func (e *proposalTxExecutor) AddChainValidatorTx(tx *txs.AddChainValidatorTx) error {
// AddChainValidatorTx is a proposal transaction until the Banff fork
// activation. Following the activation, AddChainValidatorTxs must be
// issued into StandardBlocks.
currentTimestamp := e.onCommitState.GetTimestamp()
if e.backend.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) {
return fmt.Errorf(
"%w: timestamp (%s) >= Banff fork time (%s)",
ErrProposedAddStakerTxAfterBanff,
currentTimestamp,
e.backend.Config.UpgradeConfig.BanffTime,
)
}
if err := verifyAddChainValidatorTx(
e.backend,
e.feeCalculator,
e.onCommitState,
e.tx,
tx,
); err != nil {
return err
}
txID := e.tx.ID()
// Set up the state if this tx is committed
// Consume the UTXOs
lux.Consume(e.onCommitState, tx.Ins)
// Produce the UTXOs
lux.Produce(e.onCommitState, txID, tx.Outs)
newStaker, err := state.NewPendingStaker(txID, tx)
if err != nil {
return err
}
if err := e.onCommitState.PutPendingValidator(newStaker); err != nil {
return err
}
// Set up the state if this tx is aborted
// Consume the UTXOs
lux.Consume(e.onAbortState, tx.Ins)
// Produce the UTXOs
lux.Produce(e.onAbortState, txID, tx.Outs)
return nil
}
func (e *proposalTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error {
// AddDelegatorTx is a proposal transaction until the Banff fork
// activation. Following the activation, AddDelegatorTxs must be issued into
// StandardBlocks.
currentTimestamp := e.onCommitState.GetTimestamp()
if e.backend.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) {
return fmt.Errorf(
"%w: timestamp (%s) >= Banff fork time (%s)",
ErrProposedAddStakerTxAfterBanff,
currentTimestamp,
e.backend.Config.UpgradeConfig.BanffTime,
)
}
onAbortOuts, err := verifyAddDelegatorTx(
e.backend,
e.feeCalculator,
e.onCommitState,
e.tx,
tx,
)
if err != nil {
return err
}
txID := e.tx.ID()
// Set up the state if this tx is committed
// Consume the UTXOs
lux.Consume(e.onCommitState, tx.Ins)
// Produce the UTXOs
lux.Produce(e.onCommitState, txID, tx.Outs)
newStaker, err := state.NewPendingStaker(txID, tx)
if err != nil {
return err
}
e.onCommitState.PutPendingDelegator(newStaker)
// Set up the state if this tx is aborted
// Consume the UTXOs
lux.Consume(e.onAbortState, tx.Ins)
// Produce the UTXOs
lux.Produce(e.onAbortState, txID, onAbortOuts)
return nil
}
func (e *proposalTxExecutor) AdvanceTimeTx(tx *txs.AdvanceTimeTx) error {
switch {
case tx == nil:
return txs.ErrNilTx
case len(e.tx.Creds) != 0:
return errWrongNumberOfCredentials
}
// Validate [newChainTime]
newChainTime := tx.Timestamp()
if e.backend.Config.UpgradeConfig.IsBanffActivated(newChainTime) {
return fmt.Errorf(
"%w: proposed timestamp (%s) >= Banff fork time (%s)",
ErrAdvanceTimeTxIssuedAfterBanff,
newChainTime,
e.backend.Config.UpgradeConfig.BanffTime,
)
}
now := e.backend.Clk.Time()
if err := VerifyNewChainTime(
e.backend.Config.ValidatorFeeConfig,
newChainTime,
now,
e.onCommitState,
); err != nil {
return err
}
// Note that state doesn't change if this proposal is aborted
_, err := AdvanceTimeTo(e.backend, e.onCommitState, newChainTime)
return err
}
func (e *proposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error {
switch {
case tx == nil:
return txs.ErrNilTx
case tx.TxID == ids.Empty:
return ErrInvalidID
case len(e.tx.Creds) != 0:
return errWrongNumberOfCredentials
}
currentStakerIterator, err := e.onCommitState.GetCurrentStakerIterator()
if err != nil {
return err
}
if !currentStakerIterator.Next() {
return fmt.Errorf("failed to get next staker to remove: %w", database.ErrNotFound)
}
stakerToReward := currentStakerIterator.Value()
currentStakerIterator.Release()
if stakerToReward.TxID != tx.TxID {
return fmt.Errorf(
"%w: %s != %s",
ErrRemoveWrongStaker,
stakerToReward.TxID,
tx.TxID,
)
}
// Verify that the chain's timestamp is the validator's end time
currentChainTime := e.onCommitState.GetTimestamp()
if !stakerToReward.EndTime.Equal(currentChainTime) {
return fmt.Errorf(
"%w: TxID = %s with %s < %s",
ErrRemoveStakerTooEarly,
tx.TxID,
currentChainTime,
stakerToReward.EndTime,
)
}
stakerTx, _, err := e.onCommitState.GetTx(stakerToReward.TxID)
if err != nil {
return fmt.Errorf("failed to get next removed staker tx: %w", err)
}
// Invariant: A [txs.DelegatorTx] does not also implement the
// [txs.ValidatorTx] interface.
switch uStakerTx := stakerTx.Unsigned.(type) {
case txs.ValidatorTx:
if err := e.rewardValidatorTx(uStakerTx, stakerToReward); err != nil {
return err
}
// Handle staker lifecycle.
e.onCommitState.DeleteCurrentValidator(stakerToReward)
e.onAbortState.DeleteCurrentValidator(stakerToReward)
case txs.DelegatorTx:
if err := e.rewardDelegatorTx(uStakerTx, stakerToReward); err != nil {
return err
}
// Handle staker lifecycle.
e.onCommitState.DeleteCurrentDelegator(stakerToReward)
e.onAbortState.DeleteCurrentDelegator(stakerToReward)
default:
// Invariant: Permissioned stakers are removed by the advancement of
// time and the current chain timestamp is == this staker's
// EndTime. This means only permissionless stakers should be
// left in the staker set.
return ErrShouldBePermissionlessStaker
}
// If the reward is aborted, then the current supply should be decreased.
currentSupply, err := e.onAbortState.GetCurrentSupply(stakerToReward.ChainID)
if err != nil {
return err
}
newSupply, err := math.Sub(currentSupply, stakerToReward.PotentialReward)
if err != nil {
return err
}
e.onAbortState.SetCurrentSupply(stakerToReward.ChainID, newSupply)
return nil
}
func (e *proposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, validator *state.Staker) error {
var (
txID = validator.TxID
stake = uValidatorTx.Stake()
outputs = uValidatorTx.Outputs()
// Invariant: The staked asset must be equal to the reward asset.
stakeAsset = stake[0].Asset
)
// Refund the stake only when validator is about to leave
// the staking set
for i, out := range stake {
utxo := &lux.UTXO{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: uint32(len(outputs) + i),
},
Asset: out.Asset,
Out: out.Output(),
}
e.onCommitState.AddUTXO(utxo)
e.onAbortState.AddUTXO(utxo)
}
utxosOffset := 0
// Provide the reward here
reward := validator.PotentialReward
if reward > 0 {
validationRewardsOwner := uValidatorTx.ValidationRewardsOwner()
outIntf, err := e.backend.Fx.CreateOutput(reward, validationRewardsOwner)
if err != nil {
return fmt.Errorf("failed to create output: %w", err)
}
out, ok := outIntf.(verify.State)
if !ok {
return ErrInvalidState
}
utxo := &lux.UTXO{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: uint32(len(outputs) + len(stake)),
},
Asset: stakeAsset,
Out: out,
}
e.onCommitState.AddUTXO(utxo)
e.onCommitState.AddRewardUTXO(txID, utxo)
utxosOffset++
}
// Provide the accrued delegatee rewards from successful delegations here.
delegateeReward, err := e.onCommitState.GetDelegateeReward(
validator.ChainID,
validator.NodeID,
)
if err != nil {
return fmt.Errorf("failed to fetch accrued delegatee rewards: %w", err)
}
if delegateeReward == 0 {
return nil
}
delegationRewardsOwner := uValidatorTx.DelegationRewardsOwner()
outIntf, err := e.backend.Fx.CreateOutput(delegateeReward, delegationRewardsOwner)
if err != nil {
return fmt.Errorf("failed to create output: %w", err)
}
out, ok := outIntf.(verify.State)
if !ok {
return ErrInvalidState
}
onCommitUtxo := &lux.UTXO{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: uint32(len(outputs) + len(stake) + utxosOffset),
},
Asset: stakeAsset,
Out: out,
}
e.onCommitState.AddUTXO(onCommitUtxo)
e.onCommitState.AddRewardUTXO(txID, onCommitUtxo)
// Note: There is no [offset] if the RewardValidatorTx is
// aborted, because the validator reward is not awarded.
onAbortUtxo := &lux.UTXO{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: uint32(len(outputs) + len(stake)),
},
Asset: stakeAsset,
Out: out,
}
e.onAbortState.AddUTXO(onAbortUtxo)
e.onAbortState.AddRewardUTXO(txID, onAbortUtxo)
return nil
}
func (e *proposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, delegator *state.Staker) error {
var (
txID = delegator.TxID
stake = uDelegatorTx.Stake()
outputs = uDelegatorTx.Outputs()
// Invariant: The staked asset must be equal to the reward asset.
stakeAsset = stake[0].Asset
)
// Refund the stake only when delegator is about to leave
// the staking set
for i, out := range stake {
utxo := &lux.UTXO{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: uint32(len(outputs) + i),
},
Asset: out.Asset,
Out: out.Output(),
}
e.onCommitState.AddUTXO(utxo)
e.onAbortState.AddUTXO(utxo)
}
// We're (possibly) rewarding a delegator, so we need to fetch
// the validator they are delegated to.
validator, err := e.onCommitState.GetCurrentValidator(delegator.ChainID, delegator.NodeID)
if err != nil {
return fmt.Errorf("failed to get whether %s is a validator: %w", delegator.NodeID, err)
}
vdrTxIntf, _, err := e.onCommitState.GetTx(validator.TxID)
if err != nil {
return fmt.Errorf("failed to get whether %s is a validator: %w", delegator.NodeID, err)
}
// Invariant: Delegators must only be able to reference validator
// transactions that implement [txs.ValidatorTx]. All
// validator transactions implement this interface except the
// AddChainValidatorTx.
vdrTx, ok := vdrTxIntf.Unsigned.(txs.ValidatorTx)
if !ok {
return ErrWrongTxType
}
// Calculate split of reward between delegator/delegatee
delegateeReward, delegatorReward := reward.Split(delegator.PotentialReward, vdrTx.Shares())
utxosOffset := 0
// Reward the delegator here
reward := delegatorReward
if reward > 0 {
rewardsOwner := uDelegatorTx.RewardsOwner()
outIntf, err := e.backend.Fx.CreateOutput(reward, rewardsOwner)
if err != nil {
return fmt.Errorf("failed to create output: %w", err)
}
out, ok := outIntf.(verify.State)
if !ok {
return ErrInvalidState
}
utxo := &lux.UTXO{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: uint32(len(outputs) + len(stake)),
},
Asset: stakeAsset,
Out: out,
}
e.onCommitState.AddUTXO(utxo)
e.onCommitState.AddRewardUTXO(txID, utxo)
utxosOffset++
}
if delegateeReward == 0 {
return nil
}
// Reward the delegatee here
if e.backend.Config.UpgradeConfig.IsCortinaActivated(validator.StartTime) {
previousDelegateeReward, err := e.onCommitState.GetDelegateeReward(
validator.ChainID,
validator.NodeID,
)
if err != nil {
return fmt.Errorf("failed to get delegatee reward: %w", err)
}
// Invariant: The rewards calculator can never return a
// [potentialReward] that would overflow the
// accumulated rewards.
newDelegateeReward := previousDelegateeReward + delegateeReward
// For any validators starting after [CortinaTime], we defer rewarding the
// [reward] until their staking period is over.
err = e.onCommitState.SetDelegateeReward(
validator.ChainID,
validator.NodeID,
newDelegateeReward,
)
if err != nil {
return fmt.Errorf("failed to update delegatee reward: %w", err)
}
} else {
// For any validators who started prior to [CortinaTime], we issue the
// [delegateeReward] immediately.
delegationRewardsOwner := vdrTx.DelegationRewardsOwner()
outIntf, err := e.backend.Fx.CreateOutput(delegateeReward, delegationRewardsOwner)
if err != nil {
return fmt.Errorf("failed to create output: %w", err)
}
out, ok := outIntf.(verify.State)
if !ok {
return ErrInvalidState
}
utxo := &lux.UTXO{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: uint32(len(outputs) + len(stake) + utxosOffset),
},
Asset: stakeAsset,
Out: out,
}
e.onCommitState.AddUTXO(utxo)
e.onCommitState.AddRewardUTXO(txID, utxo)
}
return nil
}