Skip to content

Commit

Permalink
feat: update to new L1 rollup contract, add blob tx
Browse files Browse the repository at this point in the history
  • Loading branch information
x-mass committed Feb 7, 2025
1 parent 2eed490 commit e18edad
Show file tree
Hide file tree
Showing 8 changed files with 2,136 additions and 87 deletions.
116 changes: 89 additions & 27 deletions nil/services/synccommittee/core/proposer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"math/big"
"time"

"github.com/NilFoundation/nil/nil/common"
Expand All @@ -15,6 +14,7 @@ import (
"github.com/NilFoundation/nil/nil/services/synccommittee/internal/storage"
scTypes "github.com/NilFoundation/nil/nil/services/synccommittee/internal/types"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/rs/zerolog"
)

Expand Down Expand Up @@ -46,7 +46,7 @@ func NewDefaultProposerParams() *ProposerParams {
return &ProposerParams{
Endpoint: "http://rpc2.sepolia.org",
PrivateKey: "0000000000000000000000000000000000000000000000000000000000000001",
ContractAddress: "0xB8E280a085c87Ed91dd6605480DD2DE9EC3699b4",
ContractAddress: "0x796baf7E572948CD0cbC374f345963bA433b47a2",
ProposingInterval: 10 * time.Second,
EthClientTimeout: 10 * time.Second,
}
Expand All @@ -68,7 +68,7 @@ func NewProposer(
logger,
)

rollupContract, err := rollupcontract.NewWrapper(ctx, params.ContractAddress, params.PrivateKey, ethClient, params.EthClientTimeout)
rollupContract, err := rollupcontract.NewWrapper(ctx, params.ContractAddress, params.PrivateKey, ethClient, params.EthClientTimeout, logger)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -173,7 +173,7 @@ func (p *Proposer) proposeNextBlock(ctx context.Context) error {
}

func (p *Proposer) getLatestProvedStateRoot(ctx context.Context) (common.Hash, error) {
var finalizedBatchIndex *big.Int
var finalizedBatchIndex string
err := p.retryRunner.Do(ctx, func(context.Context) error {
var err error
finalizedBatchIndex, err = p.rollupContract.FinalizedBatchIndex(ctx)
Expand All @@ -183,11 +183,6 @@ func (p *Proposer) getLatestProvedStateRoot(ctx context.Context) (common.Hash, e
return common.EmptyHash, err
}

finalizedBatchIndex.Sub(finalizedBatchIndex, big.NewInt(1))
if finalizedBatchIndex.Cmp(big.NewInt(0)) == -1 {
return common.EmptyHash, errors.New("value returned from FinalizedBatchIndex is less than 1")
}

var latestProvedState [32]byte
err = p.retryRunner.Do(ctx, func(context.Context) error {
var err error
Expand All @@ -199,37 +194,104 @@ func (p *Proposer) getLatestProvedStateRoot(ctx context.Context) (common.Hash, e
}

func (p *Proposer) sendProof(ctx context.Context, data *scTypes.ProposalData) error {
if data.OldProvedStateRoot.Empty() || data.NewProvedStateRoot.Empty() {
return errors.New("empty hash for state update transaction")
// TODO: populate with actual data
blobs := []kzg4844.Blob{{0x01}, {0x02}, {0x03}}
// TODO: populate with actual data
batchIndexInBlobStorage := "0x0000000000000000000000000000000000000000000000000000000000000001"

var tx *ethtypes.Transaction
batchTxSkipped := false
err := p.retryRunner.Do(ctx, func(context.Context) error {
var err error
tx, err = p.rollupContract.CommitBatch(
ctx, blobs, batchIndexInBlobStorage,
)
if errors.Is(err, rollupcontract.ErrBatchAlreadyCommitted) {
p.logger.Warn().Msg("batch is already committed, skipping blob tx")
batchTxSkipped = true
return nil
}
return err
})
if err != nil {
return fmt.Errorf("failed to upload blob: %w", err)
}

if !batchTxSkipped {
p.logger.Info().
Hex("txHash", tx.Hash().Bytes()).
Int("gasLimit", int(tx.Gas())).
Int("blobGasLimit", int(tx.BlobGas())).
Int("cost", int(tx.Cost().Uint64())).
Any("blobHases", tx.BlobHashes()).
Msg("blob transaction sent")

receipt, err := p.rollupContract.WaitForReceipt(ctx, tx.Hash())
if err != nil {
return err
}
if receipt == nil {
return errors.New("CommitBatch tx mining timout exceeded")
}
if receipt.Status != ethtypes.ReceiptStatusSuccessful {
return errors.New("CommitBatch tx failed")
}
}

blobTxSidecar := tx.BlobTxSidecar()
dataProofs, err := rollupcontract.ComputeDataProofs(blobTxSidecar)
if err != nil {
return err
}

// to make sure proofs are correct before submission, not necessary
if err := p.rollupContract.VerifyDataProofs(ctx, blobTxSidecar.BlobHashes(), dataProofs); err != nil {
return err
}

// TODO: populate with actual data
validityProof := []byte{0x0A, 0x0B, 0x0C}

p.logger.Info().
Stringer("blockHash", data.MainShardBlockHash).
Int("txCount", len(data.Transactions)).
Msg("sending proof to L1")
Msg("calling UpdateState L1 method")

proof := make([]byte, 0)
batchIndexInBlobStorage := big.NewInt(0)

var tx *ethtypes.Transaction
err := p.retryRunner.Do(ctx, func(context.Context) error {
updateTxSkipped := false
err = p.retryRunner.Do(ctx, func(context.Context) error {
var err error
tx, err = p.rollupContract.ProofBatch(ctx, data.OldProvedStateRoot, data.NewProvedStateRoot, proof, batchIndexInBlobStorage)
tx, err = p.rollupContract.UpdateState(
ctx,
batchIndexInBlobStorage,
data.OldProvedStateRoot,
data.NewProvedStateRoot,
dataProofs,
validityProof,
rollupcontract.INilRollupPublicDataInfo{
Placeholder1: []byte{0x07, 0x08, 0x09},
Placeholder2: []byte{0x07, 0x08, 0x09},
},
)
if errors.Is(err, rollupcontract.ErrBatchAlreadyFinalized) {
p.logger.Warn().Msg("batch is already committed, skipping UpdateState tx")
updateTxSkipped = true
return nil
}
return err
})
if err != nil {
return fmt.Errorf("failed to update state (eth_sendRawTransaction): %w", err)
return fmt.Errorf("failed to update state: %w", err)
}

p.logger.Info().
Hex("txHash", tx.Hash().Bytes()).
Int("gasLimit", int(tx.Gas())).
Int("blobGasLimit", int(tx.BlobGas())).
Int("cost", int(tx.Cost().Uint64())).
Msg("rollup transaction sent")
if !updateTxSkipped {
p.logger.Info().
Hex("txHash", tx.Hash().Bytes()).
Int("gasLimit", int(tx.Gas())).
Int("cost", int(tx.Cost().Uint64())).
Msg("UpdateState transaction sent")

// TODO send bloob with transactions and KZG proof
p.metrics.RecordProposerTxSent(ctx, data)
}

p.metrics.RecordProposerTxSent(ctx, data)
return nil
}
152 changes: 137 additions & 15 deletions nil/services/synccommittee/core/proposer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package core

import (
"context"
"errors"
"fmt"
"math/big"
"testing"

Expand All @@ -12,6 +14,7 @@ import (
"github.com/NilFoundation/nil/nil/services/synccommittee/internal/rollupcontract"
"github.com/NilFoundation/nil/nil/services/synccommittee/internal/storage"
"github.com/NilFoundation/nil/nil/services/synccommittee/internal/testaide"
"github.com/NilFoundation/nil/nil/services/synccommittee/internal/types"
ethereum "github.com/ethereum/go-ethereum"
ethcommon "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
Expand All @@ -24,12 +27,75 @@ type ProposerTestSuite struct {
ctx context.Context
cancellation context.CancelFunc

params *ProposerParams
db db.DB
timer common.Timer
storage storage.BlockStorage
ethClient *rollupcontract.EthClientMock
proposer *Proposer
params *ProposerParams
db db.DB
timer common.Timer
storage storage.BlockStorage
ethClient *rollupcontract.EthClientMock
proposer *Proposer
testData *types.ProposalData
callContractMock *callContractMock
}

type callContractMock struct {
methodsReturnValue map[string][][]interface{}
}

func newCallContractMock() *callContractMock {
callContractMock := callContractMock{}
callContractMock.Reset()
return &callContractMock
}

func (c *callContractMock) Reset() {
c.methodsReturnValue = make(map[string][][]interface{})
}

type noValue struct{}

func (c *callContractMock) AddExpectedCall(methodName string, returnValues ...interface{}) {
c.methodsReturnValue[methodName] = append(c.methodsReturnValue[methodName], returnValues)
}

func (c *callContractMock) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
abi, err := rollupcontract.RollupcontractMetaData.GetAbi()
if err != nil {
return nil, err
}
methodId := call.Data[:4]
method, err := abi.MethodById(methodId)
if err != nil {
return nil, err
}

returnValuesSlice, ok := c.methodsReturnValue[method.Name]
if !ok {
return nil, errors.New("method not mocked")
}

if len(returnValuesSlice) == 0 {
return nil, errors.New("not enough return values for method")
}
returnValues := returnValuesSlice[0]
c.methodsReturnValue[method.Name] = returnValuesSlice[1:]

if len(returnValues) == 1 {
if _, ok := returnValues[0].(noValue); ok {
// If it's noValue, call Pack with no arguments
return method.Outputs.Pack()
}
}

return method.Outputs.Pack(returnValues...)
}

func (c *callContractMock) EverythingCalled() error {
for methodName, returnValues := range c.methodsReturnValue {
if len(returnValues) != 0 {
return fmt.Errorf("not all calls were executed for %s", methodName)
}
}
return nil
}

func TestProposerSuite(t *testing.T) {
Expand All @@ -50,16 +116,26 @@ func (s *ProposerTestSuite) SetupSuite() {
s.timer = testaide.NewTestTimer()
s.storage = storage.NewBlockStorage(s.db, s.timer, metricsHandler, logger)
s.params = NewDefaultProposerParams()
s.testData = testaide.NewProposalData(3, s.timer.NowTime())
s.callContractMock = newCallContractMock()
s.ethClient = &rollupcontract.EthClientMock{
CallContractFunc: func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) {
return []byte{123}, nil
},
CallContractFunc: s.callContractMock.CallContract,
EstimateGasFunc: func(ctx context.Context, call ethereum.CallMsg) (uint64, error) { return 123, nil },
SuggestGasPriceFunc: func(ctx context.Context) (*big.Int, error) { return big.NewInt(123), nil },
HeaderByNumberFunc: func(ctx context.Context, number *big.Int) (*ethtypes.Header, error) { return &ethtypes.Header{}, nil },
PendingCodeAtFunc: func(ctx context.Context, account ethcommon.Address) ([]byte, error) { return []byte{123}, nil },
PendingNonceAtFunc: func(ctx context.Context, account ethcommon.Address) (uint64, error) { return 123, nil },
ChainIDFunc: func(ctx context.Context) (*big.Int, error) { return big.NewInt(0), nil },
HeaderByNumberFunc: func(ctx context.Context, number *big.Int) (*ethtypes.Header, error) {
excessBlobGas := uint64(123)
return &ethtypes.Header{BaseFee: big.NewInt(123), ExcessBlobGas: &excessBlobGas}, nil
},
PendingCodeAtFunc: func(ctx context.Context, account ethcommon.Address) ([]byte, error) { return []byte{123}, nil },
PendingNonceAtFunc: func(ctx context.Context, account ethcommon.Address) (uint64, error) { return 123, nil },
ChainIDFunc: func(ctx context.Context) (*big.Int, error) { return big.NewInt(0), nil },
SuggestGasTipCapFunc: func(ctx context.Context) (*big.Int, error) { return big.NewInt(123), nil },
CodeAtFunc: func(ctx context.Context, contract ethcommon.Address, blockNumber *big.Int) ([]byte, error) {
return []byte{123}, nil
},
TransactionReceiptFunc: func(ctx context.Context, txHash ethcommon.Hash) (*ethtypes.Receipt, error) {
return &ethtypes.Receipt{Status: ethtypes.ReceiptStatusSuccessful}, nil
},
}
s.proposer, err = NewProposer(s.ctx, s.params, s.storage, s.ethClient, metricsHandler, logger)
s.Require().NoError(err)
Expand All @@ -69,17 +145,63 @@ func (s *ProposerTestSuite) SetupTest() {
err := s.db.DropAll()
s.Require().NoError(err, "failed to clear database in SetUpTest")
s.ethClient.ResetCalls()
s.callContractMock.Reset()
}

func (s *ProposerTestSuite) TearDownSuite() {
s.cancellation()
}

func (s *ProposerTestSuite) TestSendProof() {
data := testaide.NewProposalData(3, s.timer.NowTime())
// Calls inside CommitBatch
s.callContractMock.AddExpectedCall("isBatchCommitted", false)
// Calls inside UpdateState
s.callContractMock.AddExpectedCall("verifyDataProof", noValue{})
s.callContractMock.AddExpectedCall("verifyDataProof", noValue{})
s.callContractMock.AddExpectedCall("verifyDataProof", noValue{})
s.callContractMock.AddExpectedCall("isBatchFinalized", false)
s.callContractMock.AddExpectedCall("isBatchCommitted", true)
s.callContractMock.AddExpectedCall("lastFinalizedBatchIndex", "testingFinalizedBatchIndex")
s.callContractMock.AddExpectedCall("finalizedStateRoots", s.testData.OldProvedStateRoot)

err := s.proposer.sendProof(s.ctx, data)
err := s.proposer.sendProof(s.ctx, s.testData)
s.Require().NoError(err, "failed to send proof")

s.Require().NoError(s.callContractMock.EverythingCalled())
s.Require().Len(s.ethClient.SendTransactionCalls(), 2, "wrong number of calls to rpc client")
}

// Only UpdateState tx should be created
func (s *ProposerTestSuite) TestSendProofCommitedBatch() {
// Calls inside CommitBatch
s.callContractMock.AddExpectedCall("isBatchCommitted", true)
// Calls inside UpdateState
s.callContractMock.AddExpectedCall("verifyDataProof", noValue{})
s.callContractMock.AddExpectedCall("verifyDataProof", noValue{})
s.callContractMock.AddExpectedCall("verifyDataProof", noValue{})
s.callContractMock.AddExpectedCall("isBatchFinalized", false)
s.callContractMock.AddExpectedCall("isBatchCommitted", true)
s.callContractMock.AddExpectedCall("lastFinalizedBatchIndex", "testingFinalizedBatchIndex")
s.callContractMock.AddExpectedCall("finalizedStateRoots", s.testData.OldProvedStateRoot)

err := s.proposer.sendProof(s.ctx, s.testData)
s.Require().NoError(err, "failed to send proof")

s.Require().Len(s.ethClient.SendTransactionCalls(), 1, "wrong number of calls to rpc client")
}

// No tx should be created
func (s *ProposerTestSuite) TestSendProofFinalizedBatch() {
// Calls inside CommitBatch
s.callContractMock.AddExpectedCall("isBatchCommitted", true)
// Calls inside UpdateState
s.callContractMock.AddExpectedCall("verifyDataProof", noValue{})
s.callContractMock.AddExpectedCall("verifyDataProof", noValue{})
s.callContractMock.AddExpectedCall("verifyDataProof", noValue{})
s.callContractMock.AddExpectedCall("isBatchFinalized", true)

err := s.proposer.sendProof(s.ctx, s.testData)
s.Require().NoError(err, "failed to send proof")

s.Require().Empty(s.ethClient.SendTransactionCalls(), "no tx should be created")
}
Loading

0 comments on commit e18edad

Please sign in to comment.