diff --git a/go/common/errutil/errors_util.go b/go/common/errutil/errors_util.go index 62823f6f46..2e72aeb154 100644 --- a/go/common/errutil/errors_util.go +++ b/go/common/errutil/errors_util.go @@ -23,7 +23,8 @@ var ( ErrBlockAncestorNotFound = errors.New("block ancestor not found") ErrBlockForBatchNotFound = errors.New("block for batch not found") ErrAncestorBatchNotFound = errors.New("parent for batch not found") - ErrCrossChainBundleRepublished = errors.New("Root already added to the message bus") + ErrCrossChainBundleRepublished = errors.New("root already added to the message bus") + ErrCrossChainBundleNoBatches = errors.New("no batches for cross chain bundle") ) // BlockRejectError is used as a standard format for error response from enclave for block submission errors diff --git a/go/common/gethencoding/geth_encoding.go b/go/common/gethencoding/geth_encoding.go index 812dbc527a..bac9685bfb 100644 --- a/go/common/gethencoding/geth_encoding.go +++ b/go/common/gethencoding/geth_encoding.go @@ -77,54 +77,6 @@ func NewGethEncodingService(storage storage.Storage, logger gethlog.Logger) Enco } } -// ExtractEthCallMapString extracts the eth_call gethapi.TransactionArgs from an interface{} -// it ensures that : -// - All types are string -// - All keys are lowercase -// - There is only one key per value -// - From field is set by default -func ExtractEthCallMapString(paramBytes interface{}) (map[string]string, error) { - // geth lowercase the field name and uses the last seen value - var valString string - var ok bool - callMsg := map[string]string{ - // From field is set by default - "from": gethcommon.HexToAddress("0x0").Hex(), - } - for field, val := range paramBytes.(map[string]interface{}) { - if val == nil { - continue - } - valString, ok = val.(string) - if !ok { - return nil, fmt.Errorf("unexpected type supplied in `%s` field", field) - } - if len(strings.TrimSpace(valString)) == 0 { - continue - } - switch strings.ToLower(field) { - case callFieldTo: - callMsg[callFieldTo] = valString - case CallFieldFrom: - callMsg[CallFieldFrom] = valString - case callFieldData, callFieldInput: - callMsg[callFieldInput] = valString - case callFieldValue: - callMsg[callFieldValue] = valString - case callFieldGas: - callMsg[callFieldGas] = valString - case callFieldMaxFeePerGas: - callMsg[callFieldMaxFeePerGas] = valString - case callFieldMaxPriorityFeePerGas: - callMsg[callFieldMaxPriorityFeePerGas] = valString - default: - callMsg[field] = valString - } - } - - return callMsg, nil -} - // ExtractAddress returns a gethcommon.Address given an interface{}, errors if unexpected values are used func ExtractAddress(param interface{}) (*gethcommon.Address, error) { if param == nil { @@ -140,7 +92,7 @@ func ExtractAddress(param interface{}) (*gethcommon.Address, error) { return nil, fmt.Errorf("no address specified") } - addr := gethcommon.HexToAddress(param.(string)) + addr := gethcommon.HexToAddress(paramStr) return &addr, nil } @@ -183,10 +135,13 @@ func ExtractBlockNumber(param interface{}) (*gethrpc.BlockNumberOrHash, error) { blockAndHash, ok := param.(map[string]any) if !ok { - return nil, fmt.Errorf("invalid block or hash parameter %s", param.(string)) + return nil, fmt.Errorf("invalid block or hash parameter") } if blockAndHash["blockNumber"] != nil { - b := blockAndHash["blockNumber"].(string) + b, ok := blockAndHash["blockNumber"].(string) + if !ok { + return nil, fmt.Errorf("invalid blockNumber parameter") + } blockNumber := gethrpc.BlockNumber(0) err := blockNumber.UnmarshalJSON([]byte(b)) if err != nil { @@ -195,11 +150,17 @@ func ExtractBlockNumber(param interface{}) (*gethrpc.BlockNumberOrHash, error) { blockNo = &blockNumber } if blockAndHash["blockHash"] != nil { - bh := blockAndHash["blockHash"].(gethcommon.Hash) + bh, ok := blockAndHash["blockHash"].(gethcommon.Hash) + if !ok { + return nil, fmt.Errorf("invalid blockhash parameter") + } blockHa = &bh } if blockAndHash["RequireCanonical"] != nil { - reqCanon = blockAndHash["RequireCanonical"].(bool) + reqCanon, ok = blockAndHash["RequireCanonical"].(bool) + if !ok { + return nil, fmt.Errorf("invalid RequireCanonical parameter") + } } return &gethrpc.BlockNumberOrHash{ @@ -222,7 +183,12 @@ func ExtractEthCall(param interface{}) (*gethapi.TransactionArgs, error) { // if gas is not set it should be null gas := (*hexutil.Uint64)(nil) - for field, val := range param.(map[string]interface{}) { + ethCallMap, ok := param.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid eth call parameter") + } + + for field, val := range ethCallMap { if val == nil { continue } diff --git a/go/common/gethutil/gethutil.go b/go/common/gethutil/gethutil.go index 76f8e99964..8141db2222 100644 --- a/go/common/gethutil/gethutil.go +++ b/go/common/gethutil/gethutil.go @@ -1,7 +1,6 @@ package gethutil import ( - "bytes" "context" "fmt" @@ -20,14 +19,7 @@ var EmptyHash = gethcommon.Hash{} // LCA - returns the latest common ancestor of the 2 blocks or an error if no common ancestor is found // it also returns the blocks that became canonical, and the once that are now the fork func LCA(ctx context.Context, newCanonical *types.Block, oldCanonical *types.Block, resolver storage.BlockResolver) (*common.ChainFork, error) { - b, cp, ncp, err := internalLCA(ctx, newCanonical, oldCanonical, resolver, []common.L1BlockHash{}, []common.L1BlockHash{oldCanonical.Hash()}) - // remove the common ancestor - if len(cp) > 0 { - cp = cp[0 : len(cp)-1] - } - if len(ncp) > 0 { - ncp = ncp[0 : len(ncp)-1] - } + b, cp, ncp, err := internalLCA(ctx, newCanonical, oldCanonical, resolver, []common.L1BlockHash{}, []common.L1BlockHash{}) return &common.ChainFork{ NewCanonical: newCanonical, OldCanonical: oldCanonical, @@ -41,33 +33,34 @@ func internalLCA(ctx context.Context, newCanonical *types.Block, oldCanonical *t if newCanonical.NumberU64() == common.L1GenesisHeight || oldCanonical.NumberU64() == common.L1GenesisHeight { return newCanonical, canonicalPath, nonCanonicalPath, nil } - if bytes.Equal(newCanonical.Hash().Bytes(), oldCanonical.Hash().Bytes()) { - return newCanonical, canonicalPath, nonCanonicalPath, nil + if newCanonical.Hash() == oldCanonical.Hash() { + // this is where we reach the common ancestor, which we add to the canonical path + return newCanonical, append(canonicalPath, newCanonical.Hash()), nonCanonicalPath, nil } if newCanonical.NumberU64() > oldCanonical.NumberU64() { p, err := resolver.FetchBlock(ctx, newCanonical.ParentHash()) if err != nil { - return nil, nil, nil, fmt.Errorf("could not retrieve parent block. Cause: %w", err) + return nil, nil, nil, fmt.Errorf("could not retrieve parent block %s. Cause: %w", newCanonical.ParentHash(), err) } - return internalLCA(ctx, p, oldCanonical, resolver, append(canonicalPath, p.Hash()), nonCanonicalPath) + return internalLCA(ctx, p, oldCanonical, resolver, append(canonicalPath, newCanonical.Hash()), nonCanonicalPath) } if oldCanonical.NumberU64() > newCanonical.NumberU64() { p, err := resolver.FetchBlock(ctx, oldCanonical.ParentHash()) if err != nil { - return nil, nil, nil, fmt.Errorf("could not retrieve parent block. Cause: %w", err) + return nil, nil, nil, fmt.Errorf("could not retrieve parent block %s. Cause: %w", oldCanonical.ParentHash(), err) } - return internalLCA(ctx, newCanonical, p, resolver, canonicalPath, append(nonCanonicalPath, p.Hash())) + return internalLCA(ctx, newCanonical, p, resolver, canonicalPath, append(nonCanonicalPath, oldCanonical.Hash())) } parentBlockA, err := resolver.FetchBlock(ctx, newCanonical.ParentHash()) if err != nil { - return nil, nil, nil, fmt.Errorf("could not retrieve parent block. Cause: %w", err) + return nil, nil, nil, fmt.Errorf("could not retrieve parent block %s. Cause: %w", newCanonical.ParentHash(), err) } parentBlockB, err := resolver.FetchBlock(ctx, oldCanonical.ParentHash()) if err != nil { - return nil, nil, nil, fmt.Errorf("could not retrieve parent block. Cause: %w", err) + return nil, nil, nil, fmt.Errorf("could not retrieve parent block %s. Cause: %w", oldCanonical.ParentHash(), err) } - return internalLCA(ctx, parentBlockA, parentBlockB, resolver, append(canonicalPath, parentBlockA.Hash()), append(nonCanonicalPath, parentBlockB.Hash())) + return internalLCA(ctx, parentBlockA, parentBlockB, resolver, append(canonicalPath, newCanonical.Hash()), append(nonCanonicalPath, oldCanonical.Hash())) } diff --git a/go/enclave/components/batch_executor.go b/go/enclave/components/batch_executor.go index 77aae4e75f..ea5b47516f 100644 --- a/go/enclave/components/batch_executor.go +++ b/go/enclave/components/batch_executor.go @@ -144,7 +144,7 @@ func (executor *batchExecutor) ComputeBatch(ctx context.Context, context *BatchE } // These variables will be used to create the new batch - parent, err := executor.storage.FetchBatch(ctx, context.ParentPtr) + parentBatch, err := executor.storage.FetchBatch(ctx, context.ParentPtr) if errors.Is(err, errutil.ErrNotFound) { executor.logger.Error(fmt.Sprintf("can't find parent batch %s. Seq %d", context.ParentPtr, context.SequencerNo)) return nil, errutil.ErrAncestorBatchNotFound @@ -154,17 +154,17 @@ func (executor *batchExecutor) ComputeBatch(ctx context.Context, context *BatchE } parentBlock := block - if parent.Header.L1Proof != block.Hash() { + if parentBatch.Header.L1Proof != block.Hash() { var err error - parentBlock, err = executor.storage.FetchBlock(ctx, parent.Header.L1Proof) + parentBlock, err = executor.storage.FetchBlock(ctx, parentBatch.Header.L1Proof) if err != nil { - executor.logger.Error(fmt.Sprintf("Could not retrieve a proof for batch %s", parent.Hash()), log.ErrKey, err) + executor.logger.Error(fmt.Sprintf("Could not retrieve a proof for batch %s", parentBatch.Hash()), log.ErrKey, err) return nil, err } } // Create a new batch based on the fromBlock of inclusion of the previous, including all new transactions - batch := core.DeterministicEmptyBatch(parent.Header, block, context.AtTime, context.SequencerNo, context.BaseFee, context.Creator) + batch := core.DeterministicEmptyBatch(parentBatch.Header, block, context.AtTime, context.SequencerNo, context.BaseFee, context.Creator) stateDB, err := executor.batchRegistry.GetBatchState(ctx, &batch.Header.ParentHash) if err != nil { @@ -379,7 +379,7 @@ func (executor *batchExecutor) populateOutboundCrossChainData(ctx context.Contex encodedTree, err := json.Marshal(xchainTree) if err != nil { - panic(err) //todo: figure out what to do + panic(err) // todo: figure out what to do } batch.Header.CrossChainTree = encodedTree diff --git a/go/enclave/components/batch_registry.go b/go/enclave/components/batch_registry.go index 5b069f581e..c76b967f95 100644 --- a/go/enclave/components/batch_registry.go +++ b/go/enclave/components/batch_registry.go @@ -100,12 +100,12 @@ func (br *batchRegistry) OnBatchExecuted(batch *core.Batch, receipts types.Recei } func (br *batchRegistry) HasGenesisBatch() (bool, error) { - return br.headBatchSeq != nil, nil + return br.HeadBatchSeq() != nil, nil } func (br *batchRegistry) BatchesAfter(ctx context.Context, batchSeqNo uint64, upToL1Height uint64, rollupLimiter limiters.RollupLimiter) ([]*core.Batch, []*types.Block, error) { // sanity check - headBatch, err := br.storage.FetchBatchBySeqNo(ctx, br.headBatchSeq.Uint64()) + headBatch, err := br.storage.FetchBatchBySeqNo(ctx, br.HeadBatchSeq().Uint64()) if err != nil { return nil, nil, err } @@ -199,7 +199,7 @@ func getBatchState(ctx context.Context, storage storage.Storage, batch *core.Bat } func (br *batchRegistry) GetBatchAtHeight(ctx context.Context, height gethrpc.BlockNumber) (*core.Batch, error) { - if br.headBatchSeq == nil { + if br.HeadBatchSeq() == nil { return nil, fmt.Errorf("chain not initialised") } var batch *core.Batch @@ -212,7 +212,7 @@ func (br *batchRegistry) GetBatchAtHeight(ctx context.Context, height gethrpc.Bl batch = genesisBatch // note: our API currently treats all these block statuses the same for obscuro batches case gethrpc.SafeBlockNumber, gethrpc.FinalizedBlockNumber, gethrpc.LatestBlockNumber, gethrpc.PendingBlockNumber: - headBatch, err := br.storage.FetchBatchBySeqNo(ctx, br.headBatchSeq.Uint64()) + headBatch, err := br.storage.FetchBatchBySeqNo(ctx, br.HeadBatchSeq().Uint64()) if err != nil { return nil, fmt.Errorf("batch with requested height %d was not found. Cause: %w", height, err) } diff --git a/go/enclave/components/block_processor.go b/go/enclave/components/block_processor.go index c80b518625..15cb1ebcc9 100644 --- a/go/enclave/components/block_processor.go +++ b/go/enclave/components/block_processor.go @@ -100,29 +100,27 @@ func (bp *l1BlockProcessor) HealthCheck() (bool, error) { func (bp *l1BlockProcessor) tryAndInsertBlock(ctx context.Context, br *common.BlockAndReceipts) (*BlockIngestionType, error) { block := br.Block - _, err := bp.storage.FetchBlock(ctx, block.Hash()) - if err == nil { - return nil, errutil.ErrBlockAlreadyProcessed - } - - if !errors.Is(err, errutil.ErrNotFound) { - return nil, fmt.Errorf("could not retrieve block. Cause: %w", err) - } - // We insert the block into the L1 chain and store it. + // in case the block already exists in the database, this will be treated like a fork, because the head changes to + // the block that was already saved ingestionType, err := bp.ingestBlock(ctx, block) if err != nil { // Do not store the block if the L1 chain insertion failed return nil, err } - bp.logger.Trace("Block inserted successfully", - log.BlockHeightKey, block.NumberU64(), log.BlockHashKey, block.Hash(), "ingestionType", ingestionType) + + if ingestionType.OldCanonicalBlock { + return nil, errutil.ErrBlockAlreadyProcessed + } err = bp.storage.StoreBlock(ctx, block, ingestionType.ChainFork) if err != nil { return nil, fmt.Errorf("1. could not store block. Cause: %w", err) } + bp.logger.Trace("Block inserted successfully", + log.BlockHeightKey, block.NumberU64(), log.BlockHashKey, block.Hash(), "ingestionType", ingestionType) + return ingestionType, nil } @@ -138,9 +136,17 @@ func (bp *l1BlockProcessor) ingestBlock(ctx context.Context, block *common.L1Blo } // we do a basic sanity check, comparing the received block to the head block on the chain if block.ParentHash() != prevL1Head.Hash() { + isCanon, err := bp.storage.IsBlockCanonical(ctx, block.Hash()) + if err != nil { + return nil, fmt.Errorf("could not check if block is canonical. Cause: %w", err) + } + if isCanon { + return &BlockIngestionType{OldCanonicalBlock: true}, nil + } + chainFork, err := gethutil.LCA(ctx, block, prevL1Head, bp.storage) if err != nil { - bp.logger.Trace("parent not found", + bp.logger.Trace("cannot calculate the fork for received block", "blkHeight", block.NumberU64(), log.BlockHashKey, block.Hash(), "l1HeadHeight", prevL1Head.NumberU64(), "l1HeadHash", prevL1Head.Hash(), log.ErrKey, err, @@ -149,7 +155,7 @@ func (bp *l1BlockProcessor) ingestBlock(ctx context.Context, block *common.L1Blo } if chainFork.IsFork() { - bp.logger.Info("Fork detected in the l1 chain", "can", chainFork.CommonAncestor.Hash().Hex(), "noncan", prevL1Head.Hash().Hex()) + bp.logger.Info("Fork detected in the l1 chain", "can", chainFork.CommonAncestor.Hash(), "noncan", prevL1Head.Hash()) } return &BlockIngestionType{ChainFork: chainFork, PreGenesis: false}, nil } diff --git a/go/enclave/components/interfaces.go b/go/enclave/components/interfaces.go index fa6aff38c3..cb74f0bf4b 100644 --- a/go/enclave/components/interfaces.go +++ b/go/enclave/components/interfaces.go @@ -25,6 +25,9 @@ type BlockIngestionType struct { // ChainFork contains information about the status of the new block in the chain ChainFork *common.ChainFork + + // Block that is already on the canonical chain + OldCanonicalBlock bool } func (bit *BlockIngestionType) IsFork() bool { diff --git a/go/enclave/components/rollup_compression.go b/go/enclave/components/rollup_compression.go index ef2156693a..ca4536e0ca 100644 --- a/go/enclave/components/rollup_compression.go +++ b/go/enclave/components/rollup_compression.go @@ -175,6 +175,7 @@ func (rc *RollupCompression) createRollupHeader(ctx context.Context, rollup *cor } reorgMap := make(map[uint64]bool) for _, batch := range reorgedBatches { + rc.logger.Info("Reorg batch", log.BatchSeqNoKey, batch.SeqNo().Uint64()) reorgMap[batch.SeqNo().Uint64()] = true } diff --git a/go/enclave/crosschain/common.go b/go/enclave/crosschain/common.go index b6ee881c89..1d64083dbb 100644 --- a/go/enclave/crosschain/common.go +++ b/go/enclave/crosschain/common.go @@ -234,7 +234,7 @@ func (ms MessageStructs) HashPacked(index int) gethcommon.Hash { }, } - //todo @siliev: err + // todo @siliev: err packed, _ := args.Pack(messageStruct.Sender, messageStruct.Sequence, messageStruct.Nonce, messageStruct.Topic, messageStruct.Payload, messageStruct.ConsistencyLevel) hash := crypto.Keccak256Hash(packed) return hash diff --git a/go/enclave/enclave.go b/go/enclave/enclave.go index 780693a10a..7db4324e61 100644 --- a/go/enclave/enclave.go +++ b/go/enclave/enclave.go @@ -572,7 +572,6 @@ func (e *enclaveImpl) CreateRollup(ctx context.Context, fromSeqNo uint64) (*comm return nil, responses.ToInternalError(fmt.Errorf("requested GenerateRollup with the enclave stopping")) } - // todo - remove once the db operations are more atomic e.mainMutex.Lock() defer e.mainMutex.Unlock() diff --git a/go/enclave/genesis/testnet_genesis.go b/go/enclave/genesis/testnet_genesis.go index 49b48c16ca..fed93c6f0d 100644 --- a/go/enclave/genesis/testnet_genesis.go +++ b/go/enclave/genesis/testnet_genesis.go @@ -9,8 +9,10 @@ import ( ) const TestnetPrefundedPK = "8dfb8083da6275ae3e4f41e3e8a8c19d028d32c9247e24530933782f2a05035b" // The genesis main account private key. -var GasBridgingKeys, _ = crypto.GenerateKey() // todo - make static -var GasWithdrawalKeys, _ = crypto.GenerateKey() // todo - make static +var ( + GasBridgingKeys, _ = crypto.GenerateKey() // todo - make static + GasWithdrawalKeys, _ = crypto.GenerateKey() // todo - make static +) var TestnetGenesis = Genesis{ Accounts: []Account{ diff --git a/go/enclave/nodetype/common.go b/go/enclave/nodetype/common.go index c9a28978b4..a99dc0f690 100644 --- a/go/enclave/nodetype/common.go +++ b/go/enclave/nodetype/common.go @@ -2,11 +2,11 @@ package nodetype import ( "context" - "fmt" "math/big" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ten-protocol/go-ten/go/common" + "github.com/ten-protocol/go-ten/go/common/errutil" "github.com/ten-protocol/go-ten/go/enclave/storage" ) @@ -17,7 +17,7 @@ func ExportCrossChainData(ctx context.Context, storage storage.Storage, fromSeqN } if len(canonicalBatches) == 0 { - return nil, fmt.Errorf("no batches found for export of cross chain data") + return nil, errutil.ErrCrossChainBundleNoBatches } blockHash := canonicalBatches[len(canonicalBatches)-1].Header.L1Proof @@ -40,6 +40,6 @@ func ExportCrossChainData(ctx context.Context, storage storage.Storage, fromSeqN L1BlockHash: block.Hash(), L1BlockNum: big.NewInt(0).Set(block.Header().Number), CrossChainRootHashes: crossChainHashes, - } //todo: check fromSeqNo + } // todo: check fromSeqNo return bundle, nil } diff --git a/go/enclave/nodetype/sequencer.go b/go/enclave/nodetype/sequencer.go index a0283c2312..ff7ed22dc4 100644 --- a/go/enclave/nodetype/sequencer.go +++ b/go/enclave/nodetype/sequencer.go @@ -210,7 +210,16 @@ func (s *sequencer) createNewHeadBatch(ctx context.Context, l1HeadBlock *common. return err } - // todo - sanity check that the headBatch.Header.L1Proof is an ancestor of the l1HeadBlock + // sanity check that the cached headBatch is canonical. (Might impact performance) + isCanon, err := s.storage.IsBatchCanonical(ctx, headBatchSeq.Uint64()) + if err != nil { + return err + } + if !isCanon { + return fmt.Errorf("should not happen. Current head batch %d is not canonical", headBatchSeq) + } + + // sanity check that the headBatch.Header.L1Proof is an ancestor of the l1HeadBlock b, err := s.storage.FetchBlock(ctx, headBatch.Header.L1Proof) if err != nil { return err @@ -363,8 +372,9 @@ func (s *sequencer) CreateRollup(ctx context.Context, lastBatchNo uint64) (*comm return extRollup, nil } -func (s *sequencer) duplicateBatches(ctx context.Context, l1Head *types.Block, nonCanonicalL1Path []common.L1BlockHash) error { +func (s *sequencer) duplicateBatches(ctx context.Context, l1Head *types.Block, nonCanonicalL1Path []common.L1BlockHash, canonicalL1Path []common.L1BlockHash) error { batchesToDuplicate := make([]*core.Batch, 0) + batchesToExclude := make(map[uint64]*core.Batch, 0) // read the batches attached to these blocks for _, l1BlockHash := range nonCanonicalL1Path { @@ -378,6 +388,21 @@ func (s *sequencer) duplicateBatches(ctx context.Context, l1Head *types.Block, n batchesToDuplicate = append(batchesToDuplicate, batches...) } + // check whether there are already batches on the canonical branch + // because we don't want to duplicate a batch if there is already a canonical batch of the same height + for _, l1BlockHash := range canonicalL1Path { + batches, err := s.storage.FetchBatchesByBlock(ctx, l1BlockHash) + if err != nil { + if errors.Is(err, errutil.ErrNotFound) { + continue + } + return fmt.Errorf("could not FetchBatchesByBlock %s. Cause %w", l1BlockHash, err) + } + for _, batch := range batches { + batchesToExclude[batch.NumberU64()] = batch + } + } + if len(batchesToDuplicate) == 0 { return nil } @@ -395,13 +420,18 @@ func (s *sequencer) duplicateBatches(ctx context.Context, l1Head *types.Block, n if i > 0 && batchesToDuplicate[i].Header.ParentHash != batchesToDuplicate[i-1].Hash() { s.logger.Crit("the batches that must be duplicated are invalid") } + if batchesToExclude[orphanBatch.NumberU64()] != nil { + s.logger.Info("Not duplicating batch because there is already a canonical batch on that height", log.BatchSeqNoKey, orphanBatch.SeqNo()) + currentHead = batchesToExclude[orphanBatch.NumberU64()].Hash() + continue + } sequencerNo, err := s.storage.FetchCurrentSequencerNo(ctx) if err != nil { return fmt.Errorf("could not fetch sequencer no. Cause %w", err) } sequencerNo = sequencerNo.Add(sequencerNo, big.NewInt(1)) // create the duplicate and store/broadcast it, recreate batch even if it was empty - cb, err := s.produceBatch(ctx, sequencerNo, l1Head.ParentHash(), currentHead, orphanBatch.Transactions, orphanBatch.Header.Time, false) + cb, err := s.produceBatch(ctx, sequencerNo, l1Head.Hash(), currentHead, orphanBatch.Transactions, orphanBatch.Header.Time, false) if err != nil { return fmt.Errorf("could not produce batch. Cause %w", err) } @@ -409,6 +439,16 @@ func (s *sequencer) duplicateBatches(ctx context.Context, l1Head *types.Block, n s.logger.Info("Duplicated batch", log.BatchHashKey, currentHead) } + // useful for debugging + //start := batchesToDuplicate[0].SeqNo().Uint64() + //batches, err := s.storage.FetchNonCanonicalBatchesBetween(ctx, start-1, start+uint64(len(batchesToDuplicate))+1) + //if err != nil { + // panic(err) + //} + //for _, batch := range batches { + // s.logger.Info("After duplication. Noncanonical", log.BatchHashKey, batch.Hash(), log.BatchSeqNoKey, batch.Header.SequencerOrderNo) + //} + return nil } @@ -421,7 +461,7 @@ func (s *sequencer) OnL1Fork(ctx context.Context, fork *common.ChainFork) error return nil } - err := s.duplicateBatches(ctx, fork.NewCanonical, fork.NonCanonicalPath) + err := s.duplicateBatches(ctx, fork.NewCanonical, fork.NonCanonicalPath, fork.CanonicalPath) if err != nil { return fmt.Errorf("could not duplicate batches. Cause %w", err) } @@ -468,6 +508,7 @@ func (s *sequencer) signCrossChainBundle(bundle *common.ExtCrossChainBundle) err } return nil } + func (s *sequencer) OnL1Block(ctx context.Context, block *types.Block, result *components.BlockIngestionType) error { // nothing to do return nil diff --git a/go/enclave/nodetype/validator.go b/go/enclave/nodetype/validator.go index f64c189b8c..1ccd9ce81b 100644 --- a/go/enclave/nodetype/validator.go +++ b/go/enclave/nodetype/validator.go @@ -119,7 +119,7 @@ func (val *obsValidator) ExecuteStoredBatches(ctx context.Context) error { if err != nil { return fmt.Errorf("could not determine the execution prerequisites for batch %s. Cause: %w", batch.Hash(), err) } - val.logger.Trace("Can executing stored batch", log.BatchSeqNoKey, batch.SeqNo(), "can", canExecute) + val.logger.Trace("Can execute stored batch", log.BatchSeqNoKey, batch.SeqNo(), "can", canExecute) if canExecute { receipts, err := val.batchExecutor.ExecuteBatch(ctx, batch) diff --git a/go/enclave/rpc/SubmitTx.go b/go/enclave/rpc/SubmitTx.go index ab93317f4b..fe3d2af7fe 100644 --- a/go/enclave/rpc/SubmitTx.go +++ b/go/enclave/rpc/SubmitTx.go @@ -1,6 +1,7 @@ package rpc import ( + "errors" "fmt" gethcommon "github.com/ethereum/go-ethereum/common" @@ -9,7 +10,11 @@ import ( ) func SubmitTxValidate(reqParams []any, builder *CallBuilder[common.L2Tx, gethcommon.Hash], _ *EncryptionManager) error { - l2Tx, err := ExtractTx(reqParams[0].(string)) + txStr, ok := reqParams[0].(string) + if !ok { + return errors.New("unsupported format") + } + l2Tx, err := ExtractTx(txStr) if err != nil { builder.Err = fmt.Errorf("could not extract transaction. Cause: %w", err) return nil diff --git a/go/enclave/storage/enclavedb/batch.go b/go/enclave/storage/enclavedb/batch.go index 68b783634e..6ed0cd035a 100644 --- a/go/enclave/storage/enclavedb/batch.go +++ b/go/enclave/storage/enclavedb/batch.go @@ -43,15 +43,19 @@ func WriteBatchAndTransactions(ctx context.Context, dbtx *sql.Tx, batch *core.Ba return err } - var isCanon bool - err = dbtx.QueryRowContext(ctx, - "select is_canonical from block where hash=? ", - batch.Header.L1Proof.Bytes(), - ).Scan(&isCanon) + isL1ProofCanonical, err := IsCanonicalBlock(ctx, dbtx, &batch.Header.L1Proof) + if err != nil { + return err + } + parentIsCanon, err := IsCanonicalBatch(ctx, dbtx, &batch.Header.ParentHash) if err != nil { - // if the block is not found, we assume it is non-canonical - // fmt.Printf("IsCanon %s err: %s\n", batch.Header.L1Proof, err) - isCanon = false + return err + } + parentIsCanon = parentIsCanon || batch.SeqNo().Uint64() <= common.L2GenesisSeqNo+2 + + // sanity check that the parent is canonical + if isL1ProofCanonical && !parentIsCanon { + panic(fmt.Errorf("invalid chaining. Batch %s is canonical. Parent %s is not", batch.Hash(), batch.Header.ParentHash)) } args := []any{ @@ -59,7 +63,7 @@ func WriteBatchAndTransactions(ctx context.Context, dbtx *sql.Tx, batch *core.Ba convertedHash, // converted_hash batch.Hash(), // hash batch.Header.Number.Uint64(), // height - isCanon, // is_canonical + isL1ProofCanonical, // is_canonical header, // header blob batchBodyID, // reference to the batch body batch.Header.L1Proof.Bytes(), // l1 proof hash @@ -107,6 +111,30 @@ func WriteBatchAndTransactions(ctx context.Context, dbtx *sql.Tx, batch *core.Ba return nil } +func IsCanonicalBatch(ctx context.Context, dbtx *sql.Tx, hash *gethcommon.Hash) (bool, error) { + var isCanon bool + err := dbtx.QueryRowContext(ctx, "select is_canonical from batch where hash=? ", hash.Bytes()).Scan(&isCanon) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, err + } + return isCanon, err +} + +func IsCanonicalBatchSeq(ctx context.Context, db *sql.DB, seqNo uint64) (bool, error) { + var isCanon bool + err := db.QueryRowContext(ctx, "select is_canonical from batch where sequence=? ", seqNo).Scan(&isCanon) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, err + } + return isCanon, err +} + // WriteBatchExecution - save receipts func WriteBatchExecution(ctx context.Context, dbtx *sql.Tx, seqNo *big.Int, receipts []*types.Receipt) error { _, err := dbtx.ExecContext(ctx, "update batch set is_executed=true where sequence=?", seqNo.Uint64()) @@ -176,7 +204,7 @@ func ReadCurrentHeadBatch(ctx context.Context, db *sql.DB) (*core.Batch, error) } func ReadBatchesByBlock(ctx context.Context, db *sql.DB, hash common.L1BlockHash) ([]*core.Batch, error) { - return fetchBatches(ctx, db, " join block l1b on b.l1_proof=l1b.id where l1b.hash=? order by b.sequence", hash.Bytes()) + return fetchBatches(ctx, db, " where l1_proof_hash=? order by b.sequence", hash.Bytes()) } func ReadCurrentSequencerNo(ctx context.Context, db *sql.DB) (*big.Int, error) { @@ -428,7 +456,7 @@ func ReadUnexecutedBatches(ctx context.Context, db *sql.DB, from *big.Int) ([]*c } func BatchWasExecuted(ctx context.Context, db *sql.DB, hash common.L2BatchHash) (bool, error) { - row := db.QueryRowContext(ctx, "select is_executed from batch where is_canonical=true and hash=? ", hash.Bytes()) + row := db.QueryRowContext(ctx, "select is_executed from batch where hash=? ", hash.Bytes()) var result bool err := row.Scan(&result) diff --git a/go/enclave/storage/enclavedb/block.go b/go/enclave/storage/enclavedb/block.go index c88722a010..3ded5d150d 100644 --- a/go/enclave/storage/enclavedb/block.go +++ b/go/enclave/storage/enclavedb/block.go @@ -8,6 +8,8 @@ import ( "fmt" "math/big" + gethcommon "github.com/ethereum/go-ethereum/common" + gethlog "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/core/types" @@ -31,23 +33,7 @@ func WriteBlock(ctx context.Context, dbtx *sql.Tx, b *types.Header) error { return err } -func UpdateCanonicalBlocks(ctx context.Context, dbtx *sql.Tx, canonical []common.L1BlockHash, nonCanonical []common.L1BlockHash, logger gethlog.Logger) error { - if len(nonCanonical) > 0 { - err := updateCanonicalValue(ctx, dbtx, false, nonCanonical, logger) - if err != nil { - return err - } - } - if len(canonical) > 0 { - err := updateCanonicalValue(ctx, dbtx, true, canonical, logger) - if err != nil { - return err - } - } - return nil -} - -func updateCanonicalValue(ctx context.Context, dbtx *sql.Tx, isCanonical bool, blocks []common.L1BlockHash, _ gethlog.Logger) error { +func UpdateCanonicalValue(ctx context.Context, dbtx *sql.Tx, isCanonical bool, blocks []common.L1BlockHash, _ gethlog.Logger) error { currentBlocks := repeat(" hash=? ", "OR", len(blocks)) args := make([]any, 0) @@ -71,6 +57,40 @@ func updateCanonicalValue(ctx context.Context, dbtx *sql.Tx, isCanonical bool, b return nil } +func IsCanonicalBlock(ctx context.Context, dbtx *sql.Tx, hash *gethcommon.Hash) (bool, error) { + var isCanon bool + err := dbtx.QueryRowContext(ctx, "select is_canonical from block where hash=? ", hash.Bytes()).Scan(&isCanon) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, err + } + return isCanon, err +} + +// CheckCanonicalValidity - expensive but useful for debugging races +func CheckCanonicalValidity(ctx context.Context, dbtx *sql.Tx) error { + rows, err := dbtx.QueryContext(ctx, "select count(*), height from batch where is_canonical=true group by height having count(*) >1") + if err != nil { + return err + } + defer rows.Close() + if err := rows.Err(); err != nil { + return err + } + if rows.Next() { + var cnt uint64 + var heignt uint64 + err := rows.Scan(&cnt, &heignt) + if err != nil { + return err + } + return fmt.Errorf("found multiple (%d) canonical batches for height %d", cnt, heignt) + } + return nil +} + // HandleBlockArrivedAfterBatches- handle the corner case where the block wasn't available when the batch was received func HandleBlockArrivedAfterBatches(ctx context.Context, dbtx *sql.Tx, blockId int64, blockHash common.L1BlockHash) error { _, err := dbtx.ExecContext(ctx, "update batch set l1_proof=?, is_canonical=true where l1_proof_hash=?", blockId, blockHash.Bytes()) diff --git a/go/enclave/storage/enclavedb/events.go b/go/enclave/storage/enclavedb/events.go index 2723c9a3a2..67a0a90ca7 100644 --- a/go/enclave/storage/enclavedb/events.go +++ b/go/enclave/storage/enclavedb/events.go @@ -364,7 +364,10 @@ func stringToHash(ns sql.NullString) gethcommon.Hash { if err != nil { return [32]byte{} } - s := value.(string) + s, ok := value.(string) + if !ok { + return [32]byte{} + } result := gethcommon.Hash{} result.SetBytes([]byte(s)) return result diff --git a/go/enclave/storage/interfaces.go b/go/enclave/storage/interfaces.go index 4b68859a10..88d2340be4 100644 --- a/go/enclave/storage/interfaces.go +++ b/go/enclave/storage/interfaces.go @@ -22,6 +22,7 @@ import ( type BlockResolver interface { // FetchBlock returns the L1 Block with the given hash. FetchBlock(ctx context.Context, blockHash common.L1BlockHash) (*types.Block, error) + IsBlockCanonical(ctx context.Context, blockHash common.L1BlockHash) (bool, error) // FetchCanonicaBlockByHeight - self explanatory FetchCanonicaBlockByHeight(ctx context.Context, height *big.Int) (*types.Block, error) // FetchHeadBlock - returns the head of the current chain. @@ -55,6 +56,8 @@ type BatchResolver interface { FetchNonCanonicalBatchesBetween(ctx context.Context, startSeq uint64, endSeq uint64) ([]*core.Batch, error) // FetchCanonicalBatchesBetween - returns all canon batches between the sequences FetchCanonicalBatchesBetween(ctx context.Context, startSeq uint64, endSeq uint64) ([]*core.Batch, error) + // IsBatchCanonical - true if the batch is canonical + IsBatchCanonical(ctx context.Context, seq uint64) (bool, error) // FetchCanonicalUnexecutedBatches - return the list of the unexecuted batches that are canonical FetchCanonicalUnexecutedBatches(context.Context, *big.Int) ([]*core.Batch, error) diff --git a/go/enclave/storage/storage.go b/go/enclave/storage/storage.go index 70d9b09ba4..c804424271 100644 --- a/go/enclave/storage/storage.go +++ b/go/enclave/storage/storage.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/ecdsa" + "database/sql" "errors" "fmt" "math/big" @@ -205,6 +206,11 @@ func (s *storageImpl) FetchCanonicalBatchesBetween(ctx context.Context, startSeq return enclavedb.ReadCanonicalBatches(ctx, s.db.GetSQLDB(), startSeq, endSeq) } +func (s *storageImpl) IsBatchCanonical(ctx context.Context, seq uint64) (bool, error) { + defer s.logDuration("IsBatchCanonical", measure.NewStopwatch()) + return enclavedb.IsCanonicalBatchSeq(ctx, s.db.GetSQLDB(), seq) +} + func (s *storageImpl) StoreBlock(ctx context.Context, block *types.Block, chainFork *common.ChainFork) error { defer s.logDuration("StoreBlock", measure.NewStopwatch()) dbTx, err := s.db.NewDBTransaction(ctx) @@ -213,13 +219,17 @@ func (s *storageImpl) StoreBlock(ctx context.Context, block *types.Block, chainF } defer dbTx.Rollback() - if err := enclavedb.WriteBlock(ctx, dbTx, block.Header()); err != nil { - return fmt.Errorf("2. could not store block %s. Cause: %w", block.Hash(), err) - } - + // only insert the block if it doesn't exist already blockId, err := enclavedb.GetBlockId(ctx, dbTx, block.Hash()) - if err != nil { - return fmt.Errorf("3. could not get block id - %w", err) + if errors.Is(err, sql.ErrNoRows) { + if err := enclavedb.WriteBlock(ctx, dbTx, block.Header()); err != nil { + return fmt.Errorf("2. could not store block %s. Cause: %w", block.Hash(), err) + } + + blockId, err = enclavedb.GetBlockId(ctx, dbTx, block.Hash()) + if err != nil { + return fmt.Errorf("3. could not get block id - %w", err) + } } // In case there were any batches inserted before this block was received @@ -230,12 +240,23 @@ func (s *storageImpl) StoreBlock(ctx context.Context, block *types.Block, chainF if chainFork != nil && chainFork.IsFork() { s.logger.Info(fmt.Sprintf("Update Fork. %s", chainFork)) - err = enclavedb.UpdateCanonicalBlocks(ctx, dbTx, chainFork.CanonicalPath, chainFork.NonCanonicalPath, s.logger) + err := enclavedb.UpdateCanonicalValue(ctx, dbTx, false, chainFork.NonCanonicalPath, s.logger) + if err != nil { + return err + } + err = enclavedb.UpdateCanonicalValue(ctx, dbTx, true, chainFork.CanonicalPath, s.logger) if err != nil { return err } } + // double check that there is always a single canonical batch or block per layer + // only for debugging + //err = enclavedb.CheckCanonicalValidity(ctx, dbTx) + //if err != nil { + // return err + //} + if err := dbTx.Commit(); err != nil { return fmt.Errorf("4. could not store block %s. Cause: %w", block.Hash(), err) } @@ -252,6 +273,16 @@ func (s *storageImpl) FetchBlock(ctx context.Context, blockHash common.L1BlockHa }) } +func (s *storageImpl) IsBlockCanonical(ctx context.Context, blockHash common.L1BlockHash) (bool, error) { + defer s.logDuration("IsBlockCanonical", measure.NewStopwatch()) + dbtx, err := s.db.NewDBTransaction(ctx) + if err != nil { + return false, err + } + defer dbtx.Rollback() + return enclavedb.IsCanonicalBlock(ctx, dbtx, &blockHash) +} + func (s *storageImpl) FetchCanonicaBlockByHeight(ctx context.Context, height *big.Int) (*types.Block, error) { defer s.logDuration("FetchCanonicaBlockByHeight", measure.NewStopwatch()) header, err := enclavedb.FetchBlockHeaderByHeight(ctx, s.db.GetSQLDB(), height) @@ -339,13 +370,13 @@ func (s *storageImpl) IsBlockAncestor(ctx context.Context, block *types.Block, m func (s *storageImpl) HealthCheck(ctx context.Context) (bool, error) { defer s.logDuration("HealthCheck", measure.NewStopwatch()) - headBatch, err := s.FetchHeadBatch(ctx) + seqNo, err := s.FetchCurrentSequencerNo(ctx) if err != nil { return false, err } - if headBatch == nil { - return false, fmt.Errorf("head batch is nil") + if seqNo == nil { + return false, fmt.Errorf("no batches are stored") } return true, nil @@ -507,7 +538,7 @@ func (s *storageImpl) StoreExecutedBatch(ctx context.Context, batch *core.Batch, if err := enclavedb.WriteBatchExecution(ctx, dbTx, batch.SeqNo(), receipts); err != nil { return fmt.Errorf("could not write transaction receipts. Cause: %w", err) } - + s.logger.Trace("store executed batch", log.BatchHashKey, batch.Hash(), log.BatchSeqNoKey, batch.SeqNo(), "receipts", len(receipts)) if batch.Number().Uint64() > common.L2GenesisSeqNo { stateDB, err := s.CreateStateDB(ctx, batch.Header.ParentHash) if err != nil { diff --git a/go/enclave/txpool/txpool.go b/go/enclave/txpool/txpool.go index 8653d9626f..6deca4a1a4 100644 --- a/go/enclave/txpool/txpool.go +++ b/go/enclave/txpool/txpool.go @@ -51,7 +51,7 @@ func NewTxPool(blockchain *ethchainadapter.EthChainAdapter, gasTip *big.Int, log // Start starts the pool // can only be started after t.blockchain has at least one block inside func (t *TxPool) Start() error { - if t.pool != nil { + if t.running { return fmt.Errorf("tx pool already started") } @@ -75,6 +75,9 @@ func (t *TxPool) PendingTransactions() map[gethcommon.Address][]*gethtxpool.Lazy // Add adds a new transactions to the pool func (t *TxPool) Add(transaction *common.L2Tx) error { + if !t.running { + return fmt.Errorf("tx pool not running") + } var strErrors []string for _, err := range t.pool.Add([]*types.Transaction{transaction}, false, false) { if err != nil { diff --git a/go/ethadapter/geth_rpc_client.go b/go/ethadapter/geth_rpc_client.go index aeada83646..6d9d77f22f 100644 --- a/go/ethadapter/geth_rpc_client.go +++ b/go/ethadapter/geth_rpc_client.go @@ -254,23 +254,27 @@ func (e *gethRPCClient) FetchLastBatchSeqNo(address gethcommon.Address) (*big.In // PrepareTransactionToSend takes a txData type and overrides the From, Gas and Gas Price field with current values func (e *gethRPCClient) PrepareTransactionToSend(ctx context.Context, txData types.TxData, from gethcommon.Address) (types.TxData, error) { - return e.PrepareTransactionToRetry(ctx, txData, from, 0) + nonce, err := e.EthClient().PendingNonceAt(ctx, from) + if err != nil { + return nil, fmt.Errorf("could not get nonce - %w", err) + } + return e.PrepareTransactionToRetry(ctx, txData, from, nonce, 0) } // PrepareTransactionToRetry takes a txData type and overrides the From, Gas and Gas Price field with current values // it bumps the price by a multiplier for retries. retryNumber is zero on first attempt (no multiplier on price) -func (e *gethRPCClient) PrepareTransactionToRetry(ctx context.Context, txData types.TxData, from gethcommon.Address, retryNumber int) (types.TxData, error) { +func (e *gethRPCClient) PrepareTransactionToRetry(ctx context.Context, txData types.TxData, from gethcommon.Address, nonce uint64, retryNumber int) (types.TxData, error) { switch tx := txData.(type) { case *types.LegacyTx: - return e.prepareLegacyTxToRetry(ctx, tx, from, 0) + return e.prepareLegacyTxToRetry(ctx, tx, from, nonce, 0) case *types.BlobTx: - return e.prepareBlobTxToRetry(ctx, tx, from, 0) + return e.prepareBlobTxToRetry(ctx, tx, from, nonce, 0) default: return nil, fmt.Errorf("unsupported transaction type: %T", tx) } } -func (e *gethRPCClient) prepareLegacyTxToRetry(ctx context.Context, txData types.TxData, from gethcommon.Address, retryNumber int) (types.TxData, error) { +func (e *gethRPCClient) prepareLegacyTxToRetry(ctx context.Context, txData types.TxData, from gethcommon.Address, nonce uint64, retryNumber int) (types.TxData, error) { unEstimatedTx := types.NewTx(txData) gasPrice, err := e.EthClient().SuggestGasPrice(ctx) if err != nil { @@ -298,12 +302,6 @@ func (e *gethRPCClient) prepareLegacyTxToRetry(ctx context.Context, txData types return nil, fmt.Errorf("could not estimate gas - %w", err) } - // we fetch the current nonce on every retry to avoid any risk of nonce reuse/conflicts - nonce, err := e.EthClient().PendingNonceAt(ctx, from) - if err != nil { - return nil, fmt.Errorf("could not fetch nonce - %w", err) - } - return &types.LegacyTx{ Nonce: nonce, GasPrice: retryPrice, @@ -316,7 +314,7 @@ func (e *gethRPCClient) prepareLegacyTxToRetry(ctx context.Context, txData types // PrepareBlobTransactionToRetry takes a txData type and overrides the From, Gas and Gas Price field with current values // it bumps the price by a multiplier for retries. retryNumber is zero on first attempt (no multiplier on price) -func (e *gethRPCClient) prepareBlobTxToRetry(ctx context.Context, txData types.TxData, from gethcommon.Address, retryNumber int) (types.TxData, error) { +func (e *gethRPCClient) prepareBlobTxToRetry(ctx context.Context, txData types.TxData, from gethcommon.Address, nonce uint64, retryNumber int) (types.TxData, error) { unEstimatedTx := types.NewTx(txData) to := unEstimatedTx.To() value := unEstimatedTx.Value() @@ -348,11 +346,6 @@ func (e *gethRPCClient) prepareBlobTxToRetry(ctx context.Context, txData types.T return nil, fmt.Errorf("could not estimate gas - %w", err) } - // we fetch the current nonce on every retry to avoid any risk of nonce reuse/conflicts - nonce, err := e.EthClient().PendingNonceAt(ctx, from) - if err != nil { - return nil, fmt.Errorf("could not fetch nonce - %w", err) - } //TODO calculate base fee cap blobBaseFee := big.NewInt(1) diff --git a/go/ethadapter/interface.go b/go/ethadapter/interface.go index 281f1e6abf..31a1071869 100644 --- a/go/ethadapter/interface.go +++ b/go/ethadapter/interface.go @@ -35,7 +35,7 @@ type EthClient interface { // PrepareTransactionToSend updates the tx with from address, current nonce and current estimates for the gas and the gas price PrepareTransactionToSend(ctx context.Context, txData types.TxData, from gethcommon.Address) (types.TxData, error) - PrepareTransactionToRetry(ctx context.Context, txData types.TxData, from gethcommon.Address, retries int) (types.TxData, error) + PrepareTransactionToRetry(ctx context.Context, txData types.TxData, from gethcommon.Address, nonce uint64, retries int) (types.TxData, error) FetchLastBatchSeqNo(address gethcommon.Address) (*big.Int, error) diff --git a/go/host/enclave/guardian.go b/go/host/enclave/guardian.go index 7c472e2d94..2c6d6a6a36 100644 --- a/go/host/enclave/guardian.go +++ b/go/host/enclave/guardian.go @@ -651,7 +651,9 @@ func (g *Guardian) periodicBundleSubmission() { bundle, err := g.enclaveClient.ExportCrossChainData(context.Background(), fromSequenceNumber, to.Uint64()) if err != nil { - g.logger.Error("Unable to export cross chain bundle from enclave", log.ErrKey, err) + if !errors.Is(err, errutil.ErrCrossChainBundleNoBatches) { + g.logger.Error("Unable to export cross chain bundle from enclave", log.ErrKey, err) + } continue } diff --git a/go/host/l1/publisher.go b/go/host/l1/publisher.go index cfcad40507..840c61c2e2 100644 --- a/go/host/l1/publisher.go +++ b/go/host/l1/publisher.go @@ -395,12 +395,19 @@ func (p *Publisher) publishTransaction(tx types.TxData) error { retries := -1 + // we keep trying to send the transaction with this nonce until it is included in a block + // note: this is only safe because of the sendingLock guaranteeing only one transaction in-flight at a time + nonce, err := p.ethClient.Nonce(p.hostWallet.Address()) + if err != nil { + return fmt.Errorf("could not get nonce for L1 tx: %w", err) + } + // while the publisher service is still alive we keep trying to get the transaction into the L1 for !p.hostStopper.IsStopping() { retries++ // count each attempt so we can increase gas price // update the tx gas price before each attempt - tx, err := p.ethClient.PrepareTransactionToRetry(p.sendingContext, tx, p.hostWallet.Address(), retries) + tx, err := p.ethClient.PrepareTransactionToRetry(p.sendingContext, tx, p.hostWallet.Address(), nonce, retries) if err != nil { return errors.Wrap(err, "could not estimate gas/gas price for L1 tx") } diff --git a/go/host/storage/hostdb/batch.go b/go/host/storage/hostdb/batch.go index ae908facc0..1d1cfd69cb 100644 --- a/go/host/storage/hostdb/batch.go +++ b/go/host/storage/hostdb/batch.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "math/big" + "strings" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rlp" @@ -37,6 +38,9 @@ func AddBatch(dbtx *dbTransaction, statements *SQLStatements, batch *common.ExtB extBatch, // ext_batch ) if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "unique") { + return errutil.ErrAlreadyExists + } return fmt.Errorf("host failed to insert batch: %w", err) } diff --git a/go/host/storage/storage.go b/go/host/storage/storage.go index 9607245ff6..3a82ee70b3 100644 --- a/go/host/storage/storage.go +++ b/go/host/storage/storage.go @@ -1,6 +1,7 @@ package storage import ( + "errors" "fmt" "io" "math/big" @@ -35,7 +36,10 @@ func (s *storageImpl) AddBatch(batch *common.ExtBatch) error { } if err := hostdb.AddBatch(dbtx, s.db.GetSQLStatement(), batch); err != nil { - if err := dbtx.Rollback(); err != nil { + if err1 := dbtx.Rollback(); err1 != nil { + return err1 + } + if errors.Is(err, errutil.ErrAlreadyExists) { return err } return fmt.Errorf("could not add batch to host. Cause: %w", err) diff --git a/integration/ethereummock/db.go b/integration/ethereummock/db.go index 517a4efb8b..76db7ff012 100644 --- a/integration/ethereummock/db.go +++ b/integration/ethereummock/db.go @@ -24,6 +24,11 @@ type blockResolverInMem struct { m sync.RWMutex } +func (n *blockResolverInMem) IsBlockCanonical(ctx context.Context, blockHash common.L1BlockHash) (bool, error) { + // TODO implement me + panic("implement me") +} + func (n *blockResolverInMem) FetchCanonicaBlockByHeight(_ context.Context, _ *big.Int) (*types.Block, error) { panic("implement me") } diff --git a/integration/networktest/tests/gateway/gateway_test.go b/integration/networktest/tests/gateway/gateway_test.go index eb8d5e9c13..f919db84d9 100644 --- a/integration/networktest/tests/gateway/gateway_test.go +++ b/integration/networktest/tests/gateway/gateway_test.go @@ -1,12 +1,15 @@ package gateway import ( + "context" + "fmt" "math/big" "testing" "github.com/ten-protocol/go-ten/integration/networktest" "github.com/ten-protocol/go-ten/integration/networktest/actions" "github.com/ten-protocol/go-ten/integration/networktest/env" + "github.com/ten-protocol/go-ten/integration/networktest/userwallet" "github.com/ten-protocol/go-ten/integration/simulation/devnetwork" ) @@ -37,6 +40,45 @@ func TestGatewayHappyPath(t *testing.T) { &actions.VerifyBalanceAfterTest{UserID: 1, ExpectedBalance: _transferAmount}, &actions.VerifyBalanceDiffAfterTest{UserID: 0, Snapshot: actions.SnapAfterAllocation, ExpectedDiff: big.NewInt(0).Neg(_transferAmount)}, + + // test net_version works through the gateway + actions.VerifyOnlyAction(func(ctx context.Context, network networktest.NetworkConnector) error { + user, err := actions.FetchTestUser(ctx, 0) + if err != nil { + return err + } + // verify user is a gateway user + gwUser, ok := user.(*userwallet.GatewayUser) + if !ok { + return fmt.Errorf("user is not a gateway user") + } + ethClient := gwUser.Client() + rpcClient := ethClient.Client() + // check net_version response + var result string + err = rpcClient.CallContext(ctx, &result, "net_version") + if err != nil { + return fmt.Errorf("failed to get net_version: %w", err) + } + fmt.Println("net_version response:", result) + expectedResult := "443" + if result != expectedResult { + return fmt.Errorf("expected net_version to be %s but got %s", expectedResult, result) + } + + // check web3_clientVersion response + var cvResult string + err = rpcClient.CallContext(ctx, &cvResult, "web3_clientVersion") + if err != nil { + return fmt.Errorf("failed to get web3_clientVersion: %w", err) + } + fmt.Println("web3_clientVersion response:", cvResult) + if cvResult == "" { + return fmt.Errorf("expected web3_clientVersion to be non-empty") + } + + return nil + }), ), ) } diff --git a/integration/networktest/userwallet/gateway.go b/integration/networktest/userwallet/gateway.go index 03fa361a9d..9408c4c06c 100644 --- a/integration/networktest/userwallet/gateway.go +++ b/integration/networktest/userwallet/gateway.go @@ -114,6 +114,10 @@ func (g *GatewayUser) Wallet() wallet.Wallet { return g.wal } +func (g *GatewayUser) Client() *ethclient.Client { + return g.client +} + func (g *GatewayUser) WSClient() (*ethclient.Client, error) { if g.wsClient == nil { var err error diff --git a/integration/simulation/simulation.go b/integration/simulation/simulation.go index 81540b3226..5e0fe8a8ba 100644 --- a/integration/simulation/simulation.go +++ b/integration/simulation/simulation.go @@ -55,14 +55,13 @@ func (s *Simulation) Start() { // Arbitrary sleep to wait for RPC clients to get up and running // and for all l2 nodes to receive the genesis l2 batch - time.Sleep(7 * time.Second) + // todo - instead of sleeping, it would be better to poll + time.Sleep(10 * time.Second) s.bridgeFundingToObscuro() s.trackLogs() // Create log subscriptions, to validate that they're working correctly later. s.prefundObscuroAccounts() // Prefund every L2 wallet - // wait for the validator to become up to date - time.Sleep(1 * time.Second) s.deployObscuroERC20s() // Deploy the Obscuro HOC and POC ERC20 contracts s.prefundL1Accounts() // Prefund every L1 wallet s.checkHealthStatus() // Checks the nodes health status @@ -211,9 +210,9 @@ func (s *Simulation) prefundObscuroAccounts() { faucetClient := s.RPCHandles.ObscuroWalletClient(faucetWallet.Address(), 0) // get sequencer, else errors on submission get swallowed nonce := NextNonce(s.ctx, s.RPCHandles, faucetWallet) - // Give 100 ether per account - ether is 1e18 so best convert it by code + // Give 1000 ether per account - ether is 1e18 so best convert it by code // as a lot of the hardcodes were giving way too little and choking the gas payments - allocObsWallets := big.NewInt(0).Mul(big.NewInt(100), big.NewInt(gethparams.Ether)) + allocObsWallets := big.NewInt(0).Mul(big.NewInt(1000), big.NewInt(gethparams.Ether)) testcommon.PrefundWallets(s.ctx, faucetWallet, faucetClient, nonce, s.Params.Wallets.AllObsWallets(), allocObsWallets, s.Params.ReceiptTimeout) } diff --git a/integration/simulation/simulation_in_mem_test.go b/integration/simulation/simulation_in_mem_test.go index d3d4b22d09..da278c6ed5 100644 --- a/integration/simulation/simulation_in_mem_test.go +++ b/integration/simulation/simulation_in_mem_test.go @@ -26,8 +26,8 @@ func TestInMemoryMonteCarloSimulation(t *testing.T) { simParams := params.SimParams{ NumberOfNodes: numberOfNodes, // todo (#718) - try reducing this back to 50 milliseconds once faster-finality model is optimised - AvgBlockDuration: 250 * time.Millisecond, - SimulationTime: 30 * time.Second, + AvgBlockDuration: 180 * time.Millisecond, + SimulationTime: 45 * time.Second, L1EfficiencyThreshold: 0.2, MgmtContractLib: ethereummock.NewMgmtContractLibMock(), ERC20ContractLib: ethereummock.NewERC20ContractLibMock(), @@ -36,7 +36,7 @@ func TestInMemoryMonteCarloSimulation(t *testing.T) { IsInMem: true, L1TenData: ¶ms.L1TenData{}, ReceiptTimeout: 5 * time.Second, - StoppingDelay: 4 * time.Second, + StoppingDelay: 15 * time.Second, NodeWithInboundP2PDisabled: 2, } diff --git a/integration/simulation/validate_chain.go b/integration/simulation/validate_chain.go index a394d8a525..91d417472b 100644 --- a/integration/simulation/validate_chain.go +++ b/integration/simulation/validate_chain.go @@ -778,12 +778,13 @@ func checkTotalTransactions(t *testing.T, client rpc.Client, nodeIdx int) { // Checks that we can retrieve the latest batches func checkForLatestBatches(t *testing.T, client rpc.Client, nodeIdx int) { var latestBatches common.BatchListingResponseDeprecated - pagination := common.QueryPagination{Offset: uint64(0), Size: uint(5)} + pagination := common.QueryPagination{Offset: uint64(0), Size: uint(20)} err := client.Call(&latestBatches, rpc.GetBatchListing, &pagination) if err != nil { t.Errorf("node %d: could not retrieve latest batches. Cause: %s", nodeIdx, err) } - if len(latestBatches.BatchesData) != 5 { + // the batch listing function returns the last received batches , but it might receive them in a random order + if len(latestBatches.BatchesData) < 5 { t.Errorf("node %d: expected at least %d batches, but only received %d", nodeIdx, 5, len(latestBatches.BatchesData)) } } diff --git a/tools/walletextension/rpcapi/net_api.go b/tools/walletextension/rpcapi/net_api.go new file mode 100644 index 0000000000..855c5b7a42 --- /dev/null +++ b/tools/walletextension/rpcapi/net_api.go @@ -0,0 +1,17 @@ +package rpcapi + +import ( + "context" +) + +type NetAPI struct { + we *Services +} + +func NewNetAPI(we *Services) *NetAPI { + return &NetAPI{we} +} + +func (api *NetAPI) Version(ctx context.Context) (*string, error) { + return UnauthenticatedTenRPCCall[string](ctx, api.we, &CacheCfg{CacheType: LongLiving}, "net_version") +} diff --git a/tools/walletextension/rpcapi/web3_api.go b/tools/walletextension/rpcapi/web3_api.go new file mode 100644 index 0000000000..c5e96dc663 --- /dev/null +++ b/tools/walletextension/rpcapi/web3_api.go @@ -0,0 +1,20 @@ +package rpcapi + +import ( + "context" +) + +var _hardcodedClientVersion = "Geth/v10.0.0/ten" + +type Web3API struct { + we *Services +} + +func NewWeb3API(we *Services) *Web3API { + return &Web3API{we} +} + +func (api *Web3API) ClientVersion(_ context.Context) (*string, error) { + // todo: have this return the Ten version from the node + return &_hardcodedClientVersion, nil +} diff --git a/tools/walletextension/walletextension_container.go b/tools/walletextension/walletextension_container.go index 084044af54..f5bf5d36f7 100644 --- a/tools/walletextension/walletextension_container.go +++ b/tools/walletextension/walletextension_container.go @@ -83,6 +83,12 @@ func NewContainerFromConfig(config wecommon.Config, logger gethlog.Logger) *Cont }, { Namespace: "eth", Service: rpcapi.NewFilterAPI(walletExt), + }, { + Namespace: "net", + Service: rpcapi.NewNetAPI(walletExt), + }, { + Namespace: "web3", + Service: rpcapi.NewWeb3API(walletExt), }, })