Abstract transactions in tx pool and add staking transaction to pool with error report (#2236)
* [core] Add tx-pool txn interface & update supporting components * Add txn interface (`PoolTransaction`) for tx-pool * Update tx_journal to handle pool's txn interface * Update tx_list to handle pool's txn interface * [staking] Satisfy `PoolTransaction` interface & move error sink types * Implement `Protected`, `ToShardID`, `To`, `Data`, `Value` and `Size` for `StakingTransaction` to satisfy `PoolTransaction` interface * Refactor `Price` to `GasPrice` for `StakingTransaction` to satisfy `PoolTransaction` interface * Move error sink related components to transaction.go * Expose `VerifyBLSKey` and `VerifyBLSKeys` * [core] Generalize tx pool & refactor error sink logic * Refactor txn logic to use `PoolTransaction` and `PoolTransactions` * Add `txPoolErrorReporter` to handle reporting to plainTx and stakingTx error sinks * Remove old & unpayable txs error reports (to error sink) since errs are already reported when adding the txs * Fix known transaction error report when adding txn batches * Add error sink reporting when failed to enqueue txs * [node] Fix error sink & update tx pool interaction * Integrate staking transaction in tx-pool * Remove staking transaction error sink * [hmy api] Integrate staking transactions from tx pool * Remove looking at tx pool for `GetTransactionByHash` * Add `PendingStakingTransactions` and update `PendingTransactions` to only return plainTx * [tests] Update all tests for tx pool txn interface & staking err sink * Update transactions to `PoolTransaction` interface * Remove `CommitTransactions` staking txn error sink * Add basic staking txn tests to tx pool tests * [node] Make all node broadcast staking tx and plain tx * [core + staking] Separate staking msg check and put in tx pool * Move `Validator` specific sanity check into its own method and call said method in `ValidatorWrapper` sanity check * Create staking msg verifiers and preprocessors in `staking_verifier.go` * Remove staking msg verification on all staking msg applications in `state_transition.go` and call new staking msg verifiers & preprocessors * Add staking msg verification to tx pool * Remove `ToShardID` from `PoolTransaction` interface and remove trivial implementation of `ToShardID` in `StakingTransaction`pull/2284/head
parent
ee149be6ed
commit
376178a91e
@ -0,0 +1,266 @@ |
||||
package core |
||||
|
||||
import ( |
||||
"bytes" |
||||
"math/big" |
||||
|
||||
"github.com/pkg/errors" |
||||
|
||||
"github.com/harmony-one/harmony/core/vm" |
||||
common2 "github.com/harmony-one/harmony/internal/common" |
||||
staking "github.com/harmony-one/harmony/staking/types" |
||||
) |
||||
|
||||
var ( |
||||
errStateDBIsMissing = errors.New("no stateDB was provided") |
||||
errChainContextMissing = errors.New("no chain context was provided") |
||||
errEpochMissing = errors.New("no epoch was provided") |
||||
errBlockNumMissing = errors.New("no block number was provided") |
||||
) |
||||
|
||||
// TODO: add unit tests to check staking msg verification
|
||||
|
||||
// VerifyAndCreateValidatorFromMsg verifies the create validator message using
|
||||
// the stateDB, epoch, & blocknumber and returns the validatorWrapper created
|
||||
// in the process.
|
||||
//
|
||||
// Note that this function never updates the stateDB, it only reads from stateDB.
|
||||
func VerifyAndCreateValidatorFromMsg( |
||||
stateDB vm.StateDB, epoch *big.Int, blockNum *big.Int, msg *staking.CreateValidator, |
||||
) (*staking.ValidatorWrapper, error) { |
||||
if stateDB == nil { |
||||
return nil, errStateDBIsMissing |
||||
} |
||||
if epoch == nil { |
||||
return nil, errEpochMissing |
||||
} |
||||
if blockNum == nil { |
||||
return nil, errBlockNumMissing |
||||
} |
||||
if msg.Amount.Sign() == -1 { |
||||
return nil, errNegativeAmount |
||||
} |
||||
if stateDB.IsValidator(msg.ValidatorAddress) { |
||||
return nil, errors.Wrapf(errValidatorExist, common2.MustAddressToBech32(msg.ValidatorAddress)) |
||||
} |
||||
if !CanTransfer(stateDB, msg.ValidatorAddress, msg.Amount) { |
||||
return nil, errInsufficientBalanceForStake |
||||
} |
||||
v, err := staking.CreateValidatorFromNewMsg(msg, blockNum) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
wrapper := &staking.ValidatorWrapper{} |
||||
wrapper.Validator = *v |
||||
wrapper.Delegations = []staking.Delegation{ |
||||
staking.NewDelegation(v.Address, msg.Amount), |
||||
} |
||||
wrapper.Snapshot.Epoch = epoch |
||||
wrapper.Snapshot.NumBlocksSigned = big.NewInt(0) |
||||
wrapper.Snapshot.NumBlocksToSign = big.NewInt(0) |
||||
if err := wrapper.SanityCheck(); err != nil { |
||||
return nil, err |
||||
} |
||||
return wrapper, nil |
||||
} |
||||
|
||||
// VerifyAndEditValidatorFromMsg verifies the edit validator message using
|
||||
// the stateDB, chainContext and returns the edited validatorWrapper.
|
||||
//
|
||||
// Note that this function never updates the stateDB, it only reads from stateDB.
|
||||
func VerifyAndEditValidatorFromMsg( |
||||
stateDB vm.StateDB, chainContext ChainContext, blockNum *big.Int, msg *staking.EditValidator, |
||||
) (*staking.ValidatorWrapper, error) { |
||||
if stateDB == nil { |
||||
return nil, errStateDBIsMissing |
||||
} |
||||
if chainContext == nil { |
||||
return nil, errChainContextMissing |
||||
} |
||||
if blockNum == nil { |
||||
return nil, errBlockNumMissing |
||||
} |
||||
if !stateDB.IsValidator(msg.ValidatorAddress) { |
||||
return nil, errValidatorNotExist |
||||
} |
||||
wrapper := stateDB.GetStakingInfo(msg.ValidatorAddress) |
||||
if wrapper == nil { |
||||
return nil, errValidatorNotExist |
||||
} |
||||
if err := staking.UpdateValidatorFromEditMsg(&wrapper.Validator, msg); err != nil { |
||||
return nil, err |
||||
} |
||||
newRate := wrapper.Validator.Rate |
||||
if newRate.GT(wrapper.Validator.MaxRate) { |
||||
return nil, errCommissionRateChangeTooHigh |
||||
} |
||||
|
||||
// TODO: make sure we are reading from the correct snapshot
|
||||
snapshotValidator, err := chainContext.ReadValidatorSnapshot(wrapper.Address) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
rateAtBeginningOfEpoch := snapshotValidator.Validator.Rate |
||||
|
||||
if rateAtBeginningOfEpoch.IsNil() || (!newRate.IsNil() && !rateAtBeginningOfEpoch.Equal(newRate)) { |
||||
wrapper.Validator.UpdateHeight = blockNum |
||||
} |
||||
|
||||
if newRate.Sub(rateAtBeginningOfEpoch).Abs().GT(wrapper.Validator.MaxChangeRate) { |
||||
return nil, errCommissionRateChangeTooFast |
||||
} |
||||
|
||||
if err := wrapper.SanityCheck(); err != nil { |
||||
return nil, err |
||||
} |
||||
return wrapper, nil |
||||
} |
||||
|
||||
// VerifyAndDelegateFromMsg verifies the delegate message using the stateDB
|
||||
// and returns the balance to be deducted by the delegator as well as the
|
||||
// validatorWrapper with the delegation applied to it.
|
||||
//
|
||||
// Note that this function never updates the stateDB, it only reads from stateDB.
|
||||
func VerifyAndDelegateFromMsg( |
||||
stateDB vm.StateDB, msg *staking.Delegate, |
||||
) (*staking.ValidatorWrapper, *big.Int, error) { |
||||
if stateDB == nil { |
||||
return nil, nil, errStateDBIsMissing |
||||
} |
||||
if msg.Amount.Sign() == -1 { |
||||
return nil, nil, errNegativeAmount |
||||
} |
||||
if !stateDB.IsValidator(msg.ValidatorAddress) { |
||||
return nil, nil, errValidatorNotExist |
||||
} |
||||
wrapper := stateDB.GetStakingInfo(msg.ValidatorAddress) |
||||
if wrapper == nil { |
||||
return nil, nil, errValidatorNotExist |
||||
} |
||||
// Check for redelegation
|
||||
for i := range wrapper.Delegations { |
||||
delegation := &wrapper.Delegations[i] |
||||
if bytes.Equal(delegation.DelegatorAddress.Bytes(), msg.DelegatorAddress.Bytes()) { |
||||
totalInUndelegation := delegation.TotalInUndelegation() |
||||
balance := stateDB.GetBalance(msg.DelegatorAddress) |
||||
// If the sum of normal balance and the total amount of tokens in undelegation is greater than the amount to delegate
|
||||
if big.NewInt(0).Add(totalInUndelegation, balance).Cmp(msg.Amount) >= 0 { |
||||
// Check if it can use tokens in undelegation to delegate (redelegate)
|
||||
delegateBalance := big.NewInt(0).Set(msg.Amount) |
||||
// Use the latest undelegated token first as it has the longest remaining locking time.
|
||||
i := len(delegation.Undelegations) - 1 |
||||
for ; i >= 0; i-- { |
||||
if delegation.Undelegations[i].Amount.Cmp(delegateBalance) <= 0 { |
||||
delegateBalance.Sub(delegateBalance, delegation.Undelegations[i].Amount) |
||||
} else { |
||||
delegation.Undelegations[i].Amount.Sub(delegation.Undelegations[i].Amount, delegateBalance) |
||||
delegateBalance = big.NewInt(0) |
||||
break |
||||
} |
||||
} |
||||
delegation.Undelegations = delegation.Undelegations[:i+1] |
||||
delegation.Amount.Add(delegation.Amount, msg.Amount) |
||||
if err := wrapper.SanityCheck(); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
// Return remaining balance to be deducted for delegation
|
||||
if delegateBalance.Cmp(big.NewInt(0)) < 0 { |
||||
return nil, nil, errInsufficientBalanceForStake // shouldn't really happen
|
||||
} |
||||
return wrapper, delegateBalance, nil |
||||
} |
||||
return nil, nil, errors.Wrapf( |
||||
errInsufficientBalanceForStake, |
||||
"total-delegated %s own-current-balance %s amount-to-delegate %s", |
||||
totalInUndelegation.String(), |
||||
balance.String(), |
||||
msg.Amount.String(), |
||||
) |
||||
} |
||||
} |
||||
// If no redelegation, create new delegation
|
||||
if !CanTransfer(stateDB, msg.DelegatorAddress, msg.Amount) { |
||||
return nil, nil, errInsufficientBalanceForStake |
||||
} |
||||
wrapper.Delegations = append(wrapper.Delegations, staking.NewDelegation(msg.DelegatorAddress, msg.Amount)) |
||||
if err := wrapper.SanityCheck(); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return wrapper, msg.Amount, nil |
||||
} |
||||
|
||||
// VerifyAndUndelegateFromMsg verifies the undelegate validator message
|
||||
// using the stateDB & chainContext and returns the edited validatorWrapper
|
||||
// with the undelegation applied to it.
|
||||
//
|
||||
// Note that this function never updates the stateDB, it only reads from stateDB.
|
||||
func VerifyAndUndelegateFromMsg( |
||||
stateDB vm.StateDB, epoch *big.Int, msg *staking.Undelegate, |
||||
) (*staking.ValidatorWrapper, error) { |
||||
if stateDB == nil { |
||||
return nil, errStateDBIsMissing |
||||
} |
||||
if epoch == nil { |
||||
return nil, errEpochMissing |
||||
} |
||||
if msg.Amount.Sign() == -1 { |
||||
return nil, errNegativeAmount |
||||
} |
||||
if !stateDB.IsValidator(msg.ValidatorAddress) { |
||||
return nil, errValidatorNotExist |
||||
} |
||||
wrapper := stateDB.GetStakingInfo(msg.ValidatorAddress) |
||||
if wrapper == nil { |
||||
return nil, errValidatorNotExist |
||||
} |
||||
for i := range wrapper.Delegations { |
||||
delegation := &wrapper.Delegations[i] |
||||
if bytes.Equal(delegation.DelegatorAddress.Bytes(), msg.DelegatorAddress.Bytes()) { |
||||
if err := delegation.Undelegate(epoch, msg.Amount); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := wrapper.SanityCheck(); err != nil { |
||||
return nil, err |
||||
} |
||||
return wrapper, nil |
||||
} |
||||
} |
||||
return nil, errNoDelegationToUndelegate |
||||
} |
||||
|
||||
// VerifyAndCollectRewardsFromDelegation verifies and collects rewards
|
||||
// from the given delegation slice using the stateDB. It returns all of the
|
||||
// edited validatorWrappers and the sum total of the rewards.
|
||||
//
|
||||
// Note that this function never updates the stateDB, it only reads from stateDB.
|
||||
func VerifyAndCollectRewardsFromDelegation( |
||||
stateDB vm.StateDB, delegations []staking.DelegationIndex, |
||||
) ([]*staking.ValidatorWrapper, *big.Int, error) { |
||||
if stateDB == nil { |
||||
return nil, nil, errStateDBIsMissing |
||||
} |
||||
updatedValidatorWrappers := []*staking.ValidatorWrapper{} |
||||
totalRewards := big.NewInt(0) |
||||
for i := range delegations { |
||||
delegation := &delegations[i] |
||||
wrapper := stateDB.GetStakingInfo(delegation.ValidatorAddress) |
||||
if wrapper == nil { |
||||
return nil, nil, errValidatorNotExist |
||||
} |
||||
if uint64(len(wrapper.Delegations)) > delegation.Index { |
||||
delegation := &wrapper.Delegations[delegation.Index] |
||||
if delegation.Reward.Cmp(big.NewInt(0)) > 0 { |
||||
totalRewards.Add(totalRewards, delegation.Reward) |
||||
} |
||||
delegation.Reward.SetUint64(0) |
||||
} |
||||
if err := wrapper.SanityCheck(); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
updatedValidatorWrappers = append(updatedValidatorWrappers, wrapper) |
||||
} |
||||
if totalRewards.Int64() == 0 { |
||||
return nil, nil, errNoRewardsToCollect |
||||
} |
||||
return updatedValidatorWrappers, totalRewards, nil |
||||
} |
@ -0,0 +1,96 @@ |
||||
package types |
||||
|
||||
import ( |
||||
"io" |
||||
"math/big" |
||||
|
||||
"github.com/pkg/errors" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
staking "github.com/harmony-one/harmony/staking/types" |
||||
) |
||||
|
||||
var ( |
||||
// ErrUnknownPoolTxType is returned when attempting to assert a PoolTransaction to its concrete type
|
||||
ErrUnknownPoolTxType = errors.New("unknown transaction type in tx-pool") |
||||
) |
||||
|
||||
// PoolTransaction is the general transaction interface used by the tx pool
|
||||
type PoolTransaction interface { |
||||
Hash() common.Hash |
||||
Nonce() uint64 |
||||
ChainID() *big.Int |
||||
To() *common.Address |
||||
Size() common.StorageSize |
||||
Data() []byte |
||||
GasPrice() *big.Int |
||||
Gas() uint64 |
||||
Cost() *big.Int |
||||
Value() *big.Int |
||||
EncodeRLP(w io.Writer) error |
||||
DecodeRLP(s *rlp.Stream) error |
||||
Protected() bool |
||||
} |
||||
|
||||
// PoolTransactionSender returns the address derived from the signature (V, R, S) u
|
||||
// sing secp256k1 elliptic curve and an error if it failed deriving or upon an
|
||||
// incorrect signature.
|
||||
//
|
||||
// Sender may cache the address, allowing it to be used regardless of
|
||||
// signing method. The cache is invalidated if the cached signer does
|
||||
// not match the signer used in the current call.
|
||||
//
|
||||
// Note that the signer is an interface since different txs have different signers.
|
||||
func PoolTransactionSender(signer interface{}, tx PoolTransaction) (common.Address, error) { |
||||
if plainTx, ok := tx.(*Transaction); ok { |
||||
if sig, ok := signer.(Signer); ok { |
||||
return Sender(sig, plainTx) |
||||
} |
||||
} else if stakingTx, ok := tx.(*staking.StakingTransaction); ok { |
||||
return stakingTx.SenderAddress() |
||||
} |
||||
return common.Address{}, errors.WithMessage(ErrUnknownPoolTxType, "when fetching transaction sender") |
||||
} |
||||
|
||||
// PoolTransactions is a PoolTransactions slice type for basic sorting.
|
||||
type PoolTransactions []PoolTransaction |
||||
|
||||
// Len returns the length of s.
|
||||
func (s PoolTransactions) Len() int { return len(s) } |
||||
|
||||
// Swap swaps the i'th and the j'th element in s.
|
||||
func (s PoolTransactions) Swap(i, j int) { s[i], s[j] = s[j], s[i] } |
||||
|
||||
// GetRlp implements Rlpable and returns the i'th element of s in rlp.
|
||||
func (s PoolTransactions) GetRlp(i int) []byte { |
||||
enc, _ := rlp.EncodeToBytes(s[i]) |
||||
return enc |
||||
} |
||||
|
||||
// PoolTxDifference returns a new set which is the difference between a and b.
|
||||
func PoolTxDifference(a, b PoolTransactions) PoolTransactions { |
||||
keep := make(PoolTransactions, 0, len(a)) |
||||
|
||||
remove := make(map[common.Hash]struct{}) |
||||
for _, tx := range b { |
||||
remove[tx.Hash()] = struct{}{} |
||||
} |
||||
|
||||
for _, tx := range a { |
||||
if _, ok := remove[tx.Hash()]; !ok { |
||||
keep = append(keep, tx) |
||||
} |
||||
} |
||||
|
||||
return keep |
||||
} |
||||
|
||||
// PoolTxByNonce implements the sort interface to allow sorting a list of transactions
|
||||
// by their nonces. This is usually only useful for sorting transactions from a
|
||||
// single account, otherwise a nonce comparison doesn't make much sense.
|
||||
type PoolTxByNonce PoolTransactions |
||||
|
||||
func (s PoolTxByNonce) Len() int { return len(s) } |
||||
func (s PoolTxByNonce) Less(i, j int) bool { return (s[i]).Nonce() < (s[j]).Nonce() } |
||||
func (s PoolTxByNonce) Swap(i, j int) { s[i], s[j] = s[j], s[i] } |
@ -1,9 +0,0 @@ |
||||
package types |
||||
|
||||
// RPCTransactionError ..
|
||||
type RPCTransactionError struct { |
||||
TxHashID string `json:"tx-hash-id"` |
||||
StakingDirective string `json:"directive-kind"` |
||||
TimestampOfRejection int64 `json:"time-at-rejection"` |
||||
ErrMessage string `json:"error-message"` |
||||
} |
Loading…
Reference in new issue