Compare commits

...

3 Commits

Author SHA1 Message Date
colinlyguo
35096f593d feat(rollup-relayer): auto finalize when timeout in test env 2023-11-01 14:06:31 +08:00
zimpha
67d5a860bb add new function to finalize batch without proof 2023-10-31 14:44:19 +08:00
zimpha
023a233e28 remove proof verification on finalizeBatchWithProof 2023-10-31 13:51:40 +08:00
9 changed files with 233 additions and 99 deletions

View File

@@ -5,7 +5,7 @@ import (
"runtime/debug"
)
var tag = "v4.3.28"
var tag = "v4.3.29"
var commit = func() string {
if info, ok := debug.ReadBuildInfo(); ok {

View File

@@ -83,6 +83,25 @@ Return the batch hash of a committed batch.
|---|---|---|
| _0 | bytes32 | undefined |
### finalizeBatch
```solidity
function finalizeBatch(bytes _batchHeader, bytes32 _prevStateRoot, bytes32 _postStateRoot, bytes32 _withdrawRoot) external nonpayable
```
#### Parameters
| Name | Type | Description |
|---|---|---|
| _batchHeader | bytes | undefined |
| _prevStateRoot | bytes32 | undefined |
| _postStateRoot | bytes32 | undefined |
| _withdrawRoot | bytes32 | undefined |
### finalizeBatchWithProof
```solidity

View File

@@ -83,4 +83,16 @@ interface IScrollChain {
bytes32 withdrawRoot,
bytes calldata aggrProof
) external;
/// @notice Finalize a committed batch on layer 1 without providing proof.
/// @param batchHeader The header of current batch, see the encoding in comments of `commitBatch.
/// @param prevStateRoot The state root of parent batch.
/// @param postStateRoot The state root of current batch.
/// @param withdrawRoot The withdraw trie root of current batch.
function finalizeBatch(
bytes calldata batchHeader,
bytes32 prevStateRoot,
bytes32 postStateRoot,
bytes32 withdrawRoot
) external;
}

View File

@@ -353,6 +353,63 @@ contract ScrollChain is OwnableUpgradeable, PausableUpgradeable, IScrollChain {
emit FinalizeBatch(_batchIndex, _batchHash, _postStateRoot, _withdrawRoot);
}
/// @inheritdoc IScrollChain
function finalizeBatch(
bytes calldata _batchHeader,
bytes32 _prevStateRoot,
bytes32 _postStateRoot,
bytes32 _withdrawRoot
) external override OnlyProver whenNotPaused {
require(_prevStateRoot != bytes32(0), "previous state root is zero");
require(_postStateRoot != bytes32(0), "new state root is zero");
// compute batch hash and verify
(uint256 memPtr, bytes32 _batchHash) = _loadBatchHeader(_batchHeader);
uint256 _batchIndex = BatchHeaderV0Codec.batchIndex(memPtr);
require(committedBatches[_batchIndex] == _batchHash, "incorrect batch hash");
// verify previous state root.
require(finalizedStateRoots[_batchIndex - 1] == _prevStateRoot, "incorrect previous state root");
// avoid duplicated verification
require(finalizedStateRoots[_batchIndex] == bytes32(0), "batch already verified");
// check and update lastFinalizedBatchIndex
unchecked {
require(lastFinalizedBatchIndex + 1 == _batchIndex, "incorrect batch index");
lastFinalizedBatchIndex = _batchIndex;
}
// record state root and withdraw root
finalizedStateRoots[_batchIndex] = _postStateRoot;
withdrawRoots[_batchIndex] = _withdrawRoot;
// Pop finalized and non-skipped message from L1MessageQueue.
uint256 _l1MessagePopped = BatchHeaderV0Codec.l1MessagePopped(memPtr);
if (_l1MessagePopped > 0) {
IL1MessageQueue _queue = IL1MessageQueue(messageQueue);
unchecked {
uint256 _startIndex = BatchHeaderV0Codec.totalL1MessagePopped(memPtr) - _l1MessagePopped;
for (uint256 i = 0; i < _l1MessagePopped; i += 256) {
uint256 _count = 256;
if (_l1MessagePopped - i < _count) {
_count = _l1MessagePopped - i;
}
uint256 _skippedBitmap = BatchHeaderV0Codec.skippedBitmap(memPtr, i / 256);
_queue.popCrossDomainMessage(_startIndex, _count, _skippedBitmap);
_startIndex += 256;
}
}
}
emit FinalizeBatch(_batchIndex, _batchHash, _postStateRoot, _withdrawRoot);
}
/************************
* Restricted Functions *
************************/

View File

@@ -24,7 +24,6 @@
"min_gas_price": 0,
"gas_price_diff": 50000
},
"finalize_batch_interval_sec": 0,
"gas_oracle_sender_private_key": "1313131313131313131313131313131313131313131313131313131313131313"
}
},
@@ -59,7 +58,8 @@
"try_times": 5,
"base_url": "http://localhost:8750"
},
"finalize_batch_interval_sec": 0,
"is_test_env": true,
"finalize_batch_without_proof_timeout_sec": 7200,
"gas_oracle_sender_private_key": "1313131313131313131313131313131313131313131313131313131313131313",
"commit_sender_private_key": "1414141414141414141414141414141414141414141414141414141414141414",
"finalize_sender_private_key": "1515151515151515151515151515151515151515151515151515151515151515",

View File

@@ -64,6 +64,11 @@ type RelayerConfig struct {
GasOracleSenderPrivateKey *ecdsa.PrivateKey `json:"-"`
CommitSenderPrivateKey *ecdsa.PrivateKey `json:"-"`
FinalizeSenderPrivateKey *ecdsa.PrivateKey `json:"-"`
// Indicates if the environment is a test environment.
IsTestEnv bool `json:"is_test_env"`
// The timeout in seconds for finalizing a batch without proof, only used in test env.
FinalizeBatchWithoutProofTimeoutSec uint64 `json:"finalize_batch_without_proof_timeout_sec"`
}
// GasOracleConfig The config for updating gas price oracle.

View File

@@ -50,6 +50,11 @@ func NewLayer1Relayer(ctx context.Context, db *gorm.DB, cfg *config.RelayerConfi
return nil, fmt.Errorf("new gas oracle sender failed for address %s, err: %v", addr.Hex(), err)
}
// Ensure test features aren't enabled on the mainnet.
if gasOracleSender.GetChainID() == big.NewInt(1) && cfg.IsTestEnv {
return nil, fmt.Errorf("cannot enable test env features in mainnet")
}
var minGasPrice uint64
var gasPriceDiff uint64
if cfg.GasOracleConfig != nil {

View File

@@ -88,6 +88,11 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.
return nil, fmt.Errorf("new gas oracle sender failed for address %s, err: %w", addr.Hex(), err)
}
// Ensure test features aren't enabled on the mainnet.
if commitSender.GetChainID() == big.NewInt(1) && cfg.IsTestEnv {
return nil, fmt.Errorf("cannot enable test env features in mainnet")
}
var minGasPrice uint64
var gasPriceDiff uint64
if cfg.GasOracleConfig != nil {
@@ -423,107 +428,25 @@ func (r *Layer2Relayer) ProcessCommittedBatches() {
r.metrics.rollupL2RelayerProcessCommittedBatchesTotal.Inc()
batch := batches[0]
hash := batch.Hash
status := types.ProvingStatus(batch.ProvingStatus)
switch status {
case types.ProvingTaskUnassigned, types.ProvingTaskAssigned:
// The proof for this block is not ready yet.
return
now := time.Now()
elapsedTime := now.Sub(batch.CreatedAt)
if r.cfg.IsTestEnv && elapsedTime.Seconds() > float64(r.cfg.FinalizeBatchWithoutProofTimeoutSec) {
if err := r.finalizeBatch(batch, false); err != nil {
log.Error("Failed to finalize timeout batch without proof", "index", batch.Index, "hash", batch.Hash, "err", err)
}
log.Info("Successfully finalized timeout batch without proof", "index", batch.Index, "hash", batch.Hash)
}
case types.ProvingTaskVerified:
log.Info("Start to roll up zk proof", "hash", hash)
log.Info("Start to roll up zk proof", "hash", batch.Hash)
r.metrics.rollupL2RelayerProcessCommittedBatchesFinalizedTotal.Inc()
// Check batch status before send `finalizeBatchWithProof` tx.
if r.cfg.ChainMonitor.Enabled {
var batchStatus bool
batchStatus, err = r.getBatchStatusByIndex(batch.Index)
if err != nil {
r.metrics.rollupL2ChainMonitorLatestFailedCall.Inc()
log.Warn("failed to get batch status, please check chain_monitor api server", "batch_index", batch.Index, "err", err)
return
}
if !batchStatus {
r.metrics.rollupL2ChainMonitorLatestFailedBatchStatus.Inc()
log.Error("the batch status is not right, stop finalize batch and check the reason", "batch_index", batch.Index)
return
}
if err := r.finalizeBatch(batch, true); err != nil {
log.Error("Failed to finalize batch with proof", "index", batch.Index, "hash", batch.Hash, "err", err)
}
var parentBatchStateRoot string
if batch.Index > 0 {
var parentBatch *orm.Batch
parentBatch, err = r.batchOrm.GetBatchByIndex(r.ctx, batch.Index-1)
// handle unexpected db error
if err != nil {
log.Error("Failed to get batch", "index", batch.Index-1, "err", err)
return
}
parentBatchStateRoot = parentBatch.StateRoot
}
aggProof, err := r.batchOrm.GetVerifiedProofByHash(r.ctx, hash)
if err != nil {
log.Error("get verified proof by hash failed", "hash", hash, "err", err)
return
}
if err = aggProof.SanityCheck(); err != nil {
log.Error("agg_proof sanity check fails", "hash", hash, "error", err)
return
}
data, err := r.l1RollupABI.Pack(
"finalizeBatchWithProof",
batch.BatchHeader,
common.HexToHash(parentBatchStateRoot),
common.HexToHash(batch.StateRoot),
common.HexToHash(batch.WithdrawRoot),
aggProof.Proof,
)
if err != nil {
log.Error("Pack finalizeBatchWithProof failed", "err", err)
return
}
txID := hash + "-finalize"
// add suffix `-finalize` to avoid duplication with commit tx in unit tests
txHash, err := r.finalizeSender.SendTransaction(txID, &r.cfg.RollupContractAddress, big.NewInt(0), data, 0)
finalizeTxHash := &txHash
if err != nil {
if !errors.Is(err, sender.ErrNoAvailableAccount) && !errors.Is(err, sender.ErrFullPending) {
// This can happen normally if we try to finalize 2 or more
// batches around the same time. The 2nd tx might fail since
// the client does not see the 1st tx's updates at this point.
// TODO: add more fine-grained error handling
log.Error(
"finalizeBatchWithProof in layer1 failed",
"index", batch.Index,
"hash", batch.Hash,
"RollupContractAddress", r.cfg.RollupContractAddress,
"err", err,
)
log.Debug(
"finalizeBatchWithProof in layer1 failed",
"index", batch.Index,
"hash", batch.Hash,
"RollupContractAddress", r.cfg.RollupContractAddress,
"calldata", common.Bytes2Hex(data),
"err", err,
)
}
return
}
log.Info("finalizeBatchWithProof in layer1", "index", batch.Index, "batch hash", batch.Hash, "tx hash", hash)
// record and sync with db, @todo handle db error
err = r.batchOrm.UpdateFinalizeTxHashAndRollupStatus(r.ctx, hash, finalizeTxHash.String(), types.RollupFinalizing)
if err != nil {
log.Error("UpdateFinalizeTxHashAndRollupStatus failed",
"index", batch.Index, "batch hash", batch.Hash,
"tx hash", finalizeTxHash.String(), "err", err)
}
r.processingFinalization.Store(txID, hash)
r.metrics.rollupL2RelayerProcessCommittedBatchesFinalizedSuccessTotal.Inc()
log.Info("Successfully finalized batch with proof", "index", batch.Index, "hash", batch.Hash)
case types.ProvingTaskFailed:
// We were unable to prove this batch. There are two possibilities:
@@ -542,13 +465,121 @@ func (r *Layer2Relayer) ProcessCommittedBatches() {
"ProvedAt", batch.ProvedAt,
"ProofTimeSec", batch.ProofTimeSec,
)
return
default:
log.Error("encounter unreachable case in ProcessCommittedBatches", "proving status", status)
}
}
func (r *Layer2Relayer) finalizeBatch(batch *orm.Batch, withProof bool) error {
// Check batch status before send `finalizeBatchWithProof` tx.
if r.cfg.ChainMonitor.Enabled {
var batchStatus bool
batchStatus, err := r.getBatchStatusByIndex(batch.Index)
if err != nil {
r.metrics.rollupL2ChainMonitorLatestFailedCall.Inc()
log.Warn("failed to get batch status, please check chain_monitor api server", "batch_index", batch.Index, "err", err)
return err
}
if !batchStatus {
r.metrics.rollupL2ChainMonitorLatestFailedBatchStatus.Inc()
log.Error("the batch status is not right, stop finalize batch and check the reason", "batch_index", batch.Index)
return err
}
}
var parentBatchStateRoot string
if batch.Index > 0 {
var parentBatch *orm.Batch
parentBatch, err := r.batchOrm.GetBatchByIndex(r.ctx, batch.Index-1)
// handle unexpected db error
if err != nil {
log.Error("Failed to get batch", "index", batch.Index-1, "err", err)
return err
}
parentBatchStateRoot = parentBatch.StateRoot
}
var txCalldata []byte
if withProof {
aggProof, err := r.batchOrm.GetVerifiedProofByHash(r.ctx, batch.Hash)
if err != nil {
log.Error("get verified proof by hash failed", "hash", batch.Hash, "err", err)
return err
}
if err = aggProof.SanityCheck(); err != nil {
log.Error("agg_proof sanity check fails", "hash", batch.Hash, "error", err)
return err
}
txCalldata, err = r.l1RollupABI.Pack(
"finalizeBatchWithProof",
batch.BatchHeader,
common.HexToHash(parentBatchStateRoot),
common.HexToHash(batch.StateRoot),
common.HexToHash(batch.WithdrawRoot),
aggProof.Proof,
)
if err != nil {
log.Error("Pack finalizeBatchWithProof failed", "err", err)
return err
}
} else {
var err error
txCalldata, err = r.l1RollupABI.Pack(
"finalizeBatch",
batch.BatchHeader,
common.HexToHash(parentBatchStateRoot),
common.HexToHash(batch.StateRoot),
common.HexToHash(batch.WithdrawRoot),
)
if err != nil {
log.Error("Pack finalizeBatch failed", "err", err)
return err
}
}
txID := batch.Hash + "-finalize"
// add suffix `-finalize` to avoid duplication with commit tx in unit tests
txHash, err := r.finalizeSender.SendTransaction(txID, &r.cfg.RollupContractAddress, big.NewInt(0), txCalldata, 0)
finalizeTxHash := &txHash
if err != nil {
if !errors.Is(err, sender.ErrNoAvailableAccount) && !errors.Is(err, sender.ErrFullPending) {
// This can happen normally if we try to finalize 2 or more
// batches around the same time. The 2nd tx might fail since
// the client does not see the 1st tx's updates at this point.
// TODO: add more fine-grained error handling
log.Error(
"finalizeBatchWithProof in layer1 failed",
"index", batch.Index,
"hash", batch.Hash,
"RollupContractAddress", r.cfg.RollupContractAddress,
"err", err,
)
log.Debug(
"finalizeBatchWithProof in layer1 failed",
"index", batch.Index,
"hash", batch.Hash,
"RollupContractAddress", r.cfg.RollupContractAddress,
"calldata", common.Bytes2Hex(txCalldata),
"err", err,
)
}
return err
}
log.Info("finalizeBatchWithProof in layer1", "index", batch.Index, "batch hash", batch.Hash, "tx hash", batch.Hash)
// record and sync with db, @todo handle db error
if err := r.batchOrm.UpdateFinalizeTxHashAndRollupStatus(r.ctx, batch.Hash, finalizeTxHash.String(), types.RollupFinalizing); err != nil {
log.Error("UpdateFinalizeTxHashAndRollupStatus failed", "index", batch.Index, "batch hash", batch.Hash, "tx hash", finalizeTxHash.String(), "err", err)
return err
}
r.processingFinalization.Store(txID, batch.Hash)
r.metrics.rollupL2RelayerProcessCommittedBatchesFinalizedSuccessTotal.Inc()
return nil
}
// batchStatusResponse the response schema
type batchStatusResponse struct {
ErrCode int `json:"errcode"`

View File

@@ -165,6 +165,11 @@ func (s *Sender) IsFull() bool {
return s.pendingTxs.Count() >= s.config.PendingLimit
}
// GetChainID returns the chain ID associated with the sender.
func (s *Sender) GetChainID() *big.Int {
return s.chainID
}
// Stop stop the sender module.
func (s *Sender) Stop() {
close(s.stopCh)