|
package coordinator
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum"
|
|
"github.com/ethereum/go-ethereum/accounts"
|
|
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/hermeznetwork/hermez-node/common"
|
|
"github.com/hermeznetwork/hermez-node/db/l2db"
|
|
"github.com/hermeznetwork/hermez-node/eth"
|
|
"github.com/hermeznetwork/hermez-node/log"
|
|
"github.com/hermeznetwork/hermez-node/synchronizer"
|
|
"github.com/hermeznetwork/tracerr"
|
|
)
|
|
|
|
// TxManager handles everything related to ethereum transactions: It makes the
|
|
// call to forge, waits for transaction confirmation, and keeps checking them
|
|
// until a number of confirmed blocks have passed.
|
|
type TxManager struct {
|
|
cfg Config
|
|
ethClient eth.ClientInterface
|
|
l2DB *l2db.L2DB // Used only to mark forged txs as forged in the L2DB
|
|
coord *Coordinator // Used only to send messages to stop the pipeline
|
|
batchCh chan *BatchInfo
|
|
chainID *big.Int
|
|
account accounts.Account
|
|
consts synchronizer.SCConsts
|
|
|
|
stats synchronizer.Stats
|
|
vars synchronizer.SCVariables
|
|
statsVarsCh chan statsVars
|
|
|
|
queue []*BatchInfo
|
|
// lastSuccessBatch stores the last BatchNum that who's forge call was confirmed
|
|
lastSuccessBatch common.BatchNum
|
|
lastPendingBatch common.BatchNum
|
|
lastSuccessNonce uint64
|
|
lastPendingNonce uint64
|
|
|
|
lastSentL1BatchBlockNum int64
|
|
}
|
|
|
|
// NewTxManager creates a new TxManager
|
|
func NewTxManager(ctx context.Context, cfg *Config, ethClient eth.ClientInterface, l2DB *l2db.L2DB,
|
|
coord *Coordinator, scConsts *synchronizer.SCConsts, initSCVars *synchronizer.SCVariables) (*TxManager, error) {
|
|
chainID, err := ethClient.EthChainID()
|
|
if err != nil {
|
|
return nil, tracerr.Wrap(err)
|
|
}
|
|
address, err := ethClient.EthAddress()
|
|
if err != nil {
|
|
return nil, tracerr.Wrap(err)
|
|
}
|
|
lastSuccessNonce, err := ethClient.EthNonceAt(ctx, *address, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lastPendingNonce, err := ethClient.EthPendingNonceAt(ctx, *address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if lastSuccessNonce != lastPendingNonce {
|
|
return nil, tracerr.Wrap(fmt.Errorf("lastSuccessNonce (%v) != lastPendingNonce (%v)",
|
|
lastSuccessNonce, lastPendingNonce))
|
|
}
|
|
log.Infow("TxManager started", "nonce", lastSuccessNonce)
|
|
return &TxManager{
|
|
cfg: *cfg,
|
|
ethClient: ethClient,
|
|
l2DB: l2DB,
|
|
coord: coord,
|
|
batchCh: make(chan *BatchInfo, queueLen),
|
|
statsVarsCh: make(chan statsVars, queueLen),
|
|
account: accounts.Account{
|
|
Address: *address,
|
|
},
|
|
chainID: chainID,
|
|
consts: *scConsts,
|
|
|
|
vars: *initSCVars,
|
|
|
|
lastSuccessNonce: lastSuccessNonce,
|
|
lastPendingNonce: lastPendingNonce,
|
|
}, nil
|
|
}
|
|
|
|
// AddBatch is a thread safe method to pass a new batch TxManager to be sent to
|
|
// the smart contract via the forge call
|
|
func (t *TxManager) AddBatch(ctx context.Context, batchInfo *BatchInfo) {
|
|
select {
|
|
case t.batchCh <- batchInfo:
|
|
case <-ctx.Done():
|
|
}
|
|
}
|
|
|
|
// SetSyncStatsVars is a thread safe method to sets the synchronizer Stats
|
|
func (t *TxManager) SetSyncStatsVars(ctx context.Context, stats *synchronizer.Stats, vars *synchronizer.SCVariablesPtr) {
|
|
select {
|
|
case t.statsVarsCh <- statsVars{Stats: *stats, Vars: *vars}:
|
|
case <-ctx.Done():
|
|
}
|
|
}
|
|
|
|
func (t *TxManager) syncSCVars(vars synchronizer.SCVariablesPtr) {
|
|
updateSCVars(&t.vars, vars)
|
|
}
|
|
|
|
// NewAuth generates a new auth object for an ethereum transaction
|
|
func (t *TxManager) NewAuth(ctx context.Context) (*bind.TransactOpts, error) {
|
|
gasPrice, err := t.ethClient.EthSuggestGasPrice(ctx)
|
|
if err != nil {
|
|
return nil, tracerr.Wrap(err)
|
|
}
|
|
inc := new(big.Int).Set(gasPrice)
|
|
const gasPriceDiv = 100
|
|
inc.Div(inc, new(big.Int).SetUint64(gasPriceDiv))
|
|
gasPrice.Add(gasPrice, inc)
|
|
// log.Debugw("TxManager: transaction metadata", "gasPrice", gasPrice)
|
|
|
|
auth, err := bind.NewKeyStoreTransactorWithChainID(t.ethClient.EthKeyStore(), t.account, t.chainID)
|
|
if err != nil {
|
|
return nil, tracerr.Wrap(err)
|
|
}
|
|
auth.Value = big.NewInt(0) // in wei
|
|
// TODO: Calculate GasLimit based on the contents of the ForgeBatchArgs
|
|
auth.GasLimit = 1000000
|
|
auth.GasPrice = gasPrice
|
|
auth.Nonce = nil
|
|
|
|
return auth, nil
|
|
}
|
|
|
|
func (t *TxManager) shouldSendRollupForgeBatch(batchInfo *BatchInfo) error {
|
|
nextBlock := t.stats.Eth.LastBlock.Num + 1
|
|
if !t.canForgeAt(nextBlock) {
|
|
return tracerr.Wrap(fmt.Errorf("can't forge in the next block: %v", nextBlock))
|
|
}
|
|
if t.mustL1L2Batch(nextBlock) && !batchInfo.L1Batch {
|
|
return tracerr.Wrap(fmt.Errorf("can't forge non-L1Batch in the next block: %v", nextBlock))
|
|
}
|
|
margin := t.cfg.SendBatchBlocksMarginCheck
|
|
if margin != 0 {
|
|
if !t.canForgeAt(nextBlock + margin) {
|
|
return tracerr.Wrap(fmt.Errorf("can't forge after %v blocks: %v",
|
|
margin, nextBlock))
|
|
}
|
|
if t.mustL1L2Batch(nextBlock+margin) && !batchInfo.L1Batch {
|
|
return tracerr.Wrap(fmt.Errorf("can't forge non-L1Batch after %v blocks: %v",
|
|
margin, nextBlock))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *TxManager) sendRollupForgeBatch(ctx context.Context, batchInfo *BatchInfo) error {
|
|
var ethTx *types.Transaction
|
|
var err error
|
|
auth, err := t.NewAuth(ctx)
|
|
if err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
auth.Nonce = big.NewInt(int64(t.lastPendingNonce))
|
|
t.lastPendingNonce++
|
|
for attempt := 0; attempt < t.cfg.EthClientAttempts; attempt++ {
|
|
ethTx, err = t.ethClient.RollupForgeBatch(batchInfo.ForgeBatchArgs, auth)
|
|
if err != nil {
|
|
log.Errorw("TxManager ethClient.RollupForgeBatch",
|
|
"attempt", attempt, "err", err, "block", t.stats.Eth.LastBlock.Num+1,
|
|
"batchNum", batchInfo.BatchNum)
|
|
} else {
|
|
break
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return tracerr.Wrap(common.ErrDone)
|
|
case <-time.After(t.cfg.EthClientAttemptsDelay):
|
|
}
|
|
}
|
|
if err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("reached max attempts for ethClient.RollupForgeBatch: %w", err))
|
|
}
|
|
batchInfo.EthTx = ethTx
|
|
log.Infow("TxManager ethClient.RollupForgeBatch", "batch", batchInfo.BatchNum, "tx", ethTx.Hash().Hex())
|
|
now := time.Now()
|
|
batchInfo.SendTimestamp = now
|
|
|
|
batchInfo.Debug.Status = StatusSent
|
|
batchInfo.Debug.SendBlockNum = t.stats.Eth.LastBlock.Num + 1
|
|
batchInfo.Debug.SendTimestamp = batchInfo.SendTimestamp
|
|
batchInfo.Debug.StartToSendDelay = batchInfo.Debug.SendTimestamp.Sub(
|
|
batchInfo.Debug.StartTimestamp).Seconds()
|
|
t.cfg.debugBatchStore(batchInfo)
|
|
|
|
t.lastPendingBatch = batchInfo.BatchNum
|
|
if batchInfo.L1Batch {
|
|
t.lastSentL1BatchBlockNum = t.stats.Eth.LastBlock.Num + 1
|
|
}
|
|
if err := t.l2DB.DoneForging(common.TxIDsFromL2Txs(batchInfo.L2Txs), batchInfo.BatchNum); err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkEthTransactionReceipt takes the txHash from the BatchInfo and stores
|
|
// the corresponding receipt if found
|
|
func (t *TxManager) checkEthTransactionReceipt(ctx context.Context, batchInfo *BatchInfo) error {
|
|
txHash := batchInfo.EthTx.Hash()
|
|
var receipt *types.Receipt
|
|
var err error
|
|
for attempt := 0; attempt < t.cfg.EthClientAttempts; attempt++ {
|
|
receipt, err = t.ethClient.EthTransactionReceipt(ctx, txHash)
|
|
if ctx.Err() != nil {
|
|
continue
|
|
} else if tracerr.Unwrap(err) == ethereum.NotFound {
|
|
err = nil
|
|
break
|
|
} else if err != nil {
|
|
log.Errorw("TxManager ethClient.EthTransactionReceipt",
|
|
"attempt", attempt, "err", err)
|
|
} else {
|
|
break
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return tracerr.Wrap(common.ErrDone)
|
|
case <-time.After(t.cfg.EthClientAttemptsDelay):
|
|
}
|
|
}
|
|
if err != nil {
|
|
return tracerr.Wrap(fmt.Errorf("reached max attempts for ethClient.EthTransactionReceipt: %w", err))
|
|
}
|
|
batchInfo.Receipt = receipt
|
|
t.cfg.debugBatchStore(batchInfo)
|
|
return nil
|
|
}
|
|
|
|
func (t *TxManager) handleReceipt(ctx context.Context, batchInfo *BatchInfo) (*int64, error) {
|
|
receipt := batchInfo.Receipt
|
|
if receipt != nil {
|
|
if batchInfo.EthTx.Nonce > t.lastSuccessNonce {
|
|
t.lastSuccessNonce = batchInfo.EthTx.Nonce
|
|
}
|
|
if receipt.Status == types.ReceiptStatusFailed {
|
|
batchInfo.Debug.Status = StatusFailed
|
|
t.cfg.debugBatchStore(batchInfo)
|
|
_, err := t.ethClient.EthCall(ctx, batchInfo.EthTx, receipt.BlockNumber)
|
|
log.Warnw("TxManager receipt status is failed", "tx", receipt.TxHash.Hex(),
|
|
"batch", batchInfo.BatchNum, "block", receipt.BlockNumber.Int64(),
|
|
"err", err)
|
|
if batchInfo.BatchNum <= t.lastSuccessBatch {
|
|
t.lastSuccessBatch = batchInfo.BatchNum - 1
|
|
}
|
|
return nil, tracerr.Wrap(fmt.Errorf(
|
|
"ethereum transaction receipt status is failed: %w", err))
|
|
} else if receipt.Status == types.ReceiptStatusSuccessful {
|
|
batchInfo.Debug.Status = StatusMined
|
|
batchInfo.Debug.MineBlockNum = receipt.BlockNumber.Int64()
|
|
batchInfo.Debug.StartToMineBlocksDelay = batchInfo.Debug.MineBlockNum -
|
|
batchInfo.Debug.StartBlockNum
|
|
now := time.Now()
|
|
batchInfo.Debug.StartToMineDelay = now.Sub(
|
|
batchInfo.Debug.StartTimestamp).Seconds()
|
|
t.cfg.debugBatchStore(batchInfo)
|
|
if batchInfo.BatchNum > t.lastSuccessBatch {
|
|
t.lastSuccessBatch = batchInfo.BatchNum
|
|
}
|
|
confirm := t.stats.Eth.LastBlock.Num - receipt.BlockNumber.Int64()
|
|
return &confirm, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// TODO:
|
|
// - After sending a message: CancelPipeline, stop all consecutive pending Batches (transactions)
|
|
|
|
// Run the TxManager
|
|
func (t *TxManager) Run(ctx context.Context) {
|
|
next := 0
|
|
waitDuration := longWaitDuration
|
|
|
|
var statsVars statsVars
|
|
select {
|
|
case statsVars = <-t.statsVarsCh:
|
|
case <-ctx.Done():
|
|
}
|
|
t.stats = statsVars.Stats
|
|
t.syncSCVars(statsVars.Vars)
|
|
log.Infow("TxManager: received initial statsVars",
|
|
"block", t.stats.Eth.LastBlock.Num, "batch", t.stats.Eth.LastBatch)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Info("TxManager done")
|
|
return
|
|
case statsVars := <-t.statsVarsCh:
|
|
t.stats = statsVars.Stats
|
|
t.syncSCVars(statsVars.Vars)
|
|
case batchInfo := <-t.batchCh:
|
|
if err := t.shouldSendRollupForgeBatch(batchInfo); err != nil {
|
|
log.Warnw("TxManager: shouldSend", "err", err,
|
|
"batch", batchInfo.BatchNum)
|
|
t.coord.SendMsg(ctx, MsgStopPipeline{
|
|
Reason: fmt.Sprintf("forgeBatch shouldSend: %v", err)})
|
|
continue
|
|
}
|
|
if err := t.sendRollupForgeBatch(ctx, batchInfo); ctx.Err() != nil {
|
|
continue
|
|
} else if err != nil {
|
|
// If we reach here it's because our ethNode has
|
|
// been unable to send the transaction to
|
|
// ethereum. This could be due to the ethNode
|
|
// failure, or an invalid transaction (that
|
|
// can't be mined)
|
|
log.Warnw("TxManager: forgeBatch send failed", "err", err,
|
|
"batch", batchInfo.BatchNum)
|
|
t.coord.SendMsg(ctx, MsgStopPipeline{
|
|
Reason: fmt.Sprintf("forgeBatch send: %v", err)})
|
|
continue
|
|
}
|
|
t.queue = append(t.queue, batchInfo)
|
|
waitDuration = t.cfg.TxManagerCheckInterval
|
|
case <-time.After(waitDuration):
|
|
if len(t.queue) == 0 {
|
|
waitDuration = longWaitDuration
|
|
continue
|
|
}
|
|
current := next
|
|
next = (current + 1) % len(t.queue)
|
|
batchInfo := t.queue[current]
|
|
if err := t.checkEthTransactionReceipt(ctx, batchInfo); ctx.Err() != nil {
|
|
continue
|
|
} else if err != nil { //nolint:staticcheck
|
|
// Our ethNode is giving an error different
|
|
// than "not found" when getting the receipt
|
|
// for the transaction, so we can't figure out
|
|
// if it was not mined, mined and succesfull or
|
|
// mined and failed. This could be due to the
|
|
// ethNode failure.
|
|
t.coord.SendMsg(ctx, MsgStopPipeline{
|
|
Reason: fmt.Sprintf("forgeBatch receipt: %v", err)})
|
|
}
|
|
|
|
confirm, err := t.handleReceipt(ctx, batchInfo)
|
|
if ctx.Err() != nil {
|
|
continue
|
|
} else if err != nil { //nolint:staticcheck
|
|
// Transaction was rejected
|
|
next = t.removeFromQueue(current)
|
|
t.coord.SendMsg(ctx, MsgStopPipeline{
|
|
Reason: fmt.Sprintf("forgeBatch reject: %v", err)})
|
|
continue
|
|
}
|
|
now := time.Now()
|
|
if confirm == nil && batchInfo.SendTimestamp > t.cfg.EthTxResendTimeout {
|
|
log.Infow("TxManager: forgeBatch tx not been mined timeout",
|
|
"tx", batchInfo.EthTx.Hex(), "batch", batchInfo.BatchNum)
|
|
// TODO: Resend Tx with same nonce
|
|
}
|
|
if confirm != nil && *confirm >= t.cfg.ConfirmBlocks {
|
|
log.Debugw("TxManager: forgeBatch tx confirmed",
|
|
"tx", batchInfo.EthTx.Hex(), "batch", batchInfo.BatchNum)
|
|
next = t.removeFromQueue(current)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Removes batchInfo at position from the queue, and returns the next position
|
|
func (t *TxManager) removeFromQueue(position int) (next int) {
|
|
t.queue = append(t.queue[:current], t.queue[current+1:]...)
|
|
if len(t.queue) == 0 {
|
|
next = 0
|
|
} else {
|
|
next = current % len(t.queue)
|
|
}
|
|
return next
|
|
}
|
|
|
|
func (t *TxManager) canForgeAt(blockNum int64) bool {
|
|
return canForge(&t.consts.Auction, &t.vars.Auction,
|
|
&t.stats.Sync.Auction.CurrentSlot, &t.stats.Sync.Auction.NextSlot,
|
|
t.cfg.ForgerAddress, blockNum)
|
|
}
|
|
|
|
func (t *TxManager) mustL1L2Batch(blockNum int64) bool {
|
|
lastL1BatchBlockNum := t.lastSentL1BatchBlockNum
|
|
if t.stats.Sync.LastL1BatchBlock > lastL1BatchBlockNum {
|
|
lastL1BatchBlockNum = t.stats.Sync.LastL1BatchBlock
|
|
}
|
|
return blockNum-lastL1BatchBlockNum >= t.vars.Rollup.ForgeL1L2BatchTimeout-1
|
|
}
|