Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

synccomittee(proposer): update to new L1 rollup contract, add blob tx #221

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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